@opendata-ai/openchart-engine 6.12.0 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +878 -606
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/axes.test.ts +12 -30
- package/src/__tests__/compile-chart.test.ts +4 -4
- package/src/__tests__/dimensions.test.ts +2 -2
- package/src/__tests__/encoding-sugar.test.ts +389 -0
- package/src/annotations/collisions.ts +268 -0
- package/src/annotations/compute.ts +9 -912
- package/src/annotations/constants.ts +32 -0
- package/src/annotations/geometry.ts +167 -0
- package/src/annotations/position.ts +95 -0
- package/src/annotations/resolve-range.ts +98 -0
- package/src/annotations/resolve-refline.ts +148 -0
- package/src/annotations/resolve-text.ts +134 -0
- package/src/charts/__tests__/post-process.test.ts +258 -0
- package/src/charts/bar/__tests__/labels.test.ts +31 -0
- package/src/charts/bar/compute.ts +27 -6
- package/src/charts/bar/labels.ts +7 -1
- package/src/charts/column/__tests__/compute.test.ts +99 -0
- package/src/charts/column/compute.ts +27 -6
- package/src/charts/line/area.ts +19 -2
- package/src/charts/post-process.ts +215 -0
- package/src/compile.ts +90 -158
- package/src/compiler/normalize.ts +2 -2
- package/src/layout/axes.ts +10 -13
- package/src/layout/dimensions.ts +3 -3
- package/src/layout/scales.ts +106 -29
- package/src/tooltips/__tests__/compute.test.ts +188 -0
- package/src/tooltips/compute.ts +25 -11
- package/src/transforms/__tests__/aggregate.test.ts +159 -0
- package/src/transforms/__tests__/fold.test.ts +79 -0
- package/src/transforms/aggregate.ts +130 -0
- package/src/transforms/fold.ts +49 -0
- package/src/transforms/index.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.13.0",
|
|
4
4
|
"description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"typecheck": "tsc --noEmit"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@opendata-ai/openchart-core": "6.
|
|
48
|
+
"@opendata-ai/openchart-core": "6.13.0",
|
|
49
49
|
"d3-array": "^3.2.0",
|
|
50
50
|
"d3-format": "^3.1.2",
|
|
51
51
|
"d3-interpolate": "^3.0.0",
|
|
@@ -203,10 +203,10 @@ describe('computeAxes', () => {
|
|
|
203
203
|
});
|
|
204
204
|
|
|
205
205
|
// -------------------------------------------------------------------------
|
|
206
|
-
//
|
|
206
|
+
// labelAngle propagation
|
|
207
207
|
// -------------------------------------------------------------------------
|
|
208
208
|
|
|
209
|
-
it('propagates
|
|
209
|
+
it('propagates labelAngle from encoding to x-axis layout', () => {
|
|
210
210
|
const specWithAngle: NormalizedChartSpec = {
|
|
211
211
|
...lineSpec,
|
|
212
212
|
markType: 'bar',
|
|
@@ -216,7 +216,7 @@ describe('computeAxes', () => {
|
|
|
216
216
|
{ cat: 'New York', val: 20 },
|
|
217
217
|
],
|
|
218
218
|
encoding: {
|
|
219
|
-
x: { field: 'cat', type: 'nominal', axis: {
|
|
219
|
+
x: { field: 'cat', type: 'nominal', axis: { labelAngle: -90 } },
|
|
220
220
|
y: { field: 'val', type: 'quantitative' },
|
|
221
221
|
},
|
|
222
222
|
};
|
|
@@ -234,12 +234,12 @@ describe('computeAxes', () => {
|
|
|
234
234
|
expect(axes.y!.tickAngle).toBeUndefined();
|
|
235
235
|
});
|
|
236
236
|
|
|
237
|
-
it('propagates
|
|
237
|
+
it('propagates labelAngle to y-axis layout', () => {
|
|
238
238
|
const specWithAngle: NormalizedChartSpec = {
|
|
239
239
|
...lineSpec,
|
|
240
240
|
encoding: {
|
|
241
241
|
x: { field: 'date', type: 'temporal' },
|
|
242
|
-
y: { field: 'value', type: 'quantitative', axis: {
|
|
242
|
+
y: { field: 'value', type: 'quantitative', axis: { labelAngle: -45 } },
|
|
243
243
|
},
|
|
244
244
|
};
|
|
245
245
|
const scales = computeScales(specWithAngle, chartArea, specWithAngle.data);
|
|
@@ -458,19 +458,16 @@ describe('text-aware tick density', () => {
|
|
|
458
458
|
expect(axes.x!.ticks.length).toBe(categories.length);
|
|
459
459
|
});
|
|
460
460
|
|
|
461
|
-
it('gridlines
|
|
461
|
+
it('y-axis gridlines match ticks so every gridline has a label', () => {
|
|
462
462
|
// Force thinning by using a measureText that reports wide labels
|
|
463
463
|
const wideMeasure = () => ({ width: 200, height: 12 });
|
|
464
464
|
const scales = computeScales(lineSpec, chartArea, lineSpec.data);
|
|
465
465
|
const axes = computeAxes(scales, chartArea, fullStrategy, theme, wideMeasure);
|
|
466
466
|
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (axes.y!.ticks.length < axes.y!.gridlines.length) {
|
|
472
|
-
// Gridlines retained positions that ticks lost — the fix is working
|
|
473
|
-
expect(axes.y!.gridlines.length).toBeGreaterThan(axes.y!.ticks.length);
|
|
467
|
+
// Y-axis gridlines should always match ticks 1:1 (every gridline gets a label)
|
|
468
|
+
expect(axes.y!.gridlines.length).toBe(axes.y!.ticks.length);
|
|
469
|
+
for (let i = 0; i < axes.y!.ticks.length; i++) {
|
|
470
|
+
expect(axes.y!.gridlines[i].position).toBe(axes.y!.ticks[i].position);
|
|
474
471
|
}
|
|
475
472
|
});
|
|
476
473
|
|
|
@@ -534,28 +531,14 @@ describe('axis config properties', () => {
|
|
|
534
531
|
expect(axes.y!.label).toBe('Amount ($)');
|
|
535
532
|
});
|
|
536
533
|
|
|
537
|
-
it('
|
|
538
|
-
const spec: NormalizedChartSpec = {
|
|
539
|
-
...lineSpec,
|
|
540
|
-
encoding: {
|
|
541
|
-
x: { field: 'date', type: 'temporal', axis: { label: 'Old Label' } },
|
|
542
|
-
y: { field: 'value', type: 'quantitative' },
|
|
543
|
-
},
|
|
544
|
-
};
|
|
545
|
-
const scales = computeScales(spec, chartArea, spec.data);
|
|
546
|
-
const axes = computeAxes(scales, chartArea, fullStrategy, theme);
|
|
547
|
-
|
|
548
|
-
expect(axes.x!.label).toBe('Old Label');
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it('prefers labelAngle over deprecated tickAngle', () => {
|
|
534
|
+
it('propagates labelAngle to layout tickAngle', () => {
|
|
552
535
|
const spec: NormalizedChartSpec = {
|
|
553
536
|
...lineSpec,
|
|
554
537
|
encoding: {
|
|
555
538
|
x: {
|
|
556
539
|
field: 'date',
|
|
557
540
|
type: 'temporal',
|
|
558
|
-
axis: { labelAngle: -30
|
|
541
|
+
axis: { labelAngle: -30 },
|
|
559
542
|
},
|
|
560
543
|
y: { field: 'value', type: 'quantitative' },
|
|
561
544
|
},
|
|
@@ -563,7 +546,6 @@ describe('axis config properties', () => {
|
|
|
563
546
|
const scales = computeScales(spec, chartArea, spec.data);
|
|
564
547
|
const axes = computeAxes(scales, chartArea, fullStrategy, theme);
|
|
565
548
|
|
|
566
|
-
// labelAngle takes precedence
|
|
567
549
|
expect(axes.x!.tickAngle).toBe(-30);
|
|
568
550
|
});
|
|
569
551
|
|
|
@@ -469,7 +469,7 @@ describe('compileGraph', () => {
|
|
|
469
469
|
).toThrow('compileGraph received a non-graph spec');
|
|
470
470
|
});
|
|
471
471
|
|
|
472
|
-
it('propagates
|
|
472
|
+
it('propagates labelAngle through the full compilation pipeline', () => {
|
|
473
473
|
const columnSpec = {
|
|
474
474
|
mark: 'bar' as const,
|
|
475
475
|
data: [
|
|
@@ -480,14 +480,14 @@ describe('compileGraph', () => {
|
|
|
480
480
|
{ state: 'Pennsylvania', pop: 13000000 },
|
|
481
481
|
],
|
|
482
482
|
encoding: {
|
|
483
|
-
x: { field: 'state', type: 'nominal' as const, axis: {
|
|
483
|
+
x: { field: 'state', type: 'nominal' as const, axis: { labelAngle: -90 } },
|
|
484
484
|
y: { field: 'pop', type: 'quantitative' as const },
|
|
485
485
|
},
|
|
486
486
|
};
|
|
487
487
|
|
|
488
488
|
const layout = compileChart(columnSpec, { width: 400, height: 300 });
|
|
489
489
|
|
|
490
|
-
//
|
|
490
|
+
// labelAngle should be propagated to the x-axis layout as tickAngle
|
|
491
491
|
expect(layout.axes.x!.tickAngle).toBe(-90);
|
|
492
492
|
// y-axis should not have a tickAngle
|
|
493
493
|
expect(layout.axes.y!.tickAngle).toBeUndefined();
|
|
@@ -509,7 +509,7 @@ describe('compileGraph', () => {
|
|
|
509
509
|
const rotatedColumnSpec = {
|
|
510
510
|
...baseColumnSpec,
|
|
511
511
|
encoding: {
|
|
512
|
-
x: { field: 'state', type: 'nominal' as const, axis: {
|
|
512
|
+
x: { field: 'state', type: 'nominal' as const, axis: { labelAngle: -90 } },
|
|
513
513
|
y: { field: 'pop', type: 'quantitative' as const },
|
|
514
514
|
},
|
|
515
515
|
};
|
|
@@ -161,7 +161,7 @@ describe('computeDimensions', () => {
|
|
|
161
161
|
{ category: 'Massachusetts', value: 15 },
|
|
162
162
|
],
|
|
163
163
|
encoding: {
|
|
164
|
-
x: { field: 'category', type: 'nominal', axis: {
|
|
164
|
+
x: { field: 'category', type: 'nominal', axis: { labelAngle: -90 } },
|
|
165
165
|
y: { field: 'value', type: 'quantitative' },
|
|
166
166
|
},
|
|
167
167
|
};
|
|
@@ -198,7 +198,7 @@ describe('computeDimensions', () => {
|
|
|
198
198
|
const smallAngleSpec: NormalizedChartSpec = {
|
|
199
199
|
...baseSpec,
|
|
200
200
|
encoding: {
|
|
201
|
-
x: { field: 'date', type: 'temporal', axis: {
|
|
201
|
+
x: { field: 'date', type: 'temporal', axis: { labelAngle: 5 } },
|
|
202
202
|
y: { field: 'value', type: 'quantitative' },
|
|
203
203
|
},
|
|
204
204
|
};
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for encoding-level bin, timeUnit, and sort shorthand expansion.
|
|
3
|
+
*
|
|
4
|
+
* These test the Vega-Lite-aligned encoding sugar that auto-generates
|
|
5
|
+
* transforms (bin, timeUnit) and applies sort to categorical scales.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ScaleBand, ScaleOrdinal, ScalePoint } from 'd3-scale';
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
import { expandEncodingSugar } from '../compile';
|
|
11
|
+
import type { NormalizedChartSpec } from '../compiler/types';
|
|
12
|
+
import { computeScales } from '../layout/scales';
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// expandEncodingSugar: bin
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
describe('expandEncodingSugar', () => {
|
|
19
|
+
describe('bin expansion', () => {
|
|
20
|
+
it('expands encoding.x.bin: true into a BinTransform', () => {
|
|
21
|
+
const spec = {
|
|
22
|
+
mark: 'bar',
|
|
23
|
+
data: [{ val: 10 }, { val: 20 }],
|
|
24
|
+
encoding: {
|
|
25
|
+
x: { field: 'val', type: 'quantitative', bin: true },
|
|
26
|
+
y: { field: 'count', type: 'quantitative' },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const result = expandEncodingSugar(spec);
|
|
31
|
+
|
|
32
|
+
// Should generate a bin transform
|
|
33
|
+
expect(result.transform).toHaveLength(1);
|
|
34
|
+
expect(result.transform).toEqual([{ bin: true, field: 'val', as: 'bin_val' }]);
|
|
35
|
+
|
|
36
|
+
// Encoding field should reference the binned output
|
|
37
|
+
const encoding = result.encoding as Record<string, { field: string; bin?: unknown }>;
|
|
38
|
+
expect(encoding.x.field).toBe('bin_val');
|
|
39
|
+
// bin property should be removed from the encoding channel
|
|
40
|
+
expect(encoding.x.bin).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('expands encoding.y.bin with BinParams', () => {
|
|
44
|
+
const spec = {
|
|
45
|
+
mark: 'bar',
|
|
46
|
+
data: [],
|
|
47
|
+
encoding: {
|
|
48
|
+
x: { field: 'cat', type: 'nominal' },
|
|
49
|
+
y: { field: 'score', type: 'quantitative', bin: { maxbins: 20 } },
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const result = expandEncodingSugar(spec);
|
|
54
|
+
|
|
55
|
+
expect(result.transform).toHaveLength(1);
|
|
56
|
+
expect(result.transform).toEqual([{ bin: { maxbins: 20 }, field: 'score', as: 'bin_score' }]);
|
|
57
|
+
|
|
58
|
+
const encoding = result.encoding as Record<string, { field: string }>;
|
|
59
|
+
expect(encoding.y.field).toBe('bin_score');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('does not expand bin: false', () => {
|
|
63
|
+
const spec = {
|
|
64
|
+
mark: 'bar',
|
|
65
|
+
data: [],
|
|
66
|
+
encoding: {
|
|
67
|
+
x: { field: 'val', type: 'quantitative', bin: false },
|
|
68
|
+
y: { field: 'count', type: 'quantitative' },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const result = expandEncodingSugar(spec);
|
|
73
|
+
|
|
74
|
+
// No transform generated, spec returned as-is
|
|
75
|
+
expect(result.transform).toBeUndefined();
|
|
76
|
+
expect(result).toBe(spec);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('prepends generated transforms before existing transforms', () => {
|
|
80
|
+
const spec = {
|
|
81
|
+
mark: 'bar',
|
|
82
|
+
data: [],
|
|
83
|
+
encoding: {
|
|
84
|
+
x: { field: 'val', type: 'quantitative', bin: true },
|
|
85
|
+
y: { field: 'count', type: 'quantitative' },
|
|
86
|
+
},
|
|
87
|
+
transform: [{ filter: { field: 'active', equal: true } }],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = expandEncodingSugar(spec);
|
|
91
|
+
|
|
92
|
+
expect(result.transform).toHaveLength(2);
|
|
93
|
+
// Generated bin transform comes first
|
|
94
|
+
expect((result.transform as Array<Record<string, unknown>>)[0]).toHaveProperty('bin');
|
|
95
|
+
// User-defined filter comes after
|
|
96
|
+
expect((result.transform as Array<Record<string, unknown>>)[1]).toHaveProperty('filter');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// expandEncodingSugar: timeUnit
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe('timeUnit expansion', () => {
|
|
105
|
+
it('expands encoding.x.timeUnit into a TimeUnitTransform', () => {
|
|
106
|
+
const spec = {
|
|
107
|
+
mark: 'line',
|
|
108
|
+
data: [],
|
|
109
|
+
encoding: {
|
|
110
|
+
x: { field: 'date', type: 'temporal', timeUnit: 'yearmonth' },
|
|
111
|
+
y: { field: 'value', type: 'quantitative' },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = expandEncodingSugar(spec);
|
|
116
|
+
|
|
117
|
+
expect(result.transform).toHaveLength(1);
|
|
118
|
+
expect(result.transform).toEqual([
|
|
119
|
+
{ timeUnit: 'yearmonth', field: 'date', as: 'yearmonth_date' },
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const encoding = result.encoding as Record<string, { field: string; timeUnit?: unknown }>;
|
|
123
|
+
expect(encoding.x.field).toBe('yearmonth_date');
|
|
124
|
+
expect(encoding.x.timeUnit).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('expands encoding.y.timeUnit: month', () => {
|
|
128
|
+
const spec = {
|
|
129
|
+
mark: 'bar',
|
|
130
|
+
data: [],
|
|
131
|
+
encoding: {
|
|
132
|
+
x: { field: 'cat', type: 'nominal' },
|
|
133
|
+
y: { field: 'timestamp', type: 'temporal', timeUnit: 'month' },
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const result = expandEncodingSugar(spec);
|
|
138
|
+
|
|
139
|
+
expect(result.transform).toHaveLength(1);
|
|
140
|
+
expect(result.transform).toEqual([
|
|
141
|
+
{ timeUnit: 'month', field: 'timestamp', as: 'month_timestamp' },
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
const encoding = result.encoding as Record<string, { field: string }>;
|
|
145
|
+
expect(encoding.y.field).toBe('month_timestamp');
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Combined bin + timeUnit
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
describe('combined expansions', () => {
|
|
154
|
+
it('expands both bin on x and timeUnit on y', () => {
|
|
155
|
+
const spec = {
|
|
156
|
+
mark: 'point',
|
|
157
|
+
data: [],
|
|
158
|
+
encoding: {
|
|
159
|
+
x: { field: 'amount', type: 'quantitative', bin: true },
|
|
160
|
+
y: { field: 'created', type: 'temporal', timeUnit: 'year' },
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const result = expandEncodingSugar(spec);
|
|
165
|
+
|
|
166
|
+
expect(result.transform).toHaveLength(2);
|
|
167
|
+
expect((result.transform as Array<Record<string, unknown>>)[0]).toEqual({
|
|
168
|
+
bin: true,
|
|
169
|
+
field: 'amount',
|
|
170
|
+
as: 'bin_amount',
|
|
171
|
+
});
|
|
172
|
+
expect((result.transform as Array<Record<string, unknown>>)[1]).toEqual({
|
|
173
|
+
timeUnit: 'year',
|
|
174
|
+
field: 'created',
|
|
175
|
+
as: 'year_created',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const encoding = result.encoding as Record<string, { field: string }>;
|
|
179
|
+
expect(encoding.x.field).toBe('bin_amount');
|
|
180
|
+
expect(encoding.y.field).toBe('year_created');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('returns spec unchanged when no encoding sugar is present', () => {
|
|
184
|
+
const spec = {
|
|
185
|
+
mark: 'line',
|
|
186
|
+
data: [],
|
|
187
|
+
encoding: {
|
|
188
|
+
x: { field: 'date', type: 'temporal' },
|
|
189
|
+
y: { field: 'value', type: 'quantitative' },
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const result = expandEncodingSugar(spec);
|
|
194
|
+
expect(result).toBe(spec); // Same reference, no copy needed
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('returns spec unchanged when no encoding is present', () => {
|
|
198
|
+
const spec = { mark: 'line', data: [] };
|
|
199
|
+
const result = expandEncodingSugar(spec);
|
|
200
|
+
expect(result).toBe(spec);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Sort on categorical scales
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
describe('categorical sort', () => {
|
|
210
|
+
const chartArea = { x: 0, y: 0, width: 400, height: 300 };
|
|
211
|
+
|
|
212
|
+
const makeBarSpec = (
|
|
213
|
+
sort: 'ascending' | 'descending' | null | undefined,
|
|
214
|
+
): NormalizedChartSpec => ({
|
|
215
|
+
markType: 'bar',
|
|
216
|
+
markDef: { type: 'bar' },
|
|
217
|
+
data: [
|
|
218
|
+
{ category: 'Banana', count: 10 },
|
|
219
|
+
{ category: 'Apple', count: 30 },
|
|
220
|
+
{ category: 'Cherry', count: 20 },
|
|
221
|
+
],
|
|
222
|
+
encoding: {
|
|
223
|
+
x: { field: 'count', type: 'quantitative' },
|
|
224
|
+
y: { field: 'category', type: 'nominal', sort },
|
|
225
|
+
},
|
|
226
|
+
chrome: {},
|
|
227
|
+
annotations: [],
|
|
228
|
+
responsive: true,
|
|
229
|
+
theme: {},
|
|
230
|
+
darkMode: 'off',
|
|
231
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
232
|
+
watermark: true,
|
|
233
|
+
hiddenSeries: [],
|
|
234
|
+
seriesStyles: {},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('sorts domain ascending by default (undefined sort)', () => {
|
|
238
|
+
const spec = makeBarSpec(undefined);
|
|
239
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
240
|
+
const domain = (scales.y!.scale as ScaleBand<string>).domain();
|
|
241
|
+
expect(domain).toEqual(['Apple', 'Banana', 'Cherry']);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('sorts domain ascending when sort is "ascending"', () => {
|
|
245
|
+
const spec = makeBarSpec('ascending');
|
|
246
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
247
|
+
const domain = (scales.y!.scale as ScaleBand<string>).domain();
|
|
248
|
+
expect(domain).toEqual(['Apple', 'Banana', 'Cherry']);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('sorts domain descending when sort is "descending"', () => {
|
|
252
|
+
const spec = makeBarSpec('descending');
|
|
253
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
254
|
+
const domain = (scales.y!.scale as ScaleBand<string>).domain();
|
|
255
|
+
expect(domain).toEqual(['Cherry', 'Banana', 'Apple']);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('preserves data order when sort is null', () => {
|
|
259
|
+
const spec = makeBarSpec(null);
|
|
260
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
261
|
+
const domain = (scales.y!.scale as ScaleBand<string>).domain();
|
|
262
|
+
// Data order: Banana, Apple, Cherry
|
|
263
|
+
expect(domain).toEqual(['Banana', 'Apple', 'Cherry']);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('applies sort to point scales (nominal x on line chart)', () => {
|
|
267
|
+
const spec: NormalizedChartSpec = {
|
|
268
|
+
markType: 'line',
|
|
269
|
+
markDef: { type: 'line' },
|
|
270
|
+
data: [
|
|
271
|
+
{ name: 'Zara', score: 10 },
|
|
272
|
+
{ name: 'Alice', score: 20 },
|
|
273
|
+
{ name: 'Mike', score: 15 },
|
|
274
|
+
],
|
|
275
|
+
encoding: {
|
|
276
|
+
x: { field: 'name', type: 'nominal', sort: 'descending' },
|
|
277
|
+
y: { field: 'score', type: 'quantitative' },
|
|
278
|
+
},
|
|
279
|
+
chrome: {},
|
|
280
|
+
annotations: [],
|
|
281
|
+
responsive: true,
|
|
282
|
+
theme: {},
|
|
283
|
+
darkMode: 'off',
|
|
284
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
285
|
+
watermark: true,
|
|
286
|
+
hiddenSeries: [],
|
|
287
|
+
seriesStyles: {},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
291
|
+
const domain = (scales.x!.scale as ScalePoint<string>).domain();
|
|
292
|
+
expect(domain).toEqual(['Zara', 'Mike', 'Alice']);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it('applies sort to ordinal color scales', () => {
|
|
296
|
+
const spec: NormalizedChartSpec = {
|
|
297
|
+
markType: 'line',
|
|
298
|
+
markDef: { type: 'line' },
|
|
299
|
+
data: [
|
|
300
|
+
{ date: '2020-01-01', value: 10, group: 'Gamma' },
|
|
301
|
+
{ date: '2020-01-01', value: 20, group: 'Alpha' },
|
|
302
|
+
{ date: '2020-01-01', value: 15, group: 'Beta' },
|
|
303
|
+
],
|
|
304
|
+
encoding: {
|
|
305
|
+
x: { field: 'date', type: 'temporal' },
|
|
306
|
+
y: { field: 'value', type: 'quantitative' },
|
|
307
|
+
color: { field: 'group', type: 'nominal', sort: 'ascending' },
|
|
308
|
+
},
|
|
309
|
+
chrome: {},
|
|
310
|
+
annotations: [],
|
|
311
|
+
responsive: true,
|
|
312
|
+
theme: {},
|
|
313
|
+
darkMode: 'off',
|
|
314
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
315
|
+
watermark: true,
|
|
316
|
+
hiddenSeries: [],
|
|
317
|
+
seriesStyles: {},
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
321
|
+
const domain = (scales.color!.scale as ScaleOrdinal<string, string>).domain();
|
|
322
|
+
expect(domain).toEqual(['Alpha', 'Beta', 'Gamma']);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('sorts numerically when values are numeric strings', () => {
|
|
326
|
+
const spec: NormalizedChartSpec = {
|
|
327
|
+
markType: 'bar',
|
|
328
|
+
markDef: { type: 'bar' },
|
|
329
|
+
data: [
|
|
330
|
+
{ category: '10', count: 1 },
|
|
331
|
+
{ category: '2', count: 2 },
|
|
332
|
+
{ category: '1', count: 3 },
|
|
333
|
+
],
|
|
334
|
+
encoding: {
|
|
335
|
+
x: { field: 'count', type: 'quantitative' },
|
|
336
|
+
y: { field: 'category', type: 'nominal', sort: 'ascending' },
|
|
337
|
+
},
|
|
338
|
+
chrome: {},
|
|
339
|
+
annotations: [],
|
|
340
|
+
responsive: true,
|
|
341
|
+
theme: {},
|
|
342
|
+
darkMode: 'off',
|
|
343
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
344
|
+
watermark: true,
|
|
345
|
+
hiddenSeries: [],
|
|
346
|
+
seriesStyles: {},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
350
|
+
const domain = (scales.y!.scale as ScaleBand<string>).domain();
|
|
351
|
+
// numeric: true in localeCompare means "1" < "2" < "10"
|
|
352
|
+
expect(domain).toEqual(['1', '2', '10']);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('does not sort when explicit scale.domain is provided', () => {
|
|
356
|
+
const spec: NormalizedChartSpec = {
|
|
357
|
+
markType: 'bar',
|
|
358
|
+
markDef: { type: 'bar' },
|
|
359
|
+
data: [
|
|
360
|
+
{ category: 'C', count: 10 },
|
|
361
|
+
{ category: 'A', count: 20 },
|
|
362
|
+
{ category: 'B', count: 30 },
|
|
363
|
+
],
|
|
364
|
+
encoding: {
|
|
365
|
+
x: { field: 'count', type: 'quantitative' },
|
|
366
|
+
y: {
|
|
367
|
+
field: 'category',
|
|
368
|
+
type: 'nominal',
|
|
369
|
+
sort: 'ascending',
|
|
370
|
+
scale: { domain: ['C', 'B', 'A'] },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
chrome: {},
|
|
374
|
+
annotations: [],
|
|
375
|
+
responsive: true,
|
|
376
|
+
theme: {},
|
|
377
|
+
darkMode: 'off',
|
|
378
|
+
labels: { density: 'auto', format: '', prefix: '' },
|
|
379
|
+
watermark: true,
|
|
380
|
+
hiddenSeries: [],
|
|
381
|
+
seriesStyles: {},
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const scales = computeScales(spec, chartArea, spec.data);
|
|
385
|
+
const domain = (scales.y!.scale as ScaleBand<string>).domain();
|
|
386
|
+
// Explicit domain takes precedence over sort
|
|
387
|
+
expect(domain).toEqual(['C', 'B', 'A']);
|
|
388
|
+
});
|
|
389
|
+
});
|