Files
Jack Westbrook 1ca9910736 Grafana Data: Use package.json exports for internal code (#102696)
* refactor(frontend): rename all @grafana/data/src imports to @grafana/data

* feat(grafana-data): introduce internal entrypoint for sharing code only with grafana

* feat(grafana-data): add test entrypoint for data test utils usage in core

* refactor(frontend): update import paths to use grafana/data exports entrypoints

* docs(grafana-data): update comment in internal/index.ts

* refactor(frontend): prefer public namespaced exports over re-exporting via internal

* chore(frontend): fix a couple more weird paths that typescript complains about
2025-03-25 10:48:36 +01:00

271 lines
7.4 KiB
TypeScript

import {
DataFrame,
Field,
FieldType,
getDisplayProcessor,
GrafanaTheme2,
isBooleanUnit,
TimeRange,
cacheFieldDisplayNames,
applyNullInsertThreshold,
nullToValue,
} from '@grafana/data';
import { convertFieldType } from '@grafana/data/internal';
import { GraphFieldConfig, LineInterpolation, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema';
import { buildScaleKey } from '@grafana/ui/internal';
import { HeatmapTooltip } from '../heatmap/panelcfg.gen';
type ScaleKey = string;
// this will re-enumerate all enum fields on the same scale to create one ordinal progression
// e.g. ['a','b'][0,1,0] + ['c','d'][1,0,1] -> ['a','b'][0,1,0] + ['c','d'][3,2,3]
function reEnumFields(frames: DataFrame[]): DataFrame[] {
let allTextsByKey: Map<ScaleKey, string[]> = new Map();
let frames2: DataFrame[] = frames.map((frame) => {
return {
...frame,
fields: frame.fields.map((field) => {
if (field.type === FieldType.enum) {
let scaleKey = buildScaleKey(field.config, field.type);
let allTexts = allTextsByKey.get(scaleKey);
if (!allTexts) {
allTexts = [];
allTextsByKey.set(scaleKey, allTexts);
}
let idxs: number[] = field.values.toArray().slice();
let txts = field.config.type!.enum!.text!;
// by-reference incrementing
if (allTexts.length > 0) {
for (let i = 0; i < idxs.length; i++) {
idxs[i] += allTexts.length;
}
}
allTexts.push(...txts);
// shared among all enum fields on same scale
field.config.type!.enum!.text! = allTexts;
return {
...field,
values: idxs,
};
// TODO: update displayProcessor?
}
return field;
}),
};
});
return frames2;
}
/**
* Returns null if there are no graphable fields
*/
export function prepareGraphableFields(
series: DataFrame[],
theme: GrafanaTheme2,
timeRange?: TimeRange,
// numeric X requires a single frame where the first field is numeric
xNumFieldIdx?: number
): DataFrame[] | null {
if (!series?.length) {
return null;
}
cacheFieldDisplayNames(series);
let useNumericX = xNumFieldIdx != null;
// Make sure the numeric x field is first in the frame
if (xNumFieldIdx != null && xNumFieldIdx > 0) {
series = [
{
...series[0],
fields: [series[0].fields[xNumFieldIdx], ...series[0].fields.filter((f, i) => i !== xNumFieldIdx)],
},
];
}
// some datasources simply tag the field as time, but don't convert to milli epochs
// so we're stuck with doing the parsing here to avoid Moment slowness everywhere later
// this mutates (once)
for (let frame of series) {
for (let field of frame.fields) {
if (field.type === FieldType.time && typeof field.values[0] !== 'number') {
field.values = convertFieldType(field, { destinationType: FieldType.time }).values;
}
}
}
let enumFieldsCount = 0;
loopy: for (let frame of series) {
for (let field of frame.fields) {
if (field.type === FieldType.enum && ++enumFieldsCount > 1) {
series = reEnumFields(series);
break loopy;
}
}
}
let copy: Field;
const frames: DataFrame[] = [];
for (let frame of series) {
const fields: Field[] = [];
let hasTimeField = false;
let hasValueField = false;
let nulledFrame = useNumericX
? frame
: applyNullInsertThreshold({
frame,
refFieldPseudoMin: timeRange?.from.valueOf(),
refFieldPseudoMax: timeRange?.to.valueOf(),
});
const frameFields = nullToValue(nulledFrame).fields;
for (let fieldIdx = 0; fieldIdx < (frameFields?.length || 0); fieldIdx++) {
const field = frameFields[fieldIdx];
switch (field.type) {
case FieldType.time:
hasTimeField = true;
fields.push(field);
break;
case FieldType.number:
hasValueField = useNumericX ? fieldIdx > 0 : true;
copy = {
...field,
values: field.values.map((v) => {
if (!(Number.isFinite(v) || v == null)) {
return null;
}
return v;
}),
};
fields.push(copy);
break; // ok
case FieldType.enum:
hasValueField = true;
case FieldType.string:
copy = {
...field,
values: field.values,
};
fields.push(copy);
break; // ok
case FieldType.boolean:
hasValueField = true;
const custom: GraphFieldConfig = field.config?.custom ?? {};
const config = {
...field.config,
max: 1,
min: 0,
custom,
};
// smooth and linear do not make sense
if (custom.lineInterpolation !== LineInterpolation.StepBefore) {
custom.lineInterpolation = LineInterpolation.StepAfter;
}
copy = {
...field,
config,
type: FieldType.number,
values: field.values.map((v) => {
if (v == null) {
return v;
}
return Boolean(v) ? 1 : 0;
}),
};
if (!isBooleanUnit(config.unit)) {
config.unit = 'bool';
copy.display = getDisplayProcessor({ field: copy, theme });
}
fields.push(copy);
break;
}
}
if ((useNumericX || hasTimeField) && hasValueField) {
frames.push({
...frame,
length: nulledFrame.length,
fields,
});
}
}
if (frames.length) {
setClassicPaletteIdxs(frames, theme, 0);
matchEnumColorToSeriesColor(frames, theme);
return frames;
}
return null;
}
const matchEnumColorToSeriesColor = (frames: DataFrame[], theme: GrafanaTheme2) => {
const { palette } = theme.visualization;
for (const frame of frames) {
for (const field of frame.fields) {
if (field.type === FieldType.enum) {
const namedColor = palette[field.state?.seriesIndex! % palette.length];
const hexColor = theme.visualization.getColorByName(namedColor);
const enumConfig = field.config.type!.enum!;
enumConfig.color = Array(enumConfig.text!.length).fill(hexColor);
field.display = getDisplayProcessor({ field, theme });
}
}
}
};
export const setClassicPaletteIdxs = (frames: DataFrame[], theme: GrafanaTheme2, skipFieldIdx?: number) => {
let seriesIndex = 0;
frames.forEach((frame) => {
frame.fields.forEach((field, fieldIdx) => {
if (
fieldIdx !== skipFieldIdx &&
(field.type === FieldType.number || field.type === FieldType.boolean || field.type === FieldType.enum)
) {
field.state = {
...field.state,
seriesIndex: seriesIndex++, // TODO: skip this for fields with custom renderers (e.g. Candlestick)?
};
field.display = getDisplayProcessor({ field, theme });
}
});
});
};
export function getTimezones(timezones: string[] | undefined, defaultTimezone: string): string[] {
if (!timezones || !timezones.length) {
return [defaultTimezone];
}
return timezones.map((v) => (v?.length ? v : defaultTimezone));
}
export const isTooltipScrollable = (tooltipOptions: VizTooltipOptions | HeatmapTooltip) => {
return tooltipOptions.mode === TooltipDisplayMode.Multi && tooltipOptions.maxHeight != null;
};