From b82797d1b0b5b9a471f4bb85f361e5861da818b5 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 4 Nov 2021 10:59:15 +0100 Subject: [PATCH] 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 --- .../src/field/overrides/processors.ts | 12 ++- packages/grafana-data/src/types/index.ts | 1 + packages/grafana-data/src/types/slider.ts | 1 + .../GraphNG/__snapshots__/utils.test.ts.snap | 2 + .../src/components/OptionsUI/slider.tsx | 2 + .../src/components/Slider/Slider.story.tsx | 36 +++++-- .../src/components/Slider/Slider.tsx | 7 +- .../src/components/Slider/styles.ts | 10 ++ .../grafana-ui/src/components/Slider/types.ts | 26 ++--- packages/grafana-ui/src/components/index.ts | 1 + .../uPlot/config/UPlotAxisBuilder.ts | 100 ++++++++++-------- .../uPlot/config/UPlotConfigBuilder.test.ts | 1 + .../app/plugins/panel/barchart/BarChart.tsx | 18 +++- .../plugins/panel/barchart/BarChartPanel.tsx | 20 +++- .../barchart/__snapshots__/utils.test.ts.snap | 16 +++ public/app/plugins/panel/barchart/module.tsx | 25 ++++- public/app/plugins/panel/barchart/types.ts | 2 + .../app/plugins/panel/barchart/utils.test.ts | 2 + public/app/plugins/panel/barchart/utils.ts | 60 ++++++++++- 19 files changed, 271 insertions(+), 71 deletions(-) create mode 100644 packages/grafana-data/src/types/slider.ts diff --git a/packages/grafana-data/src/field/overrides/processors.ts b/packages/grafana-data/src/field/overrides/processors.ts index 4d12da0b87d..a5707f6749e 100644 --- a/packages/grafana-data/src/field/overrides/processors.ts +++ b/packages/grafana-data/src/field/overrides/processors.ts @@ -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 = (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; } diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 480c79ede20..f115e19e184 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -38,3 +38,4 @@ export * from './geometry'; export { isUnsignedPluginSignature } from './pluginSignature'; export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config'; export * from './alerts'; +export * from './slider'; diff --git a/packages/grafana-data/src/types/slider.ts b/packages/grafana-data/src/types/slider.ts new file mode 100644 index 00000000000..b263b191e8b --- /dev/null +++ b/packages/grafana-data/src/types/slider.ts @@ -0,0 +1 @@ +export type SliderMarks = Record; diff --git a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap index 62c51603c9d..9d949b59de4 100644 --- a/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap +++ b/packages/grafana-ui/src/components/GraphNG/__snapshots__/utils.test.ts.snap @@ -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, diff --git a/packages/grafana-ui/src/components/OptionsUI/slider.tsx b/packages/grafana-ui/src/components/OptionsUI/slider.tsx index f1571a690e3..56a7699a3dd 100644 --- a/packages/grafana-ui/src/components/OptionsUI/slider.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/slider.tsx @@ -16,6 +16,8 @@ export const SliderValueEditor: React.FC diff --git a/packages/grafana-ui/src/components/Slider/Slider.story.tsx b/packages/grafana-ui/src/components/Slider/Slider.story.tsx index 9a99cfd7d4b..4263897134b 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.story.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.story.tsx @@ -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 { isStep: boolean; } @@ -38,10 +49,23 @@ export const Basic: Story = (args) => { ); }; Basic.args = { - min: 0, - max: 100, - value: 10, - isStep: false, - orientation: 'horizontal', - reverse: false, + ...commonArgs, +}; + +export const WithMarks: Story = (args) => { + return ( +
+ +
+ ); +}; +WithMarks.args = { + ...commonArgs, + marks: { 0: '0', 25: '25', 50: '50', 75: '75', 100: '100' }, }; diff --git a/packages/grafana-ui/src/components/Slider/Slider.tsx b/packages/grafana-ui/src/components/Slider/Slider.tsx index 4cc36a31426..0ce6d58e328 100644 --- a/packages/grafana-ui/src/components/Slider/Slider.tsx +++ b/packages/grafana-ui/src/components/Slider/Slider.tsx @@ -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 = ({ step, value, ariaLabelForHandle, + marks, + included, }) => { const isHorizontal = orientation === 'horizontal'; const theme = useTheme2(); const styles = getStyles(theme, isHorizontal); const SliderWithTooltip = SliderComponent; - const [sliderValue, setSliderValue] = useState(value || min); + const [sliderValue, setSliderValue] = useState(value ?? min); const onSliderChange = useCallback( (v: number) => { @@ -93,6 +96,8 @@ export const Slider: FunctionComponent = ({ vertical={!isHorizontal} reverse={reverse} ariaLabelForHandle={ariaLabelForHandle} + marks={marks} + included={included} /> {/* Uses text input so that the number spinners are not shown */} 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; } diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 62085ca9a75..3fc0ae51f63 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index 3732c9eb8a9..3816b96f645 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -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 { @@ -36,6 +37,47 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { 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 { 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 { 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 { }, 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 { } } -/* 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, diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts index 57933c09c0f..056b045570a 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -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, diff --git a/public/app/plugins/panel/barchart/BarChart.tsx b/public/app/plugins/panel/barchart/BarChart.tsx index ee416b7b7e6..6d583a83c2f 100644 --- a/public/app/plugins/panel/barchart/BarChart.tsx +++ b/public/app/plugins/panel/barchart/BarChart.tsx @@ -17,6 +17,8 @@ export interface BarChartProps const propsToDiff: Array = [ 'orientation', 'barWidth', + 'valueRotation', + 'valueMaxLength', 'groupWidth', 'stacking', 'showValue', @@ -52,7 +54,19 @@ export const BarChart: React.FC = (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 = (props) => { barWidth, showValue, groupWidth, + valueRotation, + valueMaxLength, stacking, legend, tooltip, diff --git a/public/app/plugins/panel/barchart/BarChartPanel.tsx b/public/app/plugins/panel/barchart/BarChartPanel.tsx index ec3a8a5b24a..b48548e5d54 100755 --- a/public/app/plugins/panel/barchart/BarChartPanel.tsx +++ b/public/app/plugins/panel/barchart/BarChartPanel.tsx @@ -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 = ({ 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 = ({ data, options, w height={height} {...options} orientation={orientation} + valueMaxLength={valueMaxLength} > {(config, alignedFrame) => { return ; diff --git a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap index 9c1dfd74b93..4eef55bf147 100644 --- a/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap +++ b/public/app/plugins/panel/barchart/__snapshots__/utils.test.ts.snap @@ -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, diff --git a/public/app/plugins/panel/barchart/module.tsx b/public/app/plugins/panel/barchart/module.tsx index b36bd54d19a..74494016c60 100755 --- a/public/app/plugins/panel/barchart/module.tsx +++ b/public/app/plugins/panel/barchart/module.tsx @@ -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(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', diff --git a/public/app/plugins/panel/barchart/types.ts b/public/app/plugins/panel/barchart/types.ts index 4d12c1b960e..be70fe7ae8f 100644 --- a/public/app/plugins/panel/barchart/types.ts +++ b/public/app/plugins/panel/barchart/types.ts @@ -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; } diff --git a/public/app/plugins/panel/barchart/utils.test.ts b/public/app/plugins/panel/barchart/utils.test.ts index 61fcd36a497..45fabeec84c 100644 --- a/public/app/plugins/panel/barchart/utils.test.ts +++ b/public/app/plugins/panel/barchart/utils.test.ts @@ -87,6 +87,8 @@ describe('BarChart utils', () => { placement: 'bottom', calcs: [], }, + valueRotation: 0, + valueMaxLength: 20, stacking: StackingMode.None, tooltip: { mode: TooltipDisplayMode.None, diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index ffd91bf166d..d4008f7a017 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -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 = ({ 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 = ({ 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 = ({ grid: { show: false }, ticks: false, gap: 15, + valueRotation: valueRotation * -1, theme, }); @@ -218,6 +227,51 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ 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];