mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
692 lines
18 KiB
692 lines
18 KiB
![]()
4 years ago
|
import React from 'react';
|
||
![]()
3 years ago
|
import uPlot from 'uplot';
|
||
|
|
||
![]()
4 years ago
|
import {
|
||
![]()
4 years ago
|
ArrayVector,
|
||
![]()
4 years ago
|
DataFrame,
|
||
![]()
4 years ago
|
DashboardCursorSync,
|
||
|
DataHoverPayload,
|
||
|
DataHoverEvent,
|
||
|
DataHoverClearEvent,
|
||
![]()
4 years ago
|
FALLBACK_COLOR,
|
||
|
Field,
|
||
|
FieldColorModeId,
|
||
![]()
4 years ago
|
FieldConfig,
|
||
![]()
4 years ago
|
FieldType,
|
||
![]()
4 years ago
|
formattedValueToString,
|
||
|
getFieldDisplayName,
|
||
![]()
4 years ago
|
getValueFormat,
|
||
![]()
4 years ago
|
GrafanaTheme2,
|
||
![]()
4 years ago
|
getActiveThreshold,
|
||
|
Threshold,
|
||
|
getFieldConfigWithMinMax,
|
||
![]()
4 years ago
|
ThresholdsMode,
|
||
![]()
3 years ago
|
TimeRange,
|
||
![]()
4 years ago
|
} from '@grafana/data';
|
||
![]()
3 years ago
|
import { maybeSortFrame } from '@grafana/data/src/transformations/transformers/joinDataFrames';
|
||
![]()
2 years ago
|
import {
|
||
|
VizLegendOptions,
|
||
|
AxisPlacement,
|
||
|
ScaleDirection,
|
||
|
ScaleOrientation,
|
||
|
VisibilityMode,
|
||
|
TimelineValueAlignment,
|
||
|
HideableFieldConfig,
|
||
|
} from '@grafana/schema';
|
||
![]()
4 years ago
|
import {
|
||
|
FIXED_UNIT,
|
||
|
SeriesVisibilityChangeMode,
|
||
![]()
4 years ago
|
UPlotConfigBuilder,
|
||
![]()
4 years ago
|
UPlotConfigPrepFn,
|
||
|
VizLegendItem,
|
||
|
} from '@grafana/ui';
|
||
![]()
3 years ago
|
import { applyNullInsertThreshold } from '@grafana/ui/src/components/GraphNG/nullInsertThreshold';
|
||
|
import { nullToValue } from '@grafana/ui/src/components/GraphNG/nullToValue';
|
||
![]()
4 years ago
|
import { PlotTooltipInterpolator } from '@grafana/ui/src/components/uPlot/types';
|
||
![]()
3 years ago
|
import { preparePlotData2, getStackingGroups } from '@grafana/ui/src/components/uPlot/utils';
|
||
![]()
3 years ago
|
|
||
|
import { getConfig, TimelineCoreOptions } from './timeline';
|
||
![]()
4 years ago
|
|
||
![]()
2 years ago
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
interface UPlotConfigOptions {
|
||
|
frame: DataFrame;
|
||
|
theme: GrafanaTheme2;
|
||
|
mode: TimelineMode;
|
||
|
sync?: () => DashboardCursorSync;
|
||
|
rowHeight?: number;
|
||
|
colWidth?: number;
|
||
|
showValue: VisibilityMode;
|
||
|
alignValue?: TimelineValueAlignment;
|
||
|
mergeValues?: boolean;
|
||
|
getValueColor: (frameIdx: number, fieldIdx: number, value: any) => string;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @internal
|
||
|
*/
|
||
|
interface PanelFieldConfig extends HideableFieldConfig {
|
||
|
fillOpacity?: number;
|
||
|
lineWidth?: number;
|
||
|
}
|
||
|
|
||
|
export enum TimelineMode {
|
||
|
Changes = 'changes',
|
||
|
Samples = 'samples',
|
||
|
}
|
||
|
|
||
|
const defaultConfig: PanelFieldConfig = {
|
||
![]()
4 years ago
|
lineWidth: 0,
|
||
|
fillOpacity: 80,
|
||
|
};
|
||
|
|
||
![]()
4 years ago
|
export function mapMouseEventToMode(event: React.MouseEvent): SeriesVisibilityChangeMode {
|
||
![]()
4 years ago
|
if (event.ctrlKey || event.metaKey || event.shiftKey) {
|
||
![]()
4 years ago
|
return SeriesVisibilityChangeMode.AppendToSelection;
|
||
![]()
4 years ago
|
}
|
||
![]()
4 years ago
|
return SeriesVisibilityChangeMode.ToggleSelection;
|
||
![]()
4 years ago
|
}
|
||
|
|
||
![]()
2 years ago
|
export const preparePlotConfigBuilder: UPlotConfigPrepFn<UPlotConfigOptions> = ({
|
||
![]()
4 years ago
|
frame,
|
||
|
theme,
|
||
![]()
3 years ago
|
timeZones,
|
||
![]()
4 years ago
|
getTimeRange,
|
||
|
mode,
|
||
![]()
4 years ago
|
eventBus,
|
||
|
sync,
|
||
![]()
4 years ago
|
rowHeight,
|
||
|
colWidth,
|
||
|
showValue,
|
||
![]()
4 years ago
|
alignValue,
|
||
![]()
3 years ago
|
mergeValues,
|
||
![]()
3 years ago
|
getValueColor,
|
||
![]()
4 years ago
|
}) => {
|
||
![]()
3 years ago
|
const builder = new UPlotConfigBuilder(timeZones[0]);
|
||
![]()
4 years ago
|
|
||
![]()
4 years ago
|
const xScaleUnit = 'time';
|
||
|
const xScaleKey = 'x';
|
||
|
|
||
![]()
4 years ago
|
const isDiscrete = (field: Field) => {
|
||
|
const mode = field.config?.color?.mode;
|
||
|
return !(mode && field.display && mode.startsWith('continuous-'));
|
||
|
};
|
||
|
|
||
![]()
3 years ago
|
const getValueColorFn = (seriesIdx: number, value: any) => {
|
||
![]()
4 years ago
|
const field = frame.fields[seriesIdx];
|
||
![]()
4 years ago
|
|
||
![]()
3 years ago
|
if (
|
||
|
field.state?.origin?.fieldIndex !== undefined &&
|
||
|
field.state?.origin?.frameIndex !== undefined &&
|
||
|
getValueColor
|
||
|
) {
|
||
|
return getValueColor(field.state?.origin?.frameIndex, field.state?.origin?.fieldIndex, value);
|
||
![]()
4 years ago
|
}
|
||
![]()
4 years ago
|
|
||
|
return FALLBACK_COLOR;
|
||
![]()
4 years ago
|
};
|
||
|
|
||
|
const opts: TimelineCoreOptions = {
|
||
![]()
4 years ago
|
mode: mode!,
|
||
![]()
4 years ago
|
numSeries: frame.fields.length - 1,
|
||
|
isDiscrete: (seriesIdx) => isDiscrete(frame.fields[seriesIdx]),
|
||
![]()
3 years ago
|
mergeValues,
|
||
![]()
2 years ago
|
rowHeight: rowHeight,
|
||
![]()
4 years ago
|
colWidth: colWidth,
|
||
|
showValue: showValue!,
|
||
![]()
4 years ago
|
alignValue,
|
||
![]()
4 years ago
|
theme,
|
||
![]()
4 years ago
|
label: (seriesIdx) => getFieldDisplayName(frame.fields[seriesIdx], frame),
|
||
![]()
4 years ago
|
getFieldConfig: (seriesIdx) => frame.fields[seriesIdx].config.custom,
|
||
![]()
3 years ago
|
getValueColor: getValueColorFn,
|
||
![]()
4 years ago
|
getTimeRange,
|
||
|
// hardcoded formatter for state values
|
||
|
formatValue: (seriesIdx, value) => formattedValueToString(frame.fields[seriesIdx].display!(value)),
|
||
![]()
4 years ago
|
onHover: (seriesIndex, valueIndex) => {
|
||
|
hoveredSeriesIdx = seriesIndex;
|
||
|
hoveredDataIdx = valueIndex;
|
||
![]()
4 years ago
|
shouldChangeHover = true;
|
||
![]()
4 years ago
|
},
|
||
![]()
4 years ago
|
onLeave: () => {
|
||
|
hoveredSeriesIdx = null;
|
||
|
hoveredDataIdx = null;
|
||
![]()
4 years ago
|
shouldChangeHover = true;
|
||
![]()
4 years ago
|
},
|
||
|
};
|
||
|
|
||
![]()
4 years ago
|
let shouldChangeHover = false;
|
||
![]()
4 years ago
|
let hoveredSeriesIdx: number | null = null;
|
||
|
let hoveredDataIdx: number | null = null;
|
||
|
|
||
![]()
4 years ago
|
const coreConfig = getConfig(opts);
|
||
![]()
4 years ago
|
const payload: DataHoverPayload = {
|
||
|
point: {
|
||
|
[xScaleUnit]: null,
|
||
|
[FIXED_UNIT]: null,
|
||
|
},
|
||
|
data: frame,
|
||
|
};
|
||
![]()
4 years ago
|
|
||
|
builder.addHook('init', coreConfig.init);
|
||
|
builder.addHook('drawClear', coreConfig.drawClear);
|
||
|
builder.addHook('setCursor', coreConfig.setCursor);
|
||
|
|
||
![]()
4 years ago
|
// in TooltipPlugin, this gets invoked and the result is bound to a setCursor hook
|
||
|
// which fires after the above setCursor hook, so can take advantage of hoveringOver
|
||
|
// already set by the above onHover/onLeave callbacks that fire from coreConfig.setCursor
|
||
|
const interpolateTooltip: PlotTooltipInterpolator = (
|
||
|
updateActiveSeriesIdx,
|
||
|
updateActiveDatapointIdx,
|
||
|
updateTooltipPosition
|
||
![]()
4 years ago
|
) => {
|
||
![]()
4 years ago
|
if (shouldChangeHover) {
|
||
|
if (hoveredSeriesIdx != null) {
|
||
|
updateActiveSeriesIdx(hoveredSeriesIdx);
|
||
|
updateActiveDatapointIdx(hoveredDataIdx);
|
||
|
}
|
||
|
|
||
|
shouldChangeHover = false;
|
||
![]()
4 years ago
|
}
|
||
![]()
4 years ago
|
|
||
|
updateTooltipPosition(hoveredSeriesIdx == null);
|
||
![]()
4 years ago
|
};
|
||
|
|
||
|
builder.setTooltipInterpolator(interpolateTooltip);
|
||
|
|
||
![]()
3 years ago
|
builder.setPrepData((frames) => preparePlotData2(frames[0], getStackingGroups(frames[0])));
|
||
![]()
4 years ago
|
|
||
![]()
4 years ago
|
builder.setCursor(coreConfig.cursor);
|
||
|
|
||
|
builder.addScale({
|
||
![]()
4 years ago
|
scaleKey: xScaleKey,
|
||
![]()
4 years ago
|
isTime: true,
|
||
|
orientation: ScaleOrientation.Horizontal,
|
||
|
direction: ScaleDirection.Right,
|
||
|
range: coreConfig.xRange,
|
||
|
});
|
||
|
|
||
|
builder.addScale({
|
||
|
scaleKey: FIXED_UNIT, // y
|
||
|
isTime: false,
|
||
|
orientation: ScaleOrientation.Vertical,
|
||
|
direction: ScaleDirection.Up,
|
||
|
range: coreConfig.yRange,
|
||
|
});
|
||
|
|
||
|
builder.addAxis({
|
||
![]()
4 years ago
|
scaleKey: xScaleKey,
|
||
![]()
4 years ago
|
isTime: true,
|
||
|
splits: coreConfig.xSplits!,
|
||
|
placement: AxisPlacement.Bottom,
|
||
![]()
3 years ago
|
timeZone: timeZones[0],
|
||
![]()
4 years ago
|
theme,
|
||
![]()
4 years ago
|
grid: { show: true },
|
||
![]()
4 years ago
|
});
|
||
|
|
||
|
builder.addAxis({
|
||
|
scaleKey: FIXED_UNIT, // y
|
||
|
isTime: false,
|
||
|
placement: AxisPlacement.Left,
|
||
|
splits: coreConfig.ySplits,
|
||
|
values: coreConfig.yValues,
|
||
![]()
4 years ago
|
grid: { show: false },
|
||
![]()
4 years ago
|
ticks: { show: false },
|
||
![]()
4 years ago
|
gap: 16,
|
||
|
theme,
|
||
|
});
|
||
|
|
||
|
let seriesIndex = 0;
|
||
|
|
||
|
for (let i = 0; i < frame.fields.length; i++) {
|
||
|
if (i === 0) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const field = frame.fields[i];
|
||
![]()
2 years ago
|
const config: FieldConfig<PanelFieldConfig> = field.config;
|
||
|
const customConfig: PanelFieldConfig = {
|
||
![]()
4 years ago
|
...defaultConfig,
|
||
|
...config.custom,
|
||
|
};
|
||
|
|
||
|
field.state!.seriesIndex = seriesIndex++;
|
||
|
|
||
![]()
4 years ago
|
// const scaleKey = config.unit || FIXED_UNIT;
|
||
|
// const colorMode = getFieldColorModeForField(field);
|
||
![]()
4 years ago
|
|
||
|
builder.addSeries({
|
||
|
scaleKey: FIXED_UNIT,
|
||
|
pathBuilder: coreConfig.drawPaths,
|
||
|
pointsBuilder: coreConfig.drawPoints,
|
||
|
//colorMode,
|
||
![]()
4 years ago
|
lineWidth: customConfig.lineWidth,
|
||
|
fillOpacity: customConfig.fillOpacity,
|
||
![]()
4 years ago
|
theme,
|
||
![]()
4 years ago
|
show: !customConfig.hideFrom?.viz,
|
||
![]()
4 years ago
|
thresholds: config.thresholds,
|
||
|
// The following properties are not used in the uPlot config, but are utilized as transport for legend config
|
||
|
dataFrameFieldIndex: field.state?.origin,
|
||
|
});
|
||
|
}
|
||
|
|
||
![]()
3 years ago
|
if (sync && sync() !== DashboardCursorSync.Off) {
|
||
![]()
4 years ago
|
let cursor: Partial<uPlot.Cursor> = {};
|
||
|
|
||
|
cursor.sync = {
|
||
|
key: '__global_',
|
||
|
filters: {
|
||
|
pub: (type: string, src: uPlot, x: number, y: number, w: number, h: number, dataIdx: number) => {
|
||
![]()
3 years ago
|
if (sync && sync() === DashboardCursorSync.Off) {
|
||
|
return false;
|
||
|
}
|
||
![]()
4 years ago
|
payload.rowIndex = dataIdx;
|
||
|
if (x < 0 && y < 0) {
|
||
|
payload.point[xScaleUnit] = null;
|
||
|
payload.point[FIXED_UNIT] = null;
|
||
|
eventBus.publish(new DataHoverClearEvent());
|
||
|
} else {
|
||
|
payload.point[xScaleUnit] = src.posToVal(x, xScaleKey);
|
||
|
payload.point.panelRelY = y > 0 ? y / h : 1; // used for old graph panel to position tooltip
|
||
|
payload.down = undefined;
|
||
|
eventBus.publish(new DataHoverEvent(payload));
|
||
|
}
|
||
|
return true;
|
||
|
},
|
||
|
},
|
||
![]()
3 years ago
|
scales: [xScaleKey, null],
|
||
![]()
4 years ago
|
};
|
||
|
builder.setSync();
|
||
|
builder.setCursor(cursor);
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
return builder;
|
||
![]()
4 years ago
|
};
|
||
![]()
4 years ago
|
|
||
|
export function getNamesToFieldIndex(frame: DataFrame): Map<string, number> {
|
||
|
const names = new Map<string, number>();
|
||
|
for (let i = 0; i < frame.fields.length; i++) {
|
||
|
names.set(getFieldDisplayName(frame.fields[i], frame), i);
|
||
|
}
|
||
|
return names;
|
||
|
}
|
||
![]()
4 years ago
|
|
||
|
/**
|
||
|
* If any sequential duplicate values exist, this will return a new array
|
||
|
* with the future values set to undefined.
|
||
|
*
|
||
|
* in: 1, 1,undefined, 1,2, 2,null,2,3
|
||
|
* out: 1,undefined,undefined,undefined,2,undefined,null,2,3
|
||
|
*/
|
||
![]()
3 years ago
|
export function unsetSameFutureValues(values: unknown[]): unknown[] | undefined {
|
||
![]()
4 years ago
|
let prevVal = values[0];
|
||
![]()
3 years ago
|
let clone: unknown[] | undefined = undefined;
|
||
![]()
4 years ago
|
|
||
|
for (let i = 1; i < values.length; i++) {
|
||
|
let value = values[i];
|
||
|
|
||
|
if (value === null) {
|
||
|
prevVal = null;
|
||
|
} else {
|
||
|
if (value === prevVal) {
|
||
|
if (!clone) {
|
||
|
clone = [...values];
|
||
|
}
|
||
|
clone[i] = undefined;
|
||
|
} else if (value != null) {
|
||
|
prevVal = value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return clone;
|
||
|
}
|
||
|
|
||
![]()
3 years ago
|
function getSpanNulls(field: Field) {
|
||
|
let spanNulls = field.config.custom?.spanNulls;
|
||
|
|
||
|
// magic value for join() to leave nulls alone instead of expanding null ranges
|
||
|
// should be set to -1 when spanNulls = null|undefined|false|0, which is "retain nulls, without expanding"
|
||
|
// Infinity is not optimal here since it causes spanNulls to be more expensive than simply removing all nulls unconditionally
|
||
|
return !spanNulls ? -1 : spanNulls === true ? Infinity : spanNulls;
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
/**
|
||
|
* Merge values by the threshold
|
||
|
*/
|
||
|
export function mergeThresholdValues(field: Field, theme: GrafanaTheme2): Field | undefined {
|
||
|
const thresholds = field.config.thresholds;
|
||
|
if (field.type !== FieldType.number || !thresholds || !thresholds.steps.length) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
const items = getThresholdItems(field.config, theme);
|
||
|
if (items.length !== thresholds.steps.length) {
|
||
|
return undefined; // should not happen
|
||
|
}
|
||
|
|
||
|
const thresholdToText = new Map<Threshold, string>();
|
||
|
const textToColor = new Map<string, string>();
|
||
|
for (let i = 0; i < items.length; i++) {
|
||
|
thresholdToText.set(thresholds.steps[i], items[i].label);
|
||
|
textToColor.set(items[i].label, items[i].color!);
|
||
|
}
|
||
|
|
||
|
let input = field.values.toArray();
|
||
|
const vals = new Array<String | undefined>(field.values.length);
|
||
|
if (thresholds.mode === ThresholdsMode.Percentage) {
|
||
|
const { min, max } = getFieldConfigWithMinMax(field);
|
||
|
const delta = max! - min!;
|
||
|
input = input.map((v) => {
|
||
|
if (v == null) {
|
||
|
return v;
|
||
|
}
|
||
|
return ((v - min!) / delta) * 100;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < vals.length; i++) {
|
||
|
const v = input[i];
|
||
|
if (v == null) {
|
||
|
vals[i] = v;
|
||
|
} else {
|
||
![]()
3 years ago
|
vals[i] = thresholdToText.get(getActiveThreshold(v, thresholds.steps));
|
||
![]()
4 years ago
|
}
|
||
|
}
|
||
|
|
||
|
return {
|
||
|
...field,
|
||
![]()
3 years ago
|
config: {
|
||
|
...field.config,
|
||
|
custom: {
|
||
|
...field.config.custom,
|
||
![]()
3 years ago
|
spanNulls: getSpanNulls(field),
|
||
![]()
3 years ago
|
},
|
||
|
},
|
||
![]()
4 years ago
|
type: FieldType.string,
|
||
|
values: new ArrayVector(vals),
|
||
|
display: (value: string) => ({
|
||
|
text: value,
|
||
|
color: textToColor.get(value),
|
||
|
numeric: NaN,
|
||
|
}),
|
||
|
};
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
// This will return a set of frames with only graphable values included
|
||
|
export function prepareTimelineFields(
|
||
|
series: DataFrame[] | undefined,
|
||
![]()
4 years ago
|
mergeValues: boolean,
|
||
![]()
3 years ago
|
timeRange: TimeRange,
|
||
![]()
4 years ago
|
theme: GrafanaTheme2
|
||
![]()
4 years ago
|
): { frames?: DataFrame[]; warn?: string } {
|
||
|
if (!series?.length) {
|
||
|
return { warn: 'No data in response' };
|
||
|
}
|
||
|
let hasTimeseries = false;
|
||
|
const frames: DataFrame[] = [];
|
||
![]()
3 years ago
|
|
||
![]()
4 years ago
|
for (let frame of series) {
|
||
|
let isTimeseries = false;
|
||
|
let changed = false;
|
||
![]()
3 years ago
|
let maybeSortedFrame = maybeSortFrame(
|
||
|
frame,
|
||
|
frame.fields.findIndex((f) => f.type === FieldType.time)
|
||
|
);
|
||
![]()
3 years ago
|
|
||
|
let nulledFrame = applyNullInsertThreshold({
|
||
![]()
3 years ago
|
frame: maybeSortedFrame,
|
||
![]()
3 years ago
|
refFieldPseudoMin: timeRange.from.valueOf(),
|
||
|
refFieldPseudoMax: timeRange.to.valueOf(),
|
||
|
});
|
||
|
|
||
![]()
3 years ago
|
if (nulledFrame !== frame) {
|
||
|
changed = true;
|
||
|
}
|
||
![]()
3 years ago
|
|
||
![]()
4 years ago
|
const fields: Field[] = [];
|
||
![]()
3 years ago
|
for (let field of nullToValue(nulledFrame).fields) {
|
||
![]()
4 years ago
|
switch (field.type) {
|
||
|
case FieldType.time:
|
||
|
isTimeseries = true;
|
||
|
hasTimeseries = true;
|
||
|
fields.push(field);
|
||
|
break;
|
||
|
case FieldType.number:
|
||
![]()
4 years ago
|
if (mergeValues && field.config.color?.mode === FieldColorModeId.Thresholds) {
|
||
|
const f = mergeThresholdValues(field, theme);
|
||
|
if (f) {
|
||
|
fields.push(f);
|
||
|
changed = true;
|
||
|
continue;
|
||
|
}
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
case FieldType.boolean:
|
||
|
case FieldType.string:
|
||
![]()
4 years ago
|
field = {
|
||
|
...field,
|
||
|
config: {
|
||
|
...field.config,
|
||
|
custom: {
|
||
|
...field.config.custom,
|
||
![]()
3 years ago
|
spanNulls: getSpanNulls(field),
|
||
![]()
4 years ago
|
},
|
||
|
},
|
||
|
};
|
||
![]()
4 years ago
|
fields.push(field);
|
||
|
break;
|
||
|
default:
|
||
|
changed = true;
|
||
|
}
|
||
|
}
|
||
|
if (isTimeseries && fields.length > 1) {
|
||
|
hasTimeseries = true;
|
||
|
if (changed) {
|
||
|
frames.push({
|
||
![]()
3 years ago
|
...maybeSortedFrame,
|
||
![]()
4 years ago
|
fields,
|
||
|
});
|
||
|
} else {
|
||
![]()
3 years ago
|
frames.push(maybeSortedFrame);
|
||
![]()
4 years ago
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!hasTimeseries) {
|
||
|
return { warn: 'Data does not have a time field' };
|
||
|
}
|
||
|
if (!frames.length) {
|
||
|
return { warn: 'No graphable fields' };
|
||
|
}
|
||
|
return { frames };
|
||
|
}
|
||
![]()
4 years ago
|
|
||
![]()
4 years ago
|
export function getThresholdItems(fieldConfig: FieldConfig, theme: GrafanaTheme2): VizLegendItem[] {
|
||
|
const items: VizLegendItem[] = [];
|
||
|
const thresholds = fieldConfig.thresholds;
|
||
|
if (!thresholds || !thresholds.steps.length) {
|
||
|
return items;
|
||
|
}
|
||
|
|
||
|
const steps = thresholds.steps;
|
||
|
const disp = getValueFormat(thresholds.mode === ThresholdsMode.Percentage ? 'percent' : fieldConfig.unit ?? '');
|
||
|
|
||
|
const fmt = (v: number) => formattedValueToString(disp(v));
|
||
|
|
||
![]()
3 years ago
|
for (let i = 0; i < steps.length; i++) {
|
||
|
let step = steps[i];
|
||
|
let value = step.value;
|
||
|
let pre = '';
|
||
|
let suf = '';
|
||
|
|
||
|
if (value === -Infinity && i < steps.length - 1) {
|
||
|
value = steps[i + 1].value;
|
||
|
pre = '< ';
|
||
|
} else {
|
||
|
suf = '+';
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
items.push({
|
||
![]()
3 years ago
|
label: `${pre}${fmt(value)}${suf}`,
|
||
![]()
4 years ago
|
color: theme.visualization.getColorByName(step.color),
|
||
|
yAxis: 1,
|
||
|
});
|
||
|
}
|
||
|
|
||
|
return items;
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
export function prepareTimelineLegendItems(
|
||
|
frames: DataFrame[] | undefined,
|
||
![]()
4 years ago
|
options: VizLegendOptions,
|
||
|
theme: GrafanaTheme2
|
||
![]()
4 years ago
|
): VizLegendItem[] | undefined {
|
||
![]()
3 years ago
|
if (!frames || options.showLegend === false) {
|
||
![]()
4 years ago
|
return undefined;
|
||
|
}
|
||
|
|
||
![]()
4 years ago
|
return getFieldLegendItem(allNonTimeFields(frames), theme);
|
||
|
}
|
||
|
|
||
|
export function getFieldLegendItem(fields: Field[], theme: GrafanaTheme2): VizLegendItem[] | undefined {
|
||
![]()
4 years ago
|
if (!fields.length) {
|
||
|
return undefined;
|
||
|
}
|
||
|
|
||
|
const items: VizLegendItem[] = [];
|
||
![]()
4 years ago
|
const fieldConfig = fields[0].config;
|
||
|
const colorMode = fieldConfig.color?.mode ?? FieldColorModeId.Fixed;
|
||
![]()
4 years ago
|
|
||
|
// If thresholds are enabled show each step in the legend
|
||
![]()
3 years ago
|
if (colorMode === FieldColorModeId.Thresholds) {
|
||
![]()
4 years ago
|
return getThresholdItems(fieldConfig, theme);
|
||
![]()
4 years ago
|
}
|
||
|
|
||
|
// If thresholds are enabled show each step in the legend
|
||
|
if (colorMode.startsWith('continuous')) {
|
||
|
return undefined; // eventually a color bar
|
||
|
}
|
||
|
|
||
|
let stateColors: Map<string, string | undefined> = new Map();
|
||
|
|
||
|
fields.forEach((field) => {
|
||
|
field.values.toArray().forEach((v) => {
|
||
|
let state = field.display!(v);
|
||
![]()
4 years ago
|
if (state.color) {
|
||
|
stateColors.set(state.text, state.color!);
|
||
|
}
|
||
![]()
4 years ago
|
});
|
||
|
});
|
||
|
|
||
|
stateColors.forEach((color, label) => {
|
||
|
if (label.length > 0) {
|
||
|
items.push({
|
||
|
label: label!,
|
||
![]()
4 years ago
|
color: theme.visualization.getColorByName(color ?? FALLBACK_COLOR),
|
||
![]()
4 years ago
|
yAxis: 1,
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
return items;
|
||
|
}
|
||
|
|
||
|
function allNonTimeFields(frames: DataFrame[]): Field[] {
|
||
|
const fields: Field[] = [];
|
||
|
for (const frame of frames) {
|
||
|
for (const field of frame.fields) {
|
||
|
if (field.type !== FieldType.time) {
|
||
|
fields.push(field);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return fields;
|
||
|
}
|
||
![]()
4 years ago
|
|
||
|
export function findNextStateIndex(field: Field, datapointIdx: number) {
|
||
|
let end;
|
||
|
let rightPointer = datapointIdx + 1;
|
||
|
|
||
![]()
4 years ago
|
if (rightPointer >= field.values.length) {
|
||
![]()
4 years ago
|
return null;
|
||
|
}
|
||
|
|
||
![]()
3 years ago
|
const startValue = field.values.get(datapointIdx);
|
||
|
|
||
![]()
4 years ago
|
while (end === undefined) {
|
||
![]()
4 years ago
|
if (rightPointer >= field.values.length) {
|
||
![]()
4 years ago
|
return null;
|
||
|
}
|
||
|
const rightValue = field.values.get(rightPointer);
|
||
|
|
||
![]()
3 years ago
|
if (rightValue === undefined || rightValue === startValue) {
|
||
![]()
4 years ago
|
rightPointer++;
|
||
![]()
3 years ago
|
} else {
|
||
|
end = rightPointer;
|
||
![]()
4 years ago
|
}
|
||
|
}
|
||
|
|
||
|
return end;
|
||
|
}
|
||
![]()
3 years ago
|
|
||
|
/**
|
||
|
* Returns the precise duration of a time range passed in milliseconds.
|
||
|
* This function calculates with 30 days month and 365 days year.
|
||
|
* adapted from https://gist.github.com/remino/1563878
|
||
|
* @param milliSeconds The duration in milliseconds
|
||
|
* @returns A formated string of the duration
|
||
|
*/
|
||
|
export function fmtDuration(milliSeconds: number): string {
|
||
|
if (milliSeconds < 0 || Number.isNaN(milliSeconds)) {
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
let yr: number, mo: number, wk: number, d: number, h: number, m: number, s: number, ms: number;
|
||
|
|
||
|
s = Math.floor(milliSeconds / 1000);
|
||
|
m = Math.floor(s / 60);
|
||
|
s = s % 60;
|
||
|
h = Math.floor(m / 60);
|
||
|
m = m % 60;
|
||
|
d = Math.floor(h / 24);
|
||
|
h = h % 24;
|
||
|
|
||
|
yr = Math.floor(d / 365);
|
||
|
if (yr > 0) {
|
||
|
d = d % 365;
|
||
|
}
|
||
|
|
||
|
mo = Math.floor(d / 30);
|
||
|
if (mo > 0) {
|
||
|
d = d % 30;
|
||
|
}
|
||
|
|
||
|
wk = Math.floor(d / 7);
|
||
|
|
||
|
if (wk > 0) {
|
||
|
d = d % 7;
|
||
|
}
|
||
|
|
||
|
ms = Math.round((milliSeconds % 1000) * 1000) / 1000;
|
||
|
|
||
![]()
3 years ago
|
return (
|
||
|
yr > 0
|
||
|
? yr + 'y ' + (mo > 0 ? mo + 'mo ' : '') + (wk > 0 ? wk + 'w ' : '') + (d > 0 ? d + 'd ' : '')
|
||
|
: mo > 0
|
||
|
? mo + 'mo ' + (wk > 0 ? wk + 'w ' : '') + (d > 0 ? d + 'd ' : '')
|
||
|
: wk > 0
|
||
|
? wk + 'w ' + (d > 0 ? d + 'd ' : '')
|
||
|
: d > 0
|
||
|
? d + 'd ' + (h > 0 ? h + 'h ' : '')
|
||
|
: h > 0
|
||
|
? h + 'h ' + (m > 0 ? m + 'm ' : '')
|
||
|
: m > 0
|
||
|
? m + 'm ' + (s > 0 ? s + 's ' : '')
|
||
|
: s > 0
|
||
|
? s + 's ' + (ms > 0 ? ms + 'ms ' : '')
|
||
|
: ms > 0
|
||
|
? ms + 'ms '
|
||
|
: '0'
|
||
![]()
3 years ago
|
).trim();
|
||
|
}
|