import { Field, formattedValueToString, getFieldMatcher, FieldType, getFieldDisplayName, DataFrame, FrameMatcherID, MatcherConfig, FieldColorModeId, cacheFieldDisplayNames, FieldMatcherID, FieldConfigSource, } from '@grafana/data'; import { decoupleHideFromState } from '@grafana/data/src/field/fieldState'; 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.byType, options: 'number', } ); 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[] = []; // only grabbing number fields (exclude time, string, enum, other) let onlyNumFields = frame.fields.filter((field) => field.type === FieldType.number); // only one of these per frame let x = onlyNumFields.find((field) => xMatcher(field, frame, frames)); 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, }; }