mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 05:10:56 +08:00

* XYChart: Add support for x=time * prettier * Add time axis demo to provisioned dashboard for XY * Add details about time fields to the documentation --------- Co-authored-by: Kristina Durivage <kristina.durivage@grafana.com>
365 lines
11 KiB
TypeScript
365 lines
11 KiB
TypeScript
import {
|
|
Field,
|
|
formattedValueToString,
|
|
getFieldMatcher,
|
|
FieldType,
|
|
getFieldDisplayName,
|
|
DataFrame,
|
|
FrameMatcherID,
|
|
MatcherConfig,
|
|
FieldColorModeId,
|
|
cacheFieldDisplayNames,
|
|
FieldMatcherID,
|
|
FieldConfigSource,
|
|
} from '@grafana/data';
|
|
import { decoupleHideFromState } from '@grafana/data/internal';
|
|
import { config } from '@grafana/runtime';
|
|
import { VisibilityMode } from '@grafana/schema';
|
|
|
|
import { XYShowMode, SeriesMapping, XYSeriesConfig } from './panelcfg.gen';
|
|
import { XYSeries } from './types2';
|
|
|
|
export function fmt(field: Field, val: number): string {
|
|
if (field.display) {
|
|
return formattedValueToString(field.display(val));
|
|
}
|
|
|
|
return `${val}`;
|
|
}
|
|
|
|
// cause we dont have a proper matcher for this currently
|
|
function getFrameMatcher2(config: MatcherConfig) {
|
|
if (config.id === FrameMatcherID.byIndex) {
|
|
return (frame: DataFrame, index: number) => index === config.options;
|
|
}
|
|
|
|
return () => false;
|
|
}
|
|
|
|
export function prepSeries(
|
|
mapping: SeriesMapping,
|
|
mappedSeries: XYSeriesConfig[],
|
|
frames: DataFrame[],
|
|
fieldConfig: FieldConfigSource
|
|
) {
|
|
cacheFieldDisplayNames(frames);
|
|
decoupleHideFromState(frames, fieldConfig);
|
|
|
|
let series: XYSeries[] = [];
|
|
|
|
if (mappedSeries.length === 0) {
|
|
mappedSeries = [{}];
|
|
}
|
|
|
|
const { palette, getColorByName } = config.theme2.visualization;
|
|
|
|
mappedSeries.forEach((seriesCfg, seriesIdx) => {
|
|
if (mapping === SeriesMapping.Manual) {
|
|
if (seriesCfg.frame?.matcher == null || seriesCfg.x?.matcher == null || seriesCfg.y?.matcher == null) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let xMatcher = getFieldMatcher(
|
|
seriesCfg.x?.matcher ?? {
|
|
id: FieldMatcherID.byTypes,
|
|
options: new Set(['number', 'time']),
|
|
}
|
|
);
|
|
let yMatcher = getFieldMatcher(
|
|
seriesCfg.y?.matcher ?? {
|
|
id: FieldMatcherID.byType,
|
|
options: 'number',
|
|
}
|
|
);
|
|
let colorMatcher = seriesCfg.color ? getFieldMatcher(seriesCfg.color.matcher) : null;
|
|
let sizeMatcher = seriesCfg.size ? getFieldMatcher(seriesCfg.size.matcher) : null;
|
|
// let frameMatcher = seriesCfg.frame ? getFrameMatchers(seriesCfg.frame) : null;
|
|
let frameMatcher = seriesCfg.frame ? getFrameMatcher2(seriesCfg.frame.matcher) : null;
|
|
|
|
// loop over all frames and fields, adding a new series for each y dim
|
|
frames.forEach((frame, frameIdx) => {
|
|
// must match frame in manual mode
|
|
if (frameMatcher != null && !frameMatcher(frame, frameIdx)) {
|
|
return;
|
|
}
|
|
|
|
// shared across each series in this frame
|
|
let restFields: Field[] = [];
|
|
|
|
let frameSeries: XYSeries[] = [];
|
|
|
|
let onlyNumTimeFields = frame.fields.filter(
|
|
(field) => field.type === FieldType.number || field.type === FieldType.time
|
|
);
|
|
|
|
// only one of these per frame
|
|
let x = onlyNumTimeFields.find((field) => xMatcher(field, frame, frames));
|
|
|
|
// only grabbing number fields (exclude time, string, enum, other)
|
|
let onlyNumFields = onlyNumTimeFields.filter((field) => field.type === FieldType.number);
|
|
|
|
let color =
|
|
colorMatcher != null
|
|
? onlyNumFields.find((field) => field !== x && colorMatcher!(field, frame, frames))
|
|
: undefined;
|
|
let size =
|
|
sizeMatcher != null
|
|
? onlyNumFields.find((field) => field !== x && field !== color && sizeMatcher!(field, frame, frames))
|
|
: undefined;
|
|
|
|
// x field is required
|
|
if (x != null) {
|
|
// match y fields and create series
|
|
onlyNumFields.forEach((field) => {
|
|
if (field === x) {
|
|
return;
|
|
}
|
|
|
|
// in auto mode don't reuse already-mapped fields
|
|
if (mapping === SeriesMapping.Auto && (field === color || field === size)) {
|
|
return;
|
|
}
|
|
|
|
// in manual mode only add single series for this config
|
|
if (mapping === SeriesMapping.Manual && frameSeries.length > 0) {
|
|
return;
|
|
}
|
|
|
|
// if we match non-excluded y, create series
|
|
if (yMatcher(field, frame, frames) && !field.config.custom?.hideFrom?.viz) {
|
|
let y = field;
|
|
|
|
let name = seriesCfg.name?.fixed;
|
|
|
|
if (name == null) {
|
|
// if the displayed field name is likely to have a common prefix or suffix
|
|
// (such as those from Partition by values transformation)
|
|
const likelyHasCommonParts =
|
|
frames.length > 1 && (frame.name != null || Object.keys(y.labels ?? {}).length > 0);
|
|
|
|
// if the field was explictly (re)named using config.displayName or config.displayNameFromDS
|
|
// we still want to retain any frame name prefix or suffix so that autoNameSeries() can
|
|
// properly detect + strip common parts across all series...
|
|
const { displayName, displayNameFromDS } = y.config;
|
|
const hasExplicitName = displayName != null || displayNameFromDS != null;
|
|
|
|
if (likelyHasCommonParts && hasExplicitName) {
|
|
// ...and a hacky way to do this is to temp remove the explicit name, get the auto name, then revert
|
|
const stateDisplayName = y.state!.displayName;
|
|
|
|
// clear config and cache
|
|
y.config.displayName = y.config.displayNameFromDS = y.state!.displayName = undefined;
|
|
// get default/calculated display name (maybe use calculateFieldDisplayName() here instead?)
|
|
name = getFieldDisplayName(y, frame, frames);
|
|
// replace original field name with explicit one
|
|
name = name.replace(y.name, (displayNameFromDS ?? displayName)!);
|
|
|
|
// revert
|
|
y.config.displayName = displayName;
|
|
y.config.displayNameFromDS = displayNameFromDS;
|
|
y.state!.displayName = stateDisplayName;
|
|
} else {
|
|
name = getFieldDisplayName(y, frame, frames);
|
|
}
|
|
}
|
|
|
|
let ser: XYSeries = {
|
|
// these typically come from y field
|
|
name: {
|
|
value: name,
|
|
},
|
|
|
|
showPoints: y.config.custom.show === XYShowMode.Lines ? VisibilityMode.Never : VisibilityMode.Always,
|
|
pointShape: y.config.custom.pointShape,
|
|
pointStrokeWidth: y.config.custom.pointStrokeWidth,
|
|
fillOpacity: y.config.custom.fillOpacity,
|
|
|
|
showLine: y.config.custom.show !== XYShowMode.Points,
|
|
lineWidth: y.config.custom.lineWidth ?? 2,
|
|
lineStyle: y.config.custom.lineStyle,
|
|
|
|
x: {
|
|
field: x!,
|
|
},
|
|
y: {
|
|
field: y,
|
|
},
|
|
color: {},
|
|
size: {},
|
|
_rest: restFields,
|
|
};
|
|
|
|
if (color != null) {
|
|
ser.color.field = color;
|
|
}
|
|
|
|
if (size != null) {
|
|
ser.size.field = size;
|
|
ser.size.min = size.config.custom.pointSize?.min ?? 5;
|
|
ser.size.max = size.config.custom.pointSize?.max ?? 100;
|
|
// ser.size.mode =
|
|
}
|
|
|
|
frameSeries.push(ser);
|
|
}
|
|
});
|
|
|
|
if (frameSeries.length === 0) {
|
|
// TODO: could not create series, skip & show error?
|
|
}
|
|
|
|
// populate rest fields
|
|
frame.fields.forEach((field) => {
|
|
let isUsedField = frameSeries.some(
|
|
({ x, y, color, size }) =>
|
|
x.field === field || y.field === field || color.field === field || size.field === field
|
|
);
|
|
|
|
if (!isUsedField) {
|
|
restFields.push(field);
|
|
}
|
|
});
|
|
|
|
series.push(...frameSeries);
|
|
} else {
|
|
// x is missing in this frame!
|
|
}
|
|
});
|
|
});
|
|
|
|
if (series.length === 0) {
|
|
// TODO: could not create series, skip & show error?
|
|
} else {
|
|
// assign classic palette colors by index, as fallbacks for all series
|
|
|
|
let paletteIdx = 0;
|
|
|
|
// todo: populate min, max, mode from field + hints
|
|
series.forEach((s, i) => {
|
|
if (s.color.field == null) {
|
|
// derive fixed color from y field config
|
|
let colorCfg = s.y.field.config.color ?? { mode: FieldColorModeId.PaletteClassic };
|
|
|
|
let value = '';
|
|
|
|
if (colorCfg.mode === FieldColorModeId.PaletteClassic) {
|
|
value = getColorByName(palette[paletteIdx++ % palette.length]); // todo: do this via state.seriesIdx and re-init displayProcessor
|
|
} else if (colorCfg.mode === FieldColorModeId.Fixed) {
|
|
value = getColorByName(colorCfg.fixedColor!);
|
|
}
|
|
|
|
s.color.fixed = value;
|
|
}
|
|
|
|
if (s.size.field == null) {
|
|
// derive fixed size from y field config
|
|
s.size.fixed = s.y.field.config.custom.pointSize?.fixed ?? 5;
|
|
// ser.size.mode =
|
|
}
|
|
});
|
|
|
|
autoNameSeries(series);
|
|
|
|
// TODO: re-assign y display names?
|
|
// y.state = {
|
|
// ...y.state,
|
|
// seriesIndex: series.length + ,
|
|
// };
|
|
// y.display = getDisplayProcessor({ field, theme });
|
|
}
|
|
|
|
return series;
|
|
}
|
|
|
|
// strip common prefixes and suffixes from y field names
|
|
function autoNameSeries(series: XYSeries[]) {
|
|
let names = series.map((s) => s.name.value.split(/\s+/g));
|
|
|
|
const { prefix, suffix } = findCommonPrefixSuffixLengths(names);
|
|
|
|
if (prefix < Infinity || suffix < Infinity) {
|
|
series.forEach((s, i) => {
|
|
s.name.value = names[i].slice(prefix, names[i].length - suffix).join(' ');
|
|
});
|
|
}
|
|
}
|
|
|
|
export function getCommonPrefixSuffix(strs: string[]) {
|
|
let names = strs.map((s) => s.split(/\s+/g));
|
|
|
|
let { prefix, suffix } = findCommonPrefixSuffixLengths(names);
|
|
|
|
let n = names[0];
|
|
|
|
if (n.length === 1 && prefix === 1 && suffix === 1) {
|
|
return '';
|
|
}
|
|
|
|
let parts = [];
|
|
|
|
if (prefix > 0) {
|
|
parts.push(...n.slice(0, prefix));
|
|
}
|
|
|
|
if (suffix > 0) {
|
|
parts.push(...n.slice(-suffix));
|
|
}
|
|
|
|
return parts.join(' ');
|
|
}
|
|
|
|
// lengths are in number of tokens (segments) in a phrase
|
|
function findCommonPrefixSuffixLengths(names: string[][]) {
|
|
let commonPrefixLen = Infinity;
|
|
let commonSuffixLen = Infinity;
|
|
|
|
// if auto naming strategy, rename fields by stripping common prefixes and suffixes
|
|
let segs0: string[] = names[0];
|
|
|
|
for (let i = 1; i < names.length; i++) {
|
|
if (names[i].length < segs0.length) {
|
|
segs0 = names[i];
|
|
}
|
|
}
|
|
|
|
for (let i = 1; i < names.length; i++) {
|
|
let segs = names[i];
|
|
|
|
if (segs !== segs0) {
|
|
// prefixes
|
|
let preLen = 0;
|
|
for (let j = 0; j < segs0.length; j++) {
|
|
if (segs[j] === segs0[j]) {
|
|
preLen++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (preLen < commonPrefixLen) {
|
|
commonPrefixLen = preLen;
|
|
}
|
|
|
|
// suffixes
|
|
let sufLen = 0;
|
|
for (let j = segs0.length - 1; j >= 0; j--) {
|
|
if (segs[j] === segs0[j]) {
|
|
sufLen++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (sufLen < commonSuffixLen) {
|
|
commonSuffixLen = sufLen;
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
prefix: commonPrefixLen,
|
|
suffix: commonSuffixLen,
|
|
};
|
|
}
|