Commit 71e93e52 by Ryan McKinley Committed by GitHub

GraphNG: support dashes (#30070)

parent dba4942e
......@@ -2093,10 +2093,7 @@
"y": 34
},
"id": 11,
"panels": [],
"title": "Show gaps & Connected",
"type": "row"
},
"panels": [
{
"datasource": "gdev-testdata",
"fieldConfig": {
......@@ -2557,6 +2554,315 @@
"title": "Null values & show connected",
"transformations": [],
"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,
......@@ -2714,5 +3020,5 @@
"timezone": "",
"title": "Panel Tests - Graph NG",
"uid": "TkZXxlNG3",
"version": 2
"version": 1
}
......@@ -175,6 +175,7 @@ export const GraphNG: React.FC<GraphNGProps> = ({
lineColor: customConfig.lineColor ?? seriesColor,
lineWidth: customConfig.lineWidth,
lineInterpolation: customConfig.lineInterpolation,
lineStyle: customConfig.lineStyle,
showPoints,
pointSize: customConfig.pointSize,
pointColor: customConfig.pointColor ?? seriesColor,
......
......@@ -51,11 +51,19 @@ export enum ScaleDistribution {
/**
* @alpha
*/
export interface LineStyle {
fill?: 'solid' | 'dash' | 'dot' | 'square'; // cap = 'butt' | 'round' | 'square'
dash?: number[];
}
/**
* @alpha
*/
export interface LineConfig {
lineColor?: string;
lineWidth?: number;
lineInterpolation?: LineInterpolation;
lineDash?: number[];
lineStyle?: LineStyle;
spanNulls?: boolean;
}
......
......@@ -25,6 +25,7 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
lineInterpolation,
lineColor,
lineWidth,
lineStyle,
showPoints,
pointColor,
pointSize,
......@@ -40,6 +41,12 @@ export class UPlotSeriesBuilder extends PlotConfigBuilder<SeriesProps, Series> {
} else {
lineConfig.stroke = lineColor;
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) => {
let pathsBuilder = mapDrawStyleToPathBuilder(drawStyle, lineInterpolation);
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 {
"axisPlacement": "auto",
"drawStyle": "line",
"fillOpacity": 10,
"lineStyle": Object {
"dash": Array [
10,
10,
],
"fill": "dash",
},
"lineWidth": 1,
"pointSize": 6,
"showPoints": "never",
......@@ -155,6 +162,16 @@ Object {
"id": "custom.axisLabel",
"value": "Y222",
},
Object {
"id": "custom.lineStyle",
"value": Object {
"dash": Array [
5,
8,
],
"fill": "dash",
},
},
],
},
],
......
......@@ -186,6 +186,9 @@ const twoYAxis = {
{
alias: 'B-series',
yaxis: 2,
dashLength: 5,
dashes: true,
spaceLength: 8,
},
],
thresholds: [],
......@@ -199,7 +202,7 @@ const twoYAxis = {
},
],
fillGradient: 0,
dashes: false,
dashes: true,
hiddenSeries: false,
points: false,
bars: false,
......
......@@ -16,6 +16,7 @@ import {
AxisPlacement,
DrawStyle,
LineInterpolation,
LineStyle,
PointVisibility,
} from '@grafana/ui/src/components/uPlot/config';
import { Options } from './types';
......@@ -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": [
// {
// "$$hashKey": "object:183",
......@@ -98,6 +105,8 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
},
properties: [],
};
let dashOverride: LineStyle | undefined = undefined;
for (const p of Object.keys(seriesOverride)) {
const v = seriesOverride[p];
switch (p) {
......@@ -165,10 +174,37 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
value: 2 + v * 2,
});
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:
console.log('Ignore override migration:', seriesOverride.alias, p, v);
}
}
if (dashOverride) {
rule.properties.push({
id: 'custom.lineStyle',
value: dashOverride,
});
}
if (rule.properties.length) {
overrides.push(rule);
}
......@@ -185,6 +221,9 @@ export function flotToGraphOptions(angular: any): { fieldConfig: FieldConfigSour
}
graph.lineWidth = angular.linewidth;
if (dash.fill !== 'solid') {
graph.lineStyle = dash;
}
if (isNumber(angular.pointradius)) {
graph.pointSize = 2 + angular.pointradius * 2;
......
......@@ -11,6 +11,7 @@ import {
GraphFieldConfig,
graphFieldOptions,
LegendDisplayMode,
LineStyle,
PointVisibility,
ScaleDistribution,
ScaleDistributionConfig,
......@@ -20,6 +21,7 @@ import { GraphPanel } from './GraphPanel';
import { graphPanelChangedHandler } from './migrations';
import { Options } from './types';
import { ScaleDistributionEditor } from './ScaleDistributionEditor';
import { LineStyleEditor } from './LineStyleEditor';
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
.setPanelChangeHandler(graphPanelChangedHandler)
......@@ -75,6 +77,16 @@ export const plugin = new PanelPlugin<Options, GraphFieldConfig>(GraphPanel)
},
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({
path: 'fillGradient',
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