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

* Timeline: Adds opacity & line width option

* Updated devenv dashboards, added back original timeline modes
pull/34142/head^2
Torkel Ödegaard 4 years ago committed by GitHub
parent c61a610f72
commit 953aadd6e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 414
      devenv/dev-dashboards/panel-timeline/timeline-demo.json
  2. 2
      devenv/dev-dashboards/panel-timeline/timeline-modes.json
  3. 2
      packages/grafana-data/src/themes/createColors.ts
  4. 7
      packages/grafana-data/src/utils/OptionsUIBuilders.ts
  5. 1
      packages/grafana-ui/src/components/uPlot/config/UPlotConfigBuilder.ts
  6. 21
      public/app/plugins/panel/timeline/module.tsx
  7. 93
      public/app/plugins/panel/timeline/timeline.ts
  8. 6
      public/app/plugins/panel/timeline/types.ts
  9. 25
      public/app/plugins/panel/timeline/utils.ts

@ -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
}

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

@ -257,7 +257,7 @@ export function createColors(colors: ThemeColorsInput): ThemeColors {
function getContrastText(background: string, threshold: number = contrastThreshold) { function getContrastText(background: string, threshold: number = contrastThreshold) {
const contrastText = 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 // todo, need color framework
return contrastText; return contrastText;
} }

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

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

@ -1,6 +1,6 @@
import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data'; import { FieldColorModeId, FieldConfigProperty, PanelPlugin } from '@grafana/data';
import { TimelinePanel } from './TimelinePanel'; import { TimelinePanel } from './TimelinePanel';
import { TimelineOptions, TimelineFieldConfig, TimelineMode } from './types'; import { TimelineOptions, TimelineFieldConfig, TimelineMode, defaultTimelineFieldConfig } from './types';
import { BarValueVisibility } from '@grafana/ui'; import { BarValueVisibility } from '@grafana/ui';
export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(TimelinePanel) export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(TimelinePanel)
@ -15,15 +15,12 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
}, },
}, },
}, },
/*
useCustomConfig: (builder) => { useCustomConfig: (builder) => {
const cfg = defaultBarChartFieldConfig;
builder builder
.addSliderInput({ .addSliderInput({
path: 'lineWidth', path: 'lineWidth',
name: 'Line width', name: 'Line width',
defaultValue: cfg.lineWidth, defaultValue: defaultTimelineFieldConfig.lineWidth,
settings: { settings: {
min: 0, min: 0,
max: 10, max: 10,
@ -33,26 +30,14 @@ export const plugin = new PanelPlugin<TimelineOptions, TimelineFieldConfig>(Time
.addSliderInput({ .addSliderInput({
path: 'fillOpacity', path: 'fillOpacity',
name: 'Fill opacity', name: 'Fill opacity',
defaultValue: cfg.fillOpacity, defaultValue: defaultTimelineFieldConfig.fillOpacity,
settings: { settings: {
min: 0, min: 0,
max: 100, max: 100,
step: 1, 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) => { .setPanelOptions((builder) => {
builder builder

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

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

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

Loading…
Cancel
Save