Submit
Path:
~
/
/
usr
/
share
/
grafana
/
public
/
app
/
features
/
transformers
/
timeSeriesTable
/
File Content:
timeSeriesTableTransformer.ts
import { map } from 'rxjs/operators'; import { DataFrame, DataTransformerID, DataTransformerInfo, DataFrameWithValue, Field, FieldType, MutableDataFrame, isTimeSeriesFrame, ReducerID, reduceField, TransformationApplicabilityLevels, isTimeSeriesField, } from '@grafana/data'; /** * Maps a refId to a Field which can contain * different types of data. In our case we * care about DataFrame, number, and string. */ interface RefFieldMap<T> { [index: string]: Field<T>; } /** * A map of RefIds to labels where each * label maps to a field of the given * type. It's technically possible * to use the above type to achieve * this in combination with another mapping * but the RefIds are on the outer map * in this case, so we use a different type * to avoid future issues. * * RefId: { * label1: Field<T> * label2: Field<T> * } */ interface RefLabelFieldMap<T> { [index: string]: { [index: string]: Field<T>; }; } /** * For options we have a set of options * for each refId. So we map the refId * for each setting. */ export interface TimeSeriesTableTransformerOptions { [index: string]: RefIdTransformerOptions; } /** * Counts the number of refId frames in * a given frame array. i.e. * { * A: 10 * B: 20 * C: 12 * } */ interface RefCount { [index: string]: number; } /** * For each refId we allow the following to * be configured: * * - stat: A stat to calculate for the refId * - mergeSeries: Whether separate series should be merged into one * - timeField: The time field that should be used for the time series */ export interface RefIdTransformerOptions { stat?: ReducerID; timeField?: string; inlineStat?: boolean; } export const timeSeriesTableTransformer: DataTransformerInfo<TimeSeriesTableTransformerOptions> = { id: DataTransformerID.timeSeriesTable, name: 'Time series to table', description: 'Convert time series data to table rows so that they can be viewed in tabular or sparkline format.', defaultOptions: {}, isApplicable: (data) => { for (const frame of data) { if (isTimeSeriesFrame(frame)) { return TransformationApplicabilityLevels.Applicable; } } return TransformationApplicabilityLevels.NotApplicable; }, isApplicableDescription: 'The Time series to table transformation requires at least one time series frame to function. You currently have none.', operator: (options) => (source) => source.pipe( map((data) => { return timeSeriesToTableTransform(options, data); }) ), }; /** * Converts time series frames to table frames for use with sparkline chart type. * * @remarks * For each refId (queryName) convert all time series frames into a single table frame, adding each series * as values of a "Trend" frame field. This allows "Trend" to be rendered as area chart type. * * Any non time series frames are returned unmodified. * * @param options - Transform options, currently not used * @param data - Array of data frames to transform * @returns Array of transformed data frames * * @beta */ export function timeSeriesToTableTransform(options: TimeSeriesTableTransformerOptions, data: DataFrame[]): DataFrame[] { // Initialize maps for labels, sparklines, and reduced values const refId2trends: RefLabelFieldMap<DataFrameWithValue> = {}; const refId2labelz: RefLabelFieldMap<string> = {}; // Accumulator for our final value // which we'll return const result: DataFrame[] = []; // Retreive the refIds of all the data let refIdMap = getRefData(data); // If we're merging data then rather // than creating a series per source // series we initialize fields here // so we end up with one for (const refId of Object.keys(refIdMap)) { // Get the frames with the current refId const framesForRef = data.filter((frame) => frame.refId === refId); // Intialize object for this refId refId2trends[refId] = {}; // Initialize labels object for this refId refId2labelz[refId] = {}; // Collect all existing label names across frames // so we can fill in nulls for frames that don't // have a particular label const labelNames: string[] = []; framesForRef.forEach((frame) => { frame.fields.forEach((field) => { if (field.type !== FieldType.number) { return; } if (field.labels) { Object.keys(field.labels).forEach((labelName) => { if (!labelNames.includes(labelName)) { refId2labelz[refId][labelName] = newField(labelName, FieldType.string); labelNames.push(labelName); } }); } }); }); for (let i = 0; i < framesForRef.length; i++) { const frame = framesForRef[i]; // Retrieve the time field that's been configured // If one isn't configured then use the first found let timeField = null; let timeFieldName = options[refId]?.timeField; if (timeFieldName && timeFieldName.length > 0) { timeField = frame.fields.find((field) => field.name === timeFieldName); } else { timeField = frame.fields.find((field) => isTimeSeriesField(field)); } // If it's not a time series frame we add // it unmodified to the result if (!isTimeSeriesFrame(frame)) { result.push(frame); continue; } for (const field of frame.fields) { // Skip non-number based fields // i.e. we skip time, strings, etc. if (field.type !== FieldType.number) { continue; } // Calculate the reduction of the current field // and push the frame with reduction // into the the appropriate field const reducerId = options[refId]?.stat ?? ReducerID.lastNotNull; const value = reduceField({ field, reducers: [reducerId] })[reducerId] ?? null; // Push the appropriate time and value frame // to the trend frame for the sparkline const sparklineFrame = new MutableDataFrame(); if (timeField !== undefined) { sparklineFrame.addField(timeField); sparklineFrame.addField(field); if (refId2trends[refId][`Trend #${refId}`] === undefined) { refId2trends[refId][`Trend #${refId}`] = newField(`Trend #${refId}`, FieldType.frame); } refId2trends[refId][`Trend #${refId}`].values.push({ ...sparklineFrame, value, length: field.values.length, }); } // If there are labels add them to the appropriate fields // Because we iterate each frame labelNames.forEach((labelName) => { refId2labelz[refId][labelName].values.push(field.labels?.[labelName] ?? ''); }); } } } for (const refId of Object.keys(refIdMap)) { const label2fields: RefFieldMap<string> = {}; // Allocate a new frame const table = new MutableDataFrame(); table.refId = refId; // Rather than having a label fields for each refId // we combine them into a single set of labels // taking the first value available const labels = refId2labelz[refId]; if (labels !== undefined) { for (const [labelName, labelField] of Object.entries(labels)) { if (label2fields[labelName] === undefined) { label2fields[labelName] = labelField; } } } // Add label fields to the the resulting frame for (const label of Object.values(label2fields)) { table.addField(label); } // Add trend fields to frame const refTrends = refId2trends[refId]; for (const trend of Object.values(refTrends)) { table.addField(trend); } // Finaly push to the result if (table.fields.length > 0) { result.push(table); } } return result; } /** * Create a new field with the given label and type. * * @param label * The string label for the field. * @param type * The type fo the field (e.g. number, boolean, etc.) * @returns * A new Field" */ function newField(label: string, type: FieldType) { return { name: label, type: type, config: {}, values: [], }; } /** * Get the refIds contained in an array of Data frames. * @param data * @returns A RefCount object */ export function getRefData(data: DataFrame[]) { let refMap: RefCount = {}; for (const frame of data) { if (frame.refId !== undefined) { if (refMap[frame.refId] === undefined) { refMap[frame.refId] = 1; } else { refMap[frame.refId]++; } } } return refMap; }
Submit
FILE
FOLDER
INFO
Name
Size
Permission
Action
TimeSeriesTableTransformEditor.tsx
3641 bytes
0644
timeSeriesTableTransformer.test.ts
10068 bytes
0644
timeSeriesTableTransformer.ts
8636 bytes
0644
N4ST4R_ID | Naxtarrr