mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
Chart builder: field-first shelf + value-or-field channels
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
validFieldTypes,
|
||||
defaultMark,
|
||||
isChannelTypeAllowed,
|
||||
isColumnAllowedOnChannel,
|
||||
supportsAggregate,
|
||||
supportsBin,
|
||||
supportsTimeUnit,
|
||||
@@ -13,6 +14,10 @@ import {
|
||||
builderWarnings,
|
||||
defaultBuilderConfig,
|
||||
isBuilderConfigValid,
|
||||
isValueMapping,
|
||||
channelAcceptsValue,
|
||||
defaultChannelValue,
|
||||
coerceChannelValue,
|
||||
buildChartSpec,
|
||||
buildSnippetSpecText,
|
||||
generateChartName,
|
||||
@@ -106,6 +111,23 @@ describe('isChannelTypeAllowed (Size discipline)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('isColumnAllowedOnChannel (field-shelf placement by default type)', () => {
|
||||
it('lets Size take only a numeric column (its default type is the magnitude)', () => {
|
||||
expect(isColumnAllowedOnChannel('size', 'number')).toBe(true);
|
||||
expect(isColumnAllowedOnChannel('size', 'string')).toBe(false);
|
||||
expect(isColumnAllowedOnChannel('size', 'date')).toBe(false);
|
||||
expect(isColumnAllowedOnChannel('size', 'boolean')).toBe(false);
|
||||
});
|
||||
|
||||
it('lets X/Y/Color take any column type', () => {
|
||||
for (const ch of ['x', 'y', 'color'] as const) {
|
||||
for (const t of ['number', 'string', 'date', 'boolean'] as const) {
|
||||
expect(isColumnAllowedOnChannel(ch, t)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('builderWarnings (Tier B advisories)', () => {
|
||||
it('warns when a line/area mark is missing an axis', () => {
|
||||
const w = builderWarnings({
|
||||
@@ -690,6 +712,80 @@ describe('buildChartSpec', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('value channels (constant colour / size — the Property model)', () => {
|
||||
it('emits `{ value }` and ignores field/type/transforms when a constant is set', () => {
|
||||
const spec = buildChartSpec({
|
||||
datasetName: 'D',
|
||||
mark: 'point',
|
||||
encodings: {
|
||||
x: { field: 'category', type: 'nominal' },
|
||||
// A constant carries a (preserved-but-ignored) type and even a stale field;
|
||||
// the assembler still emits only `{ value }`.
|
||||
color: { value: '#c0392b', type: 'nominal', field: 'category', aggregate: 'sum' },
|
||||
size: { value: 100, type: 'quantitative' },
|
||||
},
|
||||
});
|
||||
const enc = spec.encoding as Record<string, Record<string, unknown>>;
|
||||
expect(enc.color).toEqual({ value: '#c0392b' });
|
||||
expect(enc.size).toEqual({ value: 100 });
|
||||
});
|
||||
|
||||
it('counts a constant channel as mapped (saveable) and preserves channel order', () => {
|
||||
const config: BuilderConfig = {
|
||||
datasetName: 'D',
|
||||
mark: 'point',
|
||||
encodings: { color: { value: 'steelblue', type: 'nominal' } },
|
||||
};
|
||||
expect(isBuilderConfigValid(config)).toBe(true);
|
||||
expect(Object.keys(buildChartSpec(config).encoding as object)).toEqual(['color']);
|
||||
});
|
||||
|
||||
it('a constant Colour is not a series: no stacking, no area-split hint', () => {
|
||||
const base: BuilderConfig = {
|
||||
datasetName: 'D',
|
||||
mark: 'bar',
|
||||
encodings: {
|
||||
x: { field: 'category', type: 'nominal' },
|
||||
y: { field: 'value', type: 'quantitative' },
|
||||
color: { value: '#4c78a8', type: 'nominal' },
|
||||
},
|
||||
};
|
||||
// A field-bound Colour would enable stacking and (on area) flag the split hint;
|
||||
// a constant Colour produces neither — it draws every mark one fixed colour.
|
||||
expect(supportsStack(base)).toBe(false);
|
||||
const areaWarnings = builderWarnings({ ...base, mark: 'area' });
|
||||
expect(areaWarnings.some((w) => w.message.includes('per-series change'))).toBe(false);
|
||||
|
||||
// Sanity: the same config with a *field* Colour does enable stacking.
|
||||
const seriesColor: BuilderConfig = {
|
||||
...base,
|
||||
encodings: { ...base.encodings, color: { field: 'category', type: 'nominal' } },
|
||||
};
|
||||
expect(supportsStack(seriesColor)).toBe(true);
|
||||
});
|
||||
|
||||
it('describes the Property-model helpers', () => {
|
||||
expect(isValueMapping({ value: 10, type: 'quantitative' })).toBe(true);
|
||||
expect(isValueMapping({ field: 'value', type: 'quantitative' })).toBe(false);
|
||||
|
||||
expect(channelAcceptsValue('color')).toBe(true);
|
||||
expect(channelAcceptsValue('size')).toBe(true);
|
||||
expect(channelAcceptsValue('x')).toBe(false);
|
||||
expect(channelAcceptsValue('y')).toBe(false);
|
||||
|
||||
expect(typeof defaultChannelValue('color')).toBe('string');
|
||||
expect(defaultChannelValue('size')).toBe(100);
|
||||
});
|
||||
|
||||
it('coerces a constant value by channel (size → number, others → string)', () => {
|
||||
expect(coerceChannelValue('size', '40')).toBe(40);
|
||||
expect(coerceChannelValue('size', '')).toBe(''); // blank stays raw, not NaN
|
||||
expect(coerceChannelValue('size', 'big')).toBe('big'); // non-numeric stays raw
|
||||
expect(coerceChannelValue('color', '#ff0000')).toBe('#ff0000');
|
||||
expect(coerceChannelValue('color', 'red')).toBe('red');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transforms — aggregate / bin / timeUnit', () => {
|
||||
it('emits a field-less count encoding', () => {
|
||||
const spec = buildChartSpec({
|
||||
|
||||
Reference in New Issue
Block a user