diff --git a/public/app/features/transformers/regression/constants.ts b/public/app/features/transformers/regression/constants.ts new file mode 100644 index 00000000000..849f01e82ea --- /dev/null +++ b/public/app/features/transformers/regression/constants.ts @@ -0,0 +1,31 @@ +import { FieldMatcherID, fieldMatchers } from '@grafana/data'; +import { t } from '@grafana/i18n'; +import { FieldNamePicker } from '@grafana/ui/internal'; + +import { ModelType } from './regression'; + +export const getModelTypeOptions = () => + [ + { + label: t('transformers.regression-transformer-editor.model-type-options.label.linear', 'Linear'), + value: ModelType.linear, + }, + { + label: t('transformers.regression-transformer-editor.model-type-options.label.polynomial', 'Polynomial'), + value: ModelType.polynomial, + }, + ] as const satisfies Array<{ label: string; value: ModelType }>; + +export const fieldNamePickerSettings = { + editor: FieldNamePicker, + id: '', + name: '', + settings: { width: 24, isClearable: false }, +} as const; + +export const LABEL_WIDTH = 20; + +export const FIELD_MATCHERS = { + firstTimeMatcher: fieldMatchers.get(FieldMatcherID.firstTimeField).get({}), + numericMatcher: fieldMatchers.get(FieldMatcherID.numeric).get({}), +}; diff --git a/public/app/features/transformers/regression/regression.ts b/public/app/features/transformers/regression/regression.ts index ce350dde9ac..f830cac0758 100644 --- a/public/app/features/transformers/regression/regression.ts +++ b/public/app/features/transformers/regression/regression.ts @@ -33,6 +33,11 @@ export const DEGREES = [ { label: () => t('transformers.regression-transformer-editor.label.cubic', 'Cubic'), value: 3 }, { label: () => t('transformers.regression-transformer-editor.label.quartic', 'Quartic'), value: 4 }, { label: () => t('transformers.regression-transformer-editor.label.quintic', 'Quintic'), value: 5 }, + { label: () => t('transformers.regression-transformer-editor.label.sextic', 'Sextic'), value: 6 }, + { label: () => t('transformers.regression-transformer-editor.label.septic', 'Septic'), value: 7 }, + { label: () => t('transformers.regression-transformer-editor.label.octic', 'Octic'), value: 8 }, + { label: () => t('transformers.regression-transformer-editor.label.nonic', 'Nonic'), value: 9 }, + { label: () => t('transformers.regression-transformer-editor.label.decic', 'Decic'), value: 10 }, ]; export const getRegressionTransformer: () => SynchronousDataTransformerInfo = () => ({ diff --git a/public/app/features/transformers/regression/regressionEditor.tsx b/public/app/features/transformers/regression/regressionEditor.tsx index 86da5797d10..909e2b69285 100644 --- a/public/app/features/transformers/regression/regressionEditor.tsx +++ b/public/app/features/transformers/regression/regressionEditor.tsx @@ -1,16 +1,14 @@ -import { useEffect } from 'react'; +import { ReactElement, useEffect, useMemo } from 'react'; import { DataTransformerID, TransformerRegistryItem, TransformerUIProps, TransformerCategory, - fieldMatchers, - FieldMatcherID, Field, } from '@grafana/data'; import { t } from '@grafana/i18n'; -import { InlineField, Select } from '@grafana/ui'; +import { Combobox, InlineField } from '@grafana/ui'; import { FieldNamePicker } from '@grafana/ui/internal'; import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; @@ -18,111 +16,80 @@ import { getTransformationContent } from '../docs/getTransformationContent'; import darkImage from '../images/dark/regression.svg'; import lightImage from '../images/light/regression.svg'; +import { FIELD_MATCHERS, LABEL_WIDTH, fieldNamePickerSettings, getModelTypeOptions } from './constants'; import { DEFAULTS, DEGREES, ModelType, getRegressionTransformer, RegressionTransformerOptions } from './regression'; - -const fieldNamePickerSettings = { - editor: FieldNamePicker, - id: '', - name: '', - settings: { width: 24, isClearable: false }, -}; - -const LABEL_WIDTH = 20; +import { findFirstFieldByMatcher } from './utils'; export const RegressionTransformerEditor = ({ input, options, onChange, }: TransformerUIProps) => { - const modelTypeOptions = [ - { - label: t('transformers.regression-transformer-editor.model-type-options.label.linear', 'Linear'), - value: ModelType.linear, - }, - { - label: t('transformers.regression-transformer-editor.model-type-options.label.polynomial', 'Polynomial'), - value: ModelType.polynomial, - }, - ]; + const modelTypeOptions = useMemo(() => getModelTypeOptions(), []); + const degreeOptions = useMemo( + () => + DEGREES.map((deg) => { + return { + label: deg.label(), + value: deg.value, + }; + }), + [] + ); + // Auto-select default X and Y fields when they're not explicitly set by the user. + // This provides better UX when adding a new regression transform by automatically + // choosing sensible defaults: time field (or numeric fallback) for X, and a different + // numeric field for Y. Only runs when fields are missing due to early return optimization. useEffect(() => { let x: Field | undefined; let y: Field | undefined; + + // Both fields already selected, nothing to do, early return + if (options.xFieldName && options.yFieldName) { + return; + } + if (!options.xFieldName) { - const timeMatcher = fieldMatchers.get(FieldMatcherID.firstTimeField).get({}); - for (const frame of input) { - x = frame.fields.find((field) => timeMatcher(field, frame, input)); - if (x) { - break; - } - } - if (!x) { - const firstMatcher = fieldMatchers.get(FieldMatcherID.numeric).get({}); - for (const frame of input) { - x = frame.fields.find((field) => firstMatcher(field, frame, input)); - if (x) { - break; - } - } - } + x = + findFirstFieldByMatcher(input, FIELD_MATCHERS.firstTimeMatcher) || + findFirstFieldByMatcher(input, FIELD_MATCHERS.numericMatcher); } if (!options.yFieldName) { - const numberMatcher = fieldMatchers.get(FieldMatcherID.numeric).get({}); - for (const frame of input) { - y = frame.fields.find((field) => numberMatcher(field, frame, input) && field !== x); - if (y) { - break; - } - } + y = findFirstFieldByMatcher(input, FIELD_MATCHERS.numericMatcher, x); } if (x && y) { onChange({ ...options, xFieldName: x.name, yFieldName: y.name }); } - }); + }, [input, options, onChange]); return ( <> - + { - onChange({ ...options, xFieldName: v }); - }} - > - - + onChange={(v) => onChange({ ...options, xFieldName: v })} + /> + + { - onChange({ ...options, yFieldName: v }); - }} - > - - - - - + + { - onChange({ ...options, predictionCount: v }); - }} - > - + onChange={(v) => onChange({ ...options, predictionCount: v })} + /> + {options.modelType === ModelType.polynomial && ( - - + { - return { - label: deg.label(), - value: deg.value, - }; - })} - onChange={(v) => { - onChange({ ...options, degree: v.value }); - }} - > - + options={degreeOptions} + onChange={(v) => onChange({ ...options, degree: Number(v.value) })} + /> + )} ); }; +const RegressionField = ({ label, tooltip, children }: { label: string; tooltip?: string; children: ReactElement }) => ( + + {children} + +); + export const getRegressionTransformerRegistryItem: () => TransformerRegistryItem = () => { const regressionTransformer = getRegressionTransformer(); return { diff --git a/public/app/features/transformers/regression/utils.ts b/public/app/features/transformers/regression/utils.ts new file mode 100644 index 00000000000..c4b143b6895 --- /dev/null +++ b/public/app/features/transformers/regression/utils.ts @@ -0,0 +1,15 @@ +import { DataFrame, Field } from '@grafana/data'; + +export const findFirstFieldByMatcher = ( + input: DataFrame[], + matcher: (field: Field, frame: DataFrame, input: DataFrame[]) => boolean, + excludeField?: Field +): Field | undefined => { + for (const frame of input) { + const field = frame.fields.find((field) => matcher(field, frame, input) && field !== excludeField); + if (field) { + return field; + } + } + return undefined; +}; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 98441d484f4..3bfd1996f34 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -13642,9 +13642,14 @@ "regression-transformer-editor": { "label": { "cubic": "Cubic", + "decic": "Decic", + "nonic": "Nonic", + "octic": "Octic", "quadratic": "Quadratic", "quartic": "Quartic", - "quintic": "Quintic" + "quintic": "Quintic", + "septic": "Septic", + "sextic": "Sextic" }, "label-degree": "Degree", "label-model-type": "Model type", @@ -13661,6 +13666,7 @@ "tags": { "regression-analysis": "Regression analysis" }, + "tooltip-high-degree-polynomial": "Higher-degree polynomials (e.g., degree 4 or higher) can result in misleading trends and unstable fits. Proceed with caution.", "tooltip-number-of-xy-points-to-predict": "Number of X,Y points to predict" }, "rename-by-regex-transformer": {