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 { 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) => {
|
export const identityOverrideProcessor = <T>(value: T, _context: FieldOverrideContext, _settings: any) => {
|
||||||
return value;
|
return value;
|
||||||
@ -39,6 +47,8 @@ export interface SliderFieldConfigSettings {
|
|||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
step?: number;
|
step?: number;
|
||||||
|
included?: boolean;
|
||||||
|
marks?: SliderMarks;
|
||||||
ariaLabelForHandle?: string;
|
ariaLabelForHandle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,3 +38,4 @@ export * from './geometry';
|
|||||||
export { isUnsignedPluginSignature } from './pluginSignature';
|
export { isUnsignedPluginSignature } from './pluginSignature';
|
||||||
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';
|
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';
|
||||||
export * from './alerts';
|
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,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -37,6 +38,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "__fixed",
|
"scale": "__fixed",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
|
@ -16,6 +16,8 @@ export const SliderValueEditor: React.FC<FieldConfigEditorProps<number, SliderFi
|
|||||||
min={settings?.min || 0}
|
min={settings?.min || 0}
|
||||||
max={settings?.max || 100}
|
max={settings?.max || 100}
|
||||||
step={settings?.step}
|
step={settings?.step}
|
||||||
|
marks={settings?.marks}
|
||||||
|
included={settings?.included}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
ariaLabelForHandle={settings?.ariaLabelForHandle}
|
ariaLabelForHandle={settings?.ariaLabelForHandle}
|
||||||
/>
|
/>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Slider } from '@grafana/ui';
|
import { Slider } from '@grafana/ui';
|
||||||
import { SliderProps } from './types';
|
import { SliderProps } from './types';
|
||||||
|
import { Orientation } from '../../types/orientation';
|
||||||
import { Story, Meta } from '@storybook/react';
|
import { Story, Meta } from '@storybook/react';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -20,6 +21,16 @@ export default {
|
|||||||
},
|
},
|
||||||
} as Meta;
|
} 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> {
|
interface StoryProps extends Partial<SliderProps> {
|
||||||
isStep: boolean;
|
isStep: boolean;
|
||||||
}
|
}
|
||||||
@ -38,10 +49,23 @@ export const Basic: Story<StoryProps> = (args) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
Basic.args = {
|
Basic.args = {
|
||||||
min: 0,
|
...commonArgs,
|
||||||
max: 100,
|
};
|
||||||
value: 10,
|
|
||||||
isStep: false,
|
export const WithMarks: Story<StoryProps> = (args) => {
|
||||||
orientation: 'horizontal',
|
return (
|
||||||
reverse: false,
|
<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 React, { useState, useCallback, ChangeEvent, FunctionComponent, FocusEvent } from 'react';
|
||||||
import SliderComponent from 'rc-slider';
|
import SliderComponent from 'rc-slider';
|
||||||
|
|
||||||
import { cx } from '@emotion/css';
|
import { cx } from '@emotion/css';
|
||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
import { useTheme2 } from '../../themes/ThemeContext';
|
import { useTheme2 } from '../../themes/ThemeContext';
|
||||||
@ -20,12 +21,14 @@ export const Slider: FunctionComponent<SliderProps> = ({
|
|||||||
step,
|
step,
|
||||||
value,
|
value,
|
||||||
ariaLabelForHandle,
|
ariaLabelForHandle,
|
||||||
|
marks,
|
||||||
|
included,
|
||||||
}) => {
|
}) => {
|
||||||
const isHorizontal = orientation === 'horizontal';
|
const isHorizontal = orientation === 'horizontal';
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme, isHorizontal);
|
const styles = getStyles(theme, isHorizontal);
|
||||||
const SliderWithTooltip = SliderComponent;
|
const SliderWithTooltip = SliderComponent;
|
||||||
const [sliderValue, setSliderValue] = useState<number>(value || min);
|
const [sliderValue, setSliderValue] = useState<number>(value ?? min);
|
||||||
|
|
||||||
const onSliderChange = useCallback(
|
const onSliderChange = useCallback(
|
||||||
(v: number) => {
|
(v: number) => {
|
||||||
@ -93,6 +96,8 @@ export const Slider: FunctionComponent<SliderProps> = ({
|
|||||||
vertical={!isHorizontal}
|
vertical={!isHorizontal}
|
||||||
reverse={reverse}
|
reverse={reverse}
|
||||||
ariaLabelForHandle={ariaLabelForHandle}
|
ariaLabelForHandle={ariaLabelForHandle}
|
||||||
|
marks={marks}
|
||||||
|
included={included}
|
||||||
/>
|
/>
|
||||||
{/* Uses text input so that the number spinners are not shown */}
|
{/* Uses text input so that the number spinners are not shown */}
|
||||||
<Input
|
<Input
|
||||||
|
@ -23,6 +23,16 @@ export const getStyles = stylesFactory((theme: GrafanaTheme2, isHorizontal: bool
|
|||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-left: 7px; // half the size of the handle to align handle to the left on 0 value
|
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 {
|
.rc-slider-vertical .rc-slider-handle {
|
||||||
margin-top: -10px;
|
margin-top: -10px;
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,30 @@
|
|||||||
|
import { SliderMarks } from '@grafana/data';
|
||||||
import { Orientation } from '../../types/orientation';
|
import { Orientation } from '../../types/orientation';
|
||||||
|
|
||||||
export interface SliderProps {
|
interface CommonSliderProps {
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
orientation?: Orientation;
|
orientation?: Orientation;
|
||||||
/** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
|
/** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
|
||||||
value?: number;
|
|
||||||
reverse?: boolean;
|
reverse?: boolean;
|
||||||
step?: number;
|
step?: number;
|
||||||
tooltipAlwaysVisible?: boolean;
|
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;
|
onChange?: (value: number) => void;
|
||||||
onAfterChange?: (value?: number) => void;
|
onAfterChange?: (value?: number) => void;
|
||||||
|
formatTooltipResult?: (value: number) => number;
|
||||||
ariaLabelForHandle?: string;
|
ariaLabelForHandle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RangeSliderProps {
|
export interface RangeSliderProps extends CommonSliderProps {
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
orientation?: Orientation;
|
|
||||||
/** Set current positions of handle(s). If only 1 value supplied, only 1 handle displayed. */
|
|
||||||
value?: number[];
|
value?: number[];
|
||||||
reverse?: boolean;
|
|
||||||
step?: number;
|
|
||||||
tooltipAlwaysVisible?: boolean;
|
|
||||||
formatTooltipResult?: (value: number) => number | string;
|
|
||||||
onChange?: (value: number[]) => void;
|
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 * from './uPlot/config';
|
||||||
export { ScaleDistribution } from '@grafana/schema';
|
export { ScaleDistribution } from '@grafana/schema';
|
||||||
export { UPlotConfigBuilder } from './uPlot/config/UPlotConfigBuilder';
|
export { UPlotConfigBuilder } from './uPlot/config/UPlotConfigBuilder';
|
||||||
|
export { UPLOT_AXIS_FONT_SIZE } from './uPlot/config/UPlotAxisBuilder';
|
||||||
export { UPlotChart } from './uPlot/Plot';
|
export { UPlotChart } from './uPlot/Plot';
|
||||||
export { PlotLegend } from './uPlot/PlotLegend';
|
export { PlotLegend } from './uPlot/PlotLegend';
|
||||||
export * from './uPlot/geometries';
|
export * from './uPlot/geometries';
|
||||||
|
@ -12,6 +12,7 @@ export interface AxisProps {
|
|||||||
show?: boolean;
|
show?: boolean;
|
||||||
size?: number | null;
|
size?: number | null;
|
||||||
gap?: number;
|
gap?: number;
|
||||||
|
valueRotation?: number;
|
||||||
placement?: AxisPlacement;
|
placement?: AxisPlacement;
|
||||||
grid?: Axis.Grid;
|
grid?: Axis.Grid;
|
||||||
ticks?: boolean;
|
ticks?: boolean;
|
||||||
@ -23,7 +24,7 @@ export interface AxisProps {
|
|||||||
timeZone?: TimeZone;
|
timeZone?: TimeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fontSize = 12;
|
export const UPLOT_AXIS_FONT_SIZE = 12;
|
||||||
const labelPad = 8;
|
const labelPad = 8;
|
||||||
|
|
||||||
export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
||||||
@ -36,6 +37,47 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
|||||||
this.props.placement = props.placement;
|
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 {
|
getConfig(): Axis {
|
||||||
let {
|
let {
|
||||||
@ -52,9 +94,11 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
|||||||
isTime,
|
isTime,
|
||||||
timeZone,
|
timeZone,
|
||||||
theme,
|
theme,
|
||||||
|
valueRotation,
|
||||||
|
size,
|
||||||
} = this.props;
|
} = 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)';
|
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,
|
stroke: theme.colors.text.primary,
|
||||||
side: getUPlotSideFromAxis(placement),
|
side: getUPlotSideFromAxis(placement),
|
||||||
font,
|
font,
|
||||||
size: this.props.size ?? calculateAxisSize,
|
size:
|
||||||
|
size ??
|
||||||
|
((self, values, axisIdx) => {
|
||||||
|
return this.calculateAxisSize(self, values, axisIdx);
|
||||||
|
}),
|
||||||
|
rotate: valueRotation,
|
||||||
gap,
|
gap,
|
||||||
|
|
||||||
labelGap: 0,
|
labelGap: 0,
|
||||||
@ -86,12 +135,14 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
|
|||||||
},
|
},
|
||||||
splits,
|
splits,
|
||||||
values: values,
|
values: values,
|
||||||
space: calculateSpace,
|
space: (self, axisIdx, scaleMin, scaleMax, plotDim) => {
|
||||||
|
return this.calculateSpace(self, axisIdx, scaleMin, scaleMax, plotDim);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (label != null && label.length > 0) {
|
if (label != null && label.length > 0) {
|
||||||
config.label = label;
|
config.label = label;
|
||||||
config.labelSize = fontSize + labelPad;
|
config.labelSize = UPLOT_AXIS_FONT_SIZE + labelPad;
|
||||||
config.labelFont = font;
|
config.labelFont = font;
|
||||||
config.labelGap = labelPad;
|
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 = {
|
const timeUnitSize = {
|
||||||
second: 1000,
|
second: 1000,
|
||||||
minute: 60 * 1000,
|
minute: 60 * 1000,
|
||||||
|
@ -360,6 +360,7 @@ describe('UPlotConfigBuilder', () => {
|
|||||||
"labelFont": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
|
"labelFont": "12px \\"Roboto\\", \\"Helvetica\\", \\"Arial\\", sans-serif",
|
||||||
"labelGap": 8,
|
"labelGap": 8,
|
||||||
"labelSize": 20,
|
"labelSize": 20,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "scale-x",
|
"scale": "scale-x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
|
@ -17,6 +17,8 @@ export interface BarChartProps
|
|||||||
const propsToDiff: Array<string | PropDiffFn> = [
|
const propsToDiff: Array<string | PropDiffFn> = [
|
||||||
'orientation',
|
'orientation',
|
||||||
'barWidth',
|
'barWidth',
|
||||||
|
'valueRotation',
|
||||||
|
'valueMaxLength',
|
||||||
'groupWidth',
|
'groupWidth',
|
||||||
'stacking',
|
'stacking',
|
||||||
'showValue',
|
'showValue',
|
||||||
@ -52,7 +54,19 @@ export const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
|
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({
|
return preparePlotConfigBuilder({
|
||||||
frame: alignedFrame,
|
frame: alignedFrame,
|
||||||
@ -64,6 +78,8 @@ export const BarChart: React.FC<BarChartProps> = (props) => {
|
|||||||
barWidth,
|
barWidth,
|
||||||
showValue,
|
showValue,
|
||||||
groupWidth,
|
groupWidth,
|
||||||
|
valueRotation,
|
||||||
|
valueMaxLength,
|
||||||
stacking,
|
stacking,
|
||||||
legend,
|
legend,
|
||||||
tooltip,
|
tooltip,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { TooltipDisplayMode, StackingMode } from '@grafana/schema';
|
import { TooltipDisplayMode, StackingMode } from '@grafana/schema';
|
||||||
import { PanelProps, TimeRange, VizOrientation } from '@grafana/data';
|
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 { BarChartOptions } from './types';
|
||||||
import { BarChart } from './BarChart';
|
import { BarChart } from './BarChart';
|
||||||
import { prepareGraphableFrames } from './utils';
|
import { prepareGraphableFrames } from './utils';
|
||||||
@ -23,6 +23,23 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
return options.orientation;
|
return options.orientation;
|
||||||
}, [width, height, 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
|
// Force 'multi' tooltip setting or stacking mode
|
||||||
const tooltip = useMemo(() => {
|
const tooltip = useMemo(() => {
|
||||||
if (options.stacking === StackingMode.Normal || options.stacking === StackingMode.Percent) {
|
if (options.stacking === StackingMode.Normal || options.stacking === StackingMode.Percent) {
|
||||||
@ -49,6 +66,7 @@ export const BarChartPanel: React.FunctionComponent<Props> = ({ data, options, w
|
|||||||
height={height}
|
height={height}
|
||||||
{...options}
|
{...options}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
|
valueMaxLength={valueMaxLength}
|
||||||
>
|
>
|
||||||
{(config, alignedFrame) => {
|
{(config, alignedFrame) => {
|
||||||
return <TooltipPlugin data={alignedFrame} config={config} mode={tooltip.mode} timeZone={timeZone} />;
|
return <TooltipPlugin data={alignedFrame} config={config} mode={tooltip.mode} timeZone={timeZone} />;
|
||||||
|
@ -12,6 +12,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -37,6 +38,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -144,6 +146,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -169,6 +172,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -276,6 +280,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -301,6 +306,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -408,6 +414,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -433,6 +440,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -540,6 +548,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -565,6 +574,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -672,6 +682,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -697,6 +708,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -804,6 +816,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -829,6 +842,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
@ -936,6 +950,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": -0,
|
||||||
"scale": "x",
|
"scale": "x",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 3,
|
"side": 3,
|
||||||
@ -961,6 +976,7 @@ Object {
|
|||||||
"width": 1,
|
"width": 1,
|
||||||
},
|
},
|
||||||
"labelGap": 0,
|
"labelGap": 0,
|
||||||
|
"rotate": undefined,
|
||||||
"scale": "m/s",
|
"scale": "m/s",
|
||||||
"show": true,
|
"show": true,
|
||||||
"side": 2,
|
"side": 2,
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
import { BarChartPanel } from './BarChartPanel';
|
import { BarChartPanel } from './BarChartPanel';
|
||||||
import { StackingMode, VisibilityMode } from '@grafana/schema';
|
import { StackingMode, VisibilityMode } from '@grafana/schema';
|
||||||
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
|
import { graphFieldOptions, commonOptionsBuilder } from '@grafana/ui';
|
||||||
|
|
||||||
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from 'app/plugins/panel/barchart/types';
|
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from 'app/plugins/panel/barchart/types';
|
||||||
import { BarChartSuggestionsSupplier } from './suggestions';
|
import { BarChartSuggestionsSupplier } from './suggestions';
|
||||||
|
|
||||||
@ -77,6 +76,30 @@ export const plugin = new PanelPlugin<BarChartOptions, BarChartFieldConfig>(BarC
|
|||||||
},
|
},
|
||||||
defaultValue: VizOrientation.Auto,
|
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({
|
.addRadio({
|
||||||
path: 'showValue',
|
path: 'showValue',
|
||||||
name: 'Show values',
|
name: 'Show values',
|
||||||
|
@ -19,6 +19,8 @@ export interface BarChartOptions extends OptionsWithLegend, OptionsWithTooltip,
|
|||||||
showValue: VisibilityMode;
|
showValue: VisibilityMode;
|
||||||
barWidth: number;
|
barWidth: number;
|
||||||
groupWidth: number;
|
groupWidth: number;
|
||||||
|
valueRotation: number;
|
||||||
|
valueMaxLength: number;
|
||||||
rawValue: (seriesIdx: number, valueIdx: number) => number;
|
rawValue: (seriesIdx: number, valueIdx: number) => number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +87,8 @@ describe('BarChart utils', () => {
|
|||||||
placement: 'bottom',
|
placement: 'bottom',
|
||||||
calcs: [],
|
calcs: [],
|
||||||
},
|
},
|
||||||
|
valueRotation: 0,
|
||||||
|
valueMaxLength: 20,
|
||||||
stacking: StackingMode.None,
|
stacking: StackingMode.None,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
mode: TooltipDisplayMode.None,
|
mode: TooltipDisplayMode.None,
|
||||||
|
@ -13,6 +13,8 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from './types';
|
import { BarChartFieldConfig, BarChartOptions, defaultBarChartFieldConfig } from './types';
|
||||||
import { BarsOptions, getConfig } from './bars';
|
import { BarsOptions, getConfig } from './bars';
|
||||||
|
import { FIXED_UNIT, measureText, UPlotConfigBuilder, UPlotConfigPrepFn, UPLOT_AXIS_FONT_SIZE } from '@grafana/ui';
|
||||||
|
import { Padding } from 'uplot';
|
||||||
import {
|
import {
|
||||||
AxisPlacement,
|
AxisPlacement,
|
||||||
ScaleDirection,
|
ScaleDirection,
|
||||||
@ -21,7 +23,6 @@ import {
|
|||||||
StackingMode,
|
StackingMode,
|
||||||
VizLegendOptions,
|
VizLegendOptions,
|
||||||
} from '@grafana/schema';
|
} from '@grafana/schema';
|
||||||
import { FIXED_UNIT, UPlotConfigBuilder, UPlotConfigPrepFn } from '@grafana/ui';
|
|
||||||
import { collectStackingGroups, orderIdsByCalcs } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
import { collectStackingGroups, orderIdsByCalcs } from '../../../../../packages/grafana-ui/src/components/uPlot/utils';
|
||||||
import { orderBy } from 'lodash';
|
import { orderBy } from 'lodash';
|
||||||
|
|
||||||
@ -55,11 +56,14 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
text,
|
text,
|
||||||
rawValue,
|
rawValue,
|
||||||
allFrames,
|
allFrames,
|
||||||
|
valueRotation,
|
||||||
|
valueMaxLength,
|
||||||
legend,
|
legend,
|
||||||
}) => {
|
}) => {
|
||||||
const builder = new UPlotConfigBuilder();
|
const builder = new UPlotConfigBuilder();
|
||||||
const defaultValueFormatter = (seriesIdx: number, value: any) =>
|
const defaultValueFormatter = (seriesIdx: number, value: any) => {
|
||||||
formattedValueToString(frame.fields[seriesIdx].display!(value));
|
return shortenValue(formattedValueToString(frame.fields[seriesIdx].display!(value)), valueMaxLength);
|
||||||
|
};
|
||||||
|
|
||||||
// bar orientation -> x scale orientation & direction
|
// bar orientation -> x scale orientation & direction
|
||||||
const vizOrientation = getBarCharScaleOrientation(orientation);
|
const vizOrientation = getBarCharScaleOrientation(orientation);
|
||||||
@ -95,6 +99,10 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
|
|
||||||
builder.setTooltipInterpolator(config.interpolateTooltip);
|
builder.setTooltipInterpolator(config.interpolateTooltip);
|
||||||
|
|
||||||
|
if (vizOrientation.xOri === ScaleOrientation.Horizontal && valueRotation !== 0) {
|
||||||
|
builder.setPadding(getRotationPadding(frame, valueRotation, valueMaxLength));
|
||||||
|
}
|
||||||
|
|
||||||
builder.setPrepData(config.prepData);
|
builder.setPrepData(config.prepData);
|
||||||
|
|
||||||
builder.addScale({
|
builder.addScale({
|
||||||
@ -115,6 +123,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
grid: { show: false },
|
grid: { show: false },
|
||||||
ticks: false,
|
ticks: false,
|
||||||
gap: 15,
|
gap: 15,
|
||||||
|
valueRotation: valueRotation * -1,
|
||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -218,6 +227,51 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
|
|||||||
return builder;
|
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 */
|
/** @internal */
|
||||||
export function preparePlotFrame(data: DataFrame[]) {
|
export function preparePlotFrame(data: DataFrame[]) {
|
||||||
const firstFrame = data[0];
|
const firstFrame = data[0];
|
||||||
|
Loading…
Reference in New Issue
Block a user