2021-07-29 09:31:07 +08:00
|
|
|
import uPlot, { Cursor, Band, Hooks, Select, AlignedData } from 'uplot';
|
2021-06-04 09:05:47 +08:00
|
|
|
import { merge } from 'lodash';
|
2021-05-12 02:57:52 +08:00
|
|
|
import {
|
|
|
|
|
DataFrame,
|
|
|
|
|
DefaultTimeZone,
|
|
|
|
|
EventBus,
|
|
|
|
|
getTimeZoneInfo,
|
|
|
|
|
GrafanaTheme2,
|
|
|
|
|
TimeRange,
|
|
|
|
|
TimeZone,
|
|
|
|
|
} from '@grafana/data';
|
2021-06-03 10:43:47 +08:00
|
|
|
import { PlotConfig, PlotTooltipInterpolator } from '../types';
|
|
|
|
|
import { ScaleProps, UPlotScaleBuilder } from './UPlotScaleBuilder';
|
|
|
|
|
import { SeriesProps, UPlotSeriesBuilder } from './UPlotSeriesBuilder';
|
|
|
|
|
import { AxisProps, UPlotAxisBuilder } from './UPlotAxisBuilder';
|
|
|
|
|
import { AxisPlacement } from '../config';
|
2021-04-26 19:30:04 +08:00
|
|
|
import { pluginLog } from '../utils';
|
2021-05-04 19:03:35 +08:00
|
|
|
import { getThresholdsDrawHook, UPlotThresholdOptions } from './UPlotThresholds';
|
2021-01-30 04:52:52 +08:00
|
|
|
|
2021-06-04 09:05:47 +08:00
|
|
|
const cursorDefaults: Cursor = {
|
|
|
|
|
// prevent client-side zoom from triggering at the end of a selection
|
|
|
|
|
drag: { setScale: false },
|
|
|
|
|
points: {
|
|
|
|
|
/*@ts-ignore*/
|
|
|
|
|
size: (u, seriesIdx) => u.series[seriesIdx].points.size * 2,
|
|
|
|
|
/*@ts-ignore*/
|
|
|
|
|
width: (u, seriesIdx, size) => size / 4,
|
|
|
|
|
/*@ts-ignore*/
|
|
|
|
|
stroke: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx) + '80',
|
|
|
|
|
/*@ts-ignore*/
|
|
|
|
|
fill: (u, seriesIdx) => u.series[seriesIdx].points.stroke(u, seriesIdx),
|
|
|
|
|
},
|
|
|
|
|
focus: {
|
|
|
|
|
prox: 30,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2021-07-29 09:31:07 +08:00
|
|
|
type PrepData = (frame: DataFrame) => AlignedData;
|
|
|
|
|
|
2020-11-18 18:14:24 +08:00
|
|
|
export class UPlotConfigBuilder {
|
|
|
|
|
private series: UPlotSeriesBuilder[] = [];
|
2020-11-24 02:07:02 +08:00
|
|
|
private axes: Record<string, UPlotAxisBuilder> = {};
|
2020-11-18 18:14:24 +08:00
|
|
|
private scales: UPlotScaleBuilder[] = [];
|
2021-01-16 03:03:41 +08:00
|
|
|
private bands: Band[] = [];
|
2020-12-09 00:13:12 +08:00
|
|
|
private cursor: Cursor | undefined;
|
2021-04-15 19:00:01 +08:00
|
|
|
private isStacking = false;
|
2021-05-05 16:44:31 +08:00
|
|
|
private select: uPlot.Select | undefined;
|
2021-01-12 16:35:13 +08:00
|
|
|
private hasLeftAxis = false;
|
|
|
|
|
private hasBottomAxis = false;
|
2021-01-30 04:52:52 +08:00
|
|
|
private hooks: Hooks.Arrays = {};
|
2021-03-05 14:17:43 +08:00
|
|
|
private tz: string | undefined = undefined;
|
2021-05-27 16:51:06 +08:00
|
|
|
private sync = false;
|
2021-05-04 19:03:35 +08:00
|
|
|
// to prevent more than one threshold per scale
|
|
|
|
|
private thresholds: Record<string, UPlotThresholdOptions> = {};
|
2021-05-12 01:24:23 +08:00
|
|
|
/**
|
|
|
|
|
* Custom handler for closest datapoint and series lookup. Technicaly returns uPlots setCursor hook
|
|
|
|
|
* that sets tooltips state.
|
|
|
|
|
*/
|
2021-06-03 10:43:47 +08:00
|
|
|
tooltipInterpolator: PlotTooltipInterpolator | undefined = undefined;
|
2021-01-30 04:52:52 +08:00
|
|
|
|
2021-07-29 09:31:07 +08:00
|
|
|
prepData: PrepData | undefined = undefined;
|
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
constructor(timeZone: TimeZone = DefaultTimeZone) {
|
|
|
|
|
this.tz = getTimeZoneInfo(timeZone, Date.now())?.ianaName;
|
2021-03-05 14:17:43 +08:00
|
|
|
}
|
2021-02-15 23:46:29 +08:00
|
|
|
|
2021-05-10 20:24:23 +08:00
|
|
|
// Exposed to let the container know the primary scale keys
|
|
|
|
|
scaleKeys: [string, string] = ['', ''];
|
|
|
|
|
|
2021-04-26 19:30:04 +08:00
|
|
|
addHook<T extends keyof Hooks.Defs>(type: T, hook: Hooks.Defs[T]) {
|
|
|
|
|
pluginLog('UPlotConfigBuilder', false, 'addHook', type);
|
|
|
|
|
|
2021-01-30 04:52:52 +08:00
|
|
|
if (!this.hooks[type]) {
|
|
|
|
|
this.hooks[type] = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.hooks[type]!.push(hook as any);
|
|
|
|
|
}
|
2020-11-24 02:07:02 +08:00
|
|
|
|
2021-05-04 19:03:35 +08:00
|
|
|
addThresholds(options: UPlotThresholdOptions) {
|
|
|
|
|
if (!this.thresholds[options.scaleKey]) {
|
|
|
|
|
this.thresholds[options.scaleKey] = options;
|
|
|
|
|
this.addHook('drawClear', getThresholdsDrawHook(options));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 18:14:24 +08:00
|
|
|
addAxis(props: AxisProps) {
|
2020-11-24 02:07:02 +08:00
|
|
|
props.placement = props.placement ?? AxisPlacement.Auto;
|
|
|
|
|
|
2020-12-03 16:30:40 +08:00
|
|
|
if (this.axes[props.scaleKey]) {
|
|
|
|
|
this.axes[props.scaleKey].merge(props);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-24 02:07:02 +08:00
|
|
|
// Handle auto placement logic
|
|
|
|
|
if (props.placement === AxisPlacement.Auto) {
|
|
|
|
|
props.placement = this.hasLeftAxis ? AxisPlacement.Right : AxisPlacement.Left;
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-12 16:35:13 +08:00
|
|
|
switch (props.placement) {
|
|
|
|
|
case AxisPlacement.Left:
|
|
|
|
|
this.hasLeftAxis = true;
|
|
|
|
|
break;
|
|
|
|
|
case AxisPlacement.Bottom:
|
|
|
|
|
this.hasBottomAxis = true;
|
|
|
|
|
break;
|
2020-11-24 02:07:02 +08:00
|
|
|
}
|
|
|
|
|
|
2020-12-09 00:13:12 +08:00
|
|
|
if (props.placement === AxisPlacement.Hidden) {
|
|
|
|
|
props.show = false;
|
|
|
|
|
props.size = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-24 02:07:02 +08:00
|
|
|
this.axes[props.scaleKey] = new UPlotAxisBuilder(props);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getAxisPlacement(scaleKey: string): AxisPlacement {
|
|
|
|
|
const axis = this.axes[scaleKey];
|
|
|
|
|
return axis?.props.placement! ?? AxisPlacement.Left;
|
2020-11-18 18:14:24 +08:00
|
|
|
}
|
|
|
|
|
|
2020-12-09 00:13:12 +08:00
|
|
|
setCursor(cursor?: Cursor) {
|
2021-06-04 09:05:47 +08:00
|
|
|
this.cursor = merge({}, this.cursor, cursor);
|
2020-12-09 00:13:12 +08:00
|
|
|
}
|
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
setSelect(select: Select) {
|
2021-01-30 04:52:52 +08:00
|
|
|
this.select = select;
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 19:00:01 +08:00
|
|
|
setStacking(enabled = true) {
|
|
|
|
|
this.isStacking = enabled;
|
|
|
|
|
}
|
2021-05-14 23:24:40 +08:00
|
|
|
|
2020-11-18 18:14:24 +08:00
|
|
|
addSeries(props: SeriesProps) {
|
|
|
|
|
this.series.push(new UPlotSeriesBuilder(props));
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-15 16:14:50 +08:00
|
|
|
getSeries() {
|
|
|
|
|
return this.series;
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-03 16:30:40 +08:00
|
|
|
/** Add or update the scale with the scale key */
|
2020-11-18 18:14:24 +08:00
|
|
|
addScale(props: ScaleProps) {
|
2021-01-20 14:59:48 +08:00
|
|
|
const current = this.scales.find((v) => v.props.scaleKey === props.scaleKey);
|
2020-12-03 16:30:40 +08:00
|
|
|
if (current) {
|
|
|
|
|
current.merge(props);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-11-18 18:14:24 +08:00
|
|
|
this.scales.push(new UPlotScaleBuilder(props));
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-16 03:03:41 +08:00
|
|
|
addBand(band: Band) {
|
|
|
|
|
this.bands.push(band);
|
|
|
|
|
}
|
|
|
|
|
|
2021-06-03 10:43:47 +08:00
|
|
|
setTooltipInterpolator(interpolator: PlotTooltipInterpolator) {
|
2021-05-12 01:24:23 +08:00
|
|
|
this.tooltipInterpolator = interpolator;
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-29 09:31:07 +08:00
|
|
|
setPrepData(prepData: PrepData) {
|
|
|
|
|
this.prepData = prepData;
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-27 16:51:06 +08:00
|
|
|
setSync() {
|
|
|
|
|
this.sync = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hasSync() {
|
|
|
|
|
return this.sync;
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 18:14:24 +08:00
|
|
|
getConfig() {
|
2021-01-30 04:52:52 +08:00
|
|
|
const config: PlotConfig = { series: [{}] };
|
2021-01-20 14:59:48 +08:00
|
|
|
config.axes = this.ensureNonOverlappingAxes(Object.values(this.axes)).map((a) => a.getConfig());
|
|
|
|
|
config.series = [...config.series, ...this.series.map((s) => s.getConfig())];
|
2020-11-18 18:14:24 +08:00
|
|
|
config.scales = this.scales.reduce((acc, s) => {
|
|
|
|
|
return { ...acc, ...s.getConfig() };
|
|
|
|
|
}, {});
|
2020-12-12 03:01:55 +08:00
|
|
|
|
2021-01-30 04:52:52 +08:00
|
|
|
config.hooks = this.hooks;
|
|
|
|
|
|
|
|
|
|
config.select = this.select;
|
|
|
|
|
|
2021-06-04 09:05:47 +08:00
|
|
|
config.cursor = merge({}, cursorDefaults, this.cursor);
|
2020-12-12 03:01:55 +08:00
|
|
|
|
2021-02-15 23:46:29 +08:00
|
|
|
config.tzDate = this.tzDate;
|
|
|
|
|
|
2021-04-15 19:00:01 +08:00
|
|
|
if (this.isStacking) {
|
|
|
|
|
// Let uPlot handle bands and fills
|
2021-01-16 03:03:41 +08:00
|
|
|
config.bands = this.bands;
|
2021-04-15 19:00:01 +08:00
|
|
|
} else {
|
|
|
|
|
// When fillBelowTo option enabled, handle series bands fill manually
|
|
|
|
|
if (this.bands?.length) {
|
|
|
|
|
config.bands = this.bands;
|
|
|
|
|
const keepFill = new Set<number>();
|
|
|
|
|
for (const b of config.bands) {
|
|
|
|
|
keepFill.add(b.series[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let i = 1; i < config.series.length; i++) {
|
|
|
|
|
if (!keepFill.has(i)) {
|
|
|
|
|
config.series[i].fill = undefined;
|
|
|
|
|
}
|
2021-01-16 03:03:41 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-18 18:14:24 +08:00
|
|
|
return config;
|
|
|
|
|
}
|
2021-01-12 16:35:13 +08:00
|
|
|
|
|
|
|
|
private ensureNonOverlappingAxes(axes: UPlotAxisBuilder[]): UPlotAxisBuilder[] {
|
|
|
|
|
for (const axis of axes) {
|
|
|
|
|
if (axis.props.placement === AxisPlacement.Right && this.hasLeftAxis) {
|
|
|
|
|
axis.props.grid = false;
|
|
|
|
|
}
|
|
|
|
|
if (axis.props.placement === AxisPlacement.Top && this.hasBottomAxis) {
|
|
|
|
|
axis.props.grid = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return axes;
|
|
|
|
|
}
|
2021-02-15 23:46:29 +08:00
|
|
|
|
|
|
|
|
private tzDate = (ts: number) => {
|
2021-03-05 14:17:43 +08:00
|
|
|
let date = new Date(ts);
|
2021-02-15 23:46:29 +08:00
|
|
|
|
2021-03-05 14:17:43 +08:00
|
|
|
return this.tz ? uPlot.tzDate(date, this.tz) : date;
|
2021-02-15 23:46:29 +08:00
|
|
|
};
|
2020-11-18 18:14:24 +08:00
|
|
|
}
|
2021-05-12 02:57:52 +08:00
|
|
|
|
|
|
|
|
/** @alpha */
|
|
|
|
|
type UPlotConfigPrepOpts<T extends Record<string, any> = {}> = {
|
|
|
|
|
frame: DataFrame;
|
|
|
|
|
theme: GrafanaTheme2;
|
|
|
|
|
timeZone: TimeZone;
|
|
|
|
|
getTimeRange: () => TimeRange;
|
|
|
|
|
eventBus: EventBus;
|
2021-06-11 19:49:26 +08:00
|
|
|
allFrames: DataFrame[];
|
2021-05-12 02:57:52 +08:00
|
|
|
} & T;
|
|
|
|
|
|
|
|
|
|
/** @alpha */
|
|
|
|
|
export type UPlotConfigPrepFn<T extends {} = {}> = (opts: UPlotConfigPrepOpts<T>) => UPlotConfigBuilder;
|