GraphNG: support fill below to (bands) (#30268)

This commit is contained in:
Ryan McKinley
2021-01-15 11:03:41 -08:00
committed by GitHub
parent 62485898e4
commit 8aba480a21
8 changed files with 303 additions and 26 deletions
+169 -21
View File
@@ -1175,8 +1175,8 @@
] ]
}, },
"gridPos": { "gridPos": {
"h": 5, "h": 7,
"w": 24, "w": 12,
"x": 0, "x": 0,
"y": 13 "y": 13
}, },
@@ -1359,6 +1359,154 @@
"title": "Dashed lines", "title": "Dashed lines",
"type": "timeseries" "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, "collapsed": false,
"datasource": null, "datasource": null,
@@ -1366,7 +1514,7 @@
"h": 1, "h": 1,
"w": 24, "w": 24,
"x": 0, "x": 0,
"y": 18 "y": 20
}, },
"id": 62, "id": 62,
"panels": [], "panels": [],
@@ -1444,7 +1592,7 @@
"h": 6, "h": 6,
"w": 6, "w": 6,
"x": 0, "x": 0,
"y": 19 "y": 21
}, },
"id": 63, "id": 63,
"maxDataPoints": 10, "maxDataPoints": 10,
@@ -1582,7 +1730,7 @@
"h": 6, "h": 6,
"w": 6, "w": 6,
"x": 6, "x": 6,
"y": 19 "y": 21
}, },
"id": 64, "id": 64,
"maxDataPoints": 10, "maxDataPoints": 10,
@@ -1720,7 +1868,7 @@
"h": 6, "h": 6,
"w": 6, "w": 6,
"x": 12, "x": 12,
"y": 19 "y": 21
}, },
"id": 65, "id": 65,
"maxDataPoints": 10, "maxDataPoints": 10,
@@ -1858,7 +2006,7 @@
"h": 6, "h": 6,
"w": 6, "w": 6,
"x": 18, "x": 18,
"y": 19 "y": 21
}, },
"id": 66, "id": 66,
"maxDataPoints": 100, "maxDataPoints": 100,
@@ -1918,7 +2066,7 @@
"h": 1, "h": 1,
"w": 24, "w": 24,
"x": 0, "x": 0,
"y": 25 "y": 27
}, },
"id": 34, "id": 34,
"panels": [], "panels": [],
@@ -2024,7 +2172,7 @@
"h": 8, "h": 8,
"w": 6, "w": 6,
"x": 0, "x": 0,
"y": 26 "y": 28
}, },
"id": 32, "id": 32,
"maxDataPoints": 100, "maxDataPoints": 100,
@@ -2164,7 +2312,7 @@
"h": 8, "h": 8,
"w": 6, "w": 6,
"x": 6, "x": 6,
"y": 26 "y": 28
}, },
"id": 35, "id": 35,
"maxDataPoints": 100, "maxDataPoints": 100,
@@ -2318,7 +2466,7 @@
"h": 8, "h": 8,
"w": 6, "w": 6,
"x": 12, "x": 12,
"y": 26 "y": 28
}, },
"id": 31, "id": 31,
"maxDataPoints": 200, "maxDataPoints": 200,
@@ -2471,7 +2619,7 @@
"h": 8, "h": 8,
"w": 6, "w": 6,
"x": 18, "x": 18,
"y": 26 "y": 28
}, },
"id": 51, "id": 51,
"maxDataPoints": 200, "maxDataPoints": 200,
@@ -2532,7 +2680,7 @@
"h": 1, "h": 1,
"w": 24, "w": 24,
"x": 0, "x": 0,
"y": 34 "y": 36
}, },
"id": 17, "id": 17,
"panels": [], "panels": [],
@@ -2614,7 +2762,7 @@
"h": 7, "h": 7,
"w": 6, "w": 6,
"x": 0, "x": 0,
"y": 35 "y": 37
}, },
"id": 19, "id": 19,
"options": { "options": {
@@ -2722,7 +2870,7 @@
"h": 7, "h": 7,
"w": 6, "w": 6,
"x": 6, "x": 6,
"y": 35 "y": 37
}, },
"id": 20, "id": 20,
"options": { "options": {
@@ -2829,7 +2977,7 @@
"h": 7, "h": 7,
"w": 6, "w": 6,
"x": 12, "x": 12,
"y": 35 "y": 37
}, },
"id": 21, "id": 21,
"options": { "options": {
@@ -2937,7 +3085,7 @@
"h": 7, "h": 7,
"w": 6, "w": 6,
"x": 18, "x": 18,
"y": 35 "y": 37
}, },
"id": 9, "id": 9,
"options": { "options": {
@@ -2980,7 +3128,7 @@
"h": 1, "h": 1,
"w": 24, "w": 24,
"x": 0, "x": 0,
"y": 42 "y": 44
}, },
"id": 45, "id": 45,
"panels": [], "panels": [],
@@ -3061,7 +3209,7 @@
"h": 10, "h": 10,
"w": 12, "w": 12,
"x": 0, "x": 0,
"y": 43 "y": 45
}, },
"id": 46, "id": 46,
"options": { "options": {
@@ -3204,7 +3352,7 @@
"h": 10, "h": 10,
"w": 12, "w": 12,
"x": 12, "x": 12,
"y": 43 "y": 45
}, },
"id": 68, "id": 68,
"options": { "options": {
@@ -3269,5 +3417,5 @@
"timezone": "", "timezone": "",
"title": "Panel Tests - Graph NG", "title": "Panel Tests - Graph NG",
"uid": "TkZXxlNG3", "uid": "TkZXxlNG3",
"version": 27 "version": 34
} }
@@ -24,6 +24,7 @@ import { VizLegend } from '../VizLegend/VizLegend';
import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from '../uPlot/config/UPlotConfigBuilder';
import { useRevision } from '../uPlot/hooks'; import { useRevision } from '../uPlot/hooks';
import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types'; import { GraphNGLegendEvent, GraphNGLegendEventMode } from './types';
import { isNumber } from 'lodash';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
@@ -135,6 +136,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
theme, theme,
}); });
} }
let indexByName: Map<string, number> | undefined = undefined;
for (let i = 0; i < alignedFrame.fields.length; i++) { for (let i = 0; i < alignedFrame.fields.length; i++) {
const field = alignedFrame.fields[i]; const field = alignedFrame.fields[i];
@@ -176,6 +178,24 @@ export const GraphNG: React.FC<GraphNGProps> = ({
const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints; const showPoints = customConfig.drawStyle === DrawStyle.Points ? PointVisibility.Always : customConfig.showPoints;
const dataFrameFieldIndex = getDataFrameFieldIndex ? getDataFrameFieldIndex(i) : undefined; 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({ builder.addSeries({
scaleKey, scaleKey,
drawStyle: customConfig.drawStyle!, drawStyle: customConfig.drawStyle!,
@@ -186,7 +206,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
showPoints, showPoints,
pointSize: customConfig.pointSize, pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor, pointColor: customConfig.pointColor ?? seriesColor,
fillOpacity: customConfig.fillOpacity, fillOpacity,
spanNulls: customConfig.spanNulls || false, spanNulls: customConfig.spanNulls || false,
show: !customConfig.hideFrom?.graph, show: !customConfig.hideFrom?.graph,
fillGradient: customConfig.fillGradient, fillGradient: customConfig.fillGradient,
@@ -291,3 +311,11 @@ const mapMouseEventToMode = (event: React.MouseEvent): GraphNGLegendEventMode =>
} }
return GraphNGLegendEventMode.ToggleSelection; return GraphNGLegendEventMode.ToggleSelection;
}; };
function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
const names = new Map<string, number>();
for (let i = 0; i < frame.fields.length; i++) {
names.set(getFieldDisplayName(frame.fields[i], frame), i);
}
return names;
}
@@ -74,6 +74,7 @@ export interface FillConfig {
fillColor?: string; fillColor?: string;
fillOpacity?: number; fillOpacity?: number;
fillGradient?: FillGradientMode; fillGradient?: FillGradientMode;
fillBelowTo?: string; // name of the field
} }
/** /**
@@ -3,13 +3,14 @@ import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder'; import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder'; import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
import { AxisPlacement } from '../config'; import { AxisPlacement } from '../config';
import { Cursor } from 'uplot'; import { Cursor, Band } from 'uplot';
import { defaultsDeep } from 'lodash'; import { defaultsDeep } from 'lodash';
export class UPlotConfigBuilder { export class UPlotConfigBuilder {
private series: UPlotSeriesBuilder[] = []; private series: UPlotSeriesBuilder[] = [];
private axes: Record<string, UPlotAxisBuilder> = {}; private axes: Record<string, UPlotAxisBuilder> = {};
private scales: UPlotScaleBuilder[] = []; private scales: UPlotScaleBuilder[] = [];
private bands: Band[] = [];
private cursor: Cursor | undefined; private cursor: Cursor | undefined;
private hasLeftAxis = false; private hasLeftAxis = false;
private hasBottomAxis = false; private hasBottomAxis = false;
@@ -71,6 +72,10 @@ export class UPlotConfigBuilder {
this.scales.push(new UPlotScaleBuilder(props)); this.scales.push(new UPlotScaleBuilder(props));
} }
addBand(band: Band) {
this.bands.push(band);
}
getConfig() { getConfig() {
const config: PlotSeriesConfig = { series: [{}] }; const config: PlotSeriesConfig = { series: [{}] };
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map(a => a.getConfig()); config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map(a => a.getConfig());
@@ -81,6 +86,20 @@ export class UPlotConfigBuilder {
config.cursor = this.cursor || {}; config.cursor = this.cursor || {};
// When bands exist, only keep fill when defined
if (config.bands?.length) {
config.bands = this.bands;
const keepFill = new Set<number>();
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 = { const cursorDefaults: Cursor = {
// prevent client-side zoom from triggering at the end of a selection // prevent client-side zoom from triggering at the end of a selection
drag: { setScale: false }, drag: { setScale: false },
@@ -3,7 +3,7 @@ import uPlot, { Options, Series, Hooks } from 'uplot';
import { DataFrame, DataFrameFieldIndex, TimeRange, TimeZone } from '@grafana/data'; import { DataFrame, DataFrameFieldIndex, TimeRange, TimeZone } from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor'>; export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor' | 'bands'>;
export type PlotPlugin = { export type PlotPlugin = {
id: string; id: string;
/** can mutate provided opts as necessary */ /** can mutate provided opts as necessary */
@@ -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<FieldOverrideEditorProps<string, any>> = ({ value, context, onChange }) => {
const names = useMemo(() => {
const names: Array<SelectableValue<string>> = [];
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 (
<Select
options={names}
value={current}
onChange={v => {
onChange(v.value);
}}
/>
);
};
+18 -1
View File
@@ -1,4 +1,10 @@
import { FieldColorModeId, FieldConfigProperty, FieldType, identityOverrideProcessor } from '@grafana/data'; import {
FieldColorModeId,
FieldConfigProperty,
FieldType,
identityOverrideProcessor,
stringOverrideProcessor,
} from '@grafana/data';
import { import {
AxisPlacement, AxisPlacement,
DrawStyle, DrawStyle,
@@ -15,6 +21,7 @@ import { ScaleDistributionEditor } from './ScaleDistributionEditor';
import { LineStyleEditor } from './LineStyleEditor'; import { LineStyleEditor } from './LineStyleEditor';
import { SetFieldConfigOptionsArgs } from '@grafana/data/src/panel/PanelPlugin'; import { SetFieldConfigOptionsArgs } from '@grafana/data/src/panel/PanelPlugin';
import { FillGradientMode } from '@grafana/ui/src/components/uPlot/config'; import { FillGradientMode } from '@grafana/ui/src/components/uPlot/config';
import { FillBellowToEditor } from './FillBelowToEditor';
export const defaultGraphConfig: GraphFieldConfig = { export const defaultGraphConfig: GraphFieldConfig = {
drawStyle: DrawStyle.Line, drawStyle: DrawStyle.Line,
@@ -86,6 +93,16 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
}, },
showIf: c => !!(c.drawStyle !== DrawStyle.Points && c.fillOpacity && c.fillOpacity > 0), showIf: c => !!(c.drawStyle !== DrawStyle.Points && c.fillOpacity && c.fillOpacity > 0),
}) })
.addCustomEditor({
id: 'fillBelowTo',
path: 'fillBelowTo',
name: 'Fill below to',
editor: FillBellowToEditor,
override: FillBellowToEditor,
process: stringOverrideProcessor,
hideFromDefaults: true,
shouldApply: f => true,
})
.addCustomEditor<void, LineStyle>({ .addCustomEditor<void, LineStyle>({
id: 'lineStyle', id: 'lineStyle',
path: 'lineStyle', path: 'lineStyle',
@@ -93,6 +93,8 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
} }
} }
let hasFillBelowTo = false;
if (angular.seriesOverrides?.length) { if (angular.seriesOverrides?.length) {
for (const seriesOverride of angular.seriesOverrides) { for (const seriesOverride of angular.seriesOverrides) {
if (!seriesOverride.alias) { if (!seriesOverride.alias) {
@@ -127,6 +129,13 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
value: v * 10, // was 0-10, new graph is 0 - 100 value: v * 10, // was 0-10, new graph is 0 - 100
}); });
break; break;
case 'fillBelowTo':
hasFillBelowTo = true;
rule.properties.push({
id: 'custom.fillBelowTo',
value: v,
});
break;
case 'fillGradient': case 'fillGradient':
if (v) { if (v) {
rule.properties.push({ rule.properties.push({
@@ -162,6 +171,12 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}); });
} }
break; break;
case 'lines':
rule.properties.push({
id: 'custom.lineWidth',
value: 0, // don't show lines
});
break;
case 'linewidth': case 'linewidth':
rule.properties.push({ rule.properties.push({
id: 'custom.lineWidth', id: 'custom.lineWidth',
@@ -229,7 +244,9 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
graph.pointSize = 2 + angular.pointradius * 2; graph.pointSize = 2 + angular.pointradius * 2;
} }
if (isNumber(angular.fill)) { if (hasFillBelowTo) {
graph.fillOpacity = 35; // bands are hardcoded in flot
} else if (isNumber(angular.fill)) {
graph.fillOpacity = angular.fill * 10; // fill was 0 - 10, new is 0 to 100 graph.fillOpacity = angular.fill * 10; // fill was 0 - 10, new is 0 to 100
} }