TimeSeries: Add per-axis grid visibility toggle (#38502)

* 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 <zoltan.bedi@gmail.com>

* Update packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.test.ts

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>

* remove 'grid' from props diff array since field.config.custom.* is handled by structureRev diffing

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
This commit is contained in:
Dominik Prokop 2021-08-27 19:30:42 +02:00 committed by GitHub
parent b5e4a0a39a
commit 1be53b4f3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 159 additions and 38 deletions

View File

@ -144,6 +144,7 @@ export interface AxisConfig {
axisWidth?: number; // pixels ideally auto? axisWidth?: number; // pixels ideally auto?
axisSoftMin?: number; axisSoftMin?: number;
axisSoftMax?: number; axisSoftMax?: number;
axisGridShow?: boolean;
scaleDistribution?: ScaleDistributionConfig; scaleDistribution?: ScaleDistributionConfig;
} }

View File

@ -12,7 +12,6 @@ import {
TimeZone, TimeZone,
} from '@grafana/data'; } from '@grafana/data';
import { preparePlotFrame as defaultPreparePlotFrame } from './utils'; import { preparePlotFrame as defaultPreparePlotFrame } from './utils';
import { VizLegendOptions } from '@grafana/schema'; import { VizLegendOptions } from '@grafana/schema';
import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext'; import { PanelContext, PanelContextRoot } from '../PanelChrome/PanelContext';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';

View File

@ -19,7 +19,16 @@ export class UnthemedTimeSeries extends React.Component<TimeSeriesProps> {
prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => { prepConfig = (alignedFrame: DataFrame, allFrames: DataFrame[], getTimeRange: () => TimeRange) => {
const { eventBus, sync } = this.context; const { eventBus, sync } = this.context;
const { theme, timeZone } = this.props; 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) => { renderLegend = (config: UPlotConfigBuilder) => {

View File

@ -79,6 +79,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
timeZone, timeZone,
theme, theme,
grid: { show: xField.config.custom?.axisGridShow },
}); });
} else { } else {
// Not time! // Not time!
@ -96,6 +97,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
scaleKey: xScaleKey, scaleKey: xScaleKey,
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
theme, theme,
grid: { show: xField.config.custom?.axisGridShow },
}); });
} }
@ -147,6 +149,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
placement: customConfig.axisPlacement ?? AxisPlacement.Auto, placement: customConfig.axisPlacement ?? AxisPlacement.Auto,
formatValue: (v) => formattedValueToString(fmt(v)), formatValue: (v) => formattedValueToString(fmt(v)),
theme, theme,
grid: { show: customConfig.axisGridShow },
}); });
} }

View File

@ -13,7 +13,7 @@ export interface AxisProps {
size?: number | null; size?: number | null;
gap?: number; gap?: number;
placement?: AxisPlacement; placement?: AxisPlacement;
grid?: boolean; grid?: Axis.Grid;
ticks?: boolean; ticks?: boolean;
formatValue?: (v: any) => string; formatValue?: (v: any) => string;
incrs?: Axis.Incrs; incrs?: Axis.Incrs;
@ -43,7 +43,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
label, label,
show = true, show = true,
placement = AxisPlacement.Auto, placement = AxisPlacement.Auto,
grid = true, grid = { show: true },
ticks = true, ticks = true,
gap = 5, gap = 5,
formatValue, formatValue,
@ -74,7 +74,7 @@ export class UPlotAxisBuilder extends PlotConfigBuilder<AxisProps, Axis> {
labelGap: 0, labelGap: 0,
grid: { grid: {
show: grid, show: grid.show,
stroke: gridColor, stroke: gridColor,
width: 1 / devicePixelRatio, width: 1 / devicePixelRatio,
}, },

View File

@ -329,7 +329,7 @@ describe('UPlotConfigBuilder', () => {
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
isTime: false, isTime: false,
formatValue: () => 'test value', formatValue: () => 'test value',
grid: false, grid: { show: false },
show: true, show: true,
theme: darkTheme, theme: darkTheme,
values: [], values: [],
@ -411,7 +411,6 @@ describe('UPlotConfigBuilder', () => {
expect(builder.getAxisPlacement('y1')).toBe(AxisPlacement.Left); expect(builder.getAxisPlacement('y1')).toBe(AxisPlacement.Left);
expect(builder.getAxisPlacement('y2')).toBe(AxisPlacement.Right); expect(builder.getAxisPlacement('y2')).toBe(AxisPlacement.Right);
expect(builder.getConfig().axes![1].grid!.show).toBe(false);
}); });
it('when fillColor is not set fill', () => { it('when fillColor is not set fill', () => {
@ -709,4 +708,95 @@ describe('UPlotConfigBuilder', () => {
expect(addHookFn).toHaveBeenCalledTimes(1); 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);
});
});
}); });

View File

@ -42,7 +42,6 @@ export class UPlotConfigBuilder {
private isStacking = false; private isStacking = false;
private select: uPlot.Select | undefined; private select: uPlot.Select | undefined;
private hasLeftAxis = false; private hasLeftAxis = false;
private hasBottomAxis = false;
private hooks: Hooks.Arrays = {}; private hooks: Hooks.Arrays = {};
private tz: string | undefined = undefined; private tz: string | undefined = undefined;
private sync = false; private sync = false;
@ -80,7 +79,7 @@ export class UPlotConfigBuilder {
addAxis(props: AxisProps) { addAxis(props: AxisProps) {
props.placement = props.placement ?? AxisPlacement.Auto; props.placement = props.placement ?? AxisPlacement.Auto;
props.grid = props.grid ?? {};
if (this.axes[props.scaleKey]) { if (this.axes[props.scaleKey]) {
this.axes[props.scaleKey].merge(props); this.axes[props.scaleKey].merge(props);
return; return;
@ -91,17 +90,12 @@ export class UPlotConfigBuilder {
props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left; props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left;
} }
switch (props.placement) { if (props.placement === AxisPlacement.Left) {
case AxisPlacement.Left: this.hasLeftAxis = true;
this.hasLeftAxis = true;
break;
case AxisPlacement.Bottom:
this.hasBottomAxis = true;
break;
} }
if (props.placement === AxisPlacement.Hidden) { if (props.placement === AxisPlacement.Hidden) {
props.show = false; props.grid.show = false;
props.size = 0; props.size = 0;
} }
@ -233,24 +227,30 @@ export class UPlotConfigBuilder {
return config; 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) => { private tzDate = (ts: number) => {
let date = new Date(ts); let date = new Date(ts);
return this.tz ? uPlot.tzDate(date, this.tz) : date; 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 */ /** @alpha */

View File

@ -17,11 +17,12 @@ export function addAxisConfig(
defaultConfig: AxisConfig, defaultConfig: AxisConfig,
hideScale?: boolean hideScale?: boolean
) { ) {
const category = ['Axis'];
builder builder
.addRadio({ .addRadio({
path: 'axisPlacement', path: 'axisPlacement',
name: 'Placement', name: 'Placement',
category: ['Axis'], category,
defaultValue: graphFieldOptions.axisPlacement[0].value, defaultValue: graphFieldOptions.axisPlacement[0].value,
settings: { settings: {
options: graphFieldOptions.axisPlacement, options: graphFieldOptions.axisPlacement,
@ -30,7 +31,7 @@ export function addAxisConfig(
.addTextInput({ .addTextInput({
path: 'axisLabel', path: 'axisLabel',
name: 'Label', name: 'Label',
category: ['Axis'], category,
defaultValue: '', defaultValue: '',
settings: { settings: {
placeholder: 'Optional text', placeholder: 'Optional text',
@ -42,7 +43,7 @@ export function addAxisConfig(
.addNumberInput({ .addNumberInput({
path: 'axisWidth', path: 'axisWidth',
name: 'Width', name: 'Width',
category: ['Axis'], category,
settings: { settings: {
placeholder: 'Auto', placeholder: 'Auto',
}, },
@ -52,7 +53,7 @@ export function addAxisConfig(
path: 'axisSoftMin', path: 'axisSoftMin',
name: 'Soft min', name: 'Soft min',
defaultValue: defaultConfig.axisSoftMin, defaultValue: defaultConfig.axisSoftMin,
category: ['Axis'], category,
settings: { settings: {
placeholder: 'See: Standard options > Min', placeholder: 'See: Standard options > Min',
}, },
@ -61,17 +62,31 @@ export function addAxisConfig(
path: 'axisSoftMax', path: 'axisSoftMax',
name: 'Soft max', name: 'Soft max',
defaultValue: defaultConfig.axisSoftMax, defaultValue: defaultConfig.axisSoftMax,
category: ['Axis'], category,
settings: { settings: {
placeholder: 'See: Standard options > Max', 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) { if (!hideScale) {
builder.addCustomEditor<void, ScaleDistributionConfig>({ builder.addCustomEditor<void, ScaleDistributionConfig>({
id: 'scaleDistribution', id: 'scaleDistribution',
path: 'scaleDistribution', path: 'scaleDistribution',
name: 'Scale', name: 'Scale',
category: ['Axis'], category,
editor: ScaleDistributionEditor, editor: ScaleDistributionEditor,
override: ScaleDistributionEditor, override: ScaleDistributionEditor,
defaultValue: { type: ScaleDistribution.Linear }, defaultValue: { type: ScaleDistribution.Linear },

View File

@ -99,7 +99,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
placement: vizOrientation.xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left, placement: vizOrientation.xOri === 0 ? AxisPlacement.Bottom : AxisPlacement.Left,
splits: config.xSplits, splits: config.xSplits,
values: config.xValues, values: config.xValues,
grid: false, grid: { show: false },
ticks: false, ticks: false,
gap: 15, gap: 15,
theme, theme,
@ -174,6 +174,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<BarChartOptions> = ({
placement, placement,
formatValue: (v) => formattedValueToString(field.display!(v)), formatValue: (v) => formattedValueToString(field.display!(v)),
theme, theme,
grid: { show: customConfig.axisGridShow },
}); });
} }

View File

@ -167,6 +167,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
placement: AxisPlacement.Bottom, placement: AxisPlacement.Bottom,
timeZone, timeZone,
theme, theme,
grid: { show: true },
}); });
builder.addAxis({ builder.addAxis({
@ -175,7 +176,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
placement: AxisPlacement.Left, placement: AxisPlacement.Left,
splits: coreConfig.ySplits, splits: coreConfig.ySplits,
values: coreConfig.yValues, values: coreConfig.yValues,
grid: false, grid: { show: false },
ticks: false, ticks: false,
gap: 16, gap: 16,
theme, theme,

View File

@ -40,6 +40,7 @@ export const TimeSeriesPanel: React.FC<TimeSeriesPanelProps> = ({
} }
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
return ( return (
<TimeSeries <TimeSeries
frames={frames} frames={frames}

View File

@ -36,6 +36,7 @@ export const defaultGraphConfig: GraphFieldConfig = {
mode: StackingMode.None, mode: StackingMode.None,
group: 'A', group: 'A',
}, },
axisGridShow: true,
}; };
const categoryStyles = ['Graph styles']; const categoryStyles = ['Graph styles'];