Commit 8aba480a by Ryan McKinley Committed by GitHub

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

parent 62485898
......@@ -1175,8 +1175,8 @@
]
},
"gridPos": {
"h": 5,
"w": 24,
"h": 7,
"w": 12,
"x": 0,
"y": 13
},
......@@ -1360,13 +1360,161 @@
"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,
"gridPos": {
"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
}
......@@ -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<GraphNGProps> = ({
theme,
});
}
let indexByName: Map<string, number> | 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<GraphNGProps> = ({
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<GraphNGProps> = ({
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<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;
fillOpacity?: number;
fillGradient?: FillGradientMode;
fillBelowTo?: string; // name of the field
}
/**
......
......@@ -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<string, UPlotAxisBuilder> = {};
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<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 = {
// prevent client-side zoom from triggering at the end of a selection
drag: { setScale: false },
......
......@@ -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<Options, 'series' | 'scales' | 'axes' | 'cursor'>;
export type PlotSeriesConfig = Pick<Options, 'series' | 'scales' | 'axes' | 'cursor' | 'bands'>;
export type PlotPlugin = {
id: string;
/** can mutate provided opts as necessary */
......
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);
}}
/>
);
};
import { FieldColorModeId, FieldConfigProperty, FieldType, identityOverrideProcessor } from '@grafana/data';
import {
FieldColorModeId,
FieldConfigProperty,
FieldType,
identityOverrideProcessor,
stringOverrideProcessor,
} from '@grafana/data';
import {
AxisPlacement,
DrawStyle,
......@@ -15,6 +21,7 @@ import { ScaleDistributionEditor } from './ScaleDistributionEditor';
import { LineStyleEditor } from './LineStyleEditor';
import { SetFieldConfigOptionsArgs } from '@grafana/data/src/panel/PanelPlugin';
import { FillGradientMode } from '@grafana/ui/src/components/uPlot/config';
import { FillBellowToEditor } from './FillBelowToEditor';
export const defaultGraphConfig: GraphFieldConfig = {
drawStyle: DrawStyle.Line,
......@@ -86,6 +93,16 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
},
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>({
id: 'lineStyle',
path: 'lineStyle',
......
......@@ -93,6 +93,8 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}
}
let hasFillBelowTo = false;
if (angular.seriesOverrides?.length) {
for (const seriesOverride of angular.seriesOverrides) {
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
});
break;
case 'fillBelowTo':
hasFillBelowTo = true;
rule.properties.push({
id: 'custom.fillBelowTo',
value: v,
});
break;
case 'fillGradient':
if (v) {
rule.properties.push({
......@@ -162,6 +171,12 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
});
}
break;
case 'lines':
rule.properties.push({
id: 'custom.lineWidth',
value: 0, // don't show lines
});
break;
case 'linewidth':
rule.properties.push({
id: 'custom.lineWidth',
......@@ -229,7 +244,9 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
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
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment