mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
BarChart: rotate barchart x axis labels (#40299)
* GraphNG: add axes label rotation property * Adds rotation option and adds padding to avoid clipping labels * Slider: Enable slider marks display (#41275) Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
3b637f4b44
commit
b82797d1b0
@ -1,5 +1,13 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { DataLink, Field, FieldOverrideContext, SelectableValue, ThresholdsConfig, ValueMapping } from '../../types';
|
||||
import {
|
||||
DataLink,
|
||||
Field,
|
||||
FieldOverrideContext,
|
||||
SelectableValue,
|
||||
SliderMarks,
|
||||
ThresholdsConfig,
|
||||
ValueMapping,
|
||||
} from '../../types';
|
||||
|
||||
export const identityOverrideProcessor = <T>(value: T, _context: FieldOverrideContext, _settings: any) => {
|
||||
return value;
|
||||
@ -39,6 +47,8 @@ export interface SliderFieldConfigSettings {
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
included?: boolean;
|
||||
marks?: SliderMarks;
|
||||
ariaLabelForHandle?: string;
|
||||
}
|
||||
|
||||
|
@ -38,3 +38,4 @@ export * from './geometry';
|
||||
export { isUnsignedPluginSignature } from './pluginSignature';
|
||||
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';
|
||||
export * from './alerts';
|
||||
export * from './slider';
|
||||
|
1
packages/grafana-data/src/types/slider.ts
Normal file
1
packages/grafana-data/src/types/slider.ts
Normal file
@ -0,0 +1 @@
|
||||
export type SliderMarks = Record<number, React.ReactNode | { style?: React.CSSProperties; label?: string }>;
|
@ -12,6 +12,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -37,6 +38,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "__fixed",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
|
@ -16,6 +16,8 @@ export const SliderValueEditor: React.FC<FieldConfigEditorProps<number, SliderFi
|
||||
min={settings?.min || 0}
|
||||
max={settings?.max || 100}
|
||||
step={settings?.step}
|
||||
marks={settings?.marks}
|
||||
included={settings?.included}
|
||||
onChange={onChange}
|
||||
ariaLabelForHandle={settings?.ariaLabelForHandle}
|
||||
/>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Slider } from '@grafana/ui';
|
||||
import { SliderProps } from './types';
|
||||
import { Orientation } from '../../types/orientation';
|
||||
import { Story, Meta } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
@ -20,6 +21,16 @@ export default {
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
const commonArgs = {
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: 10,
|
||||
isStep: false,
|
||||
orientation: 'horizontal' as Orientation,
|
||||
reverse: false,
|
||||
included: true,
|
||||
};
|
||||
|
||||
interface StoryProps extends Partial<SliderProps> {
|
||||
isStep: boolean;
|
||||
}
|
||||
@ -38,10 +49,23 @@ export const Basic: Story<StoryProps> = (args) => {
|
||||
);
|
||||
};
|
||||
Basic.args = {
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: 10,
|
||||
isStep: false,
|
||||
orientation: 'horizontal',
|
||||
reverse: false,
|
||||
...commonArgs,
|
||||
};
|
||||
|
||||
export const WithMarks: Story<StoryProps> = (args) => {
|
||||
return (
|
||||
<div style={{ width: '300px', height: '300px' }}>
|
||||
<Slider
|
||||
step={args.isStep ? 10 : undefined}
|
||||
value={args.value}
|
||||
min={args.min as number}
|
||||
max={args.max as number}
|
||||
{...args}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
WithMarks.args = {
|
||||
...commonArgs,
|
||||
marks: { 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' },
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback, ChangeEvent, FunctionComponent, FocusEvent } from 'react';
|
||||
import SliderComponent from 'rc-slider';
|
||||
|
||||
import { cx } from '@emotion/css';
|
||||
import { Global } from '@emotion/react';
|
||||
import { useTheme2 } from '../../themes/ThemeContext';
|
||||
@ -20,12 +21,14 @@ export const Slider: FunctionComponent<SliderProps> = ({
|
||||
step,
|
||||
value,
|
||||
ariaLabelForHandle,
|
||||
marks,
|
||||
included,
|
||||
}) => {
|
||||
const isHorizontal = orientation === 'horizontal';
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, isHorizontal);
|
||||
const SliderWithTooltip = SliderComponent;
|
||||
const [sliderValue, setSliderValue] = useState<number>(value || min);
|
||||
const [sliderValue, setSliderValue] = useState<number>(value ?? min);
|
||||
|
||||
const onSliderChange = useCallback(
|
||||
(v: number) => {
|
||||
@ -93,6 +96,8 @@ export const Slider: FunctionComponent<SliderProps> = ({
|
||||
vertical={!isHorizontal}
|
||||
reverse={reverse}
|
||||
ariaLabelForHandle={ariaLabelForHandle}
|
||||
marks={marks}
|
||||
included={included}
|
||||
/>
|
||||
{/* Uses text input so that the number spinners are not shown */}
|
||||
<Input
|
||||
|
@ -23,6 +23,16 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, isHorizontal: bool
|
||||
flex-grow: 1;
|
||||
margin-left: 7px; // half the size of the handle to align handle to the left on 0 value
|
||||
}
|
||||
.rc-slider-mark {
|
||||
top: ${theme.spacing(1.75)};
|
||||
}
|
||||
.rc-slider-mark-text {
|
||||
color: ${theme.colors.text.disabled};
|
||||
font-size: ${theme.typography.bodySmall.fontSize};
|
||||
}
|
||||
.rc-slider-mark-text-active {
|
||||
color: ${theme.colors.text.primary};
|
||||
}
|
||||
.rc-slider-vertical .rc-slider-handle {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
@ -1,30 +1,30 @@
|
||||
import { SliderMarks } from '@grafana/data';
|
||||
import { Orientation } from '../../types/orientation';
|
||||
|
||||
export interface SliderProps {
|
||||
interface CommonSliderProps {
|
||||
min: number;
|
||||
max: number;
|
||||
orientation?: Orientation;
|
||||
/** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
|
||||
value?: number;
|
||||
reverse?: boolean;
|
||||
step?: number;
|
||||
tooltipAlwaysVisible?: boolean;
|
||||
formatTooltipResult?: (value: number) => number;
|
||||
/** Marks on the slider. The key determines the position, and the value determines what will show. If you want to set the style of a specific mark point, the value should be an object which contains style and label properties. */
|
||||
marks?: SliderMarks;
|
||||
/** If the value is true, it means a continuous value interval, otherwise, it is a independent value. */
|
||||
included?: boolean;
|
||||
}
|
||||
export interface SliderProps extends CommonSliderProps {
|
||||
value?: number;
|
||||
onChange?: (value: number) => void;
|
||||
onAfterChange?: (value?: number) => void;
|
||||
formatTooltipResult?: (value: number) => number;
|
||||
ariaLabelForHandle?: string;
|
||||
}
|
||||
|
||||
export interface RangeSliderProps {
|
||||
min: number;
|
||||
max: number;
|
||||
orientation?: Orientation;
|
||||
/** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
|
||||
export interface RangeSliderProps extends CommonSliderProps {
|
||||
value?: number[];
|
||||
reverse?: boolean;
|
||||
step?: number;
|
||||
tooltipAlwaysVisible?: boolean;
|
||||
formatTooltipResult?: (value: number) => number | string;
|
||||
onChange?: (value: number[]) => void;
|
||||
onAfterChange?: (value: number[]) => void;
|
||||
onAfterChange?: (value?: number[]) => void;
|
||||
formatTooltipResult?: (value: number) => number | string;
|
||||
}
|
||||
|
@ -252,6 +252,7 @@ export { LegacyForms, LegacyInputStatus };
|
||||
export * from './uPlot/config';
|
||||
export { ScaleDistribution } from '@grafana/schema';
|
||||
export { UPlotConfigBuilder } from './uPlot/config/UPlotConfigBuilder';
|
||||
export { UPLOT_AXIS_FONT_SIZE } from './uPlot/config/UPlotAxisBuilder';
|
||||
export { UPlotChart } from './uPlot/Plot';
|
||||
export { PlotLegend } from './uPlot/PlotLegend';
|
||||
export * from './uPlot/geometries';
|
||||
|
@ -12,6 +12,7 @@ export interface AxisProps {
|
||||
show?: boolean;
|
||||
size?: number | null;
|
||||
gap?: number;
|
||||
valueRotation?: number;
|
||||
placement?: AxisPlacement;
|
||||
grid?: Axis.Grid;
|
||||
ticks?: boolean;
|
||||
@ -23,7 +24,7 @@ export interface AxisProps {
|
||||
timeZone?: TimeZone;
|
||||
}
|
||||
|
||||
const fontSize = 12;
|
||||
export const UPLOT_AXIS_FONT_SIZE = 12;
|
||||
const labelPad = 8;
|
||||
|
||||
export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
@ -36,6 +37,47 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
this.props.placement = props.placement;
|
||||
}
|
||||
}
|
||||
/* Minimum grid & tick spacing in CSS pixels */
|
||||
calculateSpace(self: uPlot, axisIdx: number, scaleMin: number, scaleMax: number, plotDim: number): number {
|
||||
const axis = self.axes[axisIdx];
|
||||
const scale = self.scales[axis.scale!];
|
||||
|
||||
// for axis left & right
|
||||
if (axis.side !== 2 || !scale) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
const defaultSpacing = 40;
|
||||
|
||||
if (scale.time) {
|
||||
const maxTicks = plotDim / defaultSpacing;
|
||||
const increment = (scaleMax - scaleMin) / maxTicks;
|
||||
const sample = formatTime(self, [scaleMin], axisIdx, defaultSpacing, increment);
|
||||
const width = measureText(sample[0], UPLOT_AXIS_FONT_SIZE).width + 18;
|
||||
return width;
|
||||
}
|
||||
|
||||
return defaultSpacing;
|
||||
}
|
||||
|
||||
/** height of x axis or width of y axis in CSS pixels alloted for values, gap & ticks, but excluding axis label */
|
||||
calculateAxisSize(self: uPlot, values: string[], axisIdx: number) {
|
||||
const axis = self.axes[axisIdx];
|
||||
|
||||
let axisSize = axis.ticks!.size!;
|
||||
|
||||
if (axis.side === 2) {
|
||||
axisSize += axis!.gap! + UPLOT_AXIS_FONT_SIZE;
|
||||
} else if (values?.length) {
|
||||
let maxTextWidth = values.reduce(
|
||||
(acc, value) => Math.max(acc, measureText(value, UPLOT_AXIS_FONT_SIZE).width),
|
||||
0
|
||||
);
|
||||
axisSize += axis!.gap! + axis!.labelGap! + maxTextWidth;
|
||||
}
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
}
|
||||
|
||||
getConfig(): Axis {
|
||||
let {
|
||||
@ -52,9 +94,11 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
isTime,
|
||||
timeZone,
|
||||
theme,
|
||||
valueRotation,
|
||||
size,
|
||||
} = this.props;
|
||||
|
||||
const font = `${fontSize}px ${theme.typography.fontFamily}`;
|
||||
const font = `${UPLOT_AXIS_FONT_SIZE}px ${theme.typography.fontFamily}`;
|
||||
|
||||
const gridColor = theme.isDark ? 'rgba(240, 250, 255, 0.09)' : 'rgba(0, 10, 23, 0.09)';
|
||||
|
||||
@ -68,7 +112,12 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
stroke: theme.colors.text.primary,
|
||||
side: getUPlotSideFromAxis(placement),
|
||||
font,
|
||||
size: this.props.size ?? calculateAxisSize,
|
||||
size:
|
||||
size ??
|
||||
((self, values, axisIdx) => {
|
||||
return this.calculateAxisSize(self, values, axisIdx);
|
||||
}),
|
||||
rotate: valueRotation,
|
||||
gap,
|
||||
|
||||
labelGap: 0,
|
||||
@ -86,12 +135,14 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
},
|
||||
splits,
|
||||
values: values,
|
||||
space: calculateSpace,
|
||||
space: (self, axisIdx, scaleMin, scaleMax, plotDim) => {
|
||||
return this.calculateSpace(self, axisIdx, scaleMin, scaleMax, plotDim);
|
||||
},
|
||||
};
|
||||
|
||||
if (label != null && label.length > 0) {
|
||||
config.label = label;
|
||||
config.labelSize = fontSize + labelPad;
|
||||
config.labelSize = UPLOT_AXIS_FONT_SIZE + labelPad;
|
||||
config.labelFont = font;
|
||||
config.labelGap = labelPad;
|
||||
}
|
||||
@ -111,45 +162,6 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||
}
|
||||
}
|
||||
|
||||
/* Minimum grid & tick spacing in CSS pixels */
|
||||
function calculateSpace(self: uPlot, axisIdx: number, scaleMin: number, scaleMax: number, plotDim: number): number {
|
||||
const axis = self.axes[axisIdx];
|
||||
const scale = self.scales[axis.scale!];
|
||||
|
||||
// for axis left & right
|
||||
if (axis.side !== 2 || !scale) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
const defaultSpacing = 40;
|
||||
|
||||
if (scale.time) {
|
||||
const maxTicks = plotDim / defaultSpacing;
|
||||
const increment = (scaleMax - scaleMin) / maxTicks;
|
||||
const sample = formatTime(self, [scaleMin], axisIdx, defaultSpacing, increment);
|
||||
const width = measureText(sample[0], fontSize).width + 18;
|
||||
return width;
|
||||
}
|
||||
|
||||
return defaultSpacing;
|
||||
}
|
||||
|
||||
/** height of x axis or width of y axis in CSS pixels alloted for values, gap & ticks, but excluding axis label */
|
||||
function calculateAxisSize(self: uPlot, values: string[], axisIdx: number) {
|
||||
const axis = self.axes[axisIdx];
|
||||
|
||||
let axisSize = axis.ticks!.size!;
|
||||
|
||||
if (axis.side === 2) {
|
||||
axisSize += axis!.gap! + fontSize;
|
||||
} else if (values?.length) {
|
||||
let maxTextWidth = values.reduce((acc, value) => Math.max(acc, measureText(value, fontSize).width), 0);
|
||||
axisSize += axis!.gap! + axis!.labelGap! + maxTextWidth;
|
||||
}
|
||||
|
||||
return Math.ceil(axisSize);
|
||||
}
|
||||
|
||||
const timeUnitSize = {
|
||||
second: 1000,
|
||||
minute: 60 * 1000,
|
||||
|
@ -360,6 +360,7 @@ describe('UPlotConfigBuilder', () => {
|
||||
"labelFont": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
|
||||
"labelGap": 8,
|
||||
"labelSize": 20,
|
||||
"rotate": undefined,
|
||||
"scale": "scale-x",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
|
@ -17,6 +17,8 @@ export interface BarChartProps
|
||||
const propsToDiff: Array<string | PropDiffFn> = [
|
||||
'orientation',
|
||||
'barWidth',
|
||||
'valueRotation',
|
||||
'valueMaxLength',
|
||||
'groupWidth',
|
||||
'stacking',
|
||||
'showValue',
|
||||
@ -52,7 +54,19 @@ export const BarChart: React.FC<BarChartProps> = (props) => {
|
||||
};
|
||||
|
||||
const prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
||||
const { timeZone, orientation, barWidth, showValue, groupWidth, stacking, legend, tooltip, text } = props;
|
||||
const {
|
||||
timeZone,
|
||||
orientation,
|
||||
barWidth,
|
||||
showValue,
|
||||
groupWidth,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
text,
|
||||
valueRotation,
|
||||
valueMaxLength,
|
||||
} = props;
|
||||
|
||||
return preparePlotConfigBuilder({
|
||||
frame: alignedFrame,
|
||||
@ -64,6 +78,8 @@ export const BarChart: React.FC<BarChartProps> = (props) => {
|
||||
barWidth,
|
||||
showValue,
|
||||
groupWidth,
|
||||
valueRotation,
|
||||
valueMaxLength,
|
||||
stacking,
|
||||
legend,
|
||||
tooltip,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { TooltipDisplayMode, StackingMode } from '@grafana/schema';
|
||||
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
||||
import { TooltipPlugin, useTheme2 } from '@grafana/ui';
|
||||
import { measureText, TooltipPlugin, UPLOT_AXIS_FONT_SIZE, useTheme2 } from '@grafana/ui';
|
||||
import { BarChartOptions } from './types';
|
||||
import { BarChart } from './BarChart';
|
||||
import { prepareGraphableFrames } from './utils';
|
||||
@ -23,6 +23,23 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
||||
return options.orientation;
|
||||
}, [width, height, options.orientation]);
|
||||
|
||||
const valueMaxLength = useMemo(() => {
|
||||
// If no max length is set, limit the number of characters to a length where it will use a maximum of half of the height of the viz.
|
||||
if (!options.valueMaxLength) {
|
||||
const rotationAngle = options.valueRotation;
|
||||
const textSize = measureText('M', UPLOT_AXIS_FONT_SIZE).width; // M is usually the widest character so let's use that as an aproximation.
|
||||
const maxHeightForValues = height / 2;
|
||||
|
||||
return (
|
||||
maxHeightForValues /
|
||||
(Math.sin(((rotationAngle >= 0 ? rotationAngle : rotationAngle * -1) * Math.PI) / 180) * textSize) -
|
||||
3 //Subtract 3 for the "..." added to the end.
|
||||
);
|
||||
} else {
|
||||
return options.valueMaxLength;
|
||||
}
|
||||
}, [height, options.valueRotation, options.valueMaxLength]);
|
||||
|
||||
// Force 'multi' tooltip setting or stacking mode
|
||||
const tooltip = useMemo(() => {
|
||||
if (options.stacking === StackingMode.Normal || options.stacking === StackingMode.Percent) {
|
||||
@ -49,6 +66,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
||||
height={height}
|
||||
{...options}
|
||||
orientation={orientation}
|
||||
valueMaxLength={valueMaxLength}
|
||||
>
|
||||
{(config, alignedFrame) => {
|
||||
return <TooltipPlugin data={alignedFrame} config={config} mode={tooltip.mode} timeZone={timeZone} />;
|
||||
|
@ -12,6 +12,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -37,6 +38,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -144,6 +146,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -169,6 +172,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -276,6 +280,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -301,6 +306,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -408,6 +414,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -433,6 +440,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -540,6 +548,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -565,6 +574,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -672,6 +682,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -697,6 +708,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -804,6 +816,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -829,6 +842,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
@ -936,6 +950,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": -0,
|
||||
"scale": "x",
|
||||
"show": true,
|
||||
"side": 3,
|
||||
@ -961,6 +976,7 @@ Object {
|
||||
"width": 1,
|
||||
},
|
||||
"labelGap": 0,
|
||||
"rotate": undefined,
|
||||
"scale": "m/s",
|
||||
"show": true,
|
||||
"side": 2,
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
import { BarChartPanel } from './BarChartPanel';
|
||||
import { StackingMode, VisibilityMode } from '@grafana/schema';
|
||||
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
|
||||
|
||||
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from 'app/plugins/panel/barchart/types';
|
||||
import { BarChartSuggestionsSupplier } from './suggestions';
|
||||
|
||||
@ -77,6 +76,30 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
||||
},
|
||||
defaultValue: VizOrientation.Auto,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'valueRotation',
|
||||
name: 'Rotate values',
|
||||
defaultValue: 0,
|
||||
settings: {
|
||||
min: -90,
|
||||
max: 90,
|
||||
step: 15,
|
||||
marks: { '-90': '-90°', '-45': '-45°', 0: '0°', 45: '45°', 90: '90°' },
|
||||
included: false,
|
||||
},
|
||||
showIf: (opts) => {
|
||||
return opts.orientation === VizOrientation.Auto || opts.orientation === VizOrientation.Vertical;
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'valueMaxLength',
|
||||
name: 'Value max length',
|
||||
description: 'Axis value labels will be truncated to the length provided',
|
||||
settings: {
|
||||
placeholder: 'Auto',
|
||||
min: 0,
|
||||
},
|
||||
})
|
||||
.addRadio({
|
||||
path: 'showValue',
|
||||
name: 'Show values',
|
||||
|
@ -19,6 +19,8 @@ export interface BarChartOptions extends OptionsWithLegend, OptionsWithTooltip,
|
||||
showValue: VisibilityMode;
|
||||
barWidth: number;
|
||||
groupWidth: number;
|
||||
valueRotation: number;
|
||||
valueMaxLength: number;
|
||||
rawValue: (seriesIdx: number, valueIdx: number) => number;
|
||||
}
|
||||
|
||||
|
@ -87,6 +87,8 @@ describe('BarChart utils', () => {
|
||||
placement: 'bottom',
|
||||
calcs: [],
|
||||
},
|
||||
valueRotation: 0,
|
||||
valueMaxLength: 20,
|
||||
stacking: StackingMode.None,
|
||||
tooltip: {
|
||||
mode: TooltipDisplayMode.None,
|
||||
|
@ -13,6 +13,8 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from './types';
|
||||
import { BarsOptions, getConfig } from './bars';
|
||||
import { FIXED_UNIT, measureText, UPlotConfigBuilder, UPlotConfigPrepFn, UPLOT_AXIS_FONT_SIZE } from '@grafana/ui';
|
||||
import { Padding } from 'uplot';
|
||||
import {
|
||||
AxisPlacement,
|
||||
ScaleDirection,
|
||||
@ -21,7 +23,6 @@ import {
|
||||
StackingMode,
|
||||
VizLegendOptions,
|
||||
} from '@grafana/schema';
|
||||
import { FIXED_UNIT, UPlotConfigBuilder, UPlotConfigPrepFn } from '@grafana/ui';
|
||||
import { collectStackingGroups, orderIdsByCalcs } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||
import { orderBy } from 'lodash';
|
||||
|
||||
@ -55,11 +56,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
text,
|
||||
rawValue,
|
||||
allFrames,
|
||||
valueRotation,
|
||||
valueMaxLength,
|
||||
legend,
|
||||
}) => {
|
||||
const builder = new UPlotConfigBuilder();
|
||||
const defaultValueFormatter = (seriesIdx: number, value: any) =>
|
||||
formattedValueToString(frame.fields[seriesIdx].display!(value));
|
||||
const defaultValueFormatter = (seriesIdx: number, value: any) => {
|
||||
return shortenValue(formattedValueToString(frame.fields[seriesIdx].display!(value)), valueMaxLength);
|
||||
};
|
||||
|
||||
// bar orientation -> x scale orientation & direction
|
||||
const vizOrientation = getBarCharScaleOrientation(orientation);
|
||||
@ -95,6 +99,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
|
||||
builder.setTooltipInterpolator(config.interpolateTooltip);
|
||||
|
||||
if (vizOrientation.xOri === ScaleOrientation.Horizontal && valueRotation !== 0) {
|
||||
builder.setPadding(getRotationPadding(frame, valueRotation, valueMaxLength));
|
||||
}
|
||||
|
||||
builder.setPrepData(config.prepData);
|
||||
|
||||
builder.addScale({
|
||||
@ -115,6 +123,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
grid: { show: false },
|
||||
ticks: false,
|
||||
gap: 15,
|
||||
valueRotation: valueRotation * -1,
|
||||
theme,
|
||||
});
|
||||
|
||||
@ -218,6 +227,51 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
||||
return builder;
|
||||
};
|
||||
|
||||
function shortenValue(value: string, length: number) {
|
||||
if (value.length > length) {
|
||||
return value.substring(0, length).concat('...');
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
function getRotationPadding(frame: DataFrame, rotateLabel: number, valueMaxLength: number): Padding {
|
||||
const values = frame.fields[0].values;
|
||||
const fontSize = UPLOT_AXIS_FONT_SIZE;
|
||||
const displayProcessor = frame.fields[0].display ?? ((v) => v);
|
||||
let maxLength = 0;
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
let size = measureText(
|
||||
shortenValue(formattedValueToString(displayProcessor(values.get(i))), valueMaxLength),
|
||||
fontSize
|
||||
);
|
||||
maxLength = size.width > maxLength ? size.width : maxLength;
|
||||
}
|
||||
|
||||
// Add padding to the right if the labels are rotated in a way that makes the last label extend outside the graph.
|
||||
const paddingRight =
|
||||
rotateLabel > 0
|
||||
? Math.cos((rotateLabel * Math.PI) / 180) *
|
||||
measureText(
|
||||
shortenValue(formattedValueToString(displayProcessor(values.get(values.length - 1))), valueMaxLength),
|
||||
fontSize
|
||||
).width
|
||||
: 0;
|
||||
|
||||
// Add padding to the left if the labels are rotated in a way that makes the first label extend outside the graph.
|
||||
const paddingLeft =
|
||||
rotateLabel < 0
|
||||
? Math.cos((rotateLabel * -1 * Math.PI) / 180) *
|
||||
measureText(shortenValue(formattedValueToString(displayProcessor(values.get(0))), valueMaxLength), fontSize)
|
||||
.width
|
||||
: 0;
|
||||
|
||||
// Add padding to the bottom to avoid clipping the rotated labels.
|
||||
const paddingBottom = Math.sin(((rotateLabel >= 0 ? rotateLabel : rotateLabel * -1) * Math.PI) / 180) * maxLength;
|
||||
|
||||
return [0, paddingRight, paddingBottom, paddingLeft];
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function preparePlotFrame(data: DataFrame[]) {
|
||||
const firstFrame = data[0];
|
||||
|
Loading…
Reference in New Issue
Block a user