mirror of
https://github.com/grafana/grafana.git
synced 2025-02-13 00:55:47 -06:00
GraphNG: support fill below to (bands) (#30268)
This commit is contained in:
parent
62485898e4
commit
8aba480a21
@ -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
|
||||
}
|
||||
|
@ -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 */
|
||||
|
47
public/app/plugins/panel/timeseries/FillBelowToEditor.tsx
Normal file
47
public/app/plugins/panel/timeseries/FillBelowToEditor.tsx
Normal file
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,4 +1,10 @@
|
||||
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
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user