Chart builder: discoverable entry points — Build Chart door, dataset picker, no-datasets state

This commit is contained in:
2026-06-12 21:46:25 +03:00
parent dbb4522d78
commit ca70b3e491
22 changed files with 625 additions and 75 deletions
+89
View File
@@ -28,6 +28,7 @@ import {
calculatedFieldNames,
effectiveColumns,
pruneEncodings,
rebaseBuilderConfig,
type BuilderCalculate,
type BuilderColumns,
type BuilderConfig,
@@ -1290,3 +1291,91 @@ describe('pruneEncodings', () => {
expect(pruneEncodings(config, columns)).toBe(config);
});
});
describe('rebaseBuilderConfig (dataset switch)', () => {
// The new dataset shares `category` but lacks `value`/`when`/`flag`.
const newColumns: BuilderColumns = {
columns: ['category', 'profit'],
columnTypes: [
{ name: 'category', type: 'string' },
{ name: 'profit', type: 'number' },
],
};
it('keeps chart-level intent and same-name bindings, re-pointing the dataset', () => {
const config: BuilderConfig = {
datasetName: 'Old',
mark: 'bar',
title: 'Revenue by region',
subtitle: 'FY26',
width: 400,
height: 200,
sort: 'descending',
stack: 'normalize',
encodings: { x: { field: 'category', type: 'nominal' } },
};
const out = rebaseBuilderConfig(config, 'New', newColumns);
expect(out.datasetName).toBe('New');
expect(out.mark).toBe('bar');
expect(out.title).toBe('Revenue by region');
expect(out.subtitle).toBe('FY26');
expect(out.width).toBe(400);
expect(out.height).toBe(200);
expect(out.sort).toBe('descending');
expect(out.stack).toBe('normalize');
expect(out.encodings.x).toEqual({ field: 'category', type: 'nominal' });
});
it('sheds encodings and predicate filters bound to columns the new dataset lacks', () => {
const config: BuilderConfig = {
datasetName: 'Old',
mark: 'bar',
encodings: {
x: { field: 'category', type: 'nominal' },
y: { field: 'value', type: 'quantitative' },
},
filters: [
filter({ id: 'f1', field: 'value', fieldType: 'quantitative', op: 'gt', value: '0' }),
filter({ id: 'f2', field: 'category', fieldType: 'nominal', op: 'equal', value: 'A' }),
],
};
const out = rebaseBuilderConfig(config, 'New', newColumns);
expect(out.encodings.x).toEqual({ field: 'category', type: 'nominal' });
expect(out.encodings.y).toBeNull();
expect(out.filters?.map((f) => f.id)).toEqual(['f2']);
});
it('keeps expression filters, constants, count mappings, and calculated-field bindings', () => {
const config: BuilderConfig = {
datasetName: 'Old',
mark: 'bar',
encodings: {
x: { field: 'ratio', type: 'quantitative' }, // a calculated field travels along
y: { type: 'quantitative', aggregate: 'count' }, // field-less count
color: { value: '#ff0000', type: 'nominal' }, // constant — no column binding
},
calculates: [calc({ as: 'ratio', expr: 'datum.profit * 2' })],
filters: [filter({ id: 'fx', mode: 'expression', expr: 'datum.value > 0' })],
};
const out = rebaseBuilderConfig(config, 'New', newColumns);
expect(out.encodings.x).toEqual({ field: 'ratio', type: 'quantitative' });
expect(out.encodings.y).toEqual({ type: 'quantitative', aggregate: 'count' });
expect(out.encodings.color).toEqual({ value: '#ff0000', type: 'nominal' });
expect(out.calculates).toEqual(config.calculates);
expect(out.filters).toEqual(config.filters);
});
it('a same-schema dataset keeps the whole config (only the name changes)', () => {
const config: BuilderConfig = {
datasetName: 'Old',
mark: 'line',
encodings: {
x: { field: 'when', type: 'temporal' },
y: { field: 'value', type: 'quantitative', aggregate: 'sum' },
},
filters: [filter({ field: 'category', fieldType: 'nominal', op: 'equal', value: 'A' })],
};
const out = rebaseBuilderConfig(config, 'New', columns);
expect(out).toEqual({ ...config, datasetName: 'New' });
});
});