From ab982e7bd36b86c94093bddcc31008f5dc49660e Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Fri, 24 Nov 2023 15:49:16 +0100 Subject: [PATCH] Transformations: Add regression analysis transformation (#78457) * regression analysis first dragt * Swap to better regression libraries * fix name * Interpolate x points instead of using source x points * clean up ui and add feature toggle * fix merge error * change to loop for finding min max, rename resolution * Add docs * add docs and tests * change name to regression analysis * update docs * Fix editor labels * add regression images * fix docs --- .../transform-data/index.md | 13 ++ .../feature-toggles/index.md | 1 + package.json | 2 + .../src/transformations/transformers/ids.ts | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../app/features/transformers/docs/content.ts | 21 +++ .../regression/regression.test.ts | 142 ++++++++++++++++++ .../transformers/regression/regression.ts | 122 +++++++++++++++ .../regression/regressionEditor.test.tsx | 87 +++++++++++ .../regression/regressionEditor.tsx | 142 ++++++++++++++++++ .../transformers/standardTransformers.ts | 2 + .../img/transformations/dark/regression.svg | 41 +++++ .../img/transformations/light/regression.svg | 41 +++++ yarn.lock | 86 +++++++++++ 17 files changed, 714 insertions(+) create mode 100644 public/app/features/transformers/regression/regression.test.ts create mode 100644 public/app/features/transformers/regression/regression.ts create mode 100644 public/app/features/transformers/regression/regressionEditor.test.tsx create mode 100644 public/app/features/transformers/regression/regressionEditor.tsx create mode 100644 public/img/transformations/dark/regression.svg create mode 100644 public/img/transformations/light/regression.svg diff --git a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md index aed4b33b33d..ace0f4dfe21 100644 --- a/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md +++ b/docs/sources/panels-visualizations/query-transform-data/transform-data/index.md @@ -1266,6 +1266,19 @@ For each generated **Trend** field value, a calculation function can be selected > **Note:** This transformation is available in Grafana 9.5+ as an opt-in beta feature. Modify the Grafana [configuration file][] to use it. +### Regression analysis + +Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline. + +There are two different models: + +- **Linear regression** - Fits a linear function to the data. + {{< figure src="/static/img/docs/transformations/linear-regression.png" class="docs-image--no-shadow" max-width= "1100px" >}} +- **Polynomial regression** - Fits a polynomial function to the data. + {{< figure src="/static/img/docs/transformations/polynomial-regression.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +> **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the `regressionTransformation` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. + {{% docs/reference %}} [Table panel]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/table" [Table panel]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/table" diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 017f5289b76..6d1cde12d95 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -164,6 +164,7 @@ Experimental features might be changed or removed without prior notice. | `flameGraphItemCollapsing` | Allow collapsing of flame graph items | | `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected | | `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes | +| `regressionTransformation` | Enables regression analysis transformation | ## Development feature toggles diff --git a/package.json b/package.json index edb550df4b6..fb4c1a69425 100644 --- a/package.json +++ b/package.json @@ -343,6 +343,8 @@ "marked": "5.1.1", "marked-mangle": "1.1.0", "memoize-one": "6.0.0", + "ml-regression-polynomial": "^3.0.0", + "ml-regression-simple-linear": "^3.0.0", "moment": "2.29.4", "moment-timezone": "0.5.43", "monaco-editor": "0.34.0", diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index efcc57804c4..4d5f7e2dd9e 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -39,4 +39,5 @@ export enum DataTransformerID { timeSeriesTable = 'timeSeriesTable', formatTime = 'formatTime', formatString = 'formatString', + regression = 'regression', } diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 05ad75b6bfb..0b730107806 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -164,4 +164,5 @@ export interface FeatureToggles { alertingSimplifiedRouting?: boolean; logRowsPopoverMenu?: boolean; pluginsSkipHostEnvVars?: boolean; + regressionTransformation?: boolean; } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 4bd37f5ea9e..de19ffc975b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1075,6 +1075,13 @@ var ( FrontendOnly: false, Owner: grafanaPluginsPlatformSquad, }, + { + Name: "regressionTransformation", + Description: "Enables regression analysis transformation", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaBiSquad, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index d91242b8bd1..e7ba5c895f0 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -145,3 +145,4 @@ datatrails,experimental,@grafana/dashboards-squad,false,false,false,true alertingSimplifiedRouting,experimental,@grafana/alerting-squad,false,false,false,false logRowsPopoverMenu,experimental,@grafana/observability-logs,false,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false,false +regressionTransformation,experimental,@grafana/grafana-bi-squad,false,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 49daca3b6b5..5a663e65aa8 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -590,4 +590,8 @@ const ( // FlagPluginsSkipHostEnvVars // Disables passing host environment variable to plugin processes FlagPluginsSkipHostEnvVars = "pluginsSkipHostEnvVars" + + // FlagRegressionTransformation + // Enables regression analysis transformation + FlagRegressionTransformation = "regressionTransformation" ) diff --git a/public/app/features/transformers/docs/content.ts b/public/app/features/transformers/docs/content.ts index 893e44e6c85..deece5b3e36 100644 --- a/public/app/features/transformers/docs/content.ts +++ b/public/app/features/transformers/docs/content.ts @@ -1377,6 +1377,27 @@ export const transformationDocsContent: TransformationDocsContentType = { }, ], }, + regression: { + name: 'Regression analysis', + getHelperDocs: function (imageRenderType: ImageRenderType = ImageRenderType.ShortcodeFigure) { + return ` + Use this transformation to create a new data frame containing values predicted by a statistical model. This is useful for finding a trend in chaotic data. It works by fitting a mathematical function to the data, using either linear or polynomial regression. The data frame can then be used in a visualization to display a trendline. + + There are two different models: + + - **Linear regression** - Fits a linear function to the data. + ${buildImageContent('/static/img/docs/transformations/linear-regression.png', imageRenderType, 'Linear regression')} + - **Polynomial regression** - Fits a polynomial function to the data. + ${buildImageContent( + '/static/img/docs/transformations/polynomial-regression.png', + imageRenderType, + 'Polynomial regression' + )} + + > **Note:** This transformation is an experimental feature. Engineering and on-call support is not available. Documentation is either limited or not provided outside of code comments. No SLA is provided. Enable the \`regressionTransformation\` feature toggle in Grafana to use this feature. Contact Grafana Support to enable this feature in Grafana Cloud. + `; + }, + }, }; export function getLinkToDocs(): string { diff --git a/public/app/features/transformers/regression/regression.test.ts b/public/app/features/transformers/regression/regression.test.ts new file mode 100644 index 00000000000..3383c6a2b17 --- /dev/null +++ b/public/app/features/transformers/regression/regression.test.ts @@ -0,0 +1,142 @@ +import { + DataFrame, + DataFrameDTO, + DataTransformContext, + Field, + FieldType, + toDataFrame, + toDataFrameDTO, +} from '@grafana/data'; + +import { ModelType, RegressionTransformer, RegressionTransformerOptions } from './regression'; + +describe('Regression transformation', () => { + it('it should predict a linear regression to exactly fit the data when the data is f(x) = x', () => { + const source = [ + toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] }, + { name: 'value', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + ], + }), + ]; + + const config: RegressionTransformerOptions = { + modelType: ModelType.linear, + predictionCount: 6, + xFieldName: 'time', + yFieldName: 'value', + }; + + expect(toEquableDataFrames(RegressionTransformer.transformer(config, {} as DataTransformContext)(source))).toEqual( + toEquableDataFrames([ + toEquableDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} }, + { name: 'value', type: FieldType.number, values: [0, 1, 2, 3, 4, 5], config: {} }, + ], + length: 6, + }), + toEquableDataFrame({ + name: 'linear regression', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} }, + { name: 'value predicted', type: FieldType.number, values: [0, 1, 2, 3, 4, 5], config: {} }, + ], + length: 6, + }), + ]) + ); + }); + it('it should predict a linear regression where f(x) = 1', () => { + const source = [ + toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] }, + { name: 'value', type: FieldType.number, values: [0, 1, 2, 2, 1, 0] }, + ], + }), + ]; + + const config: RegressionTransformerOptions = { + modelType: ModelType.linear, + predictionCount: 6, + xFieldName: 'time', + yFieldName: 'value', + }; + + expect(toEquableDataFrames(RegressionTransformer.transformer(config, {} as DataTransformContext)(source))).toEqual( + toEquableDataFrames([ + toEquableDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} }, + { name: 'value', type: FieldType.number, values: [0, 1, 2, 2, 1, 0], config: {} }, + ], + length: 6, + }), + toEquableDataFrame({ + name: 'linear regression', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5], config: {} }, + { name: 'value predicted', type: FieldType.number, values: [1, 1, 1, 1, 1, 1], config: {} }, + ], + length: 6, + }), + ]) + ); + }); + + it('it should predict a quadratic function', () => { + const source = [ + toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] }, + { name: 'value', type: FieldType.number, values: [0, 1, 2, 2, 1, 0] }, + ], + }), + ]; + + const config: RegressionTransformerOptions = { + modelType: ModelType.polynomial, + degree: 2, + predictionCount: 6, + xFieldName: 'time', + yFieldName: 'value', + }; + + const result = RegressionTransformer.transformer(config, {} as DataTransformContext)(source); + + expect(result[1].fields[1].values[0]).toBeCloseTo(-0.1, 1); + expect(result[1].fields[1].values[1]).toBeCloseTo(1.2, 1); + expect(result[1].fields[1].values[2]).toBeCloseTo(1.86, 1); + expect(result[1].fields[1].values[3]).toBeCloseTo(1.86, 1); + expect(result[1].fields[1].values[4]).toBeCloseTo(1.2, 1); + expect(result[1].fields[1].values[5]).toBeCloseTo(-0.1, 1); + }); +}); + +function toEquableDataFrame(source: DataFrame): DataFrame { + return toDataFrame({ + ...source, + fields: source.fields.map((field: Field) => { + return { + ...field, + config: {}, + }; + }), + }); +} + +function toEquableDataFrames(data: DataFrame[]): DataFrameDTO[] { + return data.map((frame) => toDataFrameDTO(frame)); +} diff --git a/public/app/features/transformers/regression/regression.ts b/public/app/features/transformers/regression/regression.ts new file mode 100644 index 00000000000..00d15a144c7 --- /dev/null +++ b/public/app/features/transformers/regression/regression.ts @@ -0,0 +1,122 @@ +import { PolynomialRegression } from 'ml-regression-polynomial'; +import { SimpleLinearRegression } from 'ml-regression-simple-linear'; +import { map } from 'rxjs'; + +import { + DataFrame, + DataTransformerID, + FieldMatcherID, + FieldType, + SynchronousDataTransformerInfo, + fieldMatchers, +} from '@grafana/data'; + +export enum ModelType { + linear = 'linear', + polynomial = 'polynomial', +} + +export interface RegressionTransformerOptions { + modelType?: ModelType; + degree?: number; + xFieldName?: string; + yFieldName?: string; + predictionCount?: number; +} + +export const DEFAULTS = { predictionCount: 100, modelType: ModelType.linear, degree: 2 }; + +export const RegressionTransformer: SynchronousDataTransformerInfo = { + id: DataTransformerID.regression, + name: 'Regression analysis', + operator: (options, ctx) => (source) => + source.pipe(map((data) => RegressionTransformer.transformer(options, ctx)(data))), + transformer: (options, ctx) => { + return (frames: DataFrame[]) => { + const { predictionCount, modelType, degree } = { ...DEFAULTS, ...options }; + if (frames.length === 0) { + return frames; + } + const matchesY = fieldMatchers.get(FieldMatcherID.byName).get(options.yFieldName); + const matchesX = fieldMatchers.get(FieldMatcherID.byName).get(options.xFieldName); + + let xField; + let yField; + for (const frame of frames) { + const fy = frame.fields.find((f) => matchesY(f, frame, frames)); + if (fy) { + yField = fy; + const fx = frame.fields.find((f) => matchesX(f, frame, frames)); + if (fx) { + xField = fx; + break; + } else { + throw 'X and Y fields must be part of the same frame'; + } + } + } + + if (!xField || !yField) { + return frames; + } + + let xMin = xField.values[0]; + let xMax = xField.values[0]; + + for (let i = 1; i < xField.values.length; i++) { + if (xField.values[i] < xMin) { + xMin = xField.values[i]; + } + if (xField.values[i] > xMax) { + xMax = xField.values[i]; + } + } + + const resolution = (xMax - xMin + 1) / predictionCount; + + // These are the X values for which we should predict Y + const predictionPoints = [...[...Array(predictionCount - 1).keys()].map((_, i) => i * resolution + xMin), xMax]; + + // If X is a time field we normalize the time to the start of the timeseries data + const normalizationSubtrahend = xField.type === FieldType.time ? xMin : 0; + + const yValues = []; + const xValues = []; + + for (let i = 0; i < xField.values.length; i++) { + if (yField.values[i] !== null) { + xValues.push(xField.values[i] - normalizationSubtrahend); + yValues.push(yField.values[i]); + } + } + + let result: PolynomialRegression | SimpleLinearRegression; + switch (modelType) { + case ModelType.linear: + result = new SimpleLinearRegression(xValues, yValues); + break; + case ModelType.polynomial: + result = new PolynomialRegression(xValues, yValues, degree); + break; + default: + return frames; + } + + const newFrame: DataFrame = { + name: `${modelType} regression`, + length: predictionPoints.length, + fields: [ + { name: xField.name, type: xField.type, values: predictionPoints, config: {} }, + { + name: `${yField.name} predicted`, + type: yField.type, + values: predictionPoints.map((x) => result.predict(x - normalizationSubtrahend)), + config: {}, + }, + ], + }; + + return [...frames, newFrame]; + }; + }, +}; diff --git a/public/app/features/transformers/regression/regressionEditor.test.tsx b/public/app/features/transformers/regression/regressionEditor.test.tsx new file mode 100644 index 00000000000..58c91a79298 --- /dev/null +++ b/public/app/features/transformers/regression/regressionEditor.test.tsx @@ -0,0 +1,87 @@ +import { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { FieldType, toDataFrame } from '@grafana/data'; + +import { ModelType } from './regression'; +import { RegressionTransformerEditor } from './regressionEditor'; + +describe('FieldToConfigMappingEditor', () => { + it('Should try to set the first time field as X and first number field as Y', async () => { + const onChangeMock = jest.fn(); + + const df = toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'time', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] }, + { name: 'not this', type: FieldType.time, values: [0, 1, 2, 3, 4, 5] }, + { name: 'value', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + { name: 'not this either', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + ], + }); + + render(); + + expect(onChangeMock).toBeCalledTimes(1); + expect(onChangeMock).toBeCalledWith({ xFieldName: 'time', yFieldName: 'value' }); + }); + + it('Should set the first field as X and the second as Y if there are no time fields', async () => { + const onChangeMock = jest.fn(); + + const df = toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'not this', type: FieldType.string, values: [0, 1, 2, 3, 4, 5] }, + { name: 'foo', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + { name: 'bar', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + { name: 'not this either', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + ], + }); + + render(); + + expect(onChangeMock).toBeCalledTimes(1); + expect(onChangeMock).toBeCalledWith({ xFieldName: 'foo', yFieldName: 'bar' }); + }); + + it('should display degree if the model is polynomial', async () => { + const onChangeMock = jest.fn(); + + const df = toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'foo', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + { name: 'bar', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + ], + }); + + render( + + ); + + expect(await screen.findByText('Degree')).toBeInTheDocument(); + }); + + it('should not display degree if the model is linear', async () => { + const onChangeMock = jest.fn(); + + const df = toDataFrame({ + name: 'data', + refId: 'A', + fields: [ + { name: 'foo', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + { name: 'bar', type: FieldType.number, values: [0, 1, 2, 3, 4, 5] }, + ], + }); + + render( + + ); + + expect(await screen.queryByText('Degree')).not.toBeInTheDocument(); + }); +}); diff --git a/public/app/features/transformers/regression/regressionEditor.tsx b/public/app/features/transformers/regression/regressionEditor.tsx new file mode 100644 index 00000000000..21f112eca8c --- /dev/null +++ b/public/app/features/transformers/regression/regressionEditor.tsx @@ -0,0 +1,142 @@ +import React, { useEffect } from 'react'; + +import { + DataTransformerID, + TransformerRegistryItem, + TransformerUIProps, + TransformerCategory, + fieldMatchers, + FieldMatcherID, + Field, +} from '@grafana/data'; +import { InlineField, Select } from '@grafana/ui'; +import { FieldNamePicker } from '@grafana/ui/src/components/MatchersUI/FieldNamePicker'; +import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; + +import { getTransformationContent } from '../docs/getTransformationContent'; + +import { DEFAULTS, ModelType, RegressionTransformer, RegressionTransformerOptions } from './regression'; + +const fieldNamePickerSettings = { + editor: FieldNamePicker, + id: '', + name: '', + settings: { width: 24, isClearable: false }, +}; + +const LABEL_WIDTH = 20; + +export const RegressionTransformerEditor = ({ + input, + options, + onChange, +}: TransformerUIProps) => { + const modelTypeOptions = [ + { label: 'Linear', value: ModelType.linear }, + { label: 'Polynomial', value: ModelType.polynomial }, + ]; + + useEffect(() => { + let x: Field | undefined; + let y: Field | undefined; + 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; + } + } + } + } + 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; + } + } + } + + if (x && y) { + onChange({ ...options, xFieldName: x.name, yFieldName: y.name }); + } + }); + + return ( + <> + + { + onChange({ ...options, xFieldName: v }); + }} + > + + + { + onChange({ ...options, yFieldName: v }); + }} + > + + + + + + { + onChange({ ...options, predictionCount: v }); + }} + > + + {options.modelType === ModelType.polynomial && ( + + + value={options.degree ?? DEFAULTS.degree} + options={[ + { label: 'Quadratic', value: 2 }, + { label: 'Cubic', value: 3 }, + { label: 'Quartic', value: 4 }, + { label: 'Quintic', value: 5 }, + ]} + onChange={(v) => { + onChange({ ...options, degree: v.value }); + }} + > + + )} + + ); +}; + +export const regressionTransformerRegistryItem: TransformerRegistryItem = { + id: DataTransformerID.regression, + editor: RegressionTransformerEditor, + transformation: RegressionTransformer, + name: RegressionTransformer.name, + description: RegressionTransformer.description, + categories: new Set([TransformerCategory.CalculateNewFields]), + help: getTransformationContent(DataTransformerID.regression).helperDocs, +}; diff --git a/public/app/features/transformers/standardTransformers.ts b/public/app/features/transformers/standardTransformers.ts index c8af140e889..31d03807011 100644 --- a/public/app/features/transformers/standardTransformers.ts +++ b/public/app/features/transformers/standardTransformers.ts @@ -28,6 +28,7 @@ import { joinByLabelsTransformRegistryItem } from './joinByLabels/JoinByLabelsTr import { fieldLookupTransformRegistryItem } from './lookupGazetteer/FieldLookupTransformerEditor'; import { partitionByValuesTransformRegistryItem } from './partitionByValues/PartitionByValuesEditor'; import { prepareTimeseriesTransformerRegistryItem } from './prepareTimeSeries/PrepareTimeSeriesEditor'; +import { regressionTransformerRegistryItem } from './regression/regressionEditor'; import { rowsToFieldsTransformRegistryItem } from './rowsToFields/RowsToFieldsTransformerEditor'; import { spatialTransformRegistryItem } from './spatial/SpatialTransformerEditor'; import { timeSeriesTableTransformRegistryItem } from './timeSeriesTable/TimeSeriesTableTransformEditor'; @@ -62,6 +63,7 @@ export const getStandardTransformers = (): Array> = joinByLabelsTransformRegistryItem, partitionByValuesTransformRegistryItem, ...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []), + ...(config.featureToggles.regressionTransformation ? [regressionTransformerRegistryItem] : []), formatTimeTransformerRegistryItem, timeSeriesTableTransformRegistryItem, ]; diff --git a/public/img/transformations/dark/regression.svg b/public/img/transformations/dark/regression.svg new file mode 100644 index 00000000000..d3b53dfcde9 --- /dev/null +++ b/public/img/transformations/dark/regression.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/transformations/light/regression.svg b/public/img/transformations/light/regression.svg new file mode 100644 index 00000000000..b9c80a71889 --- /dev/null +++ b/public/img/transformations/light/regression.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index 8842cd5134d..11e1256a044 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12138,6 +12138,13 @@ __metadata: languageName: node linkType: hard +"cheminfo-types@npm:^1.7.2": + version: 1.7.2 + resolution: "cheminfo-types@npm:1.7.2" + checksum: 26c4db9600c786aff28276c8dbc48779f6c20705c3673558344282340d3b9d14554f306994e627396678475b50728a96134d99a794271b86301f6945fab355de + languageName: node + linkType: hard + "chokidar@npm:3.5.3, chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.3.1, chokidar@npm:^3.4.2, chokidar@npm:^3.5.3": version: 3.5.3 resolution: "chokidar@npm:3.5.3" @@ -17508,6 +17515,8 @@ __metadata: marked-mangle: "npm:1.1.0" memoize-one: "npm:6.0.0" mini-css-extract-plugin: "npm:2.7.6" + ml-regression-polynomial: "npm:^3.0.0" + ml-regression-simple-linear: "npm:^3.0.0" moment: "npm:2.29.4" moment-timezone: "npm:0.5.43" monaco-editor: "npm:0.34.0" @@ -18717,6 +18726,13 @@ __metadata: languageName: node linkType: hard +"is-any-array@npm:^2.0.0, is-any-array@npm:^2.0.1": + version: 2.0.1 + resolution: "is-any-array@npm:2.0.1" + checksum: a2caaec75abb10ccb7e926aed322df5d2f206ae8645313771282702cf47d626832d9dc3318580e0fddbd04772d899263bebccb12c692e97988017ac549654cd4 + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4, is-arguments@npm:^1.1.1": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" @@ -22223,6 +22239,76 @@ __metadata: languageName: node linkType: hard +"ml-array-max@npm:^1.2.4": + version: 1.2.4 + resolution: "ml-array-max@npm:1.2.4" + dependencies: + is-any-array: "npm:^2.0.0" + checksum: d62689d349c825a36dae212ee109a7f81f2a3c1136cb79f50a90e6c48ae5acc78dacb1334864bfd9abf6f09b78ce6a68e2455b1259b775b3b05556362fbf8470 + languageName: node + linkType: hard + +"ml-array-min@npm:^1.2.3": + version: 1.2.3 + resolution: "ml-array-min@npm:1.2.3" + dependencies: + is-any-array: "npm:^2.0.0" + checksum: bc6e0c69f20eb2b35c2c8d3a59f8c6289462683084e8fa50344ae4db3214b8b30854f96a8eaf22df510752c3065d3337cd996f98a80a95b47a88b38369beeb5b + languageName: node + linkType: hard + +"ml-array-rescale@npm:^1.3.7": + version: 1.3.7 + resolution: "ml-array-rescale@npm:1.3.7" + dependencies: + is-any-array: "npm:^2.0.0" + ml-array-max: "npm:^1.2.4" + ml-array-min: "npm:^1.2.3" + checksum: 2f9883388ebb6c921c648a1cdcf1d853a35dff5f30e7204e142511abcafe05f40c1c1543232503aeff4ecdb3f57e958e7de8d070b973af3be7bfd27381b30528 + languageName: node + linkType: hard + +"ml-matrix@npm:^6.10.5": + version: 6.10.8 + resolution: "ml-matrix@npm:6.10.8" + dependencies: + is-any-array: "npm:^2.0.1" + ml-array-rescale: "npm:^1.3.7" + checksum: a13909330f119dffb5538b08036a635895f87e12b25e3a23b0662e7333cfbb50425cea8fca55b03cd3eb8eeaa2509a2c84e0c26a0ca89c718f97f4234e2008b4 + languageName: node + linkType: hard + +"ml-regression-base@npm:^3.0.0": + version: 3.0.0 + resolution: "ml-regression-base@npm:3.0.0" + dependencies: + cheminfo-types: "npm:^1.7.2" + is-any-array: "npm:^2.0.1" + checksum: b737d4f70d6058efee607ab36a56cc92199111f6ea021e6290158792ee3f410c12b77d5dabfe6ccd55d02f5b6b072b83066a6e5cba909d4c91b85050ac15b491 + languageName: node + linkType: hard + +"ml-regression-polynomial@npm:^3.0.0": + version: 3.0.0 + resolution: "ml-regression-polynomial@npm:3.0.0" + dependencies: + cheminfo-types: "npm:^1.7.2" + ml-matrix: "npm:^6.10.5" + ml-regression-base: "npm:^3.0.0" + checksum: e5d3ee187d6f35e4e3b2191f52d7dc63a61f0b975cf2012ed20403420d6a65ae2ad7d51f80896daf778b39bdbd56d7fa9d4c17aac96a1669acea3fbd2a0a31d7 + languageName: node + linkType: hard + +"ml-regression-simple-linear@npm:^3.0.0": + version: 3.0.0 + resolution: "ml-regression-simple-linear@npm:3.0.0" + dependencies: + cheminfo-types: "npm:^1.7.2" + ml-regression-base: "npm:^3.0.0" + checksum: 2e4650026bb793455788f31de79bd25f5325e97580f20af848c4c8af4d1f1e1d3b04fbdb0f1677071727da3ffe4a34c5d0e4a0533ad81050441b5e0d745bdc57 + languageName: node + linkType: hard + "mocha@npm:10.2.0": version: 10.2.0 resolution: "mocha@npm:10.2.0"