Suggestions: Refactor getPanelDataSummary into its own method (#113251)

* Suggestions: Refactor getPanelDataSummary into its own method

* restore order

* update some imports

* update codeowners
This commit is contained in:
Paul Marbach
2025-11-07 11:33:13 -05:00
committed by GitHub
parent 02464c19b8
commit 4fee8b34ad
6 changed files with 184 additions and 88 deletions
+1 -1
View File
@@ -254,7 +254,6 @@
/devenv/dev-dashboards/all-panels.json @grafana/dataviz-squad
/devenv/dev-dashboards/dashboards.go @grafana/dataviz-squad
/devenv/dev-dashboards/home.json @grafana/dataviz-squad
/devenv/dev-dashboards/datasource-elasticsearch/ @grafana/partner-datasources
/devenv/dev-dashboards/datasource-opentsdb/ @grafana/partner-datasources
/devenv/dev-dashboards/datasource-influxdb/ @grafana/partner-datasources
@@ -550,6 +549,7 @@ i18next.config.ts @grafana/grafana-frontend-platform
/packages/grafana-data/src/geo/ @grafana/dataviz-squad
/packages/grafana-data/src/monaco/ @grafana/partner-datasources
/packages/grafana-data/src/panel/ @grafana/dashboards-squad
/packages/grafana-data/src/panel/suggestions/ @grafana/dataviz-squad
/packages/grafana-data/src/query/ @grafana/grafana-datasources-core-services
/packages/grafana-data/src/rbac/ @grafana/access-squad
/packages/grafana-data/src/table/ @grafana/dataviz-squad
+1 -1
View File
@@ -435,6 +435,7 @@ export {
isStandardFieldProp,
type OptionDefaults,
} from './panel/getPanelOptionsWithDefaults';
export { type PanelDataSummary, getPanelDataSummary } from './panel/suggestions/getPanelDataSummary';
export { createFieldConfigRegistry } from './panel/registryFactories';
export { type QueryRunner, type QueryRunnerOptions } from './types/queryRunner';
export { type GroupingToMatrixTransformerOptions } from './transformations/transformers/groupingToMatrix';
@@ -651,7 +652,6 @@ export {
type AngularPanelMenuItem,
type PanelPluginDataSupport,
type VisualizationSuggestion,
type PanelDataSummary,
type VisualizationSuggestionsSupplier,
VizOrientation,
VisualizationSuggestionScore,
@@ -0,0 +1,94 @@
import { createDataFrame } from '../../dataframe/processDataFrame';
import { FieldType } from '../../types/dataFrame';
import { getPanelDataSummary } from './getPanelDataSummary';
describe('getPanelDataSummary', () => {
describe('when called with no dataframes', () => {
it('should return summary with zero counts', () => {
const summary = getPanelDataSummary();
expect(summary.rowCountTotal).toBe(0);
expect(summary.rowCountMax).toBe(0);
expect(summary.fieldCount).toBe(0);
expect(summary.frameCount).toBe(0);
expect(summary.hasData).toBe(false);
expect(summary.fieldCountByType(FieldType.time)).toBe(0);
expect(summary.fieldCountByType(FieldType.number)).toBe(0);
expect(summary.fieldCountByType(FieldType.string)).toBe(0);
expect(summary.fieldCountByType(FieldType.boolean)).toBe(0);
expect(summary.hasFieldType(FieldType.time)).toBe(false);
expect(summary.hasFieldType(FieldType.number)).toBe(false);
expect(summary.hasFieldType(FieldType.string)).toBe(false);
expect(summary.hasFieldType(FieldType.boolean)).toBe(false);
});
});
describe('when called with a single dataframes', () => {
it('should return correct summary', () => {
const frames = [
createDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1, 2, 3] },
{ name: 'value', type: FieldType.number, values: [10, 20, 30] },
],
}),
];
const summary = getPanelDataSummary(frames);
expect(summary.rowCountTotal).toBe(3);
expect(summary.rowCountMax).toBe(3);
expect(summary.fieldCount).toBe(2);
expect(summary.frameCount).toBe(1);
expect(summary.hasData).toBe(true);
expect(summary.fieldCountByType(FieldType.time)).toBe(1);
expect(summary.fieldCountByType(FieldType.number)).toBe(1);
expect(summary.fieldCountByType(FieldType.string)).toBe(0);
expect(summary.fieldCountByType(FieldType.boolean)).toBe(0);
expect(summary.hasFieldType(FieldType.time)).toBe(true);
expect(summary.hasFieldType(FieldType.number)).toBe(true);
expect(summary.hasFieldType(FieldType.string)).toBe(false);
expect(summary.hasFieldType(FieldType.boolean)).toBe(false);
});
});
describe('when called with multiple dataframes', () => {
it('should return correct summary', () => {
const frames = [
createDataFrame({
fields: [
{ name: 'time', type: FieldType.time, values: [1, 2, 3] },
{ name: 'value', type: FieldType.number, values: [10, 20, 30] },
],
}),
createDataFrame({
fields: [
{ name: 'category', type: FieldType.string, values: ['A', 'B'] },
{ name: 'amount', type: FieldType.number, values: [100, 200] },
],
}),
];
const summary = getPanelDataSummary(frames);
expect(summary.rowCountTotal).toBe(5);
expect(summary.rowCountMax).toBe(3);
expect(summary.fieldCount).toBe(4);
expect(summary.frameCount).toBe(2);
expect(summary.hasData).toBe(true);
expect(summary.fieldCountByType(FieldType.time)).toBe(1);
expect(summary.fieldCountByType(FieldType.number)).toBe(2);
expect(summary.fieldCountByType(FieldType.string)).toBe(1);
expect(summary.fieldCountByType(FieldType.boolean)).toBe(0);
expect(summary.hasFieldType(FieldType.time)).toBe(true);
expect(summary.hasFieldType(FieldType.number)).toBe(true);
expect(summary.hasFieldType(FieldType.string)).toBe(true);
expect(summary.hasFieldType(FieldType.boolean)).toBe(false);
});
});
});
@@ -0,0 +1,82 @@
import { PreferredVisualisationType } from '../../types/data';
import { DataFrame, FieldType } from '../../types/dataFrame';
/**
* @alpha
*/
export interface PanelDataSummary {
hasData?: boolean;
rowCountTotal: number;
rowCountMax: number;
frameCount: number;
fieldCount: number;
fieldCountByType: (type: FieldType) => number;
hasFieldType: (type: FieldType) => boolean;
/** The first frame that set's this value */
preferredVisualisationType?: PreferredVisualisationType;
/* --- DEPRECATED FIELDS BELOW --- */
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.number) */
numberFieldCount: number;
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.time) */
timeFieldCount: number;
/** @deprecated use PanelDataSummary.fieldCountByType(FieldType.string) */
stringFieldCount: number;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.number) */
hasNumberField?: boolean;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.time) */
hasTimeField?: boolean;
/** @deprecated use PanelDataSummary.hasFieldType(FieldType.string) */
hasStringField?: boolean;
}
/**
* @alpha
* given a list of dataframes, summarize attributes of those frames for features like suggestions.
* @param frames - dataframes to summarize
* @returns summary of the dataframes
*/
export function getPanelDataSummary(frames: DataFrame[] = []): PanelDataSummary {
let rowCountTotal = 0;
let rowCountMax = 0;
let fieldCount = 0;
const countByType: Partial<Record<FieldType, number>> = {};
let preferredVisualisationType: PreferredVisualisationType | undefined;
for (const frame of frames) {
rowCountTotal += frame.length;
if (frame.meta?.preferredVisualisationType) {
preferredVisualisationType = frame.meta.preferredVisualisationType;
}
for (const field of frame.fields) {
fieldCount++;
countByType[field.type] = (countByType[field.type] || 0) + 1;
}
if (frame.length > rowCountMax) {
rowCountMax = frame.length;
}
}
const fieldCountByType = (f: FieldType) => countByType[f] ?? 0;
return {
rowCountTotal,
rowCountMax,
fieldCount,
preferredVisualisationType,
frameCount: frames.length,
hasData: rowCountTotal > 0,
hasFieldType: (f: FieldType) => fieldCountByType(f) > 0,
fieldCountByType,
// deprecated
numberFieldCount: fieldCountByType(FieldType.number),
timeFieldCount: fieldCountByType(FieldType.time),
stringFieldCount: fieldCountByType(FieldType.string),
hasTimeField: fieldCountByType(FieldType.time) > 0,
hasNumberField: fieldCountByType(FieldType.number) > 0,
hasStringField: fieldCountByType(FieldType.string) > 0,
};
}
+4 -77
View File
@@ -2,14 +2,15 @@ import { defaultsDeep } from 'lodash';
import { EventBus } from '../events/types';
import { StandardEditorProps } from '../field/standardFieldConfigEditorRegistry';
import { PanelDataSummary, getPanelDataSummary } from '../panel/suggestions/getPanelDataSummary';
import { Registry } from '../utils/Registry';
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
import { ScopedVars } from './ScopedVars';
import { AlertStateInfo } from './alerts';
import { PanelModel } from './dashboard';
import { LoadingState, PreferredVisualisationType } from './data';
import { DataFrame, FieldType } from './dataFrame';
import { LoadingState } from './data';
import { DataFrame } from './dataFrame';
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
import { FieldConfigSource } from './fieldOverrides';
import { IconName } from './icon';
@@ -258,25 +259,6 @@ export enum VisualizationSuggestionScore {
OK = 50,
}
/**
* @alpha
*/
export interface PanelDataSummary {
hasData?: boolean;
rowCountTotal: number;
rowCountMax: number;
frameCount: number;
fieldCount: number;
numberFieldCount: number;
timeFieldCount: number;
stringFieldCount: number;
hasNumberField?: boolean;
hasTimeField?: boolean;
hasStringField?: boolean;
/** The first frame that set's this value */
preferredVisualisationType?: PreferredVisualisationType;
}
/**
* @alpha
*/
@@ -293,68 +275,13 @@ export class VisualizationSuggestionsBuilder {
constructor(data?: PanelData, panel?: PanelModel) {
this.data = data;
this.panel = panel;
this.dataSummary = this.computeDataSummary();
this.dataSummary = getPanelDataSummary(this.data?.series);
}
getListAppender<TOptions, TFieldConfig>(defaults: VisualizationSuggestion<TOptions, TFieldConfig>) {
return new VisualizationSuggestionsListAppender<TOptions, TFieldConfig>(this.list, defaults);
}
private computeDataSummary() {
const frames = this.data?.series || [];
let numberFieldCount = 0;
let timeFieldCount = 0;
let stringFieldCount = 0;
let rowCountTotal = 0;
let rowCountMax = 0;
let fieldCount = 0;
let preferredVisualisationType: PreferredVisualisationType | undefined;
for (const frame of frames) {
rowCountTotal += frame.length;
if (frame.meta?.preferredVisualisationType) {
preferredVisualisationType = frame.meta.preferredVisualisationType;
}
for (const field of frame.fields) {
fieldCount++;
switch (field.type) {
case FieldType.number:
numberFieldCount += 1;
break;
case FieldType.time:
timeFieldCount += 1;
break;
case FieldType.string:
stringFieldCount += 1;
break;
}
}
if (frame.length > rowCountMax) {
rowCountMax = frame.length;
}
}
return {
numberFieldCount,
timeFieldCount,
stringFieldCount,
rowCountTotal,
rowCountMax,
fieldCount,
preferredVisualisationType,
frameCount: frames.length,
hasData: rowCountTotal > 0,
hasTimeField: timeFieldCount > 0,
hasNumberField: numberFieldCount > 0,
hasStringField: stringFieldCount > 0,
};
}
getList() {
return this.list;
}
@@ -1,12 +1,6 @@
import { css } from '@emotion/css';
import {
CoreApp,
GrafanaTheme2,
PanelDataSummary,
VisualizationSuggestionsBuilder,
VisualizationSuggestion,
} from '@grafana/data';
import { CoreApp, getPanelDataSummary, GrafanaTheme2, PanelDataSummary, VisualizationSuggestion } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { t, Trans } from '@grafana/i18n';
import { PanelDataErrorViewProps, locationService } from '@grafana/runtime';
@@ -27,8 +21,7 @@ import { changePanelPlugin } from '../state/actions';
export function PanelDataErrorView(props: PanelDataErrorViewProps) {
const styles = useStyles2(getStyles);
const context = usePanelContext();
const builder = new VisualizationSuggestionsBuilder(props.data);
const { dataSummary } = builder;
const dataSummary = getPanelDataSummary(props.data.series);
const message = getMessageFor(props, dataSummary);
const dispatch = useDispatch();