Commit 71e93e52 by Ryan McKinley Committed by GitHub

GraphNG: support dashes (#30070)

parent dba4942e
...@@ -2093,10 +2093,7 @@ ...@@ -2093,10 +2093,7 @@
"y": 34 "y": 34
}, },
"id": 11, "id": 11,
"panels": [], "panels": [
"title": "Show gaps & Connected",
"type": "row"
},
{ {
"datasource": "gdev-testdata", "datasource": "gdev-testdata",
"fieldConfig": { "fieldConfig": {
...@@ -2557,6 +2554,315 @@ ...@@ -2557,6 +2554,315 @@
"title": "Null values & show connected", "title": "Null values & show connected",
"transformations": [], "transformations": [],
"type": "graph3" "type": "graph3"
}
],
"title": "Show gaps & Connected",
"type": "row"
},
{
"collapsed": false,
"datasource": null,
"gridPos": {
"h": 1,
"w": 24,
"x": 0,
"y": 35
},
"id": 62,
"panels": [],
"title": "Line Styles",
"type": "row"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"align": null,
"axis": {
"grid": true,
"label": "",
"side": 3,
"width": 60
},
"axisLabel": "",
"axisPlacement": "auto",
"bars": {
"show": false
},
"drawStyle": "line",
"fill": {
"alpha": 0
},
"fillGradient": {
"label": "None"
},
"fillOpacity": 0,
"hideFrom": {
"graph": false,
"legend": false,
"tooltip": false
},
"line": {
"color": {
"mode": "palette-classic"
},
"show": true,
"width": 1
},
"lineInterpolation": "linear",
"lineStyle": {
"dash": [10, 10],
"fill": "dash"
},
"lineWidth": 2,
"nullValues": "null",
"pointSize": 5,
"points": {
"radius": 5,
"show": false
},
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "B-series"
},
"properties": [
{
"id": "custom.lineStyle",
"value": {
"dash": [10, 20],
"fill": "dash"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "C-series"
},
"properties": [
{
"id": "custom.lineStyle",
"value": {
"dash": [10, 30],
"fill": "dash"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "D-series"
},
"properties": [
{
"id": "custom.lineStyle",
"value": {
"dash": [30, 3, 3],
"fill": "dash"
}
}
]
},
{
"matcher": {
"id": "byName",
"options": "E-series"
},
"properties": [
{
"id": "custom.lineStyle"
}
]
}
]
},
"gridPos": {
"h": 7,
"w": 6,
"x": 0,
"y": 36
},
"id": 60,
"options": {
"graph": {
"realTimeUpdates": false
},
"legend": {
"asTable": false,
"displayMode": "hidden",
"isVisible": true,
"placement": "bottom"
},
"tooltipOptions": {
"mode": "single"
}
},
"pluginVersion": "7.2.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": "csv_metric_values",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": "10,10"
},
{
"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": "B",
"scenarioId": "csv_metric_values",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": "9,9"
},
{
"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": "C",
"scenarioId": "csv_metric_values",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": "8,8"
},
{
"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": "D",
"scenarioId": "csv_metric_values",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": "7,7"
},
{
"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": "E",
"scenarioId": "csv_metric_values",
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": "6,6"
}
],
"timeFrom": null,
"timeShift": null,
"title": "Dashed lines",
"type": "graph3"
}, },
{ {
"collapsed": false, "collapsed": false,
...@@ -2714,5 +3020,5 @@ ...@@ -2714,5 +3020,5 @@
"timezone": "", "timezone": "",
"title": "Panel Tests - Graph NG", "title": "Panel Tests - Graph NG",
"uid": "TkZXxlNG3", "uid": "TkZXxlNG3",
"version": 2 "version": 1
} }
...@@ -175,6 +175,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({ ...@@ -175,6 +175,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
lineColor: customConfig.lineColor ?? seriesColor, lineColor: customConfig.lineColor ?? seriesColor,
lineWidth: customConfig.lineWidth, lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation, lineInterpolation: customConfig.lineInterpolation,
lineStyle: customConfig.lineStyle,
showPoints, showPoints,
pointSize: customConfig.pointSize, pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor, pointColor: customConfig.pointColor ?? seriesColor,
......
...@@ -51,11 +51,19 @@ export enum ScaleDistribution { ...@@ -51,11 +51,19 @@ export enum ScaleDistribution {
/** /**
* @alpha * @alpha
*/ */
export interface LineStyle {
fill?: 'solid' | 'dash' | 'dot' | 'square'; // cap = 'butt' | 'round' | 'square'
dash?: number[];
}
/**
* @alpha
*/
export interface LineConfig { export interface LineConfig {
lineColor?: string; lineColor?: string;
lineWidth?: number; lineWidth?: number;
lineInterpolation?: LineInterpolation; lineInterpolation?: LineInterpolation;
lineDash?: number[]; lineStyle?: LineStyle;
spanNulls?: boolean; spanNulls?: boolean;
} }
......
...@@ -25,6 +25,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -25,6 +25,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
lineInterpolation, lineInterpolation,
lineColor, lineColor,
lineWidth, lineWidth,
lineStyle,
showPoints, showPoints,
pointColor, pointColor,
pointSize, pointSize,
...@@ -40,6 +41,12 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> { ...@@ -40,6 +41,12 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
} else { } else {
lineConfig.stroke = lineColor; lineConfig.stroke = lineColor;
lineConfig.width = lineWidth; lineConfig.width = lineWidth;
if (lineStyle && lineStyle.fill !== 'solid') {
if (lineStyle.fill === 'dot') {
// lineConfig.dashCap = 'round'; // square or butt
}
lineConfig.dash = lineStyle.dash ?? [10, 10];
}
lineConfig.paths = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => { lineConfig.paths = (self: uPlot, seriesIdx: number, idx0: number, idx1: number) => {
let pathsBuilder = mapDrawStyleToPathBuilder(drawStyle, lineInterpolation); let pathsBuilder = mapDrawStyleToPathBuilder(drawStyle, lineInterpolation);
return pathsBuilder(self, seriesIdx, idx0, idx1); return pathsBuilder(self, seriesIdx, idx0, idx1);
......
import React, { useMemo } from 'react';
import { FieldOverrideEditorProps, SelectableValue } from '@grafana/data';
import { HorizontalGroup, IconButton, LineStyle, RadioButtonGroup, Select } from '@grafana/ui';
type LineFill = 'solid' | 'dash' | 'dot';
const lineFillOptions: Array<SelectableValue<LineFill>> = [
{
label: 'Solid',
value: 'solid',
},
{
label: 'Dash',
value: 'dash',
},
// {
// label: 'Dots',
// value: 'dot',
// },
];
const dashOptions: Array<SelectableValue<string>> = [
'10, 10', // default
'10, 15',
'10, 20',
'10, 25',
'10, 30',
'10, 40',
'15, 10',
'20, 10',
'25, 10',
'30, 10',
'40, 10',
'50, 10',
'5, 10',
'30, 3, 3',
].map(txt => ({
label: txt,
value: txt,
}));
const dotOptions: Array<SelectableValue<string>> = [
'0, 10', // default
'0, 20',
'0, 30',
'0, 40',
'0, 3, 3',
].map(txt => ({
label: txt,
value: txt,
}));
export const LineStyleEditor: React.FC<FieldOverrideEditorProps<LineStyle, any>> = ({ value, onChange }) => {
const options = useMemo(() => (value?.fill === 'dash' ? dashOptions : dotOptions), [value]);
const current = useMemo(() => {
if (!value?.dash?.length) {
return options[0];
}
const str = value.dash?.join(', ');
const val = options.find(o => o.value === str);
if (!val) {
return {
label: str,
value: str,
};
}
return val;
}, [value, options]);
return (
<HorizontalGroup>
<RadioButtonGroup
value={value?.fill || 'solid'}
options={lineFillOptions}
onChange={v => {
onChange({
...value,
fill: v!,
});
}}
/>
{value?.fill && value?.fill !== 'solid' && (
<>
<Select
allowCustomValue={true}
options={options}
value={current}
width={20}
onChange={v => {
onChange({
...value,
dash: parseText(v.value ?? ''),
});
}}
formatCreateLabel={t => `Segments: ${parseText(t).join(', ')}`}
/>
<div>
&nbsp;
<a
title="The input expects a segment list"
href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash#Parameters"
target="_blank"
rel="noreferrer"
>
<IconButton name="question-circle" />
</a>
</div>
</>
)}
</HorizontalGroup>
);
};
function parseText(txt: string): number[] {
const segments: number[] = [];
for (const s of txt.split(/(?:,| )+/)) {
const num = Number.parseInt(s, 10);
if (!isNaN(num)) {
segments.push(num);
}
}
return segments;
}
...@@ -117,6 +117,13 @@ Object { ...@@ -117,6 +117,13 @@ Object {
"axisPlacement": "auto", "axisPlacement": "auto",
"drawStyle": "line", "drawStyle": "line",
"fillOpacity": 10, "fillOpacity": 10,
"lineStyle": Object {
"dash": Array [
10,
10,
],
"fill": "dash",
},
"lineWidth": 1, "lineWidth": 1,
"pointSize": 6, "pointSize": 6,
"showPoints": "never", "showPoints": "never",
...@@ -155,6 +162,16 @@ Object { ...@@ -155,6 +162,16 @@ Object {
"id": "custom.axisLabel", "id": "custom.axisLabel",
"value": "Y222", "value": "Y222",
}, },
Object {
"id": "custom.lineStyle",
"value": Object {
"dash": Array [
5,
8,
],
"fill": "dash",
},
},
], ],
}, },
], ],
......
...@@ -186,6 +186,9 @@ const twoYAxis = { ...@@ -186,6 +186,9 @@ const twoYAxis = {
{ {
alias: 'B-series', alias: 'B-series',
yaxis: 2, yaxis: 2,
dashLength: 5,
dashes: true,
spaceLength: 8,
}, },
], ],
thresholds: [], thresholds: [],
...@@ -199,7 +202,7 @@ const twoYAxis = { ...@@ -199,7 +202,7 @@ const twoYAxis = {
}, },
], ],
fillGradient: 0, fillGradient: 0,
dashes: false, dashes: true,
hiddenSeries: false, hiddenSeries: false,
points: false, points: false,
bars: false, bars: false,
......
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
AxisPlacement, AxisPlacement,
DrawStyle, DrawStyle,
LineInterpolation, LineInterpolation,
LineStyle,
PointVisibility, PointVisibility,
} from '@grafana/ui/src/components/uPlot/config'; } from '@grafana/ui/src/components/uPlot/config';
import { Options } from './types'; import { Options } from './types';
...@@ -52,6 +53,12 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -52,6 +53,12 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}; };
} }
// Dashes
const dash: LineStyle = {
fill: angular.dashes ? 'dash' : 'solid',
dash: [angular.dashLength ?? 10, angular.spaceLength ?? 10],
};
// "seriesOverrides": [ // "seriesOverrides": [
// { // {
// "$$hashKey": "object:183", // "$$hashKey": "object:183",
...@@ -98,6 +105,8 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -98,6 +105,8 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}, },
properties: [], properties: [],
}; };
let dashOverride: LineStyle | undefined = undefined;
for (const p of Object.keys(seriesOverride)) { for (const p of Object.keys(seriesOverride)) {
const v = seriesOverride[p]; const v = seriesOverride[p];
switch (p) { switch (p) {
...@@ -165,10 +174,37 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -165,10 +174,37 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
value: 2 + v * 2, value: 2 + v * 2,
}); });
break; break;
case 'dashLength':
case 'spaceLength':
case 'dashes':
if (!dashOverride) {
dashOverride = {
fill: dash.fill,
dash: [...dash.dash!],
};
}
switch (p) {
case 'dashLength':
dashOverride.dash![0] = v;
break;
case 'spaceLength':
dashOverride.dash![1] = v;
break;
case 'dashes':
dashOverride.fill = v ? 'dash' : 'solid';
break;
}
break;
default: default:
console.log('Ignore override migration:', seriesOverride.alias, p, v); console.log('Ignore override migration:', seriesOverride.alias, p, v);
} }
} }
if (dashOverride) {
rule.properties.push({
id: 'custom.lineStyle',
value: dashOverride,
});
}
if (rule.properties.length) { if (rule.properties.length) {
overrides.push(rule); overrides.push(rule);
} }
...@@ -185,6 +221,9 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour ...@@ -185,6 +221,9 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
} }
graph.lineWidth = angular.linewidth; graph.lineWidth = angular.linewidth;
if (dash.fill !== 'solid') {
graph.lineStyle = dash;
}
if (isNumber(angular.pointradius)) { if (isNumber(angular.pointradius)) {
graph.pointSize = 2 + angular.pointradius * 2; graph.pointSize = 2 + angular.pointradius * 2;
......
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
GraphFieldConfig, GraphFieldConfig,
graphFieldOptions, graphFieldOptions,
LegendDisplayMode, LegendDisplayMode,
LineStyle,
PointVisibility, PointVisibility,
ScaleDistribution, ScaleDistribution,
ScaleDistributionConfig, ScaleDistributionConfig,
...@@ -20,6 +21,7 @@ import { GraphPanel } from './GraphPanel'; ...@@ -20,6 +21,7 @@ import { GraphPanel } from './GraphPanel';
import { graphPanelChangedHandler } from './migrations'; import { graphPanelChangedHandler } from './migrations';
import { Options } from './types'; import { Options } from './types';
import { ScaleDistributionEditor } from './ScaleDistributionEditor'; import { ScaleDistributionEditor } from './ScaleDistributionEditor';
import { LineStyleEditor } from './LineStyleEditor';
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel) export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
.setPanelChangeHandler(graphPanelChangedHandler) .setPanelChangeHandler(graphPanelChangedHandler)
...@@ -75,6 +77,16 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel) ...@@ -75,6 +77,16 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
}, },
showIf: c => c.drawStyle !== DrawStyle.Points, showIf: c => c.drawStyle !== DrawStyle.Points,
}) })
.addCustomEditor<void, LineStyle>({
id: 'lineStyle',
path: 'lineStyle',
name: 'Line style',
showIf: c => c.drawStyle === DrawStyle.Line,
editor: LineStyleEditor,
override: LineStyleEditor,
process: identityOverrideProcessor,
shouldApply: f => f.type === FieldType.number,
})
.addRadio({ .addRadio({
path: 'fillGradient', path: 'fillGradient',
name: 'Fill gradient', name: 'Fill gradient',
......
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