mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: update the options config (#28917)
This commit is contained in:
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { GraphNG } from './GraphNG';
|
||||
import { render } from '@testing-library/react';
|
||||
import { ArrayVector, dateTime, FieldConfig, FieldType, MutableDataFrame } from '@grafana/data';
|
||||
import { GraphCustomFieldConfig } from '..';
|
||||
import { GraphFieldConfig, GraphMode } from '../uPlot/config';
|
||||
|
||||
const mockData = () => {
|
||||
const data = new MutableDataFrame();
|
||||
@@ -20,9 +20,9 @@ const mockData = () => {
|
||||
values: new ArrayVector([10, 20, 5]),
|
||||
config: {
|
||||
custom: {
|
||||
line: { show: true },
|
||||
mode: GraphMode.Line,
|
||||
},
|
||||
} as FieldConfig<GraphCustomFieldConfig>,
|
||||
} as FieldConfig<GraphFieldConfig>,
|
||||
});
|
||||
|
||||
const timeRange = {
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
} from '@grafana/data';
|
||||
import { alignAndSortDataFramesByFieldName } from './utils';
|
||||
import { UPlotChart } from '../uPlot/Plot';
|
||||
import { AxisSide, GraphCustomFieldConfig, PlotProps } from '../uPlot/types';
|
||||
import { PlotProps } from '../uPlot/types';
|
||||
import { AxisPlacement, getUPlotSideFromAxis, GraphFieldConfig, GraphMode, PointMode } from '../uPlot/config';
|
||||
import { useTheme } from '../../themes';
|
||||
import { VizLayout } from '../VizLayout/VizLayout';
|
||||
import { LegendDisplayMode, LegendItem, LegendOptions } from '../Legend/Legend';
|
||||
@@ -25,6 +26,12 @@ interface GraphNGProps extends Omit<PlotProps, 'data' | 'config'> {
|
||||
legend?: LegendOptions;
|
||||
}
|
||||
|
||||
const defaultConfig: GraphFieldConfig = {
|
||||
mode: GraphMode.Line,
|
||||
points: PointMode.Auto,
|
||||
axisPlacement: AxisPlacement.Auto,
|
||||
};
|
||||
|
||||
export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
data,
|
||||
children,
|
||||
@@ -68,18 +75,20 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
builder.addAxis({
|
||||
scaleKey: 'x',
|
||||
isTime: true,
|
||||
side: AxisSide.Bottom,
|
||||
side: getUPlotSideFromAxis(AxisPlacement.Bottom),
|
||||
timeZone,
|
||||
theme,
|
||||
});
|
||||
|
||||
let seriesIdx = 0;
|
||||
const legendItems: LegendItem[] = [];
|
||||
let hasLeftAxis = false;
|
||||
let hasYAxis = false;
|
||||
|
||||
for (let i = 0; i < alignedData.fields.length; i++) {
|
||||
const field = alignedData.fields[i];
|
||||
const config = field.config as FieldConfig<GraphCustomFieldConfig>;
|
||||
const customConfig = config.custom;
|
||||
const config = field.config as FieldConfig<GraphFieldConfig>;
|
||||
const customConfig = config.custom || defaultConfig;
|
||||
|
||||
if (i === timeIndex || field.type !== FieldType.number) {
|
||||
continue;
|
||||
@@ -87,18 +96,23 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
|
||||
const fmt = field.display ?? defaultFormatter;
|
||||
const scale = config.unit || '__fixed';
|
||||
const side = customConfig.axisPlacement ?? (hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left);
|
||||
|
||||
if (!builder.hasScale(scale) && customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
||||
if (side === AxisPlacement.Left) {
|
||||
hasLeftAxis = true;
|
||||
}
|
||||
|
||||
if (!builder.hasScale(scale)) {
|
||||
builder.addScale({ scaleKey: scale });
|
||||
builder.addAxis({
|
||||
scaleKey: scale,
|
||||
label: config.custom?.axis?.label,
|
||||
size: config.custom?.axis?.width,
|
||||
side: config.custom?.axis?.side || AxisSide.Left,
|
||||
grid: config.custom?.axis?.grid,
|
||||
label: customConfig.axisLabel,
|
||||
side: getUPlotSideFromAxis(side),
|
||||
grid: !hasYAxis,
|
||||
formatValue: v => formattedValueToString(fmt(v)),
|
||||
theme,
|
||||
});
|
||||
hasYAxis = true;
|
||||
}
|
||||
|
||||
// need to update field state here because we use a transform to merge framesP
|
||||
@@ -109,14 +123,14 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
|
||||
builder.addSeries({
|
||||
scaleKey: scale,
|
||||
line: customConfig?.line?.show,
|
||||
line: (customConfig.mode ?? GraphMode.Line) === GraphMode.Line,
|
||||
lineColor: seriesColor,
|
||||
lineWidth: customConfig?.line?.width,
|
||||
points: customConfig?.points?.show,
|
||||
pointSize: customConfig?.points?.radius,
|
||||
lineWidth: customConfig.lineWidth,
|
||||
points: customConfig.points !== PointMode.Never,
|
||||
pointSize: customConfig.pointRadius,
|
||||
pointColor: seriesColor,
|
||||
fill: customConfig?.fill?.alpha !== undefined,
|
||||
fillOpacity: customConfig?.fill?.alpha,
|
||||
fill: customConfig.fillAlpha !== undefined,
|
||||
fillOpacity: customConfig.fillAlpha,
|
||||
fillColor: seriesColor,
|
||||
});
|
||||
|
||||
@@ -124,7 +138,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
|
||||
legendItems.push({
|
||||
color: seriesColor,
|
||||
label: getFieldDisplayName(field, alignedData),
|
||||
yAxis: customConfig?.axis?.side === 1 ? 3 : 1,
|
||||
yAxis: side === AxisPlacement.Right ? 3 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ const LegacyForms = {
|
||||
export { LegacyForms, LegacyInputStatus };
|
||||
|
||||
// WIP, need renames and exports cleanup
|
||||
export { GraphCustomFieldConfig, AxisSide } from './uPlot/types';
|
||||
export { GraphFieldConfig, graphFieldOptions } from './uPlot/config';
|
||||
export { UPlotChart } from './uPlot/Plot';
|
||||
export * from './uPlot/geometries';
|
||||
export * from './uPlot/plugins';
|
||||
|
||||
85
packages/grafana-ui/src/components/uPlot/config.ts
Normal file
85
packages/grafana-ui/src/components/uPlot/config.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
export enum AxisPlacement {
|
||||
Auto = 'auto', // First axis on the left, the rest on the right
|
||||
Top = 'top',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
Hidden = 'hidden',
|
||||
}
|
||||
|
||||
export function getUPlotSideFromAxis(axis: AxisPlacement) {
|
||||
switch (axis) {
|
||||
case AxisPlacement.Top:
|
||||
return 0;
|
||||
case AxisPlacement.Right:
|
||||
return 1;
|
||||
case AxisPlacement.Bottom:
|
||||
return 2;
|
||||
case AxisPlacement.Left:
|
||||
}
|
||||
return 3; // default everythign to the left
|
||||
}
|
||||
|
||||
export enum PointMode {
|
||||
Auto = 'auto', // will show points when the density is low or line is hidden
|
||||
Always = 'always',
|
||||
Never = 'never',
|
||||
}
|
||||
|
||||
export enum GraphMode {
|
||||
Line = 'line', // default
|
||||
Bar = 'bar', // will also have a gap percent
|
||||
Points = 'points', // Only show points
|
||||
}
|
||||
|
||||
export enum LineInterpolation {
|
||||
Linear = 'linear',
|
||||
Staircase = 'staircase', // https://leeoniya.github.io/uPlot/demos/line-stepped.html
|
||||
Smooth = 'smooth', // https://leeoniya.github.io/uPlot/demos/line-smoothing.html
|
||||
}
|
||||
|
||||
export interface GraphFieldConfig {
|
||||
mode: GraphMode;
|
||||
|
||||
lineMode?: LineInterpolation;
|
||||
lineWidth?: number; // pixels
|
||||
fillAlpha?: number; // 0-1
|
||||
|
||||
points?: PointMode;
|
||||
pointRadius?: number; // pixels
|
||||
symbol?: string; // eventually dot,star, etc
|
||||
|
||||
// Axis is actually unique based on the unit... not each field!
|
||||
axisPlacement?: AxisPlacement;
|
||||
axisLabel?: string;
|
||||
axisWidth?: number; // pixels ideally auto?
|
||||
}
|
||||
|
||||
export const graphFieldOptions = {
|
||||
mode: [
|
||||
{ label: 'Lines', value: GraphMode.Line },
|
||||
{ label: 'Bars', value: GraphMode.Bar },
|
||||
{ label: 'Points', value: GraphMode.Points },
|
||||
] as Array<SelectableValue<GraphMode>>,
|
||||
|
||||
lineMode: [
|
||||
{ label: 'Linear', value: LineInterpolation.Linear },
|
||||
{ label: 'Staircase', value: LineInterpolation.Staircase },
|
||||
{ label: 'Smooth', value: LineInterpolation.Smooth },
|
||||
] as Array<SelectableValue<LineInterpolation>>,
|
||||
|
||||
points: [
|
||||
{ label: 'Auto', value: PointMode.Auto, description: 'Show points when the density is low' },
|
||||
{ label: 'Always', value: PointMode.Always },
|
||||
{ label: 'Never', value: PointMode.Never },
|
||||
] as Array<SelectableValue<PointMode>>,
|
||||
|
||||
axisPlacement: [
|
||||
{ label: 'Auto', value: AxisPlacement.Auto, description: 'First field on the left, everything else on the right' },
|
||||
{ label: 'Left', value: AxisPlacement.Left },
|
||||
{ label: 'Right', value: AxisPlacement.Right },
|
||||
{ label: 'Hidden', value: AxisPlacement.Hidden },
|
||||
] as Array<SelectableValue<AxisPlacement>>,
|
||||
};
|
||||
@@ -1,49 +1,8 @@
|
||||
import React from 'react';
|
||||
import uPlot from 'uplot';
|
||||
import { DataFrame, FieldColor, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { DataFrame, TimeRange, TimeZone } from '@grafana/data';
|
||||
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
|
||||
|
||||
export type NullValuesMode = 'null' | 'connected' | 'asZero';
|
||||
|
||||
export enum AxisSide {
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
Left,
|
||||
}
|
||||
|
||||
interface AxisConfig {
|
||||
label: string;
|
||||
side: AxisSide;
|
||||
grid: boolean;
|
||||
width: number;
|
||||
}
|
||||
|
||||
interface LineConfig {
|
||||
show: boolean;
|
||||
width: number;
|
||||
color: FieldColor;
|
||||
}
|
||||
interface PointConfig {
|
||||
show: boolean;
|
||||
radius: number;
|
||||
}
|
||||
interface BarsConfig {
|
||||
show: boolean;
|
||||
}
|
||||
interface FillConfig {
|
||||
alpha: number;
|
||||
}
|
||||
|
||||
export interface GraphCustomFieldConfig {
|
||||
axis: AxisConfig;
|
||||
line: LineConfig;
|
||||
points: PointConfig;
|
||||
bars: BarsConfig;
|
||||
fill: FillConfig;
|
||||
nullValues: NullValuesMode;
|
||||
}
|
||||
|
||||
export type PlotSeriesConfig = Pick<uPlot.Options, 'series' | 'scales' | 'axes'>;
|
||||
export type PlotPlugin = {
|
||||
id: string;
|
||||
@@ -74,3 +33,10 @@ export abstract class PlotConfigBuilder<P, T> {
|
||||
constructor(protected props: P) {}
|
||||
abstract getConfig(): T;
|
||||
}
|
||||
|
||||
export enum AxisSide {
|
||||
Top, // 0
|
||||
Right, // 1
|
||||
Bottom, // 2
|
||||
Left, // 3
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
|
||||
import { AxisSide, GraphCustomFieldConfig, LegendDisplayMode } from '@grafana/ui';
|
||||
import { LegendDisplayMode } from '@grafana/ui';
|
||||
import {
|
||||
GraphFieldConfig,
|
||||
PointMode,
|
||||
GraphMode,
|
||||
AxisPlacement,
|
||||
graphFieldOptions,
|
||||
} from '@grafana/ui/src/components/uPlot/config';
|
||||
import { GraphPanel } from './GraphPanel';
|
||||
import { Options } from './types';
|
||||
|
||||
export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPanel)
|
||||
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
|
||||
.useFieldConfig({
|
||||
standardOptions: {
|
||||
[FieldConfigProperty.Color]: {
|
||||
@@ -17,14 +24,26 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
},
|
||||
useCustomConfig: builder => {
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'line.show',
|
||||
name: 'Show lines',
|
||||
description: '',
|
||||
defaultValue: true,
|
||||
.addRadio({
|
||||
path: 'mode',
|
||||
name: 'Display',
|
||||
defaultValue: graphFieldOptions.mode[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.mode,
|
||||
},
|
||||
})
|
||||
.addRadio({
|
||||
path: 'lineMode',
|
||||
name: 'Line interpolation',
|
||||
description: 'NOTE: not implemented yet',
|
||||
defaultValue: graphFieldOptions.lineMode[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.lineMode,
|
||||
},
|
||||
showIf: c => !(c.mode === GraphMode.Bar || c.mode === GraphMode.Points),
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'line.width',
|
||||
path: 'lineWidth',
|
||||
name: 'Line width',
|
||||
defaultValue: 1,
|
||||
settings: {
|
||||
@@ -32,18 +51,30 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
max: 10,
|
||||
step: 1,
|
||||
},
|
||||
showIf: c => {
|
||||
return c.line.show;
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'points.show',
|
||||
name: 'Show points',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
showIf: c => !(c.mode === GraphMode.Bar || c.mode === GraphMode.Points),
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'points.radius',
|
||||
path: 'fillAlpha',
|
||||
name: 'Fill area opacity',
|
||||
defaultValue: 0.1,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
},
|
||||
showIf: c => !(c.mode === GraphMode.Bar || c.mode === GraphMode.Points),
|
||||
})
|
||||
.addRadio({
|
||||
path: 'points',
|
||||
name: 'Points',
|
||||
description: 'NOTE: auto vs always are currently the same',
|
||||
defaultValue: graphFieldOptions.points[0].value,
|
||||
settings: {
|
||||
options: graphFieldOptions.points,
|
||||
},
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'pointRadius',
|
||||
name: 'Point radius',
|
||||
defaultValue: 4,
|
||||
settings: {
|
||||
@@ -51,75 +82,38 @@ export const plugin = new PanelPlugin<Options, GraphCustomFieldConfig>(GraphPane
|
||||
max: 10,
|
||||
step: 1,
|
||||
},
|
||||
showIf: c => c.points.show,
|
||||
showIf: c => c.points !== PointMode.Never,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'bars.show',
|
||||
name: 'Show bars',
|
||||
description: '',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addSliderInput({
|
||||
path: 'fill.alpha',
|
||||
name: 'Fill area opacity',
|
||||
defaultValue: 0,
|
||||
.addRadio({
|
||||
path: 'axisPlacement',
|
||||
name: 'Placement',
|
||||
category: ['Axis'],
|
||||
defaultValue: graphFieldOptions.axisPlacement[0].value,
|
||||
settings: {
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
options: graphFieldOptions.axisPlacement,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'axis.label',
|
||||
name: 'Axis Label',
|
||||
path: 'axisLabel',
|
||||
name: 'Label',
|
||||
category: ['Axis'],
|
||||
defaultValue: '',
|
||||
settings: {
|
||||
placeholder: 'Optional text',
|
||||
},
|
||||
showIf: c => c.axisPlacement !== AxisPlacement.Hidden,
|
||||
// no matter what the field type is
|
||||
shouldApply: () => true,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'axis.side',
|
||||
name: 'Y axis side',
|
||||
category: ['Axis'],
|
||||
defaultValue: AxisSide.Left,
|
||||
settings: {
|
||||
options: [
|
||||
{ value: AxisSide.Left, label: 'Left' },
|
||||
{ value: AxisSide.Right, label: 'Right' },
|
||||
],
|
||||
},
|
||||
})
|
||||
.addNumberInput({
|
||||
path: 'axis.width',
|
||||
name: 'Y axis width',
|
||||
path: 'axisWidth',
|
||||
name: 'Width',
|
||||
category: ['Axis'],
|
||||
defaultValue: 60,
|
||||
settings: {
|
||||
placeholder: '60',
|
||||
},
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'axis.grid',
|
||||
name: 'Show axis grid',
|
||||
category: ['Axis'],
|
||||
description: '',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'nullValues',
|
||||
name: 'Display null values as',
|
||||
description: '',
|
||||
defaultValue: 'null',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: 'null', label: 'null' },
|
||||
{ value: 'connected', label: 'Connected' },
|
||||
{ value: 'asZero', label: 'Zero' },
|
||||
],
|
||||
},
|
||||
showIf: c => c.axisPlacement !== AxisPlacement.Hidden,
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user