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 { scatterSeriesBuilder_data, scatterSeriesBuilder_labels } from './scatterSeriesBuilder';
|
||||
import { createDayjsDate } from '@/lib/date';
|
||||
import type {
|
||||
DatasetOptionsWithTicks,
|
||||
DatasetOption
|
||||
} from '../../../chartHooks/useDatasetOptions/interfaces';
|
||||
import type { DatasetOptionsWithTicks } from '../../../chartHooks/useDatasetOptions/interfaces';
|
||||
import type { SimplifiedColumnType } from '@/api/asset_interfaces/metric';
|
||||
import type { SeriesBuilderProps } from './interfaces';
|
||||
import { ChartType } from '@/api/asset_interfaces/metric/charts/enum';
|
||||
import type { LabelBuilderProps } from './useSeriesOptions';
|
||||
import { DEFAULT_COLUMN_LABEL_FORMAT } from '@/api/asset_interfaces/metric';
|
||||
|
||||
describe('scatterSeriesBuilder_data', () => {
|
||||
const mockColors = ['#FF0000', '#00FF00'];
|
||||
|
@ -126,67 +123,118 @@ describe('scatterSeriesBuilder_data', () => {
|
|||
});
|
||||
|
||||
describe('scatterSeriesBuilder_labels', () => {
|
||||
const baseTrendlineSeries = [
|
||||
{
|
||||
yAxisKey: 'metric1',
|
||||
data: [10, 20, 30],
|
||||
label: 'Trendline 1',
|
||||
tooltipData: [],
|
||||
xAxisKeys: ['timestamp'],
|
||||
type: 'line' as const,
|
||||
borderColor: '#FF0000',
|
||||
borderWidth: 2,
|
||||
pointRadius: 0
|
||||
}
|
||||
];
|
||||
|
||||
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: []
|
||||
});
|
||||
test('should return undefined when trendlineSeries is empty', () => {
|
||||
const props: LabelBuilderProps = {
|
||||
trendlineSeries: [],
|
||||
datasetOptions: {
|
||||
ticks: [],
|
||||
datasets: [],
|
||||
ticksKey: []
|
||||
},
|
||||
columnLabelFormats: {},
|
||||
xAxisKeys: ['x'],
|
||||
sizeKey: [],
|
||||
columnSettings: {}
|
||||
};
|
||||
|
||||
const result = scatterSeriesBuilder_labels(props);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should process trendline series correctly', () => {
|
||||
const result = scatterSeriesBuilder_labels(baseProps);
|
||||
expect(result).toBeDefined();
|
||||
test('should process date labels directly when x-axis is date type', () => {
|
||||
const dateString1 = '2023-01-01';
|
||||
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) => {
|
||||
const { trendlineSeries, datasetOptions } = props;
|
||||
const { trendlineSeries, datasetOptions, columnLabelFormats, xAxisKeys } = props;
|
||||
|
||||
if (trendlineSeries.length > 0) {
|
||||
// Create a Set of relevant yAxisKeys for O(1) lookup
|
||||
const relevantYAxisKeys = new Set(trendlineSeries.map((t) => t.yAxisKey));
|
||||
if (!trendlineSeries.length) return undefined;
|
||||
|
||||
// Combine filtering, flattening and uniqueness in a single pass
|
||||
const allTicksForScatter = datasetOptions.datasets
|
||||
.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]);
|
||||
// Create a Set of relevant yAxisKeys for O(1) lookup
|
||||
const relevantYAxisKeys = new Set(trendlineSeries.map((t) => t.yAxisKey));
|
||||
|
||||
const modifyFiedProps = {
|
||||
...props,
|
||||
datasetOptions: {
|
||||
...props.datasetOptions,
|
||||
ticks: allTicksForScatter
|
||||
}
|
||||
};
|
||||
// Get X-axis format information once
|
||||
const xColumnLabelFormat = columnLabelFormats[xAxisKeys[0]] || DEFAULT_COLUMN_LABEL_FORMAT;
|
||||
const useDateLabels =
|
||||
xAxisKeys.length === 1 &&
|
||||
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/
|
||||
|
||||
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 { DATASET_IDS } from '../config';
|
||||
import { isDateColumnType, isNumericColumnType } from '@/lib/messages';
|
||||
|
@ -230,9 +230,6 @@ export const trendlineDatasetCreator: Record<
|
|||
|
||||
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);
|
||||
|
||||
// Get mapped data points using the updated dataMapper
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue