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
This commit is contained in:
Oscar Kilhed 2023-11-24 15:49:16 +01:00 committed by GitHub
parent 7fa73d2b21
commit ab982e7bd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 714 additions and 0 deletions

View File

@ -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/<GRAFANA VERSION>/panels-visualizations/visualizations/table"
[Table panel]: "/docs/grafana-cloud/ -> /docs/grafana/<GRAFANA VERSION>/panels-visualizations/visualizations/table"

View File

@ -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

View File

@ -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",

View File

@ -39,4 +39,5 @@ export enum DataTransformerID {
timeSeriesTable = 'timeSeriesTable',
formatTime = 'formatTime',
formatString = 'formatString',
regression = 'regression',
}

View File

@ -164,4 +164,5 @@ export interface FeatureToggles {
alertingSimplifiedRouting?: boolean;
logRowsPopoverMenu?: boolean;
pluginsSkipHostEnvVars?: boolean;
regressionTransformation?: boolean;
}

View File

@ -1075,6 +1075,13 @@ var (
FrontendOnly: false,
Owner: grafanaPluginsPlatformSquad,
},
{
Name: "regressionTransformation",
Description: "Enables regression analysis transformation",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaBiSquad,
},
}
)

View File

@ -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

1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
145 alertingSimplifiedRouting experimental @grafana/alerting-squad false false false false
146 logRowsPopoverMenu experimental @grafana/observability-logs false false false true
147 pluginsSkipHostEnvVars experimental @grafana/plugins-platform-backend false false false false
148 regressionTransformation experimental @grafana/grafana-bi-squad false false false true

View File

@ -590,4 +590,8 @@ const (
// FlagPluginsSkipHostEnvVars
// Disables passing host environment variable to plugin processes
FlagPluginsSkipHostEnvVars = "pluginsSkipHostEnvVars"
// FlagRegressionTransformation
// Enables regression analysis transformation
FlagRegressionTransformation = "regressionTransformation"
)

View File

@ -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 {

View File

@ -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));
}

View File

@ -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<RegressionTransformerOptions> = {
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];
};
},
};

View File

@ -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(<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{}} />);
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(<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{}} />);
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(
<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{ modelType: ModelType.polynomial }} />
);
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(
<RegressionTransformerEditor input={[df]} onChange={onChangeMock} options={{ modelType: ModelType.linear }} />
);
expect(await screen.queryByText('Degree')).not.toBeInTheDocument();
});
});

View File

@ -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<RegressionTransformerOptions>) => {
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 (
<>
<InlineField labelWidth={LABEL_WIDTH} label="X field">
<FieldNamePicker
context={{ data: input }}
value={options.xFieldName ?? ''}
item={fieldNamePickerSettings}
onChange={(v) => {
onChange({ ...options, xFieldName: v });
}}
></FieldNamePicker>
</InlineField>
<InlineField labelWidth={LABEL_WIDTH} label="Y field">
<FieldNamePicker
context={{ data: input }}
value={options.yFieldName ?? ''}
item={fieldNamePickerSettings}
onChange={(v) => {
onChange({ ...options, yFieldName: v });
}}
></FieldNamePicker>
</InlineField>
<InlineField labelWidth={LABEL_WIDTH} label="Model type">
<Select
value={options.modelType ?? DEFAULTS.modelType}
onChange={(v) => {
onChange({ ...options, modelType: v.value ?? DEFAULTS.modelType });
}}
options={modelTypeOptions}
></Select>
</InlineField>
<InlineField labelWidth={LABEL_WIDTH} label="Predicted points" tooltip={'Number of X,Y points to predict'}>
<NumberInput
value={options.predictionCount ?? DEFAULTS.predictionCount}
onChange={(v) => {
onChange({ ...options, predictionCount: v });
}}
></NumberInput>
</InlineField>
{options.modelType === ModelType.polynomial && (
<InlineField labelWidth={LABEL_WIDTH} label="Degree">
<Select<number>
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 });
}}
></Select>
</InlineField>
)}
</>
);
};
export const regressionTransformerRegistryItem: TransformerRegistryItem<RegressionTransformerOptions> = {
id: DataTransformerID.regression,
editor: RegressionTransformerEditor,
transformation: RegressionTransformer,
name: RegressionTransformer.name,
description: RegressionTransformer.description,
categories: new Set([TransformerCategory.CalculateNewFields]),
help: getTransformationContent(DataTransformerID.regression).helperDocs,
};

View File

@ -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<TransformerRegistryItem<any>> =
joinByLabelsTransformRegistryItem,
partitionByValuesTransformRegistryItem,
...(config.featureToggles.formatString ? [formatStringTransformerRegistryItem] : []),
...(config.featureToggles.regressionTransformation ? [regressionTransformerRegistryItem] : []),
formatTimeTransformerRegistryItem,
timeSeriesTableTransformRegistryItem,
];

View File

@ -0,0 +1,41 @@
<svg width="95" height="48" viewBox="0 0 95 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1834_14939)">
<path d="M0 0.611699V7.64012H14.394V0H0.601504C0.441975 0 0.28898 0.0644467 0.176176 0.179162C0.0633725 0.293878 0 0.449466 0 0.611699Z" fill="url(#paint0_linear_1834_14939)"/>
<path d="M33.606 7.64012H48V0.611699C48 0.449466 47.9366 0.293878 47.8238 0.179162C47.711 0.0644467 47.558 0 47.3985 0L33.606 0V7.64012Z" fill="url(#paint1_linear_1834_14939)"/>
<path d="M31.194 0H16.8V7.64012H31.194V0Z" fill="url(#paint2_linear_1834_14939)"/>
<path d="M31.194 10.0869H16.8V17.727H31.194V10.0869Z" fill="#84AFF1"/>
<path d="M14.394 10.0869H0V17.727H14.394V10.0869Z" fill="#84AFF1"/>
<path d="M48 10.0869H33.606V17.727H48V10.0869Z" fill="#84AFF1"/>
<path d="M48 20.1799H33.606V27.8262H48V20.1799Z" fill="#3865AB"/>
<path d="M31.194 20.1799H16.8V27.8262H31.194V20.1799Z" fill="#3865AB"/>
<path d="M14.394 20.1799H0V27.8262H14.394V20.1799Z" fill="#3865AB"/>
<path d="M31.194 30.2668H16.8V37.907H31.194V30.2668Z" fill="#84AFF1"/>
<path d="M48 30.2668H33.606V37.907H48V30.2668Z" fill="#84AFF1"/>
<path d="M14.394 30.2668H0V37.907H14.394V30.2668Z" fill="#84AFF1"/>
<path d="M31.194 40.3599H16.8V48H31.194V40.3599Z" fill="#3865AB"/>
<path d="M48 47.3883V40.3721H33.606V48H47.3985C47.558 48 47.711 47.9355 47.8238 47.8208C47.9366 47.7061 48 47.5505 48 47.3883Z" fill="#3865AB"/>
<path d="M14.394 48V40.3721H0V47.3883C0 47.5505 0.0633725 47.7061 0.176176 47.8208C0.28898 47.9355 0.441975 48 0.601504 48H14.394Z" fill="#3865AB"/>
</g>
<path d="M61.9067 30.4001C62.6011 30.4001 67.9327 26.1334 67.9327 24.0001C67.9327 21.8668 62.7357 17.6001 61.9067 17.6001C61.0777 17.6001 60.4023 18.1334 60.4023 19.1741C60.4023 20.2147 63.9067 22.8487 63.9067 22.8487C63.9067 22.8487 56.2539 22.1334 56 22.8487C55.7461 23.564 55.7461 24.4362 56 25.1515C56.2539 25.8668 63.9067 25.1515 63.9067 25.1515C63.9067 25.1515 60.4023 28.0001 60.4023 28.8323C60.4023 29.6644 61.2123 30.4001 61.9067 30.4001Z" fill="#CCCCDC"/>
<path d="M93.9457 25.1875C93.9457 26.9347 93.4911 28.3835 92.4542 29.6477H90.5366C91.3178 28.7102 91.9002 26.8637 91.9002 25.1875C91.9002 23.5114 91.3178 21.6648 90.5366 20.7273H92.4542C93.4911 21.9915 93.9457 23.4404 93.9457 25.1875Z" fill="white"/>
<path d="M86.6153 22.5457L87.4107 24.236L88.2488 22.5457H90.1948L88.7317 25.2729L90.2658 28.0002H88.334L87.4107 26.2672L86.5158 28.0002H84.5556L86.1039 25.2729L84.655 22.5457H86.6153Z" fill="white"/>
<path d="M80.8757 25.1875C80.8757 23.4404 81.3303 21.9915 82.3672 20.7273H84.2848C83.5036 21.6648 82.9212 23.5114 82.9212 25.1875C82.9212 26.8637 83.5036 28.7102 84.2848 29.6477H82.3672C81.3303 28.3835 80.8757 26.9347 80.8757 25.1875Z" fill="white"/>
<path d="M79.7642 22.5456V23.966H78.7699V28.0001H76.8097V23.966H76.1136V22.5456H76.8097V22.3893C76.8097 21.0399 77.5767 20.3865 78.7699 20.3865C79.2671 20.3865 79.7926 20.4859 80.0199 20.5427L79.7358 21.949C79.5938 21.9064 79.3807 21.878 79.2386 21.878C78.8693 21.878 78.7699 22.0484 78.7699 22.3041V22.5456H79.7642Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_1834_14939" x1="0" y1="3.82312" x2="14.394" y2="3.82312" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2CC0C"/>
<stop offset="1" stop-color="#FF9830"/>
</linearGradient>
<linearGradient id="paint1_linear_1834_14939" x1="33.606" y1="3.82312" x2="48" y2="3.82312" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2CC0C"/>
<stop offset="1" stop-color="#FF9830"/>
</linearGradient>
<linearGradient id="paint2_linear_1834_14939" x1="16.8" y1="3.82312" x2="31.2" y2="3.82312" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2CC0C"/>
<stop offset="1" stop-color="#FF9830"/>
</linearGradient>
<clipPath id="clip0_1834_14939">
<rect width="48" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -0,0 +1,41 @@
<svg width="95" height="48" viewBox="0 0 95 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1834_45844)">
<path d="M0 0.611699V7.64012H14.394V0H0.601504C0.441975 0 0.28898 0.0644467 0.176176 0.179162C0.0633725 0.293878 0 0.449466 0 0.611699Z" fill="url(#paint0_linear_1834_45844)"/>
<path d="M33.606 7.64012H48V0.611699C48 0.449466 47.9366 0.293878 47.8238 0.179162C47.711 0.0644467 47.558 0 47.3985 0L33.606 0V7.64012Z" fill="url(#paint1_linear_1834_45844)"/>
<path d="M31.194 0H16.8V7.64012H31.194V0Z" fill="url(#paint2_linear_1834_45844)"/>
<path d="M31.194 10.0869H16.8V17.727H31.194V10.0869Z" fill="#84AFF1"/>
<path d="M14.394 10.0869H0V17.727H14.394V10.0869Z" fill="#84AFF1"/>
<path d="M48 10.0869H33.606V17.727H48V10.0869Z" fill="#84AFF1"/>
<path d="M48 20.1799H33.606V27.8262H48V20.1799Z" fill="#3865AB"/>
<path d="M31.194 20.1799H16.8V27.8262H31.194V20.1799Z" fill="#3865AB"/>
<path d="M14.394 20.1799H0V27.8262H14.394V20.1799Z" fill="#3865AB"/>
<path d="M31.194 30.2668H16.8V37.907H31.194V30.2668Z" fill="#84AFF1"/>
<path d="M48 30.2668H33.606V37.907H48V30.2668Z" fill="#84AFF1"/>
<path d="M14.394 30.2668H0V37.907H14.394V30.2668Z" fill="#84AFF1"/>
<path d="M31.194 40.3599H16.8V48H31.194V40.3599Z" fill="#3865AB"/>
<path d="M48 47.3883V40.3721H33.606V48H47.3985C47.558 48 47.711 47.9355 47.8238 47.8208C47.9366 47.7061 48 47.5505 48 47.3883Z" fill="#3865AB"/>
<path d="M14.394 48V40.3721H0V47.3883C0 47.5505 0.0633725 47.7061 0.176176 47.8208C0.28898 47.9355 0.441975 48 0.601504 48H14.394Z" fill="#3865AB"/>
</g>
<path d="M61.9067 30.4001C62.6011 30.4001 67.9327 26.1334 67.9327 24.0001C67.9327 21.8668 62.7357 17.6001 61.9067 17.6001C61.0778 17.6001 60.4023 18.1334 60.4023 19.1741C60.4023 20.2147 63.9067 22.8487 63.9067 22.8487C63.9067 22.8487 56.2539 22.1334 56 22.8487C55.7461 23.564 55.7461 24.4362 56 25.1515C56.2539 25.8668 63.9067 25.1515 63.9067 25.1515C63.9067 25.1515 60.4023 28.0001 60.4023 28.8323C60.4023 29.6644 61.2124 30.4001 61.9067 30.4001Z" fill="#CCCCDC"/>
<path d="M93.832 24.801C93.832 26.5482 93.3775 27.9971 92.3406 29.2613H90.4229C91.2042 28.3238 91.7866 26.4772 91.7866 24.801C91.7866 23.1249 91.2042 21.2783 90.4229 20.3408H92.3406C93.3775 21.605 93.832 23.0539 93.832 24.801Z" fill="#24292E"/>
<path d="M86.5016 22.1592L87.297 23.8495L88.1351 22.1592H90.0811L88.6181 24.8865L90.1522 27.6137H88.2203L87.297 25.8808L86.4022 27.6137H84.4419L85.9902 24.8865L84.5414 22.1592H86.5016Z" fill="#24292E"/>
<path d="M80.7621 24.801C80.7621 23.0539 81.2166 21.605 82.2536 20.3408H84.1712C83.3899 21.2783 82.8075 23.1249 82.8075 24.801C82.8075 26.4772 83.3899 28.3238 84.1712 29.2613H82.2536C81.2166 27.9971 80.7621 26.5482 80.7621 24.801Z" fill="#24292E"/>
<path d="M79.6506 22.1591V23.5795H78.6563V27.6136H76.696V23.5795H76V22.1591H76.696V22.0028C76.696 20.6534 77.4631 20 78.6563 20C79.1534 20 79.679 20.0994 79.9063 20.1562L79.6222 21.5625C79.4801 21.5199 79.267 21.4915 79.125 21.4915C78.7557 21.4915 78.6563 21.6619 78.6563 21.9176V22.1591H79.6506Z" fill="#24292E"/>
<defs>
<linearGradient id="paint0_linear_1834_45844" x1="0" y1="3.82312" x2="14.394" y2="3.82312" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2CC0C"/>
<stop offset="1" stop-color="#FF9830"/>
</linearGradient>
<linearGradient id="paint1_linear_1834_45844" x1="33.606" y1="3.82312" x2="48" y2="3.82312" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2CC0C"/>
<stop offset="1" stop-color="#FF9830"/>
</linearGradient>
<linearGradient id="paint2_linear_1834_45844" x1="16.8" y1="3.82312" x2="31.2" y2="3.82312" gradientUnits="userSpaceOnUse">
<stop stop-color="#F2CC0C"/>
<stop offset="1" stop-color="#FF9830"/>
</linearGradient>
<clipPath id="clip0_1834_45844">
<rect width="48" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -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"