From 1be53b4f3b81803761b04adbfb9383b1a03c26cd Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 27 Aug 2021 19:30:42 +0200 Subject: [PATCH] TimeSeries: Add per-axis grid visibility toggle (#38502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow grid lines visibility control to XYChart and TimeSeries * Move grid lines config to field config (axis) * Fix merge * Auto grid mode * Fix ts * Align naming * Update packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts Co-authored-by: Zoltán Bedi * Update packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts Co-authored-by: Zoltán Bedi * remove 'grid' from props diff array since field.config.custom.* is handled by structureRev diffing Co-authored-by: Zoltán Bedi Co-authored-by: Leon Sorokin --- .../grafana-schema/src/schema/graph.gen.ts | 1 + .../src/components/GraphNG/GraphNG.tsx | 1 - .../src/components/TimeSeries/TimeSeries.tsx | 11 ++- .../src/components/TimeSeries/utils.ts | 3 + .../uPlot/config/UPlotAxisBuilder.ts | 6 +- .../uPlot/config/UPlotConfigBuilder.test.ts | 94 ++++++++++++++++++- .../uPlot/config/UPlotConfigBuilder.ts | 46 ++++----- .../grafana-ui/src/options/builder/axis.tsx | 27 ++++-- public/app/plugins/panel/barchart/utils.ts | 3 +- .../app/plugins/panel/state-timeline/utils.ts | 3 +- .../panel/timeseries/TimeSeriesPanel.tsx | 1 + public/app/plugins/panel/timeseries/config.ts | 1 + 12 files changed, 159 insertions(+), 38 deletions(-) diff --git a/packages/grafana-schema/src/schema/graph.gen.ts b/packages/grafana-schema/src/schema/graph.gen.ts index 9c879077e31..89260ac9289 100644 --- a/packages/grafana-schema/src/schema/graph.gen.ts +++ b/packages/grafana-schema/src/schema/graph.gen.ts @@ -144,6 +144,7 @@ export interface AxisConfig { axisWidth?: number; // pixels ideally auto? axisSoftMin?: number; axisSoftMax?: number; + axisGridShow?: boolean; scaleDistribution?: ScaleDistributionConfig; } diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index f2f33232daa..628ef871852 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -12,7 +12,6 @@ import { TimeZone, } from '@grafana/data'; import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; - import { VizLegendOptions } from '@grafana/schema'; import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; import { Subscription } from 'rxjs'; diff --git a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx index 26fdd92a032..67a2671df9b 100644 --- a/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx +++ b/packages/grafana-ui/src/components/TimeSeries/TimeSeries.tsx @@ -19,7 +19,16 @@ export class UnthemedTimeSeries extends React.Component { prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { const { eventBus, sync } = this.context; const { theme, timeZone } = this.props; - return preparePlotConfigBuilder({ frame: alignedFrame, theme, timeZone, getTimeRange, eventBus, sync, allFrames }); + + return preparePlotConfigBuilder({ + frame: alignedFrame, + theme, + timeZone, + getTimeRange, + eventBus, + sync, + allFrames, + }); }; renderLegend = (config: UPlotConfigBuilder) => { diff --git a/packages/grafana-ui/src/components/TimeSeries/utils.ts b/packages/grafana-ui/src/components/TimeSeries/utils.ts index f53b2d7b077..f6568a3ee87 100644 --- a/packages/grafana-ui/src/components/TimeSeries/utils.ts +++ b/packages/grafana-ui/src/components/TimeSeries/utils.ts @@ -79,6 +79,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor placement: AxisPlacement.Bottom, timeZone, theme, + grid: { show: xField.config.custom?.axisGridShow }, }); } else { // Not time! @@ -96,6 +97,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor scaleKey: xScaleKey, placement: AxisPlacement.Bottom, theme, + grid: { show: xField.config.custom?.axisGridShow }, }); } @@ -147,6 +149,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor placement: customConfig.axisPlacement ?? AxisPlacement.Auto, formatValue: (v) => formattedValueToString(fmt(v)), theme, + grid: { show: customConfig.axisGridShow }, }); } diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts index f807324bf77..743a16f4056 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotAxisBuilder.ts @@ -13,7 +13,7 @@ export interface AxisProps { size?: number | null; gap?: number; placement?: AxisPlacement; - grid?: boolean; + grid?: Axis.Grid; ticks?: boolean; formatValue?: (v: any) => string; incrs?: Axis.Incrs; @@ -43,7 +43,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { label, show = true, placement = AxisPlacement.Auto, - grid = true, + grid = { show: true }, ticks = true, gap = 5, formatValue, @@ -74,7 +74,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder { labelGap: 0, grid: { - show: grid, + show: grid.show, stroke: gridColor, width: 1 / devicePixelRatio, }, 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 b578fd8e09e..ea3e80c40fc 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts @@ -329,7 +329,7 @@ describe('UPlotConfigBuilder', () => { placement: AxisPlacement.Bottom, isTime: false, formatValue: () => 'test value', - grid: false, + grid: { show: false }, show: true, theme: darkTheme, values: [], @@ -411,7 +411,6 @@ describe('UPlotConfigBuilder', () => { expect(builder.getAxisPlacement('y1')).toBe(AxisPlacement.Left); expect(builder.getAxisPlacement('y2')).toBe(AxisPlacement.Right); - expect(builder.getConfig().axes![1].grid!.show).toBe(false); }); it('when fillColor is not set fill', () => { @@ -709,4 +708,95 @@ describe('UPlotConfigBuilder', () => { expect(addHookFn).toHaveBeenCalledTimes(1); }); }); + + describe('Grid lines visibility', () => { + it('handles auto behaviour', () => { + const builder = new UPlotConfigBuilder(); + builder.addAxis({ + scaleKey: 'x', + placement: AxisPlacement.Bottom, + theme: darkTheme, + }); + + builder.addAxis({ + scaleKey: 'y1', + theme: darkTheme, + }); + + builder.addAxis({ + scaleKey: 'y2', + theme: darkTheme, + }); + builder.addAxis({ + scaleKey: 'y3', + theme: darkTheme, + }); + + const axesConfig = builder.getConfig().axes!; + + expect(axesConfig[0].grid!.show).toBe(true); + expect(axesConfig[1].grid!.show).toBe(true); + expect(axesConfig[2].grid!.show).toBe(false); + expect(axesConfig[3].grid!.show).toBe(false); + }); + + it('handles auto behaviour with explicite grid settings', () => { + const builder = new UPlotConfigBuilder(); + builder.addAxis({ + scaleKey: 'x', + placement: AxisPlacement.Bottom, + theme: darkTheme, + }); + + builder.addAxis({ + scaleKey: 'y1', + theme: darkTheme, + }); + + builder.addAxis({ + scaleKey: 'y2', + grid: { show: true }, + theme: darkTheme, + }); + builder.addAxis({ + scaleKey: 'y3', + theme: darkTheme, + }); + + const axesConfig = builder.getConfig().axes!; + + expect(axesConfig[0].grid!.show).toBe(true); + expect(axesConfig[1].grid!.show).toBe(true); + expect(axesConfig[2].grid!.show).toBe(true); + expect(axesConfig[3].grid!.show).toBe(false); + }); + + it('handles explicit grid settings', () => { + const builder = new UPlotConfigBuilder(); + builder.addAxis({ + scaleKey: 'x', + grid: { show: false }, + placement: AxisPlacement.Bottom, + theme: darkTheme, + }); + + builder.addAxis({ + scaleKey: 'y1', + grid: { show: false }, + theme: darkTheme, + }); + + builder.addAxis({ + scaleKey: 'y2', + grid: { show: true }, + theme: darkTheme, + }); + + const axesConfig = builder.getConfig().axes!; + + expect(axesConfig[0].grid!.show).toBe(false); + expect(axesConfig[1].grid!.show).toBe(false); + expect(axesConfig[2].grid!.show).toBe(true); + }); + }); }); diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index d475c8ef05e..e31791e4282 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -42,7 +42,6 @@ export class UPlotConfigBuilder { private isStacking = false; private select: uPlot.Select | undefined; private hasLeftAxis = false; - private hasBottomAxis = false; private hooks: Hooks.Arrays = {}; private tz: string | undefined = undefined; private sync = false; @@ -80,7 +79,7 @@ export class UPlotConfigBuilder { addAxis(props: AxisProps) { props.placement = props.placement ?? AxisPlacement.Auto; - + props.grid = props.grid ?? {}; if (this.axes[props.scaleKey]) { this.axes[props.scaleKey].merge(props); return; @@ -91,17 +90,12 @@ export class UPlotConfigBuilder { props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left; } - switch (props.placement) { - case AxisPlacement.Left: - this.hasLeftAxis = true; - break; - case AxisPlacement.Bottom: - this.hasBottomAxis = true; - break; + if (props.placement === AxisPlacement.Left) { + this.hasLeftAxis = true; } if (props.placement === AxisPlacement.Hidden) { - props.show = false; + props.grid.show = false; props.size = 0; } @@ -233,24 +227,30 @@ export class UPlotConfigBuilder { return config; } - private ensureNonOverlappingAxes(axes: UPlotAxisBuilder[]): UPlotAxisBuilder[] { - for (const axis of axes) { - if (axis.props.placement === AxisPlacement.Right && this.hasLeftAxis) { - axis.props.grid = false; - } - if (axis.props.placement === AxisPlacement.Top && this.hasBottomAxis) { - axis.props.grid = false; - } - } - - return axes; - } - private tzDate = (ts: number) => { let date = new Date(ts); return this.tz ? uPlot.tzDate(date, this.tz) : date; }; + + private ensureNonOverlappingAxes(axes: UPlotAxisBuilder[]): UPlotAxisBuilder[] { + const xAxis = axes.find((a) => a.props.scaleKey === 'x'); + const axesWithoutGridSet = axes.filter((a) => a.props.grid?.show === undefined); + const firstValueAxisIdx = axesWithoutGridSet.findIndex( + (a) => a.props.placement === AxisPlacement.Left || (a.props.placement === AxisPlacement.Bottom && a !== xAxis) + ); + + // For all axes with no grid set, set the grid automatically (grid only for first left axis ) + for (let i = 0; i < axesWithoutGridSet.length; i++) { + if (axesWithoutGridSet[i] === xAxis || i === firstValueAxisIdx) { + axesWithoutGridSet[i].props.grid!.show = true; + } else { + axesWithoutGridSet[i].props.grid!.show = false; + } + } + + return axes; + } } /** @alpha */ diff --git a/packages/grafana-ui/src/options/builder/axis.tsx b/packages/grafana-ui/src/options/builder/axis.tsx index ec5d31aa550..2335dfb05bb 100644 --- a/packages/grafana-ui/src/options/builder/axis.tsx +++ b/packages/grafana-ui/src/options/builder/axis.tsx @@ -17,11 +17,12 @@ export function addAxisConfig( defaultConfig: AxisConfig, hideScale?: boolean ) { + const category = ['Axis']; builder .addRadio({ path: 'axisPlacement', name: 'Placement', - category: ['Axis'], + category, defaultValue: graphFieldOptions.axisPlacement[0].value, settings: { options: graphFieldOptions.axisPlacement, @@ -30,7 +31,7 @@ export function addAxisConfig( .addTextInput({ path: 'axisLabel', name: 'Label', - category: ['Axis'], + category, defaultValue: '', settings: { placeholder: 'Optional text', @@ -42,7 +43,7 @@ export function addAxisConfig( .addNumberInput({ path: 'axisWidth', name: 'Width', - category: ['Axis'], + category, settings: { placeholder: 'Auto', }, @@ -52,7 +53,7 @@ export function addAxisConfig( path: 'axisSoftMin', name: 'Soft min', defaultValue: defaultConfig.axisSoftMin, - category: ['Axis'], + category, settings: { placeholder: 'See: Standard options > Min', }, @@ -61,17 +62,31 @@ export function addAxisConfig( path: 'axisSoftMax', name: 'Soft max', defaultValue: defaultConfig.axisSoftMax, - category: ['Axis'], + category, settings: { placeholder: 'See: Standard options > Max', }, + }) + .addRadio({ + path: 'axisGridShow', + name: 'Show grid lines', + category, + defaultValue: undefined, + settings: { + options: [ + { value: undefined, label: 'Auto' }, + { value: true, label: 'On' }, + { value: false, label: 'Off' }, + ], + }, }); + if (!hideScale) { builder.addCustomEditor({ id: 'scaleDistribution', path: 'scaleDistribution', name: 'Scale', - category: ['Axis'], + category, editor: ScaleDistributionEditor, override: ScaleDistributionEditor, defaultValue: { type: ScaleDistribution.Linear }, diff --git a/public/app/plugins/panel/barchart/utils.ts b/public/app/plugins/panel/barchart/utils.ts index 261ef27ecf9..2215e23ba78 100644 --- a/public/app/plugins/panel/barchart/utils.ts +++ b/public/app/plugins/panel/barchart/utils.ts @@ -99,7 +99,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ placement: vizOrientation.xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left, splits: config.xSplits, values: config.xValues, - grid: false, + grid: { show: false }, ticks: false, gap: 15, theme, @@ -174,6 +174,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ placement, formatValue: (v) => formattedValueToString(field.display!(v)), theme, + grid: { show: customConfig.axisGridShow }, }); } diff --git a/public/app/plugins/panel/state-timeline/utils.ts b/public/app/plugins/panel/state-timeline/utils.ts index fd1d9d77e03..2cac419a049 100644 --- a/public/app/plugins/panel/state-timeline/utils.ts +++ b/public/app/plugins/panel/state-timeline/utils.ts @@ -167,6 +167,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ placement: AxisPlacement.Bottom, timeZone, theme, + grid: { show: true }, }); builder.addAxis({ @@ -175,7 +176,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({ placement: AxisPlacement.Left, splits: coreConfig.ySplits, values: coreConfig.yValues, - grid: false, + grid: { show: false }, ticks: false, gap: 16, theme, diff --git a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx index 44590af243f..c873febd83b 100644 --- a/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx +++ b/public/app/plugins/panel/timeseries/TimeSeriesPanel.tsx @@ -40,6 +40,7 @@ export const TimeSeriesPanel: React.FC = ({ } const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); + return (