Timeline: Adds opacity & line width option (#34118)

* Timeline: Adds opacity & line width option

* Updated devenv dashboards, added back original timeline modes
This commit is contained in:
Torkel Ödegaard 2021-05-14 17:24:40 +02:00 committed by GitHub
parent c61a610f72
commit 953aadd6e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 487 additions and 84 deletions

View File

@ -0,0 +1,414 @@
{
"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": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"fillOpacity": 80,
"lineWidth": 1
},
"mappings": [
{
"options": {
"CRITICAL": {
"color": "red",
"index": 3
},
"HIGH": {
"color": "orange",
"index": 2
},
"LOW": {
"color": "blue",
"index": 0
},
"NORMAL": {
"color": "green",
"index": 1
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 8,
"w": 24,
"x": 0,
"y": 0
},
"id": 9,
"options": {
"alignValue": "center",
"colWidth": 0.9,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"mode": "changes",
"rowHeight": 0.98,
"showValue": "always"
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "SensorA",
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "LOW,HIGH,NORMAL,NORMAL,NORMAL,LOW,LOW,NORMAL,HIGH,CRITICAL"
},
{
"alias": "SensorB",
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "NORMAL,LOW,LOW,CRITICAL,CRITICAL,LOW,LOW,NORMAL,HIGH,CRITICAL"
},
{
"alias": "SensorA",
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "NORMAL,NORMAL,NORMAL,NORMAL,CRITICAL,LOW,NORMAL,NORMAL,NORMAL,LOW"
}
],
"title": "State changes strings",
"type": "timeline"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 70,
"lineWidth": 1
},
"mappings": [
{
"options": {
"match": "true",
"result": {
"color": "semi-dark-green",
"index": 0,
"text": "ON"
}
},
"type": "special"
},
{
"options": {
"match": "false",
"result": {
"color": "red",
"index": 1,
"text": "OFF"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 13,
"x": 0,
"y": 8
},
"id": 13,
"options": {
"alignValue": "center",
"colWidth": 1,
"mode": "changes",
"rowHeight": 0.98,
"showValue": "always"
},
"targets": [
{
"alias": "",
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,true,true,true,true,false,false"
},
{
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "false,true,false,true,true,false,false,false,true,true"
},
{
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,true,true"
},
{
"hide": false,
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "false,true,false,true,true"
}
],
"title": "State changes with boolean values",
"type": "timeline"
},
{
"datasource": null,
"description": "Should show gaps",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"fillOpacity": 80,
"lineWidth": 1
},
"mappings": [
{
"options": {
"match": "true",
"result": {
"color": "semi-dark-green",
"index": 0,
"text": "ON"
}
},
"type": "special"
},
{
"options": {
"match": "false",
"result": {
"color": "red",
"index": 1,
"text": "OFF"
}
},
"type": "special"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 11,
"w": 11,
"x": 13,
"y": 8
},
"id": 12,
"options": {
"alignValue": "center",
"colWidth": 1,
"mode": "changes",
"rowHeight": 0.98,
"showValue": "always"
},
"targets": [
{
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,true,true,true,true,false,false"
},
{
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "false,true,false,true,true,false,false,false,true,true"
},
{
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "true,false,null,true,true"
},
{
"hide": false,
"refId": "D",
"scenarioId": "csv_metric_values",
"stringInput": "false,null,null,false,true,true"
}
],
"title": "State changes with nulls",
"type": "timeline"
},
{
"datasource": null,
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"custom": {
"fillOpacity": 96,
"lineWidth": 0
},
"decimals": 0,
"mappings": [],
"max": 30,
"min": -10,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"unit": "short"
},
"overrides": []
},
"gridPos": {
"h": 12,
"w": 24,
"x": 0,
"y": 19
},
"id": 4,
"interval": null,
"maxDataPoints": 20,
"options": {
"alignValue": "center",
"colWidth": 0.96,
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom"
},
"mode": "samples",
"rowHeight": 0.98,
"showValue": "always"
},
"pluginVersion": "7.5.0-pre",
"targets": [
{
"alias": "",
"csvWave": {
"timeStep": 60,
"valuesCSV": "0,0,2,2,1,1"
},
"lines": 10,
"max": 30,
"min": -10,
"noise": 2,
"points": [],
"pulseWave": {
"offCount": 3,
"offValue": 1,
"onCount": 3,
"onValue": 2,
"timeStep": 60
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 4,
"spread": 15,
"startValue": 5,
"stream": {
"bands": 1,
"noise": 2.2,
"speed": 250,
"spread": 3.5,
"type": "signal"
},
"stringInput": ""
}
],
"title": "\"Periodic samples\" Mode",
"type": "timeline"
}
],
"refresh": false,
"schemaVersion": 30,
"style": "dark",
"tags": [
"gdev",
"demo"
],
"templating": {
"list": []
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {},
"timezone": "utc",
"title": "Timeline Demo",
"uid": "mIJjFy8Kz",
"version": 13
}

View File

@ -338,7 +338,7 @@
"refresh": false,
"schemaVersion": 29,
"style": "dark",
"tags": [],
"tags": ["gdev", "panel-tests"],
"templating": {
"list": []
},

View File

@ -257,7 +257,7 @@ export function createColors(colors: ThemeColorsInput): ThemeColors {
function getContrastText(background: string, threshold: number = contrastThreshold) {
const contrastText =
getContrastRatio(background, dark.text.maxContrast) >= threshold ? dark.text.maxContrast : light.text.maxContrast;
getContrastRatio(dark.text.maxContrast, background) >= threshold ? dark.text.maxContrast : light.text.maxContrast;
// todo, need color framework
return contrastText;
}

View File

@ -1,6 +1,5 @@
import { FieldConfigEditorProps, FieldConfigPropertyItem, FieldConfigEditorConfig } from '../types/fieldOverrides';
import { OptionsUIRegistryBuilder } from '../types/OptionsUIRegistryBuilder';
import { FieldType } from '../types/dataFrame';
import { PanelOptionsEditorConfig, PanelOptionsEditorItem } from '../types/panel';
import {
numberOverrideProcessor,
@ -33,7 +32,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
override: standardEditorsRegistry.get('number').editor as any,
editor: standardEditorsRegistry.get('number').editor as any,
process: numberOverrideProcessor,
shouldApply: config.shouldApply ? config.shouldApply : (field) => field.type === FieldType.number,
shouldApply: config.shouldApply ?? (() => true),
settings: config.settings || {},
});
}
@ -45,7 +44,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
override: standardEditorsRegistry.get('slider').editor as any,
editor: standardEditorsRegistry.get('slider').editor as any,
process: numberOverrideProcessor,
shouldApply: config.shouldApply ? config.shouldApply : (field) => field.type === FieldType.number,
shouldApply: config.shouldApply ?? (() => true),
settings: config.settings || {},
});
}
@ -57,7 +56,7 @@ export class FieldConfigEditorBuilder<TOptions> extends OptionsUIRegistryBuilder
override: standardEditorsRegistry.get('text').editor as any,
editor: standardEditorsRegistry.get('text').editor as any,
process: stringOverrideProcessor,
shouldApply: config.shouldApply ? config.shouldApply : (field) => field.type === FieldType.string,
shouldApply: config.shouldApply ?? (() => true),
settings: config.settings || {},
});
}

View File

@ -107,6 +107,7 @@ export class UPlotConfigBuilder {
setStacking(enabled = true) {
this.isStacking = enabled;
}
addSeries(props: SeriesProps) {
this.series.push(new UPlotSeriesBuilder(props));
}

View File

@ -1,6 +1,6 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { TimelinePanel } from './TimelinePanel';
import { TimelineOptions, TimelineFieldConfig, TimelineMode } from './types';
import { TimelineOptions, TimelineFieldConfig, TimelineMode, defaultTimelineFieldConfig } from './types';
import { BarValueVisibility } from '@grafana/ui';
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(TimelinePanel)
@ -15,15 +15,12 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
},
},
},
/*
useCustomConfig: (builder) => {
const cfg = defaultBarChartFieldConfig;
builder
.addSliderInput({
path: 'lineWidth',
name: 'Line width',
defaultValue: cfg.lineWidth,
defaultValue: defaultTimelineFieldConfig.lineWidth,
settings: {
min: 0,
max: 10,
@ -33,26 +30,14 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
.addSliderInput({
path: 'fillOpacity',
name: 'Fill opacity',
defaultValue: cfg.fillOpacity,
defaultValue: defaultTimelineFieldConfig.fillOpacity,
settings: {
min: 0,
max: 100,
step: 1,
},
})
.addRadio({
path: 'gradientMode',
name: 'Gradient mode',
defaultValue: graphFieldOptions.fillGradient[0].value,
settings: {
options: graphFieldOptions.fillGradient,
},
});
// addAxisConfig(builder, cfg, true);
addHideFrom(builder);
},
*/
})
.setPanelOptions((builder) => {
builder

View File

@ -2,9 +2,10 @@ import uPlot, { Series, Cursor } from 'uplot';
import { FIXED_UNIT } from '@grafana/ui/src/components/GraphNG/GraphNG';
import { Quadtree, Rect, pointWithin } from 'app/plugins/panel/barchart/quadtree';
import { distribute, SPACE_BETWEEN } from 'app/plugins/panel/barchart/distribute';
import { TimelineMode, TimelineValueAlignment } from './types';
import { TimelineFieldConfig, TimelineMode, TimelineValueAlignment } from './types';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { BarValueVisibility } from '@grafana/ui';
import tinycolor from 'tinycolor2';
const { round, min, ceil } = Math;
@ -26,6 +27,7 @@ function walk(rowHeight: number, yIdx: number | null, count: number, dim: number
interface TimelineBoxRect extends Rect {
left: number;
strokeWidth: number;
fillColor: string;
}
/**
@ -40,12 +42,11 @@ export interface TimelineCoreOptions {
showValue: BarValueVisibility;
alignValue: TimelineValueAlignment;
isDiscrete: (seriesIdx: number) => boolean;
colorLookup: (seriesIdx: number, value: any) => string;
getValueColor: (seriesIdx: number, value: any) => string;
label: (seriesIdx: number) => string;
fill: (seriesIdx: number, value: any) => CanvasRenderingContext2D['fillStyle'];
stroke: (seriesIdx: number, value: any) => CanvasRenderingContext2D['strokeStyle'];
getTimeRange: () => TimeRange;
formatValue?: (seriesIdx: number, value: any) => string;
getFieldConfig: (seriesIdx: number) => TimelineFieldConfig;
onHover?: (seriesIdx: number, valueIdx: number) => void;
onLeave?: (seriesIdx: number, valueIdx: number) => void;
}
@ -64,11 +65,10 @@ export function getConfig(opts: TimelineCoreOptions) {
alignValue,
theme,
label,
fill,
stroke,
formatValue,
getTimeRange,
colorLookup,
getValueColor,
getFieldConfig,
// onHover,
// onLeave,
} = opts;
@ -85,7 +85,7 @@ export function getConfig(opts: TimelineCoreOptions) {
return mark;
});
// alignement-aware text position cache filled by drawPaths->putBox for use in drawPoints
// Needed for to calculate text positions
let boxRectsBySeries: TimelineBoxRect[][];
const resetBoxRectsBySeries = (count: number) => {
@ -134,8 +134,32 @@ export function getConfig(opts: TimelineCoreOptions) {
value: any,
discrete: boolean
) {
// do not render super small boxes
if (boxWidth < 1) {
return;
}
const valueColor = getValueColor(seriesIdx + 1, value);
const fieldConfig = getFieldConfig(seriesIdx);
const fillColor = getFillColor(fieldConfig, valueColor);
const boxRect = (boxRectsBySeries[seriesIdx][valueIdx] = {
x: round(left - xOff),
y: round(top - yOff),
w: boxWidth,
h: boxHeight,
sidx: seriesIdx + 1,
didx: valueIdx,
// These two are needed for later text positioning
left: left,
strokeWidth,
fillColor,
});
qt.add(boxRect);
if (discrete) {
let fillStyle = fill(seriesIdx + 1, value);
let fillStyle = fillColor;
let fillPath = fillPaths.get(fillStyle);
if (fillPath == null) {
@ -145,7 +169,7 @@ export function getConfig(opts: TimelineCoreOptions) {
rect(fillPath, left, top, boxWidth, boxHeight);
if (strokeWidth) {
let strokeStyle = stroke(seriesIdx + 1, value);
let strokeStyle = valueColor;
let strokePath = strokePaths.get(strokeStyle);
if (strokePath == null) {
@ -163,30 +187,17 @@ export function getConfig(opts: TimelineCoreOptions) {
} else {
ctx.beginPath();
rect(ctx, left, top, boxWidth, boxHeight);
ctx.fillStyle = fill(seriesIdx, value);
ctx.fillStyle = fillColor;
ctx.fill();
if (strokeWidth) {
ctx.beginPath();
rect(ctx, left + strokeWidth / 2, top + strokeWidth / 2, boxWidth - strokeWidth, boxHeight - strokeWidth);
ctx.strokeStyle = stroke(seriesIdx, value);
ctx.strokeStyle = valueColor;
ctx.lineWidth = strokeWidth;
ctx.stroke();
}
}
const boxRect = (boxRectsBySeries[seriesIdx][valueIdx] = {
x: round(left - xOff),
y: round(top - yOff),
w: boxWidth,
h: boxHeight,
sidx: seriesIdx + 1,
didx: valueIdx,
// These two are needed for later text positioning
left: left,
strokeWidth,
});
qt.add(boxRect);
}
const drawPaths: Series.PathBuilder = (u, sidx, idx0, idx1) => {
@ -202,17 +213,17 @@ export function getConfig(opts: TimelineCoreOptions) {
rect(u.ctx, u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height);
u.ctx.clip();
walk(rowHeight, sidx - 1, numSeries, yDim, (iy, y0, hgt) => {
walk(rowHeight, sidx - 1, numSeries, yDim, (iy, y0, height) => {
if (mode === TimelineMode.Changes) {
for (let ix = 0; ix < dataY.length; ix++) {
if (dataY[ix] != null) {
let lft = Math.round(valToPosX(dataX[ix], scaleX, xDim, xOff));
let left = Math.round(valToPosX(dataX[ix], scaleX, xDim, xOff));
let nextIx = ix;
while (dataY[++nextIx] === undefined && nextIx < dataY.length) {}
// to now (not to end of chart)
let rgt =
let right =
nextIx === dataY.length
? xOff + xDim + strokeWidth
: Math.round(valToPosX(dataX[nextIx], scaleX, xDim, xOff));
@ -222,10 +233,10 @@ export function getConfig(opts: TimelineCoreOptions) {
rect,
xOff,
yOff,
lft,
left,
round(yOff + y0),
rgt - lft - 2,
round(hgt),
right - left - 2,
round(height),
strokeWidth,
iy,
ix,
@ -246,17 +257,17 @@ export function getConfig(opts: TimelineCoreOptions) {
for (let ix = idx0; ix <= idx1; ix++) {
if (dataY[ix] != null) {
// TODO: all xPos can be pre-computed once for all series in aligned set
let lft = valToPosX(dataX[ix], scaleX, xDim, xOff);
let left = valToPosX(dataX[ix], scaleX, xDim, xOff);
putBox(
u.ctx,
rect,
xOff,
yOff,
round(lft - xShift),
round(left - xShift),
round(yOff + y0),
barWid,
round(hgt),
round(height),
strokeWidth,
iy,
ix,
@ -315,14 +326,13 @@ export function getConfig(opts: TimelineCoreOptions) {
const boxRect = boxRectsBySeries[sidx - 1][ix];
// Todo refine this to better know when to not render text (when values do not fit)
if (boxRect.w < 20) {
if (!boxRect || boxRect.w < 20) {
continue;
}
const x = getTextPositionOffet(boxRect, alignValue);
const valueColor = colorLookup(sidx, dataY[ix]);
u.ctx.fillStyle = theme.colors.getContrastText(valueColor, 3);
u.ctx.fillStyle = theme.colors.getContrastText(boxRect.fillColor, 3);
u.ctx.fillText(formatValue(sidx, dataY[ix]), x, y);
}
}
@ -455,8 +465,8 @@ export function getConfig(opts: TimelineCoreOptions) {
return ySplits;
},
yValues: (u: uPlot, splits: number[]) => splits.map((v, i) => label(i + 1)),
yValues: (u: uPlot, splits: number[]) => splits.map((v, i) => label(i + 1)),
yRange: [0, 1] as uPlot.Range.MinMax,
// pathbuilders
@ -482,3 +492,8 @@ function getTextPositionOffet(rect: TimelineBoxRect, alignValue: TimelineValueAl
textPadding
);
}
function getFillColor(fieldConfig: TimelineFieldConfig, color: string) {
const opacityPercent = (fieldConfig.fillOpacity ?? 100) / 100;
return tinycolor(color).setAlpha(opacityPercent).toString();
}

View File

@ -1,4 +1,4 @@
import { VizLegendOptions, GraphGradientMode, HideableFieldConfig, BarValueVisibility } from '@grafana/ui';
import { VizLegendOptions, HideableFieldConfig, BarValueVisibility } from '@grafana/ui';
/**
* @alpha
@ -20,7 +20,6 @@ export type TimelineValueAlignment = 'center' | 'left' | 'right';
export interface TimelineFieldConfig extends HideableFieldConfig {
lineWidth?: number; // 0
fillOpacity?: number; // 100
gradientMode?: GraphGradientMode;
}
/**
@ -28,8 +27,7 @@ export interface TimelineFieldConfig extends HideableFieldConfig {
*/
export const defaultTimelineFieldConfig: TimelineFieldConfig = {
lineWidth: 1,
fillOpacity: 80,
gradientMode: GraphGradientMode.None,
fillOpacity: 70,
};
/**

View File

@ -11,20 +11,13 @@ import {
} from '@grafana/data';
import { UPlotConfigBuilder, FIXED_UNIT, SeriesVisibilityChangeMode, UPlotConfigPrepFn } from '@grafana/ui';
import { TimelineCoreOptions, getConfig } from './timeline';
import {
AxisPlacement,
GraphGradientMode,
ScaleDirection,
ScaleOrientation,
} from '@grafana/ui/src/components/uPlot/config';
import { AxisPlacement, ScaleDirection, ScaleOrientation } from '@grafana/ui/src/components/uPlot/config';
import { measureText } from '@grafana/ui/src/utils/measureText';
import { TimelineFieldConfig, TimelineOptions } from './types';
const defaultConfig: TimelineFieldConfig = {
lineWidth: 0,
fillOpacity: 80,
gradientMode: GraphGradientMode.None,
};
export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityChangeMode {
@ -61,7 +54,7 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
return !(mode && field.display && mode.startsWith('continuous-'));
};
const colorLookup = (seriesIdx: number, value: any) => {
const getValueColor = (seriesIdx: number, value: any) => {
const field = frame.fields[seriesIdx];
if (field.display) {
@ -93,9 +86,8 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
alignValue,
theme,
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
fill: colorLookup,
stroke: colorLookup,
colorLookup,
getFieldConfig: (seriesIdx) => frame.fields[seriesIdx].config.custom,
getValueColor,
getTimeRange,
// hardcoded formatter for state values
formatValue: (seriesIdx, value) => formattedValueToString(frame.fields[seriesIdx].display!(value)),
@ -170,17 +162,16 @@ export const preparePlotConfigBuilder: UPlotConfigPrepFn<TimelineOptions> = ({
field.state!.seriesIndex = seriesIndex++;
//const scaleKey = config.unit || FIXED_UNIT;
//const colorMode = getFieldColorModeForField(field);
let { fillOpacity } = customConfig;
// const scaleKey = config.unit || FIXED_UNIT;
// const colorMode = getFieldColorModeForField(field);
builder.addSeries({
scaleKey: FIXED_UNIT,
pathBuilder: coreConfig.drawPaths,
pointsBuilder: coreConfig.drawPoints,
//colorMode,
fillOpacity,
lineWidth: customConfig.lineWidth,
fillOpacity: customConfig.fillOpacity,
theme,
show: !customConfig.hideFrom?.viz,
thresholds: config.thresholds,