mirror of https://github.com/buster-so/buster.git
Nate/linear regressions (#290)
Scatter chart fixes for regression lines
This commit is contained in:
parent
8ecea82a88
commit
fdde29b9ce
|
@ -1,14 +1,11 @@
|
||||||
import { type IColumnLabelFormat } from '@/api/asset_interfaces/metric';
|
import { type IColumnLabelFormat } from '@/api/asset_interfaces/metric';
|
||||||
import { scatterSeriesBuilder_data, scatterSeriesBuilder_labels } from './scatterSeriesBuilder';
|
import { scatterSeriesBuilder_data, scatterSeriesBuilder_labels } from './scatterSeriesBuilder';
|
||||||
import { createDayjsDate } from '@/lib/date';
|
import { createDayjsDate } from '@/lib/date';
|
||||||
import type {
|
import type { DatasetOptionsWithTicks } from '../../../chartHooks/useDatasetOptions/interfaces';
|
||||||
DatasetOptionsWithTicks,
|
|
||||||
DatasetOption
|
|
||||||
} from '../../../chartHooks/useDatasetOptions/interfaces';
|
|
||||||
import type { SimplifiedColumnType } from '@/api/asset_interfaces/metric';
|
import type { SimplifiedColumnType } from '@/api/asset_interfaces/metric';
|
||||||
import type { SeriesBuilderProps } from './interfaces';
|
import type { SeriesBuilderProps } from './interfaces';
|
||||||
import { ChartType } from '@/api/asset_interfaces/metric/charts/enum';
|
|
||||||
import type { LabelBuilderProps } from './useSeriesOptions';
|
import type { LabelBuilderProps } from './useSeriesOptions';
|
||||||
|
import { DEFAULT_COLUMN_LABEL_FORMAT } from '@/api/asset_interfaces/metric';
|
||||||
|
|
||||||
describe('scatterSeriesBuilder_data', () => {
|
describe('scatterSeriesBuilder_data', () => {
|
||||||
const mockColors = ['#FF0000', '#00FF00'];
|
const mockColors = ['#FF0000', '#00FF00'];
|
||||||
|
@ -126,67 +123,118 @@ describe('scatterSeriesBuilder_data', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('scatterSeriesBuilder_labels', () => {
|
describe('scatterSeriesBuilder_labels', () => {
|
||||||
const baseTrendlineSeries = [
|
test('should return undefined when trendlineSeries is empty', () => {
|
||||||
{
|
const props: LabelBuilderProps = {
|
||||||
yAxisKey: 'metric1',
|
trendlineSeries: [],
|
||||||
data: [10, 20, 30],
|
datasetOptions: {
|
||||||
label: 'Trendline 1',
|
ticks: [],
|
||||||
tooltipData: [],
|
datasets: [],
|
||||||
xAxisKeys: ['timestamp'],
|
ticksKey: []
|
||||||
type: 'line' as const,
|
},
|
||||||
borderColor: '#FF0000',
|
columnLabelFormats: {},
|
||||||
borderWidth: 2,
|
xAxisKeys: ['x'],
|
||||||
pointRadius: 0
|
sizeKey: [],
|
||||||
}
|
columnSettings: {}
|
||||||
];
|
};
|
||||||
|
|
||||||
const baseProps: LabelBuilderProps = {
|
|
||||||
trendlineSeries: baseTrendlineSeries,
|
|
||||||
datasetOptions: {
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
dataKey: 'metric1',
|
|
||||||
data: [10, 20, 30],
|
|
||||||
ticksForScatter: [
|
|
||||||
[1000, 'Jan 1'],
|
|
||||||
[2000, 'Jan 2'],
|
|
||||||
[3000, 'Jan 3']
|
|
||||||
],
|
|
||||||
label: [{ key: 'metric1', value: 'Dataset 1' }],
|
|
||||||
axisType: 'y',
|
|
||||||
tooltipData: []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
ticks: [
|
|
||||||
[1000, 'Jan 1'],
|
|
||||||
[2000, 'Jan 2'],
|
|
||||||
[3000, 'Jan 3']
|
|
||||||
],
|
|
||||||
ticksKey: [{ key: 'timestamp', value: 'Timestamp' }]
|
|
||||||
},
|
|
||||||
columnLabelFormats: {
|
|
||||||
timestamp: {
|
|
||||||
columnType: 'timestamp' as SimplifiedColumnType,
|
|
||||||
style: 'date'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
xAxisKeys: ['timestamp'],
|
|
||||||
sizeKey: [],
|
|
||||||
columnSettings: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should return undefined when no trendlines exist', () => {
|
|
||||||
const result = scatterSeriesBuilder_labels({
|
|
||||||
...baseProps,
|
|
||||||
trendlineSeries: []
|
|
||||||
});
|
|
||||||
|
|
||||||
|
const result = scatterSeriesBuilder_labels(props);
|
||||||
expect(result).toBeUndefined();
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process trendline series correctly', () => {
|
test('should process date labels directly when x-axis is date type', () => {
|
||||||
const result = scatterSeriesBuilder_labels(baseProps);
|
const dateString1 = '2023-01-01';
|
||||||
expect(result).toBeDefined();
|
const dateString2 = '2023-01-02';
|
||||||
|
|
||||||
|
const props: LabelBuilderProps = {
|
||||||
|
trendlineSeries: [{ yAxisKey: 'y1' } as any],
|
||||||
|
datasetOptions: {
|
||||||
|
ticks: [[dateString1], [dateString2]],
|
||||||
|
datasets: [{ dataKey: 'y1', data: [10, 20] } as any],
|
||||||
|
ticksKey: [{ key: 'x', value: 'X Axis' }]
|
||||||
|
} as any,
|
||||||
|
columnLabelFormats: {
|
||||||
|
x: {
|
||||||
|
columnType: 'date',
|
||||||
|
style: 'date'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
xAxisKeys: ['x'],
|
||||||
|
sizeKey: [],
|
||||||
|
columnSettings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = scatterSeriesBuilder_labels(props);
|
||||||
|
expect(result).toEqual([
|
||||||
|
createDayjsDate(dateString1).toDate(),
|
||||||
|
createDayjsDate(dateString2).toDate()
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should collect all ticks without deduplication', () => {
|
||||||
|
const props: LabelBuilderProps = {
|
||||||
|
trendlineSeries: [{ yAxisKey: 'y1' } as any, { yAxisKey: 'y2' } as any],
|
||||||
|
datasetOptions: {
|
||||||
|
ticks: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
dataKey: 'y1',
|
||||||
|
data: [10, 20],
|
||||||
|
ticksForScatter: [
|
||||||
|
[1, 'A'],
|
||||||
|
[2, 'B']
|
||||||
|
]
|
||||||
|
} as any,
|
||||||
|
{
|
||||||
|
dataKey: 'y2',
|
||||||
|
data: [30, 40, 50],
|
||||||
|
ticksForScatter: [
|
||||||
|
[1, 'A'],
|
||||||
|
[3, 'C'],
|
||||||
|
[5, 'D']
|
||||||
|
]
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
ticksKey: [{ key: 'x', value: 'X Axis' }]
|
||||||
|
} as any,
|
||||||
|
columnLabelFormats: {
|
||||||
|
x: DEFAULT_COLUMN_LABEL_FORMAT
|
||||||
|
},
|
||||||
|
xAxisKeys: ['x'],
|
||||||
|
sizeKey: [],
|
||||||
|
columnSettings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = scatterSeriesBuilder_labels(props);
|
||||||
|
// Should include duplicate [1, 'A'] from both datasets
|
||||||
|
expect(result).toEqual([1, 'A', 1, 'A', 2, 'B', 3, 'C', 5, 'D']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined when no relevant datasets are found', () => {
|
||||||
|
const props: LabelBuilderProps = {
|
||||||
|
trendlineSeries: [{ yAxisKey: 'y1' } as any],
|
||||||
|
datasetOptions: {
|
||||||
|
ticks: [],
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
dataKey: 'y2', // Not matching with trendlineSeries
|
||||||
|
data: [10, 20],
|
||||||
|
ticksForScatter: [
|
||||||
|
[1, 'A'],
|
||||||
|
[2, 'B']
|
||||||
|
]
|
||||||
|
} as any
|
||||||
|
],
|
||||||
|
ticksKey: [{ key: 'x', value: 'X Axis' }]
|
||||||
|
} as any,
|
||||||
|
columnLabelFormats: {
|
||||||
|
x: DEFAULT_COLUMN_LABEL_FORMAT
|
||||||
|
},
|
||||||
|
xAxisKeys: ['x'],
|
||||||
|
sizeKey: [],
|
||||||
|
columnSettings: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = scatterSeriesBuilder_labels(props);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -143,33 +143,44 @@ const computeSizeRatio = (
|
||||||
};
|
};
|
||||||
|
|
||||||
export const scatterSeriesBuilder_labels = (props: LabelBuilderProps) => {
|
export const scatterSeriesBuilder_labels = (props: LabelBuilderProps) => {
|
||||||
const { trendlineSeries, datasetOptions } = props;
|
const { trendlineSeries, datasetOptions, columnLabelFormats, xAxisKeys } = props;
|
||||||
|
|
||||||
if (trendlineSeries.length > 0) {
|
if (!trendlineSeries.length) return undefined;
|
||||||
// Create a Set of relevant yAxisKeys for O(1) lookup
|
|
||||||
const relevantYAxisKeys = new Set(trendlineSeries.map((t) => t.yAxisKey));
|
|
||||||
|
|
||||||
// Combine filtering, flattening and uniqueness in a single pass
|
// Create a Set of relevant yAxisKeys for O(1) lookup
|
||||||
const allTicksForScatter = datasetOptions.datasets
|
const relevantYAxisKeys = new Set(trendlineSeries.map((t) => t.yAxisKey));
|
||||||
.filter((dataset) => relevantYAxisKeys.has(dataset.dataKey))
|
|
||||||
.flatMap((dataset) => dataset.ticksForScatter || [])
|
|
||||||
.sort((a, b) => {
|
|
||||||
const aVal = a[0],
|
|
||||||
bVal = b[0];
|
|
||||||
return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
|
|
||||||
})
|
|
||||||
.filter((tick, index, array) => index === 0 || tick[0] !== array[index - 1][0]);
|
|
||||||
|
|
||||||
const modifyFiedProps = {
|
// Get X-axis format information once
|
||||||
...props,
|
const xColumnLabelFormat = columnLabelFormats[xAxisKeys[0]] || DEFAULT_COLUMN_LABEL_FORMAT;
|
||||||
datasetOptions: {
|
const useDateLabels =
|
||||||
...props.datasetOptions,
|
xAxisKeys.length === 1 &&
|
||||||
ticks: allTicksForScatter
|
datasetOptions.ticks[0]?.length === 1 &&
|
||||||
}
|
xColumnLabelFormat.columnType === 'date' &&
|
||||||
};
|
xColumnLabelFormat.style === 'date';
|
||||||
|
|
||||||
return lineSeriesBuilder_labels(modifyFiedProps);
|
if (useDateLabels) {
|
||||||
|
// Process date labels directly without extra iterations
|
||||||
|
return datasetOptions.ticks.flatMap((item) =>
|
||||||
|
item.map<Date>((dateItem) => createDayjsDate(dateItem as string).toDate())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
// Only process relevant datasets
|
||||||
|
const relevantDatasets = datasetOptions.datasets.filter((dataset) =>
|
||||||
|
relevantYAxisKeys.has(dataset.dataKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Early return for no relevant datasets
|
||||||
|
if (!relevantDatasets.length) return undefined;
|
||||||
|
|
||||||
|
// Collect all ticks without deduplication
|
||||||
|
const allTicks: (string | number)[][] = [];
|
||||||
|
relevantDatasets.forEach((dataset) => {
|
||||||
|
dataset.ticksForScatter?.forEach((tick) => {
|
||||||
|
allTicks.push(tick);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort and flatten
|
||||||
|
return allTicks.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).flat();
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Also consider modifying this package to make it work with chartjs 4 https://pomgui.github.io/chartjs-plugin-regression/demo/
|
// Also consider modifying this package to make it work with chartjs 4 https://pomgui.github.io/chartjs-plugin-regression/demo/
|
||||||
|
|
||||||
import type { BusterChartProps, Trendline } from '@/api/asset_interfaces/metric/charts';
|
import type { BusterChartProps, Trendline } from '@/api/asset_interfaces/metric/charts';
|
||||||
import type { DatasetOption, DatasetOptionsWithTicks } from '../interfaces';
|
import type { DatasetOptionsWithTicks } from '../interfaces';
|
||||||
import type { TrendlineDataset } from './trendlineDataset.types';
|
import type { TrendlineDataset } from './trendlineDataset.types';
|
||||||
import { DATASET_IDS } from '../config';
|
import { DATASET_IDS } from '../config';
|
||||||
import { isDateColumnType, isNumericColumnType } from '@/lib/messages';
|
import { isDateColumnType, isNumericColumnType } from '@/lib/messages';
|
||||||
|
@ -230,9 +230,6 @@ export const trendlineDatasetCreator: Record<
|
||||||
|
|
||||||
if (!selectedDatasets || selectedDatasets.length === 0 || validData.length === 0) return [];
|
if (!selectedDatasets || selectedDatasets.length === 0 || validData.length === 0) return [];
|
||||||
|
|
||||||
const isXAxisNumeric = isNumericColumnType(
|
|
||||||
columnLabelFormats[xAxisColumn]?.columnType || DEFAULT_COLUMN_LABEL_FORMAT.columnType
|
|
||||||
);
|
|
||||||
const isXAxisDate = isDateColumnType(columnLabelFormats[xAxisColumn]?.columnType);
|
const isXAxisDate = isDateColumnType(columnLabelFormats[xAxisColumn]?.columnType);
|
||||||
|
|
||||||
// Get mapped data points using the updated dataMapper
|
// Get mapped data points using the updated dataMapper
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue