diff --git a/devenv/dev-dashboards/panel-graph/graph-ng.json b/devenv/dev-dashboards/panel-graph/graph-ng.json index cceea9626aa..43176b3d224 100644 --- a/devenv/dev-dashboards/panel-graph/graph-ng.json +++ b/devenv/dev-dashboards/panel-graph/graph-ng.json @@ -1175,8 +1175,8 @@ ] }, "gridPos": { - "h": 5, - "w": 24, + "h": 7, + "w": 12, "x": 0, "y": 13 }, @@ -1359,6 +1359,154 @@ "title": "Dashed lines", "type": "timeseries" }, + { + "datasource": null, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "drawStyle": "line", + "fillGradient": "hue", + "fillOpacity": 25, + "hideFrom": { + "graph": false, + "legend": false, + "tooltip": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 6, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true + }, + "mappings": [], + "nullValueMode": "null", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max" + }, + "properties": [ + { + "id": "custom.fillBelowTo", + "value": "Min" + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "color", + "value": { + "fixedColor": "red", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Min" + }, + "properties": [ + { + "id": "custom.lineWidth", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Value" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "blue", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 13 + }, + "id": 70, + "interval": null, + "maxDataPoints": 100, + "options": { + "graph": {}, + "legend": { + "displayMode": "list", + "placement": "right" + }, + "tooltipOptions": { + "mode": "single" + } + }, + "pluginVersion": "7.4.0-pre", + "targets": [ + { + "alias": "", + "csvWave": { + "timeStep": 60, + "valuesCSV": "0,0,2,2,1,1" + }, + "lines": 10, + "points": [], + "pulseWave": { + "offCount": 3, + "offValue": 1, + "onCount": 3, + "onValue": 2, + "timeStep": 60 + }, + "refId": "A", + "scenarioId": "random_walk_table", + "stream": { + "bands": 1, + "noise": 2.2, + "speed": 250, + "spread": 3.5, + "type": "signal" + }, + "stringInput": "" + } + ], + "title": "Fill below to", + "type": "timeseries" + }, { "collapsed": false, "datasource": null, @@ -1366,7 +1514,7 @@ "h": 1, "w": 24, "x": 0, - "y": 18 + "y": 20 }, "id": 62, "panels": [], @@ -1444,7 +1592,7 @@ "h": 6, "w": 6, "x": 0, - "y": 19 + "y": 21 }, "id": 63, "maxDataPoints": 10, @@ -1582,7 +1730,7 @@ "h": 6, "w": 6, "x": 6, - "y": 19 + "y": 21 }, "id": 64, "maxDataPoints": 10, @@ -1720,7 +1868,7 @@ "h": 6, "w": 6, "x": 12, - "y": 19 + "y": 21 }, "id": 65, "maxDataPoints": 10, @@ -1858,7 +2006,7 @@ "h": 6, "w": 6, "x": 18, - "y": 19 + "y": 21 }, "id": 66, "maxDataPoints": 100, @@ -1918,7 +2066,7 @@ "h": 1, "w": 24, "x": 0, - "y": 25 + "y": 27 }, "id": 34, "panels": [], @@ -2024,7 +2172,7 @@ "h": 8, "w": 6, "x": 0, - "y": 26 + "y": 28 }, "id": 32, "maxDataPoints": 100, @@ -2164,7 +2312,7 @@ "h": 8, "w": 6, "x": 6, - "y": 26 + "y": 28 }, "id": 35, "maxDataPoints": 100, @@ -2318,7 +2466,7 @@ "h": 8, "w": 6, "x": 12, - "y": 26 + "y": 28 }, "id": 31, "maxDataPoints": 200, @@ -2471,7 +2619,7 @@ "h": 8, "w": 6, "x": 18, - "y": 26 + "y": 28 }, "id": 51, "maxDataPoints": 200, @@ -2532,7 +2680,7 @@ "h": 1, "w": 24, "x": 0, - "y": 34 + "y": 36 }, "id": 17, "panels": [], @@ -2614,7 +2762,7 @@ "h": 7, "w": 6, "x": 0, - "y": 35 + "y": 37 }, "id": 19, "options": { @@ -2722,7 +2870,7 @@ "h": 7, "w": 6, "x": 6, - "y": 35 + "y": 37 }, "id": 20, "options": { @@ -2829,7 +2977,7 @@ "h": 7, "w": 6, "x": 12, - "y": 35 + "y": 37 }, "id": 21, "options": { @@ -2937,7 +3085,7 @@ "h": 7, "w": 6, "x": 18, - "y": 35 + "y": 37 }, "id": 9, "options": { @@ -2980,7 +3128,7 @@ "h": 1, "w": 24, "x": 0, - "y": 42 + "y": 44 }, "id": 45, "panels": [], @@ -3061,7 +3209,7 @@ "h": 10, "w": 12, "x": 0, - "y": 43 + "y": 45 }, "id": 46, "options": { @@ -3204,7 +3352,7 @@ "h": 10, "w": 12, "x": 12, - "y": 43 + "y": 45 }, "id": 68, "options": { @@ -3269,5 +3417,5 @@ "timezone": "", "title": "Panel Tests - Graph NG", "uid": "TkZXxlNG3", - "version": 27 + "version": 34 } diff --git a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx index 0faa74f01e4..9933ce80645 100755 --- a/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx +++ b/packages/grafana-ui/src/components/GraphNG/GraphNG.tsx @@ -24,6 +24,7 @@ import { VizLegend } from '../VizLegend/VizLegend'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { useRevision } from '../uPlot/hooks'; import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types'; +import { isNumber } from 'lodash'; const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); @@ -135,6 +136,7 @@ export const GraphNG: React.FC = ({ theme, }); } + let indexByName: Map | undefined = undefined; for (let i = 0; i < alignedFrame.fields.length; i++) { const field = alignedFrame.fields[i]; @@ -176,6 +178,24 @@ export const GraphNG: React.FC = ({ const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints; const dataFrameFieldIndex = getDataFrameFieldIndex ? getDataFrameFieldIndex(i) : undefined; + let { fillOpacity } = customConfig; + if (customConfig.fillBelowTo) { + if (!indexByName) { + indexByName = getNamesToFieldIndex(alignedFrame); + } + const t = indexByName.get(getFieldDisplayName(field, alignedFrame)); + const b = indexByName.get(customConfig.fillBelowTo); + if (isNumber(b) && isNumber(t)) { + builder.addBand({ + series: [t, b], + fill: null as any, // using null will have the band use fill options from `t` + }); + } + if (!fillOpacity) { + fillOpacity = 35; // default from flot + } + } + builder.addSeries({ scaleKey, drawStyle: customConfig.drawStyle!, @@ -186,7 +206,7 @@ export const GraphNG: React.FC = ({ showPoints, pointSize: customConfig.pointSize, pointColor: customConfig.pointColor ?? seriesColor, - fillOpacity: customConfig.fillOpacity, + fillOpacity, spanNulls: customConfig.spanNulls || false, show: !customConfig.hideFrom?.graph, fillGradient: customConfig.fillGradient, @@ -291,3 +311,11 @@ const mapMouseEventToMode = (event: React.MouseEvent): GraphNGLegendEventMode => } return GraphNGLegendEventMode.ToggleSelection; }; + +function getNamesToFieldIndex(frame: DataFrame): Map { + const names = new Map(); + for (let i = 0; i < frame.fields.length; i++) { + names.set(getFieldDisplayName(frame.fields[i], frame), i); + } + return names; +} diff --git a/packages/grafana-ui/src/components/uPlot/config.ts b/packages/grafana-ui/src/components/uPlot/config.ts index 2b5f39a9240..4a5e9456a25 100644 --- a/packages/grafana-ui/src/components/uPlot/config.ts +++ b/packages/grafana-ui/src/components/uPlot/config.ts @@ -74,6 +74,7 @@ export interface FillConfig { fillColor?: string; fillOpacity?: number; fillGradient?: FillGradientMode; + fillBelowTo?: string; // name of the field } /** diff --git a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts index a3fb4ccf8b9..4fade5d9920 100644 --- a/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts +++ b/packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts @@ -3,13 +3,14 @@ import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder'; import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder'; import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder'; import { AxisPlacement } from '../config'; -import { Cursor } from 'uplot'; +import { Cursor, Band } from 'uplot'; import { defaultsDeep } from 'lodash'; export class UPlotConfigBuilder { private series: UPlotSeriesBuilder[] = []; private axes: Record = {}; private scales: UPlotScaleBuilder[] = []; + private bands: Band[] = []; private cursor: Cursor | undefined; private hasLeftAxis = false; private hasBottomAxis = false; @@ -71,6 +72,10 @@ export class UPlotConfigBuilder { this.scales.push(new UPlotScaleBuilder(props)); } + addBand(band: Band) { + this.bands.push(band); + } + getConfig() { const config: PlotSeriesConfig = { series: [{}] }; config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map(a => a.getConfig()); @@ -81,6 +86,20 @@ export class UPlotConfigBuilder { config.cursor = this.cursor || {}; + // When bands exist, only keep fill when defined + if (config.bands?.length) { + config.bands = this.bands; + const keepFill = new Set(); + for (const b of config.bands) { + keepFill.add(b.series[0]); + } + for (let i = 1; i < config.series.length; i++) { + if (!keepFill.has(i)) { + config.series[i].fill = undefined; + } + } + } + const cursorDefaults: Cursor = { // prevent client-side zoom from triggering at the end of a selection drag: { setScale: false }, diff --git a/packages/grafana-ui/src/components/uPlot/types.ts b/packages/grafana-ui/src/components/uPlot/types.ts index bbb81724a12..b7fb7d8a82c 100755 --- a/packages/grafana-ui/src/components/uPlot/types.ts +++ b/packages/grafana-ui/src/components/uPlot/types.ts @@ -3,7 +3,7 @@ import uPlot, { Options, Series, Hooks } from 'uplot'; import { DataFrame, DataFrameFieldIndex, TimeRange, TimeZone } from '@grafana/data'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; -export type PlotSeriesConfig = Pick; +export type PlotSeriesConfig = Pick; export type PlotPlugin = { id: string; /** can mutate provided opts as necessary */ diff --git a/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx b/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx new file mode 100644 index 00000000000..4e0a3e19dfc --- /dev/null +++ b/public/app/plugins/panel/timeseries/FillBelowToEditor.tsx @@ -0,0 +1,47 @@ +import React, { useMemo } from 'react'; +import { FieldOverrideEditorProps, FieldType, getFieldDisplayName, SelectableValue } from '@grafana/data'; +import { Select } from '@grafana/ui'; + +export const FillBellowToEditor: React.FC> = ({ value, context, onChange }) => { + const names = useMemo(() => { + const names: Array> = []; + if (context.data.length) { + for (const frame of context.data) { + for (const field of frame.fields) { + if (field.type === FieldType.number) { + const label = getFieldDisplayName(field, frame, context.data); + names.push({ + label, + value: label, + }); + } + } + } + } + return names; + }, [context]); + + const current = useMemo(() => { + const found = names.find(v => v.value === value); + if (found) { + return found; + } + if (value) { + return { + label: value, + value, + }; + } + return undefined; + }, names); + + return ( +