TimeSeries: Support coloring series and line by thresholds or gradient color scales (#35910)

* TimeSeries: Adds support for color scheme series and line colors

* Updates

* fixed device issue

* Evaluate series color in legend

* two fixes

* It works with points

* Added test dashboard

* Minor fix

* Reset color mode to palette when switching to panel that supports by series mode

* Add support for relative thresholds

* Updated snapshots
This commit is contained in:
Torkel Ödegaard 2021-07-07 12:40:40 +02:00 committed by GitHub
parent 333d520528
commit a241f03167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 739 additions and 74 deletions

View File

@ -0,0 +1,593 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": "-- Grafana --",
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"links": [],
"panels": [
{
"datasource": "-- Dashboard --",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 5,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 11,
"x": 0,
"y": 0
},
"id": 9,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color line by discrete tresholds",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 84,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 0,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 13,
"x": 11,
"y": 0
},
"id": 4,
"interval": "80s",
"maxDataPoints": 42,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"max": 40,
"min": 0,
"noise": 1,
"refId": "A",
"scenarioId": "random_walk",
"spread": 20,
"startValue": 1
}
],
"timeFrom": null,
"title": "Color bars by discrete thresholds",
"type": "timeseries"
},
{
"datasource": "-- Dashboard --",
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 11,
"x": 0,
"y": 7
},
"id": 6,
"maxDataPoints": 50,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color line by color scale",
"type": "timeseries"
},
{
"datasource": "-- Dashboard --",
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "bars",
"fillOpacity": 64,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 13,
"x": 11,
"y": 7
},
"id": 10,
"maxDataPoints": 45,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color bars by color scale",
"type": "timeseries"
},
{
"datasource": "-- Dashboard --",
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 64,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 6,
"w": 24,
"x": 0,
"y": 15
},
"id": 7,
"maxDataPoints": 50,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"panelId": 4,
"refId": "A"
}
],
"title": "Color line by color scale",
"type": "timeseries"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "points",
"fillOpacity": 10,
"gradientMode": "scheme",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "smooth",
"lineWidth": 3,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "never",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"mappings": [],
"max": 50,
"min": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "blue",
"value": null
},
{
"color": "green",
"value": 0
},
{
"color": "orange",
"value": 20
},
{
"color": "red",
"value": 30
}
]
},
"unit": "degree"
},
"overrides": []
},
"gridPos": {
"h": 13,
"w": 24,
"x": 0,
"y": 21
},
"id": 8,
"maxDataPoints": 250,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"tooltip": {
"mode": "single"
}
},
"targets": [
{
"max": 45,
"min": 20,
"noise": 0,
"refId": "A",
"scenarioId": "random_walk",
"spread": 12,
"startValue": 40
},
{
"hide": false,
"max": 20,
"min": 1,
"noise": 0,
"refId": "B",
"scenarioId": "random_walk",
"spread": 10
}
],
"title": "Color line by color scale",
"type": "timeseries"
}
],
"refresh": false,
"schemaVersion": 30,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Panel Tests - Graph NG - By value color schemes",
"uid": "aBXrJ0R7z",
"version": 20
}

View File

@ -126,11 +126,11 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#808080", "fill": "#ff0000",
"filter": [Function], "filter": [Function],
"show": true, "show": true,
"size": undefined, "size": undefined,
"stroke": "#808080", "stroke": "#ff0000",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "__fixed", "scale": "__fixed",
@ -147,11 +147,11 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#808080", "fill": "#ff0000",
"filter": [Function], "filter": [Function],
"show": true, "show": true,
"size": undefined, "size": undefined,
"stroke": "#808080", "stroke": "#ff0000",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "__fixed", "scale": "__fixed",
@ -168,11 +168,11 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#808080", "fill": "#ff0000",
"filter": [Function], "filter": [Function],
"show": true, "show": true,
"size": undefined, "size": undefined,
"stroke": "#808080", "stroke": "#ff0000",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "__fixed", "scale": "__fixed",
@ -189,11 +189,11 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#808080", "fill": "#ff0000",
"filter": [Function], "filter": [Function],
"show": true, "show": true,
"size": undefined, "size": undefined,
"stroke": "#808080", "stroke": "#ff0000",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "__fixed", "scale": "__fixed",
@ -210,11 +210,11 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#808080", "fill": "#ff0000",
"filter": [Function], "filter": [Function],
"show": true, "show": true,
"size": undefined, "size": undefined,
"stroke": "#808080", "stroke": "#ff0000",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "__fixed", "scale": "__fixed",

View File

@ -164,7 +164,6 @@ export class Sparkline extends PureComponent<SparklineProps, State> {
lineInterpolation: customConfig.lineInterpolation, lineInterpolation: customConfig.lineInterpolation,
showPoints: pointsMode, showPoints: pointsMode,
pointSize: customConfig.pointSize, pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor,
fillOpacity: customConfig.fillOpacity, fillOpacity: customConfig.fillOpacity,
fillColor: customConfig.fillColor ?? seriesColor, fillColor: customConfig.fillColor ?? seriesColor,
}); });

View File

@ -226,7 +226,6 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<{ sync: DashboardCursor
barWidthFactor: customConfig.barWidthFactor, barWidthFactor: customConfig.barWidthFactor,
barMaxWidth: customConfig.barMaxWidth, barMaxWidth: customConfig.barMaxWidth,
pointSize: customConfig.pointSize, pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor,
spanNulls: customConfig.spanNulls || false, spanNulls: customConfig.spanNulls || false,
show: !customConfig.hideFrom?.viz, show: !customConfig.hideFrom?.viz,
gradientMode: customConfig.gradientMode, gradientMode: customConfig.gradientMode,

View File

@ -1,11 +1,19 @@
import React from 'react'; import React from 'react';
import { DataFrame, DisplayValue, fieldReducers, getFieldDisplayName, reduceField } from '@grafana/data'; import {
DataFrame,
DisplayValue,
fieldReducers,
getFieldDisplayName,
getFieldSeriesColor,
reduceField,
} from '@grafana/data';
import { UPlotConfigBuilder } from './config/UPlotConfigBuilder'; import { UPlotConfigBuilder } from './config/UPlotConfigBuilder';
import { VizLegendItem } from '../VizLegend/types'; import { VizLegendItem } from '../VizLegend/types';
import { VizLegendOptions } from '../VizLegend/models.gen'; import { VizLegendOptions } from '../VizLegend/models.gen';
import { AxisPlacement } from './config'; import { AxisPlacement } from './config';
import { VizLayout, VizLayoutLegendProps } from '../VizLayout/VizLayout'; import { VizLayout, VizLayoutLegendProps } from '../VizLayout/VizLayout';
import { VizLegend } from '../VizLegend/VizLegend'; import { VizLegend } from '../VizLegend/VizLegend';
import { useTheme2 } from '../../themes';
const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1)); const defaultFormatter = (v: any) => (v == null ? '-' : v.toFixed(1));
@ -22,6 +30,7 @@ export const PlotLegend: React.FC<PlotLegendProps> = ({
displayMode, displayMode,
...vizLayoutLegendProps ...vizLayoutLegendProps
}) => { }) => {
const theme = useTheme2();
const legendItems = config const legendItems = config
.getSeries() .getSeries()
.map<VizLegendItem | undefined>((s) => { .map<VizLegendItem | undefined>((s) => {
@ -40,10 +49,13 @@ export const PlotLegend: React.FC<PlotLegendProps> = ({
} }
const label = getFieldDisplayName(field, data[fieldIndex.frameIndex]!, data); const label = getFieldDisplayName(field, data[fieldIndex.frameIndex]!, data);
const scaleColor = getFieldSeriesColor(field, theme);
const seriesColor = scaleColor.color;
return { return {
disabled: !seriesConfig.show ?? false, disabled: !seriesConfig.show ?? false,
fieldIndex, fieldIndex,
color: seriesConfig.lineColor!, color: seriesColor,
label, label,
yAxis: axisPlacement === AxisPlacement.Left ? 1 : 2, yAxis: axisPlacement === AxisPlacement.Left ? 1 : 2,
getDisplayValues: () => { getDisplayValues: () => {

View File

@ -137,7 +137,6 @@ export enum GraphGradientMode {
export interface PointsConfig { export interface PointsConfig {
showPoints?: PointVisibility; showPoints?: PointVisibility;
pointSize?: number; pointSize?: number;
pointColor?: string;
pointSymbol?: string; // eventually dot,star, etc pointSymbol?: string; // eventually dot,star, etc
} }
@ -274,9 +273,13 @@ export const graphFieldOptions = {
fillGradient: [ fillGradient: [
{ label: 'None', value: GraphGradientMode.None }, { label: 'None', value: GraphGradientMode.None },
{ label: 'Opacity', value: GraphGradientMode.Opacity }, { label: 'Opacity', value: GraphGradientMode.Opacity, description: 'Enable fill opacity gradient' },
{ label: 'Hue', value: GraphGradientMode.Hue }, { label: 'Hue', value: GraphGradientMode.Hue, description: 'Small color hue gradient' },
// { label: 'Color scheme', value: GraphGradientMode.Scheme }, {
label: 'Scheme',
value: GraphGradientMode.Scheme,
description: 'Use color scheme to define gradient',
},
] as Array<SelectableValue<GraphGradientMode>>, ] as Array<SelectableValue<GraphGradientMode>>,
stacking: [ stacking: [

View File

@ -464,7 +464,6 @@ describe('UPlotConfigBuilder', () => {
gradientMode: GraphGradientMode.Opacity, gradientMode: GraphGradientMode.Opacity,
showPoints: PointVisibility.Auto, showPoints: PointVisibility.Auto,
pointSize: 5, pointSize: 5,
pointColor: '#00ff00',
lineColor: '#0000ff', lineColor: '#0000ff',
lineWidth: 1, lineWidth: 1,
spanNulls: false, spanNulls: false,
@ -497,10 +496,10 @@ describe('UPlotConfigBuilder', () => {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": "#00ff00", "fill": "#0000ff",
"filter": undefined, "filter": undefined,
"size": 5, "size": 5,
"stroke": "#00ff00", "stroke": "#0000ff",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "scale-x", "scale": "scale-x",
@ -607,10 +606,10 @@ describe('UPlotConfigBuilder', () => {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#0000ff",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#0000ff",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "scale-x", "scale": "scale-x",
@ -623,10 +622,10 @@ describe('UPlotConfigBuilder', () => {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#00ff00",
"filter": undefined, "filter": undefined,
"size": 5, "size": 5,
"stroke": undefined, "stroke": "#00ff00",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "scale-x", "scale": "scale-x",
@ -639,10 +638,10 @@ describe('UPlotConfigBuilder', () => {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#ff0000",
"filter": undefined, "filter": undefined,
"size": 5, "size": 5,
"stroke": undefined, "stroke": "#ff0000",
}, },
"pxAlign": undefined, "pxAlign": undefined,
"scale": "scale-x", "scale": "scale-x",

View File

@ -46,7 +46,6 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
barWidthFactor, barWidthFactor,
barMaxWidth, barMaxWidth,
showPoints, showPoints,
pointColor,
pointSize, pointSize,
scaleKey, scaleKey,
pxAlign, pxAlign,
@ -55,15 +54,16 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
} = this.props; } = this.props;
let lineConfig: Partial<Series> = {}; let lineConfig: Partial<Series> = {};
const lineColor = this.getLineColor();
if (pathBuilder != null) { if (pathBuilder != null) {
lineConfig.paths = pathBuilder; lineConfig.paths = pathBuilder;
lineConfig.stroke = this.getLineColor(); lineConfig.stroke = lineColor;
lineConfig.width = lineWidth; lineConfig.width = lineWidth;
} else if (drawStyle === DrawStyle.Points) { } else if (drawStyle === DrawStyle.Points) {
lineConfig.paths = () => null; lineConfig.paths = () => null;
} else if (drawStyle != null) { } else if (drawStyle != null) {
lineConfig.stroke = this.getLineColor(); lineConfig.stroke = lineColor;
lineConfig.width = lineWidth; lineConfig.width = lineWidth;
if (lineStyle && lineStyle.fill !== 'solid') { if (lineStyle && lineStyle.fill !== 'solid') {
if (lineStyle.fill === 'dot') { if (lineStyle.fill === 'dot') {
@ -85,8 +85,8 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
const pointsConfig: Partial<Series> = { const pointsConfig: Partial<Series> = {
points: { points: {
stroke: pointColor, stroke: lineColor,
fill: pointColor, fill: lineColor,
size: pointSize, size: pointSize,
filter: pointsFilter, filter: pointsFilter,
}, },
@ -123,10 +123,10 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
} }
private getLineColor(): Series.Stroke { private getLineColor(): Series.Stroke {
const { lineColor, gradientMode, colorMode, thresholds } = this.props; const { lineColor, gradientMode, colorMode, thresholds, theme } = this.props;
if (gradientMode === GraphGradientMode.Scheme) { if (gradientMode === GraphGradientMode.Scheme) {
return getScaleGradientFn(1, colorMode, thresholds); return getScaleGradientFn(1, theme, colorMode, thresholds);
} }
return lineColor ?? FALLBACK_COLOR; return lineColor ?? FALLBACK_COLOR;
@ -148,7 +148,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
case GraphGradientMode.Hue: case GraphGradientMode.Hue:
return getHueGradientFn((fillColor ?? lineColor)!, opacityPercent, theme); return getHueGradientFn((fillColor ?? lineColor)!, opacityPercent, theme);
case GraphGradientMode.Scheme: case GraphGradientMode.Scheme:
return getScaleGradientFn(opacityPercent, colorMode, thresholds); return getScaleGradientFn(opacityPercent, theme, colorMode, thresholds);
default: default:
if (opacityPercent > 0) { if (opacityPercent > 0) {
return tinycolor(lineColor).setAlpha(opacityPercent).toString(); return tinycolor(lineColor).setAlpha(opacityPercent).toString();

View File

@ -1,4 +1,4 @@
import { FieldColorMode, GrafanaTheme2, ThresholdsConfig } from '@grafana/data'; import { FieldColorMode, FieldColorModeId, GrafanaTheme2, ThresholdsConfig, ThresholdsMode } from '@grafana/data';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import uPlot from 'uplot'; import uPlot from 'uplot';
import { getCanvasContext } from '../../../utils/measureText'; import { getCanvasContext } from '../../../utils/measureText';
@ -45,9 +45,10 @@ export function getHueGradientFn(
*/ */
export function getScaleGradientFn( export function getScaleGradientFn(
opacity: number, opacity: number,
theme: GrafanaTheme2,
colorMode?: FieldColorMode, colorMode?: FieldColorMode,
thresholds?: ThresholdsConfig thresholds?: ThresholdsConfig
): (self: uPlot, seriesIdx: number) => CanvasGradient { ): (self: uPlot, seriesIdx: number) => CanvasGradient | string {
if (!colorMode) { if (!colorMode) {
throw Error('Missing colorMode required for color scheme gradients'); throw Error('Missing colorMode required for color scheme gradients');
} }
@ -57,35 +58,67 @@ export function getScaleGradientFn(
} }
return (plot: uPlot, seriesIdx: number) => { return (plot: uPlot, seriesIdx: number) => {
// A uplot bug (I think) where this is called before there is bbox
// Color used for cursor highlight, not sure what to do here as this is called before we have bbox
// and only once so same color is used for all points
if (plot.bbox.top == null) {
return theme.colors.text.primary;
}
const ctx = getCanvasContext(); const ctx = getCanvasContext();
const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height); const gradient = ctx.createLinearGradient(0, plot.bbox.top, 0, plot.bbox.top + plot.bbox.height);
const canvasHeight = plot.bbox.height;
const series = plot.series[seriesIdx]; const series = plot.series[seriesIdx];
const scale = plot.scales[series.scale!]; const scale = plot.scales[series.scale!];
const range = plot.bbox.height; const scaleMin = scale.min ?? 0;
const scaleMax = scale.max ?? 100;
console.log('scale', scale); const scaleRange = scaleMax - scaleMin;
console.log('series.min', series.min);
console.log('series.max', series.max);
const getColorWithAlpha = (color: string) => {
return 'rgb(255, 0, 0)';
};
const addColorStop = (value: number, color: string) => { const addColorStop = (value: number, color: string) => {
const pos = plot.valToPos(value, series.scale!); const pos = plot.valToPos(value, series.scale!, true);
const percent = pos / range; // when above range we get negative values here
console.log(`addColorStop(value = ${value}, xPos=${pos})`); if (pos < 0) {
gradient.addColorStop(Math.min(percent, 1), getColorWithAlpha(color)); return;
}
const percent = Math.max(pos / canvasHeight, 0);
const realColor = tinycolor(theme.visualization.getColorByName(color)).setAlpha(opacity).toString();
const colorStopPos = Math.min(percent, 1);
gradient.addColorStop(colorStopPos, realColor);
}; };
for (let idx = 0; idx < thresholds.steps.length; idx++) { if (colorMode.id === FieldColorModeId.Thresholds) {
const step = thresholds.steps[idx]; for (let idx = 0; idx < thresholds.steps.length; idx++) {
const value = step.value === -Infinity ? 0 : step.value; const step = thresholds.steps[idx];
addColorStop(value, step.color);
// to make the gradient discrete if (thresholds.mode === ThresholdsMode.Absolute) {
if (thresholds.steps.length > idx + 1) { const value = step.value === -Infinity ? scaleMin : step.value;
addColorStop(thresholds.steps[idx + 1].value - 0.0000001, step.color); addColorStop(value, step.color);
if (thresholds.steps.length > idx + 1) {
// to make the gradient discrete
addColorStop(thresholds.steps[idx + 1].value - 0.00000001, step.color);
}
} else {
const percent = step.value === -Infinity ? 0 : step.value;
const realValue = (percent / 100) * scaleRange;
addColorStop(realValue, step.color);
// to make the gradient discrete
if (thresholds.steps.length > idx + 1) {
// to make the gradient discrete
const nextValue = (thresholds.steps[idx + 1].value / 100) * scaleRange - 0.0000001;
addColorStop(nextValue, step.color);
}
}
}
} else if (colorMode.getColors) {
const colors = colorMode.getColors(theme);
const stepValue = (scaleMax - scaleMin) / colors.length;
for (let idx = 0; idx < colors.length; idx++) {
addColorStop(scaleMin + stepValue * idx, colors[idx]);
} }
} }

View File

@ -229,6 +229,24 @@ describe('getPanelOptionsWithDefaults', () => {
}); });
expect(result.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.Thresholds); expect(result.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.Thresholds);
}); });
it('should change to classic mode when panel supports bySeries', () => {
const result = runScenario({
defaults: {
color: { mode: FieldColorModeId.Thresholds },
},
standardOptions: {
[FieldConfigProperty.Color]: {
settings: {
byValueSupport: true,
bySeriesSupport: true,
},
},
},
isAfterPluginChange: true,
});
expect(result.fieldConfig.defaults.color!.mode).toBe(FieldColorModeId.PaletteClassic);
});
}); });
describe('when changing panel type to one that does not use standard field config', () => { describe('when changing panel type to one that does not use standard field config', () => {

View File

@ -166,6 +166,15 @@ function adaptFieldColorMode(
return fieldConfig; return fieldConfig;
} }
} }
// If panel support bySeries then we should default to that when switching to this panel as that is most likely
// what users will expect. Example scenario a user who has a graph panel (time series) and switches to Gauge and
// then back to time series we want the graph panel color mode to reset to classic palette and not preserve the
// Gauge prefered thresholds mode.
if (colorSettings.bySeriesSupport && mode?.isByValue) {
fieldConfig.defaults.color = { mode: FieldColorModeId.PaletteClassic };
return fieldConfig;
}
} }
return fieldConfig; return fieldConfig;
} }

View File

@ -108,10 +108,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -233,10 +233,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -358,10 +358,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -483,10 +483,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -608,10 +608,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -733,10 +733,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -858,10 +858,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",
@ -983,10 +983,10 @@ Object {
"fill": [Function], "fill": [Function],
"paths": [Function], "paths": [Function],
"points": Object { "points": Object {
"fill": undefined, "fill": "#808080",
"filter": undefined, "filter": undefined,
"size": undefined, "size": undefined,
"stroke": undefined, "stroke": "#808080",
}, },
"pxAlign": false, "pxAlign": false,
"scale": "m/s", "scale": "m/s",

View File

@ -44,7 +44,7 @@ export function getGraphFieldConfig(cfg: GraphFieldConfig): SetFieldConfigOption
standardOptions: { standardOptions: {
[FieldConfigProperty.Color]: { [FieldConfigProperty.Color]: {
settings: { settings: {
byValueSupport: false, byValueSupport: true,
bySeriesSupport: true, bySeriesSupport: true,
preferThresholdsMode: false, preferThresholdsMode: false,
}, },