Chart builder: SelectControl pickers, channel chooser, per-type aggregates

This commit is contained in:
2026-06-12 14:45:31 +03:00
parent 4dcff4601d
commit ed66fe9c05
15 changed files with 1232 additions and 276 deletions
+86 -1
View File
@@ -6,6 +6,7 @@ import {
isChannelTypeAllowed,
isColumnAllowedOnChannel,
supportsAggregate,
validAggregateOps,
supportsBin,
supportsTimeUnit,
supportsSort,
@@ -840,12 +841,96 @@ describe('transforms — aggregate / bin / timeUnit', () => {
it('exposes the transform-applicability predicates by field type', () => {
expect(supportsAggregate('quantitative')).toBe(true);
expect(supportsAggregate('nominal')).toBe(false);
// Every type now takes at least one aggregate (`distinct` applies to anything).
expect(supportsAggregate('nominal')).toBe(true);
expect(supportsBin('quantitative')).toBe(true);
expect(supportsBin('temporal')).toBe(false);
expect(supportsTimeUnit('temporal')).toBe(true);
expect(supportsTimeUnit('quantitative')).toBe(false);
});
it('offers per-type aggregate menus: arithmetic needs numbers, min/max an ordering, distinct anything', () => {
expect(validAggregateOps('quantitative')).toEqual([
'sum',
'mean',
'median',
'min',
'max',
'distinct',
]);
expect(validAggregateOps('temporal')).toEqual(['min', 'max', 'distinct']);
expect(validAggregateOps('ordinal')).toEqual(['min', 'max', 'distinct']);
expect(validAggregateOps('nominal')).toEqual(['distinct']);
});
it('emits a distinct-count of a categorical field as a quantitative measure', () => {
const spec = buildChartSpec({
datasetName: 'D',
mark: 'bar',
encodings: {
x: { field: 'region', type: 'nominal' },
y: { field: 'customer', type: 'nominal', aggregate: 'distinct' },
},
});
const enc = spec.encoding as Record<string, Record<string, unknown>>;
// The carried (nominal) type is for round-tripping; the emitted type is the
// effective one — a count of unique values reads as a quantitative measure.
expect(enc.y).toEqual({ field: 'customer', type: 'quantitative', aggregate: 'distinct' });
});
it('keeps the field type on order-preserving aggregates (a temporal min is still temporal)', () => {
const spec = buildChartSpec({
datasetName: 'D',
mark: 'point',
encodings: {
x: { field: 'region', type: 'nominal' },
y: { field: 'orderDate', type: 'temporal', aggregate: 'min' },
},
});
const enc = spec.encoding as Record<string, Record<string, unknown>>;
expect(enc.y).toEqual({ field: 'orderDate', type: 'temporal', aggregate: 'min' });
});
it('emits title as a bare string, the object form with a subtitle, nothing without a title', () => {
const base: BuilderConfig = {
datasetName: 'D',
mark: 'bar',
encodings: { x: { field: 'region', type: 'nominal' } },
};
expect(buildChartSpec({ ...base, title: 'Sales by region' }).title).toBe('Sales by region');
expect(buildChartSpec({ ...base, title: 'Sales', subtitle: 'FY26' }).title).toEqual({
text: 'Sales',
subtitle: 'FY26',
});
// A subtitle alone is not emitted (VL has no standalone subtitle), nor is a
// whitespace-only title.
expect(buildChartSpec({ ...base, subtitle: 'orphan' }).title).toBeUndefined();
expect(buildChartSpec({ ...base, title: ' ' }).title).toBeUndefined();
});
it('prefers a user-written title as the generated snippet name', () => {
expect(
generateChartName({
datasetName: 'D',
mark: 'bar',
title: 'Quarterly revenue',
encodings: { x: { field: 'region', type: 'nominal' } },
}),
).toBe('Quarterly revenue');
});
it('names a distinct-count chart with a "unique" phrase', () => {
expect(
generateChartName({
datasetName: 'D',
mark: 'bar',
encodings: {
x: { field: 'region', type: 'nominal' },
y: { field: 'customer', type: 'nominal', aggregate: 'distinct' },
},
}),
).toBe('Bar chart of unique customer by region');
});
});
describe('sort (ranking)', () => {