diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.test.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.test.tsx index 9aa8ff38450..bd88405639a 100644 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.test.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.test.tsx @@ -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, + } as FieldConfig, }); const timeRange = { diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 7f631d54103..ad159dfcdea 100644 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -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 { legend?: LegendOptions; } +const defaultConfig: GraphFieldConfig = { + mode: GraphMode.Line, + points: PointMode.Auto, + axisPlacement: AxisPlacement.Auto, +}; + export const GraphNG: React.FC = ({ data, children, @@ -68,18 +75,20 @@ export const GraphNG: React.FC = ({ 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; - const customConfig = config.custom; + const config = field.config as FieldConfig; + const customConfig = config.custom || defaultConfig; if (i === timeIndex || field.type !== FieldType.number) { continue; @@ -87,18 +96,23 @@ export const GraphNG: React.FC = ({ 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 = ({ 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 = ({ legendItems.push({ color: seriesColor, label: getFieldDisplayName(field, alignedData), - yAxis: customConfig?.axis?.side === 1 ? 3 : 1, + yAxis: side === AxisPlacement.Right ? 3 : 1, }); } diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 68415ddbb24..44a15dd7f1a 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts new file mode 100644 index 00000000000..977fff04e07 --- /dev/null +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -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>, + + lineMode: [ + { label: 'Linear', value: LineInterpolation.Linear }, + { label: 'Staircase', value: LineInterpolation.Staircase }, + { label: 'Smooth', value: LineInterpolation.Smooth }, + ] as Array>, + + 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>, + + 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>, +}; diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts index caa99c6a04c..3ffc16ed49c 100644 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -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; export type PlotPlugin = { id: string; @@ -74,3 +33,10 @@ export abstract class PlotConfigBuilder { constructor(protected props: P) {} abstract getConfig(): T; } + +export enum AxisSide { + Top, // 0 + Right, // 1 + Bottom, // 2 + Left, // 3 +} diff --git a/public/app/plugins/panel/graph3/module.tsx b/public/app/plugins/panel/graph3/module.tsx index c026899846c..85093b110f6 100644 --- a/public/app/plugins/panel/graph3/module.tsx +++ b/public/app/plugins/panel/graph3/module.tsx @@ -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(GraphPanel) +export const plugin = new PanelPlugin(GraphPanel) .useFieldConfig({ standardOptions: { [FieldConfigProperty.Color]: { @@ -17,14 +24,26 @@ export const plugin = new PanelPlugin(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(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(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, }); }, })