2021-05-05 16:44:31 +08:00
|
|
|
import { isNumber } from 'lodash';
|
2022-04-22 21:33:13 +08:00
|
|
|
import uPlot from 'uplot';
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
import {
|
|
|
|
DataFrame,
|
|
|
|
FieldConfig,
|
|
|
|
FieldType,
|
|
|
|
formattedValueToString,
|
|
|
|
getFieldColorModeForField,
|
|
|
|
getFieldSeriesColor,
|
2021-06-11 19:49:26 +08:00
|
|
|
getFieldDisplayName,
|
2022-01-10 17:18:56 +08:00
|
|
|
getDisplayProcessor,
|
2022-05-04 16:41:03 +08:00
|
|
|
FieldColorModeId,
|
2022-07-28 14:58:42 +08:00
|
|
|
DecimalCount,
|
2021-05-05 16:44:31 +08:00
|
|
|
} from '@grafana/data';
|
2023-11-02 12:59:55 +08:00
|
|
|
// eslint-disable-next-line import/order
|
2021-05-05 16:44:31 +08:00
|
|
|
import {
|
|
|
|
AxisPlacement,
|
2021-08-26 00:59:03 +08:00
|
|
|
GraphDrawStyle,
|
2021-05-05 16:44:31 +08:00
|
|
|
GraphFieldConfig,
|
2024-02-07 00:20:42 +08:00
|
|
|
GraphThresholdsStyleMode,
|
2021-09-21 03:25:56 +08:00
|
|
|
VisibilityMode,
|
2021-05-05 16:44:31 +08:00
|
|
|
ScaleDirection,
|
|
|
|
ScaleOrientation,
|
2022-01-10 17:18:56 +08:00
|
|
|
StackingMode,
|
2022-06-28 08:02:05 +08:00
|
|
|
GraphTransform,
|
2022-07-19 06:50:07 +08:00
|
|
|
AxisColorMode,
|
|
|
|
GraphGradientMode,
|
2024-02-29 16:53:00 +08:00
|
|
|
VizOrientation,
|
2025-08-29 13:02:49 +08:00
|
|
|
ScaleDistributionConfig,
|
2021-08-24 23:22:34 +08:00
|
|
|
} from '@grafana/schema';
|
2022-04-22 21:33:13 +08:00
|
|
|
|
2022-12-10 01:25:44 +08:00
|
|
|
// unit lookup needed to determine if we want power-of-2 or power-of-10 axis ticks
|
|
|
|
// see categories.ts is @grafana/data
|
|
|
|
const IEC_UNITS = new Set([
|
|
|
|
'bytes',
|
|
|
|
'bits',
|
|
|
|
'kbytes',
|
|
|
|
'mbytes',
|
|
|
|
'gbytes',
|
|
|
|
'tbytes',
|
|
|
|
'pbytes',
|
|
|
|
'binBps',
|
|
|
|
'binbps',
|
|
|
|
'KiBs',
|
|
|
|
'Kibits',
|
|
|
|
'MiBs',
|
|
|
|
'Mibits',
|
|
|
|
'GiBs',
|
|
|
|
'Gibits',
|
|
|
|
'TiBs',
|
|
|
|
'Tibits',
|
|
|
|
'PiBs',
|
|
|
|
'Pibits',
|
|
|
|
]);
|
|
|
|
|
|
|
|
const BIN_INCRS = Array(53);
|
|
|
|
|
|
|
|
for (let i = 0; i < BIN_INCRS.length; i++) {
|
|
|
|
BIN_INCRS[i] = 2 ** i;
|
|
|
|
}
|
|
|
|
|
2025-09-27 02:42:59 +08:00
|
|
|
import * as common from '@grafana/schema/dist/esm/index';
|
2025-08-23 20:33:45 +08:00
|
|
|
import { DrawStyle } from '@grafana/ui';
|
2025-03-12 21:14:32 +08:00
|
|
|
import {
|
|
|
|
UPlotConfigBuilder,
|
|
|
|
UPlotConfigPrepFn,
|
|
|
|
getScaleGradientFn,
|
|
|
|
buildScaleKey,
|
|
|
|
getStackingGroups,
|
|
|
|
preparePlotData2,
|
2025-09-25 01:48:22 +08:00
|
|
|
AxisProps,
|
2025-03-12 21:14:32 +08:00
|
|
|
} from '@grafana/ui/internal';
|
2021-05-05 16:44:31 +08:00
|
|
|
|
2025-09-24 22:21:16 +08:00
|
|
|
import { ANNOTATION_LANE_SIZE } from '../../../plugins/panel/timeseries/plugins/utils';
|
|
|
|
|
2025-09-25 01:48:22 +08:00
|
|
|
// See UPlotAxisBuilder.ts::calculateAxisSize for default axis size calculation
|
2025-10-08 01:53:21 +08:00
|
|
|
export const UPLOT_DEFAULT_AXIS_SIZE = 17;
|
|
|
|
export const UPLOT_DEFAULT_AXIS_GAP = 5;
|
2025-09-24 22:21:16 +08:00
|
|
|
|
2022-07-28 14:58:42 +08:00
|
|
|
const defaultFormatter = (v: any, decimals: DecimalCount = 1) => (v == null ? '-' : v.toFixed(decimals));
|
2021-05-05 16:44:31 +08:00
|
|
|
|
|
|
|
const defaultConfig: GraphFieldConfig = {
|
2021-08-26 00:59:03 +08:00
|
|
|
drawStyle: GraphDrawStyle.Line,
|
2021-09-21 03:25:56 +08:00
|
|
|
showPoints: VisibilityMode.Auto,
|
2021-05-05 16:44:31 +08:00
|
|
|
axisPlacement: AxisPlacement.Auto,
|
2025-08-23 20:33:45 +08:00
|
|
|
showValues: false,
|
2021-05-05 16:44:31 +08:00
|
|
|
};
|
|
|
|
|
2024-04-03 04:32:46 +08:00
|
|
|
export const preparePlotConfigBuilder: UPlotConfigPrepFn = ({
|
2021-05-12 02:57:52 +08:00
|
|
|
frame,
|
|
|
|
theme,
|
2022-07-23 11:18:27 +08:00
|
|
|
timeZones,
|
2021-05-12 02:57:52 +08:00
|
|
|
getTimeRange,
|
2021-06-11 19:49:26 +08:00
|
|
|
allFrames,
|
2021-11-06 09:01:26 +08:00
|
|
|
renderers,
|
|
|
|
tweakScale = (opts) => opts,
|
|
|
|
tweakAxis = (opts) => opts,
|
2024-02-16 06:29:36 +08:00
|
|
|
hoverProximity,
|
2024-02-29 16:53:00 +08:00
|
|
|
orientation = VizOrientation.Horizontal,
|
2025-09-27 02:42:59 +08:00
|
|
|
xAxisConfig,
|
2021-05-12 02:57:52 +08:00
|
|
|
}) => {
|
2024-02-29 16:53:00 +08:00
|
|
|
// we want the Auto and Horizontal orientation to default to Horizontal
|
|
|
|
const isHorizontal = orientation !== VizOrientation.Vertical;
|
2022-07-23 11:18:27 +08:00
|
|
|
const builder = new UPlotConfigBuilder(timeZones[0]);
|
2021-05-05 16:44:31 +08:00
|
|
|
|
2022-05-04 16:41:03 +08:00
|
|
|
let alignedFrame: DataFrame;
|
|
|
|
|
|
|
|
builder.setPrepData((frames) => {
|
|
|
|
// cache alignedFrame
|
|
|
|
alignedFrame = frames[0];
|
|
|
|
|
|
|
|
return preparePlotData2(frames[0], builder.getStackingGroups());
|
|
|
|
});
|
2021-07-29 09:31:07 +08:00
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
// X is the first field in the aligned frame
|
|
|
|
const xField = frame.fields[0];
|
|
|
|
if (!xField) {
|
|
|
|
return builder; // empty frame with no options
|
|
|
|
}
|
|
|
|
|
2021-05-11 09:43:44 +08:00
|
|
|
const xScaleKey = 'x';
|
2021-05-10 20:24:23 +08:00
|
|
|
let yScaleKey = '';
|
|
|
|
|
2022-01-20 16:38:32 +08:00
|
|
|
const xFieldAxisPlacement =
|
2024-02-29 16:53:00 +08:00
|
|
|
xField.config.custom?.axisPlacement === AxisPlacement.Hidden
|
|
|
|
? AxisPlacement.Hidden
|
|
|
|
: isHorizontal
|
|
|
|
? AxisPlacement.Bottom
|
|
|
|
: AxisPlacement.Left;
|
2022-01-20 16:38:32 +08:00
|
|
|
const xFieldAxisShow = xField.config.custom?.axisPlacement !== AxisPlacement.Hidden;
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
if (xField.type === FieldType.time) {
|
|
|
|
builder.addScale({
|
2021-05-10 20:24:23 +08:00
|
|
|
scaleKey: xScaleKey,
|
2024-02-29 16:53:00 +08:00
|
|
|
orientation: isHorizontal ? ScaleOrientation.Horizontal : ScaleOrientation.Vertical,
|
|
|
|
direction: isHorizontal ? ScaleDirection.Right : ScaleDirection.Up,
|
2021-05-05 16:44:31 +08:00
|
|
|
isTime: true,
|
|
|
|
range: () => {
|
2021-05-10 20:24:23 +08:00
|
|
|
const r = getTimeRange();
|
|
|
|
return [r.from.valueOf(), r.to.valueOf()];
|
2021-05-05 16:44:31 +08:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2022-07-23 11:18:27 +08:00
|
|
|
// filters first 2 ticks to make space for timezone labels
|
|
|
|
const filterTicks: uPlot.Axis.Filter | undefined =
|
|
|
|
timeZones.length > 1
|
|
|
|
? (u, splits) => {
|
2024-02-29 16:53:00 +08:00
|
|
|
if (isHorizontal) {
|
|
|
|
return splits.map((v, i) => (i < 2 ? null : v));
|
|
|
|
}
|
|
|
|
return splits;
|
2022-07-23 11:18:27 +08:00
|
|
|
}
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
for (let i = 0; i < timeZones.length; i++) {
|
|
|
|
const timeZone = timeZones[i];
|
|
|
|
builder.addAxis({
|
|
|
|
scaleKey: xScaleKey,
|
|
|
|
isTime: true,
|
|
|
|
placement: xFieldAxisPlacement,
|
|
|
|
show: xFieldAxisShow,
|
|
|
|
label: xField.config.custom?.axisLabel,
|
|
|
|
timeZone,
|
|
|
|
theme,
|
|
|
|
grid: { show: i === 0 && xField.config.custom?.axisGridShow },
|
|
|
|
filter: filterTicks,
|
2025-09-27 02:42:59 +08:00
|
|
|
...xAxisConfig,
|
2022-07-23 11:18:27 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// render timezone labels
|
|
|
|
if (timeZones.length > 1) {
|
|
|
|
builder.addHook('drawAxes', (u: uPlot) => {
|
|
|
|
u.ctx.save();
|
|
|
|
|
|
|
|
let i = 0;
|
|
|
|
u.axes.forEach((a) => {
|
2024-02-29 16:53:00 +08:00
|
|
|
if (isHorizontal && a.side === 2) {
|
|
|
|
u.ctx.fillStyle = theme.colors.text.primary;
|
|
|
|
u.ctx.textAlign = 'left';
|
|
|
|
u.ctx.textBaseline = 'bottom';
|
2022-07-23 11:18:27 +08:00
|
|
|
//@ts-ignore
|
|
|
|
let cssBaseline: number = a._pos + a._size;
|
|
|
|
u.ctx.fillText(timeZones[i], u.bbox.left, cssBaseline * uPlot.pxRatio);
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
u.ctx.restore();
|
|
|
|
});
|
|
|
|
}
|
2021-05-05 16:44:31 +08:00
|
|
|
} else {
|
2025-08-29 13:02:49 +08:00
|
|
|
let custom = xField.config.custom;
|
|
|
|
let scaleDistr: ScaleDistributionConfig = { ...custom?.scaleDistribution };
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
builder.addScale({
|
2021-05-10 20:24:23 +08:00
|
|
|
scaleKey: xScaleKey,
|
2024-02-29 16:53:00 +08:00
|
|
|
orientation: isHorizontal ? ScaleOrientation.Horizontal : ScaleOrientation.Vertical,
|
|
|
|
direction: isHorizontal ? ScaleDirection.Right : ScaleDirection.Up,
|
2025-08-29 13:02:49 +08:00
|
|
|
distribution: scaleDistr?.type,
|
|
|
|
log: scaleDistr?.log,
|
|
|
|
linearThreshold: scaleDistr?.linearThreshold,
|
|
|
|
min: xField.config.min,
|
|
|
|
max: xField.config.max,
|
|
|
|
softMin: custom?.axisSoftMin,
|
|
|
|
softMax: custom?.axisSoftMax,
|
|
|
|
centeredZero: custom?.axisCenteredZero,
|
|
|
|
decimals: xField.config.decimals,
|
|
|
|
padMinBy: 0,
|
|
|
|
padMaxBy: 0,
|
2021-05-05 16:44:31 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
builder.addAxis({
|
2021-05-10 20:24:23 +08:00
|
|
|
scaleKey: xScaleKey,
|
2022-01-20 16:38:32 +08:00
|
|
|
placement: xFieldAxisPlacement,
|
|
|
|
show: xFieldAxisShow,
|
2025-08-29 13:02:49 +08:00
|
|
|
label: custom?.axisLabel,
|
2021-05-05 16:44:31 +08:00
|
|
|
theme,
|
2025-08-29 13:02:49 +08:00
|
|
|
grid: { show: custom?.axisGridShow },
|
2023-06-17 00:10:53 +08:00
|
|
|
formatValue: (v, decimals) => formattedValueToString(xField.display!(v, decimals)),
|
2021-05-05 16:44:31 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-11-06 09:01:26 +08:00
|
|
|
let customRenderedFields =
|
|
|
|
renderers?.flatMap((r) => Object.values(r.fieldMap).filter((name) => r.indicesOnly.indexOf(name) === -1)) ?? [];
|
|
|
|
|
2021-06-11 19:49:26 +08:00
|
|
|
let indexByName: Map<string, number> | undefined;
|
2021-05-05 16:44:31 +08:00
|
|
|
|
2021-05-10 20:24:23 +08:00
|
|
|
for (let i = 1; i < frame.fields.length; i++) {
|
2021-05-05 16:44:31 +08:00
|
|
|
const field = frame.fields[i];
|
2021-11-18 15:55:26 +08:00
|
|
|
|
2022-10-07 22:22:21 +08:00
|
|
|
const config: FieldConfig<GraphFieldConfig> = {
|
2021-11-18 15:55:26 +08:00
|
|
|
...field.config,
|
|
|
|
custom: {
|
|
|
|
...defaultConfig,
|
|
|
|
...field.config.custom,
|
|
|
|
},
|
2022-10-07 22:22:21 +08:00
|
|
|
};
|
2021-11-18 15:55:26 +08:00
|
|
|
|
|
|
|
const customConfig: GraphFieldConfig = config.custom!;
|
2021-05-05 16:44:31 +08:00
|
|
|
|
2023-07-22 00:38:11 +08:00
|
|
|
if (field === xField || (field.type !== FieldType.number && field.type !== FieldType.enum)) {
|
2021-05-05 16:44:31 +08:00
|
|
|
continue;
|
|
|
|
}
|
2021-11-06 09:01:26 +08:00
|
|
|
|
2022-01-10 17:18:56 +08:00
|
|
|
let fmt = field.display ?? defaultFormatter;
|
|
|
|
if (field.config.custom?.stacking?.mode === StackingMode.Percent) {
|
|
|
|
fmt = getDisplayProcessor({
|
|
|
|
field: {
|
|
|
|
...field,
|
|
|
|
config: {
|
|
|
|
...field.config,
|
|
|
|
unit: 'percentunit',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
theme,
|
|
|
|
});
|
|
|
|
}
|
2023-07-22 00:38:11 +08:00
|
|
|
const scaleKey = buildScaleKey(config, field.type);
|
2021-05-05 16:44:31 +08:00
|
|
|
const colorMode = getFieldColorModeForField(field);
|
|
|
|
const scaleColor = getFieldSeriesColor(field, theme);
|
|
|
|
const seriesColor = scaleColor.color;
|
|
|
|
|
|
|
|
// The builder will manage unique scaleKeys and combine where appropriate
|
2021-11-06 09:01:26 +08:00
|
|
|
builder.addScale(
|
2021-11-19 10:31:18 +08:00
|
|
|
tweakScale(
|
|
|
|
{
|
|
|
|
scaleKey,
|
2024-02-29 16:53:00 +08:00
|
|
|
orientation: isHorizontal ? ScaleOrientation.Vertical : ScaleOrientation.Horizontal,
|
|
|
|
direction: isHorizontal ? ScaleDirection.Up : ScaleDirection.Right,
|
2021-11-19 10:31:18 +08:00
|
|
|
distribution: customConfig.scaleDistribution?.type,
|
|
|
|
log: customConfig.scaleDistribution?.log,
|
2022-09-09 00:52:57 +08:00
|
|
|
linearThreshold: customConfig.scaleDistribution?.linearThreshold,
|
2021-11-19 10:31:18 +08:00
|
|
|
min: field.config.min,
|
|
|
|
max: field.config.max,
|
|
|
|
softMin: customConfig.axisSoftMin,
|
|
|
|
softMax: customConfig.axisSoftMax,
|
2022-07-22 12:38:22 +08:00
|
|
|
centeredZero: customConfig.axisCenteredZero,
|
2024-11-05 01:02:09 +08:00
|
|
|
stackingMode: customConfig.stacking?.mode,
|
2022-10-22 05:45:00 +08:00
|
|
|
range:
|
2024-11-05 01:02:09 +08:00
|
|
|
field.type === FieldType.enum
|
2022-10-22 05:45:00 +08:00
|
|
|
? (u: uPlot, dataMin: number, dataMax: number) => {
|
2024-11-05 01:02:09 +08:00
|
|
|
// this is the exhaustive enum (stable)
|
|
|
|
let len = field.config.type!.enum!.text!.length;
|
2023-07-22 00:38:11 +08:00
|
|
|
|
2024-11-05 01:02:09 +08:00
|
|
|
return [-1, len];
|
2023-07-22 00:38:11 +08:00
|
|
|
|
2024-11-05 01:02:09 +08:00
|
|
|
// these are only values that are present
|
|
|
|
// return [dataMin - 1, dataMax + 1]
|
|
|
|
}
|
|
|
|
: undefined,
|
2022-09-07 04:12:09 +08:00
|
|
|
decimals: field.config.decimals,
|
2021-11-19 10:31:18 +08:00
|
|
|
},
|
|
|
|
field
|
|
|
|
)
|
2021-11-06 09:01:26 +08:00
|
|
|
);
|
2021-05-05 16:44:31 +08:00
|
|
|
|
2021-05-10 20:24:23 +08:00
|
|
|
if (!yScaleKey) {
|
|
|
|
yScaleKey = scaleKey;
|
|
|
|
}
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
if (customConfig.axisPlacement !== AxisPlacement.Hidden) {
|
2022-07-19 06:50:07 +08:00
|
|
|
let axisColor: uPlot.Axis.Stroke | undefined;
|
|
|
|
|
|
|
|
if (customConfig.axisColorMode === AxisColorMode.Series) {
|
|
|
|
if (
|
|
|
|
colorMode.isByValue &&
|
|
|
|
field.config.custom?.gradientMode === GraphGradientMode.Scheme &&
|
|
|
|
colorMode.id === FieldColorModeId.Thresholds
|
|
|
|
) {
|
|
|
|
axisColor = getScaleGradientFn(1, theme, colorMode, field.config.thresholds);
|
|
|
|
} else {
|
|
|
|
axisColor = seriesColor;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-31 09:57:44 +08:00
|
|
|
const axisDisplayOptions = {
|
|
|
|
border: {
|
2023-09-20 04:43:15 +08:00
|
|
|
show: customConfig.axisBorderShow || false,
|
2023-09-21 00:15:29 +08:00
|
|
|
width: 1 / devicePixelRatio,
|
2023-08-31 09:57:44 +08:00
|
|
|
stroke: axisColor || theme.colors.text.primary,
|
|
|
|
},
|
|
|
|
ticks: {
|
2023-09-20 04:43:15 +08:00
|
|
|
show: customConfig.axisBorderShow || false,
|
2023-08-31 09:57:44 +08:00
|
|
|
stroke: axisColor || theme.colors.text.primary,
|
|
|
|
},
|
|
|
|
color: axisColor || theme.colors.text.primary,
|
|
|
|
};
|
2022-07-19 06:50:07 +08:00
|
|
|
|
2022-12-10 01:25:44 +08:00
|
|
|
let incrs: uPlot.Axis.Incrs | undefined;
|
|
|
|
|
2023-07-22 00:38:11 +08:00
|
|
|
// TODO: these will be dynamic with frame updates, so need to accept getYTickLabels()
|
|
|
|
let values: uPlot.Axis.Values | undefined;
|
|
|
|
let splits: uPlot.Axis.Splits | undefined;
|
|
|
|
|
2022-12-10 01:25:44 +08:00
|
|
|
if (IEC_UNITS.has(config.unit!)) {
|
|
|
|
incrs = BIN_INCRS;
|
2023-07-22 00:38:11 +08:00
|
|
|
} else if (field.type === FieldType.enum) {
|
|
|
|
let text = field.config.type!.enum!.text!;
|
|
|
|
splits = text.map((v: string, i: number) => i);
|
|
|
|
values = text;
|
2022-12-10 01:25:44 +08:00
|
|
|
}
|
|
|
|
|
2021-11-06 09:01:26 +08:00
|
|
|
builder.addAxis(
|
2021-11-19 10:31:18 +08:00
|
|
|
tweakAxis(
|
|
|
|
{
|
|
|
|
scaleKey,
|
|
|
|
label: customConfig.axisLabel,
|
|
|
|
size: customConfig.axisWidth,
|
2024-07-23 20:56:57 +08:00
|
|
|
placement: isHorizontal ? (customConfig.axisPlacement ?? AxisPlacement.Auto) : AxisPlacement.Bottom,
|
2022-10-22 11:35:29 +08:00
|
|
|
formatValue: (v, decimals) => formattedValueToString(fmt(v, decimals)),
|
2021-11-19 10:31:18 +08:00
|
|
|
theme,
|
|
|
|
grid: { show: customConfig.axisGridShow },
|
2022-09-07 04:12:09 +08:00
|
|
|
decimals: field.config.decimals,
|
2022-09-28 04:50:41 +08:00
|
|
|
distr: customConfig.scaleDistribution?.type,
|
2023-07-22 00:38:11 +08:00
|
|
|
splits,
|
|
|
|
values,
|
2022-12-10 01:25:44 +08:00
|
|
|
incrs,
|
2023-08-31 09:57:44 +08:00
|
|
|
...axisDisplayOptions,
|
2021-11-19 10:31:18 +08:00
|
|
|
},
|
|
|
|
field
|
|
|
|
)
|
2021-11-06 09:01:26 +08:00
|
|
|
);
|
2021-05-05 16:44:31 +08:00
|
|
|
}
|
|
|
|
|
2021-08-26 00:59:03 +08:00
|
|
|
const showPoints =
|
2021-09-21 03:25:56 +08:00
|
|
|
customConfig.drawStyle === GraphDrawStyle.Points ? VisibilityMode.Always : customConfig.showPoints;
|
2021-05-05 16:44:31 +08:00
|
|
|
|
2021-06-18 04:44:26 +08:00
|
|
|
let pointsFilter: uPlot.Series.Points.Filter = () => null;
|
|
|
|
|
2025-07-02 04:03:53 +08:00
|
|
|
if (customConfig.spanNulls !== true && showPoints === VisibilityMode.Auto) {
|
2021-06-18 04:44:26 +08:00
|
|
|
pointsFilter = (u, seriesIdx, show, gaps) => {
|
|
|
|
let filtered = [];
|
|
|
|
|
2025-06-12 04:43:54 +08:00
|
|
|
if (!show) {
|
2022-05-17 12:24:41 +08:00
|
|
|
const yData = u.data[seriesIdx];
|
2021-06-18 04:44:26 +08:00
|
|
|
|
2025-06-12 04:43:54 +08:00
|
|
|
if (gaps && gaps.length) {
|
|
|
|
const firstIdx = u.posToIdx(gaps[0][0], true);
|
2021-06-18 04:44:26 +08:00
|
|
|
|
2025-06-12 04:43:54 +08:00
|
|
|
if (yData[firstIdx - 1] == null) {
|
|
|
|
filtered.push(firstIdx);
|
|
|
|
}
|
|
|
|
|
|
|
|
// show single points between consecutive gaps that share end/start
|
|
|
|
for (let i = 0; i < gaps.length; i++) {
|
|
|
|
let thisGap = gaps[i];
|
|
|
|
let nextGap = gaps[i + 1];
|
|
|
|
|
|
|
|
if (nextGap && thisGap[1] === nextGap[0]) {
|
|
|
|
// approx when data density is > 1pt/px, since gap start/end pixels are rounded
|
|
|
|
let approxIdx = u.posToIdx(thisGap[1], true);
|
|
|
|
|
|
|
|
if (yData[approxIdx] == null) {
|
|
|
|
// scan left/right alternating to find closest index with non-null value
|
|
|
|
for (let j = 1; j < 100; j++) {
|
|
|
|
if (yData[approxIdx + j] != null) {
|
|
|
|
approxIdx += j;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (yData[approxIdx - j] != null) {
|
|
|
|
approxIdx -= j;
|
|
|
|
break;
|
|
|
|
}
|
2022-05-17 12:24:41 +08:00
|
|
|
}
|
|
|
|
}
|
2025-06-12 04:43:54 +08:00
|
|
|
|
|
|
|
filtered.push(approxIdx);
|
2022-05-17 12:24:41 +08:00
|
|
|
}
|
2025-06-12 04:43:54 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const lastIdx = u.posToIdx(gaps[gaps.length - 1][1], true);
|
2022-05-17 12:24:41 +08:00
|
|
|
|
2025-06-12 04:43:54 +08:00
|
|
|
if (yData[lastIdx + 1] == null) {
|
|
|
|
filtered.push(lastIdx);
|
2021-06-18 04:44:26 +08:00
|
|
|
}
|
|
|
|
}
|
2025-07-02 04:03:53 +08:00
|
|
|
// single point
|
2025-06-12 04:43:54 +08:00
|
|
|
else {
|
2025-07-02 04:03:53 +08:00
|
|
|
// scan right
|
|
|
|
let leftIdx = 0;
|
2025-07-04 17:26:43 +08:00
|
|
|
while (yData[leftIdx] === null) {
|
2025-07-02 04:03:53 +08:00
|
|
|
leftIdx++;
|
|
|
|
}
|
|
|
|
|
|
|
|
// scan left
|
|
|
|
let rightIdx = yData.length - 1;
|
2025-07-04 17:26:43 +08:00
|
|
|
while (rightIdx >= leftIdx && yData[rightIdx] === null) {
|
2025-07-02 04:03:53 +08:00
|
|
|
rightIdx--;
|
|
|
|
}
|
|
|
|
|
|
|
|
// render if same
|
|
|
|
if (leftIdx === rightIdx) {
|
|
|
|
filtered.push(leftIdx);
|
2025-06-12 04:43:54 +08:00
|
|
|
}
|
2021-06-18 04:44:26 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return filtered.length ? filtered : null;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
let { fillOpacity } = customConfig;
|
|
|
|
|
2021-11-06 09:01:26 +08:00
|
|
|
let pathBuilder: uPlot.Series.PathBuilder | null = null;
|
|
|
|
let pointsBuilder: uPlot.Series.Points.Show | null = null;
|
|
|
|
|
|
|
|
if (field.state?.origin) {
|
2021-05-05 16:44:31 +08:00
|
|
|
if (!indexByName) {
|
2021-06-11 19:49:26 +08:00
|
|
|
indexByName = getNamesToFieldIndex(frame, allFrames);
|
2021-05-05 16:44:31 +08:00
|
|
|
}
|
2021-06-11 19:49:26 +08:00
|
|
|
|
|
|
|
const originFrame = allFrames[field.state.origin.frameIndex];
|
2021-11-06 09:01:26 +08:00
|
|
|
const originField = originFrame?.fields[field.state.origin.fieldIndex];
|
2021-06-11 19:49:26 +08:00
|
|
|
|
2021-11-06 09:01:26 +08:00
|
|
|
const dispName = getFieldDisplayName(originField ?? field, originFrame, allFrames);
|
|
|
|
|
|
|
|
// disable default renderers
|
|
|
|
if (customRenderedFields.indexOf(dispName) >= 0) {
|
|
|
|
pathBuilder = () => null;
|
|
|
|
pointsBuilder = () => undefined;
|
2022-06-28 08:02:05 +08:00
|
|
|
} else if (customConfig.transform === GraphTransform.Constant) {
|
|
|
|
// patch some monkeys!
|
|
|
|
const defaultBuilder = uPlot.paths!.linear!();
|
|
|
|
|
|
|
|
pathBuilder = (u, seriesIdx) => {
|
|
|
|
//eslint-disable-next-line
|
|
|
|
const _data: any[] = (u as any)._data; // uplot.AlignedData not exposed in types
|
|
|
|
|
|
|
|
// the data we want the line renderer to pull is x at each plot edge with paired flat y values
|
|
|
|
|
|
|
|
const r = getTimeRange();
|
|
|
|
let xData = [r.from.valueOf(), r.to.valueOf()];
|
|
|
|
let firstY = _data[seriesIdx].find((v: number | null | undefined) => v != null);
|
|
|
|
let yData = [firstY, firstY];
|
|
|
|
let fauxData = _data.slice();
|
|
|
|
fauxData[0] = xData;
|
|
|
|
fauxData[seriesIdx] = yData;
|
|
|
|
|
|
|
|
//eslint-disable-next-line
|
|
|
|
return defaultBuilder(
|
|
|
|
{
|
|
|
|
...u,
|
|
|
|
_data: fauxData,
|
|
|
|
} as any,
|
|
|
|
seriesIdx,
|
|
|
|
0,
|
|
|
|
1
|
|
|
|
);
|
|
|
|
};
|
2021-05-05 16:44:31 +08:00
|
|
|
}
|
2021-11-06 09:01:26 +08:00
|
|
|
|
|
|
|
if (customConfig.fillBelowTo) {
|
2022-11-06 22:19:50 +08:00
|
|
|
const fillBelowToField = frame.fields.find(
|
|
|
|
(f) =>
|
|
|
|
customConfig.fillBelowTo === f.name ||
|
|
|
|
customConfig.fillBelowTo === f.config?.displayNameFromDS ||
|
|
|
|
customConfig.fillBelowTo === getFieldDisplayName(f, frame, allFrames)
|
|
|
|
);
|
|
|
|
|
|
|
|
const fillBelowDispName = fillBelowToField
|
|
|
|
? getFieldDisplayName(fillBelowToField, frame, allFrames)
|
|
|
|
: customConfig.fillBelowTo;
|
2022-10-28 14:27:54 +08:00
|
|
|
|
2021-11-06 09:01:26 +08:00
|
|
|
const t = indexByName.get(dispName);
|
2022-10-28 14:27:54 +08:00
|
|
|
const b = indexByName.get(fillBelowDispName);
|
2021-11-06 09:01:26 +08:00
|
|
|
if (isNumber(b) && isNumber(t)) {
|
|
|
|
builder.addBand({
|
|
|
|
series: [t, b],
|
|
|
|
fill: undefined, // using null will have the band use fill options from `t`
|
|
|
|
});
|
2022-01-27 07:06:11 +08:00
|
|
|
|
|
|
|
if (!fillOpacity) {
|
|
|
|
fillOpacity = 35; // default from flot
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
fillOpacity = 0;
|
2021-11-06 09:01:26 +08:00
|
|
|
}
|
2021-05-05 16:44:31 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-04 16:41:03 +08:00
|
|
|
let dynamicSeriesColor: ((seriesIdx: number) => string | undefined) | undefined = undefined;
|
|
|
|
|
|
|
|
if (colorMode.id === FieldColorModeId.Thresholds) {
|
|
|
|
dynamicSeriesColor = (seriesIdx) => getFieldSeriesColor(alignedFrame.fields[seriesIdx], theme).color;
|
|
|
|
}
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
builder.addSeries({
|
2021-11-06 09:01:26 +08:00
|
|
|
pathBuilder,
|
|
|
|
pointsBuilder,
|
2021-05-05 16:44:31 +08:00
|
|
|
scaleKey,
|
|
|
|
showPoints,
|
2021-06-18 04:44:26 +08:00
|
|
|
pointsFilter,
|
2021-05-05 16:44:31 +08:00
|
|
|
colorMode,
|
|
|
|
fillOpacity,
|
|
|
|
theme,
|
2022-05-04 16:41:03 +08:00
|
|
|
dynamicSeriesColor,
|
2021-05-05 16:44:31 +08:00
|
|
|
drawStyle: customConfig.drawStyle!,
|
|
|
|
lineColor: customConfig.lineColor ?? seriesColor,
|
|
|
|
lineWidth: customConfig.lineWidth,
|
|
|
|
lineInterpolation: customConfig.lineInterpolation,
|
|
|
|
lineStyle: customConfig.lineStyle,
|
|
|
|
barAlignment: customConfig.barAlignment,
|
2021-06-01 15:28:25 +08:00
|
|
|
barWidthFactor: customConfig.barWidthFactor,
|
|
|
|
barMaxWidth: customConfig.barMaxWidth,
|
2021-05-05 16:44:31 +08:00
|
|
|
pointSize: customConfig.pointSize,
|
|
|
|
spanNulls: customConfig.spanNulls || false,
|
2021-05-07 03:22:03 +08:00
|
|
|
show: !customConfig.hideFrom?.viz,
|
2021-05-05 16:44:31 +08:00
|
|
|
gradientMode: customConfig.gradientMode,
|
|
|
|
thresholds: config.thresholds,
|
2021-08-30 04:46:17 +08:00
|
|
|
hardMin: field.config.min,
|
|
|
|
hardMax: field.config.max,
|
|
|
|
softMin: customConfig.axisSoftMin,
|
|
|
|
softMax: customConfig.axisSoftMax,
|
2021-05-05 16:44:31 +08:00
|
|
|
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
|
|
|
dataFrameFieldIndex: field.state?.origin,
|
2025-08-23 20:33:45 +08:00
|
|
|
showValues: customConfig.showValues,
|
2021-05-05 16:44:31 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
// Render thresholds in graph
|
|
|
|
if (customConfig.thresholdsStyle && config.thresholds) {
|
2024-02-07 00:20:42 +08:00
|
|
|
const thresholdDisplay = customConfig.thresholdsStyle.mode ?? GraphThresholdsStyleMode.Off;
|
|
|
|
if (thresholdDisplay !== GraphThresholdsStyleMode.Off) {
|
2021-05-05 16:44:31 +08:00
|
|
|
builder.addThresholds({
|
|
|
|
config: customConfig.thresholdsStyle,
|
|
|
|
thresholds: config.thresholds,
|
|
|
|
scaleKey,
|
|
|
|
theme,
|
2021-08-30 04:46:17 +08:00
|
|
|
hardMin: field.config.min,
|
|
|
|
hardMax: field.config.max,
|
|
|
|
softMin: customConfig.axisSoftMin,
|
|
|
|
softMax: customConfig.axisSoftMax,
|
2021-05-05 16:44:31 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-13 23:29:03 +08:00
|
|
|
let stackingGroups = getStackingGroups(frame);
|
|
|
|
|
|
|
|
builder.setStackingGroups(stackingGroups);
|
2021-05-10 20:24:23 +08:00
|
|
|
|
2025-08-23 20:33:45 +08:00
|
|
|
const mightShowValues = frame.fields.some((field, i) => {
|
|
|
|
if (i === 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const customConfig = field.config.custom ?? {};
|
|
|
|
|
|
|
|
return (
|
|
|
|
customConfig.showValues &&
|
|
|
|
(customConfig.drawStyle === GraphDrawStyle.Points || customConfig.showPoints !== VisibilityMode.Never)
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
if (mightShowValues) {
|
|
|
|
// since bars style doesnt show points in Auto mode, we can't piggyback on series.points.show()
|
|
|
|
// so we make a simple density-based callback to use here
|
|
|
|
const barsShowValues = (u: uPlot) => {
|
|
|
|
let width = u.bbox.width / uPlot.pxRatio;
|
|
|
|
let count = u.data[0].length;
|
|
|
|
|
|
|
|
// render values when each has at least 30px of width available
|
|
|
|
return width / count >= 30;
|
|
|
|
};
|
|
|
|
|
|
|
|
builder.addHook('draw', (u: uPlot) => {
|
|
|
|
const baseFontSize = 12;
|
|
|
|
const font = `${baseFontSize * uPlot.pxRatio}px ${theme.typography.fontFamily}`;
|
|
|
|
|
|
|
|
const { ctx } = u;
|
|
|
|
|
|
|
|
ctx.save();
|
|
|
|
ctx.fillStyle = theme.colors.text.primary;
|
|
|
|
ctx.font = font;
|
|
|
|
ctx.textAlign = 'center';
|
|
|
|
|
|
|
|
for (let seriesIdx = 1; seriesIdx < u.data.length; seriesIdx++) {
|
|
|
|
const series = u.series[seriesIdx];
|
|
|
|
const field = frame.fields[seriesIdx];
|
|
|
|
|
|
|
|
if (
|
|
|
|
field.config.custom?.showValues &&
|
|
|
|
// @ts-ignore points.show() is always callable on the instance (but may be boolean when passed to uPlot as init option)
|
|
|
|
(series.points?.show?.(u, seriesIdx) ||
|
|
|
|
(field.config.custom?.drawStyle === DrawStyle.Bars && barsShowValues(u)))
|
|
|
|
) {
|
|
|
|
const xData = u.data[0];
|
|
|
|
const yData = u.data[seriesIdx];
|
|
|
|
const yScale = series.scale!;
|
|
|
|
|
|
|
|
for (let dataIdx = 0; dataIdx < yData.length; dataIdx++) {
|
|
|
|
const yVal = yData[dataIdx];
|
|
|
|
|
|
|
|
if (yVal != null) {
|
|
|
|
const text = formattedValueToString(field.display!(yVal));
|
|
|
|
|
|
|
|
const isNegative = yVal < 0;
|
|
|
|
const textOffset = isNegative ? 15 : -5;
|
|
|
|
ctx.textBaseline = isNegative ? 'top' : 'bottom';
|
|
|
|
|
|
|
|
const xVal = xData[dataIdx];
|
|
|
|
const x = u.valToPos(xVal, 'x', true);
|
|
|
|
const y = u.valToPos(yVal, yScale, true);
|
|
|
|
|
|
|
|
ctx.fillText(text, x, y + textOffset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.restore();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-11-06 09:01:26 +08:00
|
|
|
// hook up custom/composite renderers
|
|
|
|
renderers?.forEach((r) => {
|
2021-11-20 08:39:21 +08:00
|
|
|
if (!indexByName) {
|
|
|
|
indexByName = getNamesToFieldIndex(frame, allFrames);
|
|
|
|
}
|
2021-11-06 09:01:26 +08:00
|
|
|
let fieldIndices: Record<string, number> = {};
|
|
|
|
|
|
|
|
for (let key in r.fieldMap) {
|
|
|
|
let dispName = r.fieldMap[key];
|
2021-11-20 08:39:21 +08:00
|
|
|
fieldIndices[key] = indexByName.get(dispName)!;
|
2021-11-06 09:01:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
r.init(builder, fieldIndices);
|
|
|
|
});
|
|
|
|
|
2021-07-24 05:05:09 +08:00
|
|
|
// if hovered value is null, how far we may scan left/right to hover nearest non-null
|
2024-02-16 06:29:36 +08:00
|
|
|
const DEFAULT_HOVER_NULL_PROXIMITY = 15;
|
|
|
|
const DEFAULT_FOCUS_PROXIMITY = 30;
|
2021-07-24 05:05:09 +08:00
|
|
|
|
|
|
|
let cursor: Partial<uPlot.Cursor> = {
|
2024-02-16 06:29:36 +08:00
|
|
|
// horizontal proximity / point hover behavior
|
2024-01-30 05:34:43 +08:00
|
|
|
hover: {
|
|
|
|
prox: (self, seriesIdx, hoveredIdx) => {
|
2024-02-16 06:29:36 +08:00
|
|
|
if (hoverProximity != null) {
|
|
|
|
return hoverProximity;
|
|
|
|
}
|
|
|
|
|
|
|
|
// when hovering null values, scan data left/right up to 15px
|
2024-01-30 05:34:43 +08:00
|
|
|
const yVal = self.data[seriesIdx][hoveredIdx];
|
|
|
|
if (yVal === null) {
|
2024-02-16 06:29:36 +08:00
|
|
|
return DEFAULT_HOVER_NULL_PROXIMITY;
|
2021-07-24 05:05:09 +08:00
|
|
|
}
|
|
|
|
|
2024-02-16 06:29:36 +08:00
|
|
|
// no proximity limit
|
2024-01-30 05:34:43 +08:00
|
|
|
return null;
|
|
|
|
},
|
|
|
|
skip: [null],
|
2021-07-24 05:05:09 +08:00
|
|
|
},
|
2024-02-16 06:29:36 +08:00
|
|
|
// vertical proximity / series focus behavior
|
|
|
|
focus: {
|
|
|
|
prox: hoverProximity ?? DEFAULT_FOCUS_PROXIMITY,
|
|
|
|
},
|
2025-03-19 00:06:10 +08:00
|
|
|
points: { one: true },
|
2021-07-24 05:05:09 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
builder.setCursor(cursor);
|
|
|
|
|
2021-05-05 16:44:31 +08:00
|
|
|
return builder;
|
|
|
|
};
|
|
|
|
|
2024-04-03 04:32:46 +08:00
|
|
|
function getNamesToFieldIndex(frame: DataFrame, allFrames: DataFrame[]): Map<string, number> {
|
2021-06-11 19:49:26 +08:00
|
|
|
const originNames = new Map<string, number>();
|
2021-11-06 09:01:26 +08:00
|
|
|
frame.fields.forEach((field, i) => {
|
|
|
|
const origin = field.state?.origin;
|
2021-06-11 19:49:26 +08:00
|
|
|
if (origin) {
|
2021-11-06 09:01:26 +08:00
|
|
|
const origField = allFrames[origin.frameIndex]?.fields[origin.fieldIndex];
|
|
|
|
if (origField) {
|
|
|
|
originNames.set(getFieldDisplayName(origField, allFrames[origin.frameIndex], allFrames), i);
|
|
|
|
}
|
2021-06-11 19:49:26 +08:00
|
|
|
}
|
2021-11-06 09:01:26 +08:00
|
|
|
});
|
2021-06-11 19:49:26 +08:00
|
|
|
return originNames;
|
2021-05-05 16:44:31 +08:00
|
|
|
}
|
2025-09-25 01:48:22 +08:00
|
|
|
|
2025-09-27 02:42:59 +08:00
|
|
|
export function calculateAnnotationLaneSizes(
|
|
|
|
annotationLanes = 0,
|
|
|
|
annotationConfig?: common.VizAnnotations
|
|
|
|
): Pick<AxisProps, 'size' | 'gap' | 'ticks'> {
|
2025-10-08 01:53:21 +08:00
|
|
|
if (annotationConfig?.multiLane && annotationLanes > 1) {
|
2025-09-27 02:42:59 +08:00
|
|
|
const annotationLanesSize = annotationLanes * ANNOTATION_LANE_SIZE;
|
|
|
|
// Add an extra lane's worth of height below the annotation lanes in order to show the gridlines through the annotation lanes
|
|
|
|
const axisSize = annotationLanes > 0 ? annotationLanesSize + UPLOT_DEFAULT_AXIS_GAP : 0;
|
|
|
|
// Consistent gap between gridlines and x-axis labels
|
|
|
|
const gap = UPLOT_DEFAULT_AXIS_GAP;
|
|
|
|
// Axis size is: default size + gap size + annotationLaneSize
|
|
|
|
const size = annotationLanes > 0 ? UPLOT_DEFAULT_AXIS_SIZE + gap + annotationLanesSize : UPLOT_DEFAULT_AXIS_SIZE;
|
|
|
|
|
|
|
|
return {
|
|
|
|
size,
|
|
|
|
gap,
|
|
|
|
ticks: {
|
|
|
|
size: axisSize,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {};
|
2025-09-25 01:48:22 +08:00
|
|
|
}
|