@opendata-ai/openchart-engine 8.0.0-rc.8 → 8.0.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.d.ts +149 -12
- package/dist/index.js +894 -171
- package/dist/index.js.map +1 -1
- package/package.json +3 -4
- package/src/__test-fixtures__/specs.ts +0 -182
- package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
- package/src/__tests__/annotation-behavior.test.ts +0 -852
- package/src/__tests__/axes.test.ts +0 -1579
- package/src/__tests__/compile-animation.test.ts +0 -161
- package/src/__tests__/compile-chart.test.ts +0 -1303
- package/src/__tests__/compile-footnotes.test.ts +0 -277
- package/src/__tests__/compile-layer.test.ts +0 -969
- package/src/__tests__/compile-snapshot.test.ts +0 -156
- package/src/__tests__/compound-labels.test.ts +0 -147
- package/src/__tests__/dimensions.test.ts +0 -918
- package/src/__tests__/encoding-sugar.test.ts +0 -390
- package/src/__tests__/facet.test.ts +0 -215
- package/src/__tests__/helpers/invariants.ts +0 -299
- package/src/__tests__/known-bugs.test.ts +0 -566
- package/src/__tests__/layout-fuzz.test.ts +0 -358
- package/src/__tests__/layout-invariants.test.ts +0 -535
- package/src/__tests__/legend.test.ts +0 -721
- package/src/__tests__/mark-keys.test.ts +0 -294
- package/src/__tests__/ranking-recipes.test.ts +0 -260
- package/src/__tests__/rotation.test.ts +0 -83
- package/src/__tests__/scales.test.ts +0 -323
- package/src/__tests__/series-search.test.ts +0 -92
- package/src/__tests__/spec-sugar.test.ts +0 -801
- package/src/__tests__/you-draw-it.test.ts +0 -160
- package/src/annotations/__tests__/compute.test.ts +0 -1672
- package/src/annotations/__tests__/geometry.test.ts +0 -66
- package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
- package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
- package/src/annotations/__tests__/placement.test.ts +0 -316
- package/src/annotations/__tests__/resolve-text.test.ts +0 -788
- package/src/annotations/__tests__/rich-text.test.ts +0 -91
- package/src/annotations/__tests__/thinning.test.ts +0 -285
- package/src/annotations/collisions.ts +0 -326
- package/src/annotations/compute.ts +0 -333
- package/src/annotations/constants.ts +0 -162
- package/src/annotations/geometry.ts +0 -376
- package/src/annotations/move.ts +0 -135
- package/src/annotations/placement.ts +0 -636
- package/src/annotations/position.ts +0 -131
- package/src/annotations/resolve-range.ts +0 -117
- package/src/annotations/resolve-refline.ts +0 -157
- package/src/annotations/resolve-text.ts +0 -545
- package/src/annotations/rich-text.ts +0 -65
- package/src/annotations/thinning.ts +0 -190
- package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
- package/src/barlist/compile-barlist.ts +0 -391
- package/src/barlist/types.ts +0 -28
- package/src/charts/__tests__/post-process.test.ts +0 -237
- package/src/charts/__tests__/registry.test.ts +0 -116
- package/src/charts/__tests__/utils.test.ts +0 -195
- package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
- package/src/charts/_shared/density-filter.ts +0 -26
- package/src/charts/_shared/format-label-value.ts +0 -16
- package/src/charts/bar/__tests__/compute.test.ts +0 -934
- package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
- package/src/charts/bar/__tests__/labels.test.ts +0 -292
- package/src/charts/bar/compute.ts +0 -539
- package/src/charts/bar/index.ts +0 -60
- package/src/charts/bar/labels.ts +0 -311
- package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
- package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
- package/src/charts/beeswarm/compute.ts +0 -203
- package/src/charts/beeswarm/dodge.ts +0 -155
- package/src/charts/beeswarm/index.ts +0 -32
- package/src/charts/builtin.ts +0 -75
- package/src/charts/calendar/__tests__/compute.test.ts +0 -417
- package/src/charts/calendar/compute.ts +0 -319
- package/src/charts/calendar/index.ts +0 -32
- package/src/charts/column/__tests__/compute.test.ts +0 -540
- package/src/charts/column/__tests__/labels.test.ts +0 -179
- package/src/charts/column/compute.ts +0 -493
- package/src/charts/column/index.ts +0 -53
- package/src/charts/column/labels.ts +0 -229
- package/src/charts/dot/__tests__/compute.test.ts +0 -380
- package/src/charts/dot/__tests__/labels.test.ts +0 -98
- package/src/charts/dot/compute.ts +0 -285
- package/src/charts/dot/index.ts +0 -62
- package/src/charts/dot/labels.ts +0 -121
- package/src/charts/line/__tests__/compute.test.ts +0 -1194
- package/src/charts/line/__tests__/curves.test.ts +0 -51
- package/src/charts/line/__tests__/labels.test.ts +0 -94
- package/src/charts/line/__tests__/mark-options.test.ts +0 -175
- package/src/charts/line/area.ts +0 -484
- package/src/charts/line/compute.ts +0 -356
- package/src/charts/line/curves.ts +0 -85
- package/src/charts/line/index.ts +0 -176
- package/src/charts/line/labels.ts +0 -177
- package/src/charts/parliament/__tests__/compute.test.ts +0 -411
- package/src/charts/parliament/compute.ts +0 -313
- package/src/charts/parliament/index.ts +0 -32
- package/src/charts/pie/__tests__/compute.test.ts +0 -384
- package/src/charts/pie/__tests__/labels.test.ts +0 -132
- package/src/charts/pie/compute.ts +0 -324
- package/src/charts/pie/index.ts +0 -49
- package/src/charts/pie/labels.ts +0 -143
- package/src/charts/post-process.ts +0 -229
- package/src/charts/range/__tests__/compute.test.ts +0 -486
- package/src/charts/range/compute.ts +0 -273
- package/src/charts/range/index.ts +0 -82
- package/src/charts/range/labels.ts +0 -169
- package/src/charts/rect/__tests__/compute.test.ts +0 -120
- package/src/charts/rect/index.ts +0 -127
- package/src/charts/registry.ts +0 -72
- package/src/charts/rule/index.ts +0 -139
- package/src/charts/scatter/__tests__/compute.test.ts +0 -488
- package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
- package/src/charts/scatter/compute.ts +0 -192
- package/src/charts/scatter/index.ts +0 -46
- package/src/charts/scatter/trendline.ts +0 -100
- package/src/charts/text/__tests__/compute.test.ts +0 -307
- package/src/charts/text/index.ts +0 -141
- package/src/charts/tick/__tests__/compute.test.ts +0 -75
- package/src/charts/tick/index.ts +0 -99
- package/src/charts/utils.ts +0 -170
- package/src/charts/waffle/__tests__/compute.test.ts +0 -305
- package/src/charts/waffle/compute.ts +0 -197
- package/src/charts/waffle/index.ts +0 -31
- package/src/compile/__tests__/color-scale-range.test.ts +0 -158
- package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
- package/src/compile/__tests__/data-clip.test.ts +0 -127
- package/src/compile/__tests__/fill-patterns.test.ts +0 -154
- package/src/compile/__tests__/size-scale.test.ts +0 -108
- package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
- package/src/compile/color-scale-range.ts +0 -101
- package/src/compile/data-clip.ts +0 -48
- package/src/compile/fill-patterns.ts +0 -74
- package/src/compile/layer.ts +0 -626
- package/src/compile/size-scale.ts +0 -141
- package/src/compile/spec-sugar.ts +0 -758
- package/src/compile/watermark-obstacle.ts +0 -54
- package/src/compile/you-draw-it.ts +0 -123
- package/src/compile.ts +0 -1450
- package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
- package/src/compiler/__tests__/animation.test.ts +0 -177
- package/src/compiler/__tests__/compile.test.ts +0 -87
- package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
- package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
- package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
- package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
- package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
- package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
- package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
- package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
- package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
- package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
- package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
- package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
- package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
- package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
- package/src/compiler/__tests__/keys.test.ts +0 -83
- package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
- package/src/compiler/__tests__/normalize.test.ts +0 -399
- package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
- package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
- package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
- package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
- package/src/compiler/__tests__/validate.test.ts +0 -629
- package/src/compiler/__tests__/vl-parity.test.ts +0 -68
- package/src/compiler/a11y-warnings.ts +0 -110
- package/src/compiler/animation.ts +0 -142
- package/src/compiler/index.ts +0 -47
- package/src/compiler/keys.ts +0 -55
- package/src/compiler/normalize.ts +0 -734
- package/src/compiler/sparkline-defaults.ts +0 -138
- package/src/compiler/types.ts +0 -241
- package/src/compiler/validate.ts +0 -2041
- package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
- package/src/endpoint-labels/compute.ts +0 -621
- package/src/endpoint-labels/constants.ts +0 -54
- package/src/endpoint-labels/format.ts +0 -60
- package/src/endpoint-labels/predict.ts +0 -139
- package/src/format/__tests__/field-format.test.ts +0 -164
- package/src/format/field-format.ts +0 -27
- package/src/geo/__tests__/compile-map.test.ts +0 -1004
- package/src/geo/compile-map.ts +0 -1341
- package/src/geo/join.ts +0 -115
- package/src/geo/projections.ts +0 -45
- package/src/geo/types.ts +0 -26
- package/src/graphs/__tests__/community.test.ts +0 -228
- package/src/graphs/__tests__/compile-graph.test.ts +0 -396
- package/src/graphs/__tests__/encoding.test.ts +0 -415
- package/src/graphs/community.ts +0 -92
- package/src/graphs/compile-graph.ts +0 -329
- package/src/graphs/encoding.ts +0 -356
- package/src/graphs/types.ts +0 -106
- package/src/index.ts +0 -177
- package/src/layout/__tests__/metrics.test.ts +0 -97
- package/src/layout/__tests__/scales-highlight.test.ts +0 -192
- package/src/layout/axes/rotation.ts +0 -152
- package/src/layout/axes/thinning.ts +0 -98
- package/src/layout/axes/ticks.ts +0 -409
- package/src/layout/axes.ts +0 -757
- package/src/layout/dimensions.ts +0 -912
- package/src/layout/facet.ts +0 -91
- package/src/layout/gridlines.ts +0 -84
- package/src/layout/metrics.ts +0 -132
- package/src/layout/plan.ts +0 -631
- package/src/layout/scales.ts +0 -1053
- package/src/layout/shared.ts +0 -44
- package/src/legend/__tests__/continuous.test.ts +0 -318
- package/src/legend/__tests__/redundancy.test.ts +0 -108
- package/src/legend/__tests__/size.test.ts +0 -348
- package/src/legend/__tests__/suppression.test.ts +0 -382
- package/src/legend/compute.ts +0 -747
- package/src/legend/continuous.ts +0 -327
- package/src/legend/size.ts +0 -407
- package/src/legend/suppression.ts +0 -226
- package/src/legend/wrap.ts +0 -117
- package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
- package/src/sankey/__tests__/layout.test.ts +0 -165
- package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
- package/src/sankey/__tests__/node-sort.test.ts +0 -45
- package/src/sankey/compile-sankey.ts +0 -706
- package/src/sankey/layout.ts +0 -184
- package/src/sankey/types.ts +0 -41
- package/src/tables/__tests__/bar-column.test.ts +0 -147
- package/src/tables/__tests__/category-colors.test.ts +0 -230
- package/src/tables/__tests__/compile-table.test.ts +0 -236
- package/src/tables/__tests__/format-cells.test.ts +0 -143
- package/src/tables/__tests__/heatmap.test.ts +0 -148
- package/src/tables/__tests__/pagination.test.ts +0 -78
- package/src/tables/__tests__/search.test.ts +0 -94
- package/src/tables/__tests__/sort.test.ts +0 -107
- package/src/tables/__tests__/sparkline.test.ts +0 -122
- package/src/tables/bar-column.ts +0 -94
- package/src/tables/category-colors.ts +0 -78
- package/src/tables/compile-table.ts +0 -427
- package/src/tables/format-cells.ts +0 -115
- package/src/tables/heatmap.ts +0 -141
- package/src/tables/pagination.ts +0 -46
- package/src/tables/search.ts +0 -66
- package/src/tables/sort.ts +0 -69
- package/src/tables/sparkline.ts +0 -113
- package/src/tables/utils.ts +0 -16
- package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
- package/src/tilemap/compile-tilemap.ts +0 -755
- package/src/tilemap/layout.ts +0 -172
- package/src/tilemap/types.ts +0 -33
- package/src/tooltips/__tests__/compute.test.ts +0 -582
- package/src/tooltips/compute.ts +0 -700
- package/src/transforms/__tests__/aggregate.test.ts +0 -159
- package/src/transforms/__tests__/bin.test.ts +0 -88
- package/src/transforms/__tests__/calculate.test.ts +0 -146
- package/src/transforms/__tests__/conditional.test.ts +0 -127
- package/src/transforms/__tests__/filter-relative.test.ts +0 -202
- package/src/transforms/__tests__/filter.test.ts +0 -59
- package/src/transforms/__tests__/fold.test.ts +0 -79
- package/src/transforms/__tests__/index.test.ts +0 -93
- package/src/transforms/__tests__/predicates.test.ts +0 -193
- package/src/transforms/__tests__/timeunit.test.ts +0 -129
- package/src/transforms/__tests__/window.test.ts +0 -286
- package/src/transforms/aggregate.ts +0 -131
- package/src/transforms/bin.ts +0 -87
- package/src/transforms/calculate.ts +0 -60
- package/src/transforms/conditional.ts +0 -46
- package/src/transforms/filter.ts +0 -122
- package/src/transforms/fold.ts +0 -49
- package/src/transforms/index.ts +0 -60
- package/src/transforms/predicates.ts +0 -123
- package/src/transforms/timeunit.ts +0 -88
- package/src/transforms/window.ts +0 -185
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Calendar heatmap tests.
|
|
3
|
-
*
|
|
4
|
-
* Integration-first through compileChart: assert the compiled layout
|
|
5
|
-
* (marks, labels, legend, tooltips, axes) rather than internal helpers.
|
|
6
|
-
* Date math is all UTC, so expectations hold on any host timezone.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { RectMark, TextMarkLayout } from '@opendata-ai/openchart-core';
|
|
10
|
-
import { describe, expect, it } from 'vitest';
|
|
11
|
-
import { compileChart } from '../../../compile';
|
|
12
|
-
import { validateSpec } from '../../../compiler/index';
|
|
13
|
-
import { weekdayRow } from '../compute';
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Fixtures
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
const DAY_MS = 86400000;
|
|
20
|
-
|
|
21
|
-
/** One row per day starting at `start` (ISO date), deterministic values. */
|
|
22
|
-
function dailyRows(start: string, count: number): Array<{ date: string; value: number }> {
|
|
23
|
-
const rows: Array<{ date: string; value: number }> = [];
|
|
24
|
-
const t0 = Date.parse(start);
|
|
25
|
-
for (let i = 0; i < count; i++) {
|
|
26
|
-
rows.push({
|
|
27
|
-
date: new Date(t0 + i * DAY_MS).toISOString().slice(0, 10),
|
|
28
|
-
value: (i % 30) - 10,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
return rows;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function calendarSpec(data: Array<Record<string, unknown>>, mark: unknown = 'calendar') {
|
|
35
|
-
return {
|
|
36
|
-
mark,
|
|
37
|
-
data,
|
|
38
|
-
encoding: {
|
|
39
|
-
x: { field: 'date', type: 'temporal' },
|
|
40
|
-
color: { field: 'value', type: 'quantitative' },
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function compile(spec: unknown, options: Partial<Parameters<typeof compileChart>[1]> = {}) {
|
|
46
|
-
return compileChart(spec, { width: 700, height: 360, ...options });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function rectsOf(layout: ReturnType<typeof compileChart>): RectMark[] {
|
|
50
|
-
return layout.marks.filter((m): m is RectMark => m.type === 'rect');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function textsOf(layout: ReturnType<typeof compileChart>): TextMarkLayout[] {
|
|
54
|
-
return layout.marks.filter((m): m is TextMarkLayout => m.type === 'textMark');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function dataCells(layout: ReturnType<typeof compileChart>): RectMark[] {
|
|
58
|
-
return rectsOf(layout).filter((r) => !r.aria.decorative);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function emptyCells(layout: ReturnType<typeof compileChart>): RectMark[] {
|
|
62
|
-
return rectsOf(layout).filter((r) => r.aria.decorative);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Distinct sorted x positions (week columns) of the day cells. */
|
|
66
|
-
function columnsOf(rects: RectMark[]): number[] {
|
|
67
|
-
return Array.from(new Set(rects.map((r) => r.x))).sort((a, b) => a - b);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** Distinct sorted y positions (weekday rows) of the day cells. */
|
|
71
|
-
function rowsOf(rects: RectMark[]): number[] {
|
|
72
|
-
return Array.from(new Set(rects.map((r) => r.y))).sort((a, b) => a - b);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
// Grid shape
|
|
77
|
-
// ---------------------------------------------------------------------------
|
|
78
|
-
|
|
79
|
-
describe('calendar grid shape', () => {
|
|
80
|
-
// 2023: Jan 1 falls on a Sunday, so under the default monday weekStart the
|
|
81
|
-
// first column starts on Dec 26, 2022 (a non-January-1st column start).
|
|
82
|
-
const layout = compile(calendarSpec(dailyRows('2023-01-01', 365)));
|
|
83
|
-
const rects = rectsOf(layout);
|
|
84
|
-
|
|
85
|
-
it('renders 53 week columns x 7 weekday rows for a 365-day year', () => {
|
|
86
|
-
expect(rects).toHaveLength(365);
|
|
87
|
-
expect(columnsOf(rects)).toHaveLength(53);
|
|
88
|
-
expect(rowsOf(rects)).toHaveLength(7);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('places month labels at the column containing the 1st of each month', () => {
|
|
92
|
-
const texts = textsOf(layout);
|
|
93
|
-
const cols = columnsOf(rects);
|
|
94
|
-
const step = cols[1] - cols[0];
|
|
95
|
-
const gridLeft = cols[0];
|
|
96
|
-
|
|
97
|
-
// Feb 1, 2023 is a Wednesday in the 6th week column (index 5): five
|
|
98
|
-
// Mondays (Jan 2/9/16/23/30) sit between the Dec 26 grid start and Feb 1.
|
|
99
|
-
const feb = texts.find((t) => t.text === 'Feb');
|
|
100
|
-
expect(feb).toBeDefined();
|
|
101
|
-
expect(feb!.x).toBeCloseTo(gridLeft + 5 * step, 6);
|
|
102
|
-
|
|
103
|
-
// Jan 1 is in the first column even though the column starts in 2022.
|
|
104
|
-
const jan = texts.find((t) => t.text === 'Jan');
|
|
105
|
-
expect(jan!.x).toBeCloseTo(gridLeft, 6);
|
|
106
|
-
|
|
107
|
-
// All 12 months present at this width.
|
|
108
|
-
const monthTexts = texts.filter((t) =>
|
|
109
|
-
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'].includes(
|
|
110
|
-
t.text,
|
|
111
|
-
),
|
|
112
|
-
);
|
|
113
|
-
expect(monthTexts).toHaveLength(12);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('renders Mon/Wed/Fri weekday labels left of the grid', () => {
|
|
117
|
-
const texts = textsOf(layout);
|
|
118
|
-
const gridLeft = columnsOf(rects)[0];
|
|
119
|
-
for (const label of ['Mon', 'Wed', 'Fri']) {
|
|
120
|
-
const mark = texts.find((t) => t.text === label);
|
|
121
|
-
expect(mark).toBeDefined();
|
|
122
|
-
expect(mark!.textAnchor).toBe('end');
|
|
123
|
-
expect(mark!.x).toBeLessThan(gridLeft);
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('suppresses axes and gridlines', () => {
|
|
128
|
-
expect(layout.axes.x).toBeUndefined();
|
|
129
|
-
expect(layout.axes.y).toBeUndefined();
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('keys every cell by its ISO date', () => {
|
|
133
|
-
expect(rects.find((r) => r.key === '2023-06-15')).toBeDefined();
|
|
134
|
-
const keys = new Set(rects.map((r) => r.key));
|
|
135
|
-
expect(keys.size).toBe(rects.length);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Calendar labels hand-compute their y against the SVG default (alphabetic)
|
|
139
|
-
// baseline — see the `+ labelFont * 0.35` centering in compute.ts. Setting a
|
|
140
|
-
// dominantBaseline here would double-correct them and shift every label.
|
|
141
|
-
it('leaves dominantBaseline unset so its hand-computed baselines still hold', () => {
|
|
142
|
-
const texts = textsOf(layout);
|
|
143
|
-
expect(texts.length).toBeGreaterThan(0);
|
|
144
|
-
for (const text of texts) {
|
|
145
|
-
expect(text.dominantBaseline).toBeUndefined();
|
|
146
|
-
}
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// ---------------------------------------------------------------------------
|
|
151
|
-
// Leap years and missing days
|
|
152
|
-
// ---------------------------------------------------------------------------
|
|
153
|
-
|
|
154
|
-
describe('calendar leap years and missing days', () => {
|
|
155
|
-
// 60 days from Feb 1, 2024 cover Feb 29 in a leap year; the rest of the
|
|
156
|
-
// year renders as empty cells.
|
|
157
|
-
const spec = calendarSpec(dailyRows('2024-02-01', 60));
|
|
158
|
-
const layout = compile(spec);
|
|
159
|
-
|
|
160
|
-
it('renders Feb 29 in a leap year as a data cell', () => {
|
|
161
|
-
expect(rectsOf(layout)).toHaveLength(366);
|
|
162
|
-
const feb29 = rectsOf(layout).find((r) => r.key === '2024-02-29');
|
|
163
|
-
expect(feb29).toBeDefined();
|
|
164
|
-
expect(feb29!.aria.decorative).toBeUndefined();
|
|
165
|
-
expect(feb29!.aria.label).toContain('Feb 29, 2024');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('renders missing days as surface-tinted empty cells, distinct from the scale minimum', () => {
|
|
169
|
-
const empty = emptyCells(layout);
|
|
170
|
-
const data = dataCells(layout);
|
|
171
|
-
expect(empty).toHaveLength(306);
|
|
172
|
-
expect(data).toHaveLength(60);
|
|
173
|
-
|
|
174
|
-
// Empty cells carry the theme's achromatic surface tint...
|
|
175
|
-
for (const cell of empty.slice(0, 5)) {
|
|
176
|
-
expect(cell.fill).toBe(layout.theme.colors.annotationFill);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// ...which differs from the chromatic fill of the lowest data value.
|
|
180
|
-
const minValue = Math.min(...data.map((r) => Number(r.data.value)));
|
|
181
|
-
const minCell = data.find((r) => Number(r.data.value) === minValue)!;
|
|
182
|
-
expect(minCell.fill).not.toBe(empty[0].fill);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('keeps the empty/minimum distinction in dark mode', () => {
|
|
186
|
-
const dark = compile(spec, { darkMode: true });
|
|
187
|
-
const empty = emptyCells(dark);
|
|
188
|
-
const data = dataCells(dark);
|
|
189
|
-
const minValue = Math.min(...data.map((r) => Number(r.data.value)));
|
|
190
|
-
const minCell = data.find((r) => Number(r.data.value) === minValue)!;
|
|
191
|
-
|
|
192
|
-
expect(empty[0].fill).toBe(dark.theme.colors.annotationFill);
|
|
193
|
-
expect(empty[0].fill).not.toBe(minCell.fill);
|
|
194
|
-
// Dark mode swaps in its own tint (not the light-mode one).
|
|
195
|
-
expect(empty[0].fill).not.toBe(layout.theme.colors.annotationFill);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('gives data cells a date-titled tooltip and empty cells none', () => {
|
|
199
|
-
expect(layout.tooltipDescriptors.size).toBe(60);
|
|
200
|
-
const firstDataIndex = layout.marks.findIndex((m) => m.type === 'rect' && !m.aria.decorative);
|
|
201
|
-
const descriptor = layout.tooltipDescriptors.get(`rect-${firstDataIndex}`);
|
|
202
|
-
expect(descriptor).toBeDefined();
|
|
203
|
-
expect(descriptor!.title).toContain('2024');
|
|
204
|
-
expect(descriptor!.fields[0].label).toBe('value');
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// Multi-year stacking
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
describe('calendar multi-year stacking', () => {
|
|
213
|
-
const layout = compile(calendarSpec(dailyRows('2023-01-01', 730)), { height: 500 });
|
|
214
|
-
const rects = rectsOf(layout);
|
|
215
|
-
|
|
216
|
-
it('renders one labeled band per year', () => {
|
|
217
|
-
// 2023 (365) + 2024 leap (366)
|
|
218
|
-
expect(rects).toHaveLength(731);
|
|
219
|
-
const yearLabels = textsOf(layout)
|
|
220
|
-
.filter((t) => /^\d{4}$/.test(t.text))
|
|
221
|
-
.map((t) => t.text);
|
|
222
|
-
expect(yearLabels).toEqual(['2023', '2024']);
|
|
223
|
-
|
|
224
|
-
// Two vertically separated bands: 14 distinct row positions, and the
|
|
225
|
-
// top of the 2024 band sits below the bottom of the 2023 band.
|
|
226
|
-
const rows = rowsOf(rects);
|
|
227
|
-
expect(rows).toHaveLength(14);
|
|
228
|
-
const band2023Bottom = Math.max(
|
|
229
|
-
...rects.filter((r) => r.key!.startsWith('2023')).map((r) => r.y),
|
|
230
|
-
);
|
|
231
|
-
const band2024Top = Math.min(...rects.filter((r) => r.key!.startsWith('2024')).map((r) => r.y));
|
|
232
|
-
expect(band2024Top).toBeGreaterThan(band2023Bottom);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('shares one color scale and one legend across years', () => {
|
|
236
|
-
// dailyRows cycles values every 30 days, so day 9 of each January
|
|
237
|
-
// differs while equal values across years must share a fill.
|
|
238
|
-
const sameValuePairs = rects.filter((r) => !r.aria.decorative && Number(r.data.value) === 5);
|
|
239
|
-
const fills = new Set(sameValuePairs.map((r) => String(r.fill)));
|
|
240
|
-
expect(sameValuePairs.length).toBeGreaterThan(2);
|
|
241
|
-
expect(fills.size).toBe(1);
|
|
242
|
-
|
|
243
|
-
// Continuous legend, on by default, exactly one.
|
|
244
|
-
expect(layout.legend.type).toBe('continuous');
|
|
245
|
-
expect(layout.legend.bounds.height).toBeGreaterThan(0);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it('hides the year label for single-year data', () => {
|
|
249
|
-
const single = compile(calendarSpec(dailyRows('2023-01-01', 365)));
|
|
250
|
-
const yearLabels = textsOf(single).filter((t) => /^\d{4}$/.test(t.text));
|
|
251
|
-
expect(yearLabels).toHaveLength(0);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// ---------------------------------------------------------------------------
|
|
256
|
-
// weekStart
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
|
|
259
|
-
describe('calendar weekStart', () => {
|
|
260
|
-
it('computes weekday rows for known dates', () => {
|
|
261
|
-
const wed = new Date(Date.UTC(2024, 1, 29)); // Feb 29, 2024: Thursday
|
|
262
|
-
const sun = new Date(Date.UTC(2024, 1, 4)); // Feb 4, 2024: Sunday
|
|
263
|
-
const mon = new Date(Date.UTC(2024, 0, 1)); // Jan 1, 2024: Monday
|
|
264
|
-
|
|
265
|
-
expect(weekdayRow(mon, 'monday')).toBe(0);
|
|
266
|
-
expect(weekdayRow(mon, 'sunday')).toBe(1);
|
|
267
|
-
expect(weekdayRow(wed, 'monday')).toBe(3);
|
|
268
|
-
expect(weekdayRow(wed, 'sunday')).toBe(4);
|
|
269
|
-
expect(weekdayRow(sun, 'monday')).toBe(6);
|
|
270
|
-
expect(weekdayRow(sun, 'sunday')).toBe(0);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('holds the shift property: sunday rows = (monday rows + 1) mod 7', () => {
|
|
274
|
-
// Property over every day of a month that spans a leap boundary.
|
|
275
|
-
for (let d = 1; d <= 29; d++) {
|
|
276
|
-
const day = new Date(Date.UTC(2024, 1, d));
|
|
277
|
-
expect(weekdayRow(day, 'sunday')).toBe((weekdayRow(day, 'monday') + 1) % 7);
|
|
278
|
-
}
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('shifts compiled rows when weekStart is sunday', () => {
|
|
282
|
-
const data = dailyRows('2024-02-01', 60);
|
|
283
|
-
const monday = compile(calendarSpec(data));
|
|
284
|
-
const sunday = compile(calendarSpec(data, { type: 'calendar', weekStart: 'sunday' }));
|
|
285
|
-
|
|
286
|
-
const rowIndex = (layout: ReturnType<typeof compileChart>, key: string) => {
|
|
287
|
-
const rects = rectsOf(layout);
|
|
288
|
-
return rowsOf(rects).indexOf(rects.find((r) => r.key === key)!.y);
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
// Feb 29, 2024 (Thursday): row 3 under monday start, row 4 under sunday.
|
|
292
|
-
expect(rowIndex(monday, '2024-02-29')).toBe(3);
|
|
293
|
-
expect(rowIndex(sunday, '2024-02-29')).toBe(4);
|
|
294
|
-
// Feb 4, 2024 (Sunday): last row under monday start, first under sunday.
|
|
295
|
-
expect(rowIndex(monday, '2024-02-04')).toBe(6);
|
|
296
|
-
expect(rowIndex(sunday, '2024-02-04')).toBe(0);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// ---------------------------------------------------------------------------
|
|
301
|
-
// Mark options and responsive floor
|
|
302
|
-
// ---------------------------------------------------------------------------
|
|
303
|
-
|
|
304
|
-
describe('calendar mark options and responsive floor', () => {
|
|
305
|
-
it('applies cellRadius to day cells (default 1)', () => {
|
|
306
|
-
const data = dailyRows('2024-02-01', 30);
|
|
307
|
-
const defaulted = compile(calendarSpec(data));
|
|
308
|
-
expect(rectsOf(defaulted)[0].cornerRadius).toBe(1);
|
|
309
|
-
|
|
310
|
-
const rounded = compile(calendarSpec(data, { type: 'calendar', cellRadius: 3 }));
|
|
311
|
-
expect(rectsOf(rounded)[0].cornerRadius).toBe(3);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('floors cell size at 7px on narrow containers instead of shrinking further', () => {
|
|
315
|
-
const layout = compile(calendarSpec(dailyRows('2023-01-01', 365)), {
|
|
316
|
-
width: 320,
|
|
317
|
-
height: 400,
|
|
318
|
-
});
|
|
319
|
-
for (const rect of rectsOf(layout)) {
|
|
320
|
-
expect(rect.width).toBeGreaterThanOrEqual(7);
|
|
321
|
-
expect(rect.height).toBeGreaterThanOrEqual(7);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
// Validation
|
|
328
|
-
// ---------------------------------------------------------------------------
|
|
329
|
-
|
|
330
|
-
describe('calendar validation', () => {
|
|
331
|
-
const rows = dailyRows('2024-01-01', 10);
|
|
332
|
-
|
|
333
|
-
it('rejects a declared non-temporal x with the allowed types', () => {
|
|
334
|
-
const spec = {
|
|
335
|
-
mark: 'calendar',
|
|
336
|
-
data: [{ cat: 'a', value: 1 }],
|
|
337
|
-
encoding: {
|
|
338
|
-
x: { field: 'cat', type: 'nominal' },
|
|
339
|
-
color: { field: 'value', type: 'quantitative' },
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
expect(() => compile(spec)).toThrow(/does not accept type "nominal".*temporal/s);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it('rejects an inferred non-temporal x with a fix suggestion', () => {
|
|
346
|
-
const spec = {
|
|
347
|
-
mark: 'calendar',
|
|
348
|
-
data: [{ cat: 'apple', value: 1 }],
|
|
349
|
-
encoding: { x: { field: 'cat' }, color: { field: 'value', type: 'quantitative' } },
|
|
350
|
-
};
|
|
351
|
-
expect(() => compile(spec)).toThrow(/requires temporal encoding\.x.*inferred as nominal/s);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('rejects unparseable dates naming the offending row', () => {
|
|
355
|
-
const spec = calendarSpec([...rows, { date: 'not-a-date', value: 3 }]);
|
|
356
|
-
expect(() => compile(spec)).toThrow(/unparseable date at data\[10\].*"not-a-date"/s);
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('rejects sub-daily granularity with a timeUnit aggregation hint', () => {
|
|
360
|
-
const spec = calendarSpec([
|
|
361
|
-
{ date: '2024-01-01T02:00:00Z', value: 1 },
|
|
362
|
-
{ date: '2024-01-01T14:00:00Z', value: 2 },
|
|
363
|
-
]);
|
|
364
|
-
expect(() => compile(spec)).toThrow(/daily granularity.*same day/s);
|
|
365
|
-
// The fix lives in the structured suggestion (compile joins messages only).
|
|
366
|
-
const result = validateSpec(spec);
|
|
367
|
-
const granularityError = result.errors.find((e) => /daily granularity/.test(e.message));
|
|
368
|
-
expect(granularityError?.suggestion).toMatch(/timeUnit/);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('rejects encoding.y', () => {
|
|
372
|
-
const spec = {
|
|
373
|
-
mark: 'calendar',
|
|
374
|
-
data: rows,
|
|
375
|
-
encoding: {
|
|
376
|
-
x: { field: 'date', type: 'temporal' },
|
|
377
|
-
y: { field: 'value', type: 'quantitative' },
|
|
378
|
-
color: { field: 'value', type: 'quantitative' },
|
|
379
|
-
},
|
|
380
|
-
};
|
|
381
|
-
expect(() => compile(spec)).toThrow(/does not accept encoding\.y/);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('rejects an invalid weekStart listing the valid options', () => {
|
|
385
|
-
const spec = calendarSpec(rows, { type: 'calendar', weekStart: 'tuesday' });
|
|
386
|
-
expect(() => compile(spec)).toThrow(/weekStart "tuesday"/);
|
|
387
|
-
const result = validateSpec(spec);
|
|
388
|
-
const weekStartError = result.errors.find((e) => /weekStart/.test(e.message));
|
|
389
|
-
expect(weekStartError?.suggestion).toMatch(/monday.*sunday/s);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
it('requires the color channel', () => {
|
|
393
|
-
const spec = {
|
|
394
|
-
mark: 'calendar',
|
|
395
|
-
data: rows,
|
|
396
|
-
encoding: { x: { field: 'date', type: 'temporal' } },
|
|
397
|
-
};
|
|
398
|
-
expect(() => compile(spec)).toThrow(/requires encoding\.color/);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('skips per-row date checks when transforms will reshape the data', () => {
|
|
402
|
-
const spec = {
|
|
403
|
-
...calendarSpec([
|
|
404
|
-
{ date: '2024-01-01T02:00:00Z', value: 1 },
|
|
405
|
-
{ date: '2024-01-01T14:00:00Z', value: 2 },
|
|
406
|
-
]),
|
|
407
|
-
transform: [{ timeUnit: 'yearmonthdate' as const, field: 'date', as: 'day' }],
|
|
408
|
-
encoding: {
|
|
409
|
-
x: { field: 'day', type: 'temporal' as const },
|
|
410
|
-
color: { field: 'value', type: 'quantitative' as const },
|
|
411
|
-
},
|
|
412
|
-
};
|
|
413
|
-
// Sub-daily rows plus an aggregating transform must not trip the
|
|
414
|
-
// granularity check at validation time.
|
|
415
|
-
expect(() => compile(spec)).not.toThrow(/daily granularity/);
|
|
416
|
-
});
|
|
417
|
-
});
|