@opendata-ai/openchart-engine 8.0.0-rc.12 → 8.0.0-rc.3

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.
Files changed (271) hide show
  1. package/dist/index.d.ts +0 -7
  2. package/dist/index.js +161 -579
  3. package/dist/index.js.map +1 -1
  4. package/package.json +4 -3
  5. package/src/__test-fixtures__/specs.ts +182 -0
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +2339 -0
  7. package/src/__tests__/annotation-behavior.test.ts +721 -0
  8. package/src/__tests__/axes.test.ts +1579 -0
  9. package/src/__tests__/compile-animation.test.ts +161 -0
  10. package/src/__tests__/compile-chart.test.ts +1303 -0
  11. package/src/__tests__/compile-footnotes.test.ts +277 -0
  12. package/src/__tests__/compile-layer.test.ts +969 -0
  13. package/src/__tests__/compile-snapshot.test.ts +156 -0
  14. package/src/__tests__/compound-labels.test.ts +147 -0
  15. package/src/__tests__/dimensions.test.ts +918 -0
  16. package/src/__tests__/encoding-sugar.test.ts +390 -0
  17. package/src/__tests__/facet.test.ts +215 -0
  18. package/src/__tests__/helpers/invariants.ts +299 -0
  19. package/src/__tests__/known-bugs.test.ts +566 -0
  20. package/src/__tests__/layout-fuzz.test.ts +358 -0
  21. package/src/__tests__/layout-invariants.test.ts +535 -0
  22. package/src/__tests__/legend.test.ts +721 -0
  23. package/src/__tests__/mark-keys.test.ts +294 -0
  24. package/src/__tests__/ranking-recipes.test.ts +260 -0
  25. package/src/__tests__/rotation.test.ts +83 -0
  26. package/src/__tests__/scales.test.ts +323 -0
  27. package/src/__tests__/series-search.test.ts +92 -0
  28. package/src/__tests__/spec-sugar.test.ts +801 -0
  29. package/src/__tests__/you-draw-it.test.ts +160 -0
  30. package/src/annotations/__tests__/compute.test.ts +1672 -0
  31. package/src/annotations/__tests__/geometry.test.ts +66 -0
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +256 -0
  33. package/src/annotations/__tests__/placement-invariants.test.ts +345 -0
  34. package/src/annotations/__tests__/placement.test.ts +316 -0
  35. package/src/annotations/__tests__/resolve-text.test.ts +788 -0
  36. package/src/annotations/__tests__/rich-text.test.ts +91 -0
  37. package/src/annotations/__tests__/thinning.test.ts +285 -0
  38. package/src/annotations/collisions.ts +326 -0
  39. package/src/annotations/compute.ts +333 -0
  40. package/src/annotations/constants.ts +162 -0
  41. package/src/annotations/geometry.ts +370 -0
  42. package/src/annotations/move.ts +135 -0
  43. package/src/annotations/placement.ts +636 -0
  44. package/src/annotations/position.ts +131 -0
  45. package/src/annotations/resolve-range.ts +117 -0
  46. package/src/annotations/resolve-refline.ts +157 -0
  47. package/src/annotations/resolve-text.ts +539 -0
  48. package/src/annotations/rich-text.ts +65 -0
  49. package/src/annotations/thinning.ts +190 -0
  50. package/src/barlist/__tests__/compile-barlist.test.ts +200 -0
  51. package/src/barlist/compile-barlist.ts +391 -0
  52. package/src/barlist/types.ts +28 -0
  53. package/src/charts/__tests__/post-process.test.ts +237 -0
  54. package/src/charts/__tests__/registry.test.ts +116 -0
  55. package/src/charts/__tests__/utils.test.ts +195 -0
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +32 -0
  57. package/src/charts/_shared/density-filter.ts +26 -0
  58. package/src/charts/_shared/format-label-value.ts +16 -0
  59. package/src/charts/bar/__tests__/compute.test.ts +934 -0
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +127 -0
  61. package/src/charts/bar/__tests__/labels.test.ts +292 -0
  62. package/src/charts/bar/compute.ts +539 -0
  63. package/src/charts/bar/index.ts +60 -0
  64. package/src/charts/bar/labels.ts +311 -0
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +280 -0
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +148 -0
  67. package/src/charts/beeswarm/compute.ts +203 -0
  68. package/src/charts/beeswarm/dodge.ts +155 -0
  69. package/src/charts/beeswarm/index.ts +32 -0
  70. package/src/charts/builtin.ts +75 -0
  71. package/src/charts/calendar/__tests__/compute.test.ts +417 -0
  72. package/src/charts/calendar/compute.ts +319 -0
  73. package/src/charts/calendar/index.ts +32 -0
  74. package/src/charts/column/__tests__/compute.test.ts +540 -0
  75. package/src/charts/column/__tests__/labels.test.ts +179 -0
  76. package/src/charts/column/compute.ts +493 -0
  77. package/src/charts/column/index.ts +53 -0
  78. package/src/charts/column/labels.ts +229 -0
  79. package/src/charts/dot/__tests__/compute.test.ts +380 -0
  80. package/src/charts/dot/__tests__/labels.test.ts +98 -0
  81. package/src/charts/dot/compute.ts +285 -0
  82. package/src/charts/dot/index.ts +62 -0
  83. package/src/charts/dot/labels.ts +121 -0
  84. package/src/charts/line/__tests__/compute.test.ts +1194 -0
  85. package/src/charts/line/__tests__/curves.test.ts +51 -0
  86. package/src/charts/line/__tests__/labels.test.ts +94 -0
  87. package/src/charts/line/__tests__/mark-options.test.ts +175 -0
  88. package/src/charts/line/area.ts +484 -0
  89. package/src/charts/line/compute.ts +356 -0
  90. package/src/charts/line/curves.ts +85 -0
  91. package/src/charts/line/index.ts +176 -0
  92. package/src/charts/line/labels.ts +177 -0
  93. package/src/charts/parliament/__tests__/compute.test.ts +411 -0
  94. package/src/charts/parliament/compute.ts +313 -0
  95. package/src/charts/parliament/index.ts +32 -0
  96. package/src/charts/pie/__tests__/compute.test.ts +384 -0
  97. package/src/charts/pie/__tests__/labels.test.ts +132 -0
  98. package/src/charts/pie/compute.ts +324 -0
  99. package/src/charts/pie/index.ts +49 -0
  100. package/src/charts/pie/labels.ts +143 -0
  101. package/src/charts/post-process.ts +229 -0
  102. package/src/charts/range/__tests__/compute.test.ts +486 -0
  103. package/src/charts/range/compute.ts +273 -0
  104. package/src/charts/range/index.ts +82 -0
  105. package/src/charts/range/labels.ts +169 -0
  106. package/src/charts/rect/__tests__/compute.test.ts +120 -0
  107. package/src/charts/rect/index.ts +127 -0
  108. package/src/charts/registry.ts +72 -0
  109. package/src/charts/rule/index.ts +139 -0
  110. package/src/charts/scatter/__tests__/compute.test.ts +488 -0
  111. package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
  112. package/src/charts/scatter/compute.ts +192 -0
  113. package/src/charts/scatter/index.ts +46 -0
  114. package/src/charts/scatter/trendline.ts +100 -0
  115. package/src/charts/text/__tests__/compute.test.ts +307 -0
  116. package/src/charts/text/index.ts +141 -0
  117. package/src/charts/tick/__tests__/compute.test.ts +75 -0
  118. package/src/charts/tick/index.ts +99 -0
  119. package/src/charts/utils.ts +170 -0
  120. package/src/charts/waffle/__tests__/compute.test.ts +305 -0
  121. package/src/charts/waffle/compute.ts +197 -0
  122. package/src/charts/waffle/index.ts +31 -0
  123. package/src/compile/__tests__/color-scale-range.test.ts +158 -0
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +89 -0
  125. package/src/compile/__tests__/data-clip.test.ts +127 -0
  126. package/src/compile/__tests__/fill-patterns.test.ts +154 -0
  127. package/src/compile/__tests__/size-scale.test.ts +108 -0
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +93 -0
  129. package/src/compile/color-scale-range.ts +101 -0
  130. package/src/compile/data-clip.ts +48 -0
  131. package/src/compile/fill-patterns.ts +74 -0
  132. package/src/compile/layer.ts +626 -0
  133. package/src/compile/size-scale.ts +141 -0
  134. package/src/compile/spec-sugar.ts +758 -0
  135. package/src/compile/watermark-obstacle.ts +54 -0
  136. package/src/compile/you-draw-it.ts +123 -0
  137. package/src/compile.ts +1450 -0
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +148 -0
  139. package/src/compiler/__tests__/animation.test.ts +177 -0
  140. package/src/compiler/__tests__/compile.test.ts +87 -0
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +19 -0
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +20 -0
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +22 -0
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +19 -0
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +14 -0
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +34 -0
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +22 -0
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +41 -0
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +38 -0
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +28 -0
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +28 -0
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +30 -0
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +29 -0
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +32 -0
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +25 -0
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +19 -0
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +19 -0
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +21 -0
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +23 -0
  160. package/src/compiler/__tests__/keys.test.ts +83 -0
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +103 -0
  162. package/src/compiler/__tests__/normalize.test.ts +399 -0
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +153 -0
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +86 -0
  165. package/src/compiler/__tests__/validate-highlight.test.ts +112 -0
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +89 -0
  167. package/src/compiler/__tests__/validate.test.ts +629 -0
  168. package/src/compiler/__tests__/vl-parity.test.ts +68 -0
  169. package/src/compiler/a11y-warnings.ts +110 -0
  170. package/src/compiler/animation.ts +142 -0
  171. package/src/compiler/index.ts +47 -0
  172. package/src/compiler/keys.ts +55 -0
  173. package/src/compiler/normalize.ts +734 -0
  174. package/src/compiler/sparkline-defaults.ts +138 -0
  175. package/src/compiler/types.ts +241 -0
  176. package/src/compiler/validate.ts +1871 -0
  177. package/src/endpoint-labels/__tests__/compute.test.ts +587 -0
  178. package/src/endpoint-labels/compute.ts +621 -0
  179. package/src/endpoint-labels/constants.ts +54 -0
  180. package/src/endpoint-labels/format.ts +60 -0
  181. package/src/endpoint-labels/predict.ts +139 -0
  182. package/src/format/__tests__/field-format.test.ts +164 -0
  183. package/src/format/field-format.ts +27 -0
  184. package/src/geo/__tests__/compile-map.test.ts +832 -0
  185. package/src/geo/compile-map.ts +1243 -0
  186. package/src/geo/join.ts +115 -0
  187. package/src/geo/projections.ts +45 -0
  188. package/src/geo/types.ts +26 -0
  189. package/src/graphs/__tests__/community.test.ts +228 -0
  190. package/src/graphs/__tests__/compile-graph.test.ts +396 -0
  191. package/src/graphs/__tests__/encoding.test.ts +415 -0
  192. package/src/graphs/community.ts +92 -0
  193. package/src/graphs/compile-graph.ts +329 -0
  194. package/src/graphs/encoding.ts +356 -0
  195. package/src/graphs/types.ts +106 -0
  196. package/src/index.ts +177 -0
  197. package/src/layout/__tests__/metrics.test.ts +97 -0
  198. package/src/layout/__tests__/scales-highlight.test.ts +192 -0
  199. package/src/layout/axes/rotation.ts +152 -0
  200. package/src/layout/axes/thinning.ts +98 -0
  201. package/src/layout/axes/ticks.ts +409 -0
  202. package/src/layout/axes.ts +757 -0
  203. package/src/layout/dimensions.ts +912 -0
  204. package/src/layout/facet.ts +91 -0
  205. package/src/layout/gridlines.ts +84 -0
  206. package/src/layout/metrics.ts +132 -0
  207. package/src/layout/plan.ts +631 -0
  208. package/src/layout/scales.ts +1053 -0
  209. package/src/layout/shared.ts +44 -0
  210. package/src/legend/__tests__/continuous.test.ts +318 -0
  211. package/src/legend/__tests__/redundancy.test.ts +108 -0
  212. package/src/legend/__tests__/size.test.ts +348 -0
  213. package/src/legend/__tests__/suppression.test.ts +382 -0
  214. package/src/legend/compute.ts +747 -0
  215. package/src/legend/continuous.ts +327 -0
  216. package/src/legend/size.ts +407 -0
  217. package/src/legend/suppression.ts +226 -0
  218. package/src/legend/wrap.ts +117 -0
  219. package/src/sankey/__tests__/compile-sankey.test.ts +458 -0
  220. package/src/sankey/__tests__/layout.test.ts +165 -0
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +114 -0
  222. package/src/sankey/__tests__/node-sort.test.ts +45 -0
  223. package/src/sankey/compile-sankey.ts +706 -0
  224. package/src/sankey/layout.ts +184 -0
  225. package/src/sankey/types.ts +41 -0
  226. package/src/tables/__tests__/bar-column.test.ts +147 -0
  227. package/src/tables/__tests__/category-colors.test.ts +230 -0
  228. package/src/tables/__tests__/compile-table.test.ts +236 -0
  229. package/src/tables/__tests__/format-cells.test.ts +143 -0
  230. package/src/tables/__tests__/heatmap.test.ts +148 -0
  231. package/src/tables/__tests__/pagination.test.ts +78 -0
  232. package/src/tables/__tests__/search.test.ts +94 -0
  233. package/src/tables/__tests__/sort.test.ts +107 -0
  234. package/src/tables/__tests__/sparkline.test.ts +122 -0
  235. package/src/tables/bar-column.ts +94 -0
  236. package/src/tables/category-colors.ts +78 -0
  237. package/src/tables/compile-table.ts +427 -0
  238. package/src/tables/format-cells.ts +115 -0
  239. package/src/tables/heatmap.ts +141 -0
  240. package/src/tables/pagination.ts +46 -0
  241. package/src/tables/search.ts +66 -0
  242. package/src/tables/sort.ts +69 -0
  243. package/src/tables/sparkline.ts +113 -0
  244. package/src/tables/utils.ts +16 -0
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +576 -0
  246. package/src/tilemap/compile-tilemap.ts +755 -0
  247. package/src/tilemap/layout.ts +172 -0
  248. package/src/tilemap/types.ts +33 -0
  249. package/src/tooltips/__tests__/compute.test.ts +582 -0
  250. package/src/tooltips/compute.ts +700 -0
  251. package/src/transforms/__tests__/aggregate.test.ts +159 -0
  252. package/src/transforms/__tests__/bin.test.ts +88 -0
  253. package/src/transforms/__tests__/calculate.test.ts +146 -0
  254. package/src/transforms/__tests__/conditional.test.ts +127 -0
  255. package/src/transforms/__tests__/filter-relative.test.ts +202 -0
  256. package/src/transforms/__tests__/filter.test.ts +59 -0
  257. package/src/transforms/__tests__/fold.test.ts +79 -0
  258. package/src/transforms/__tests__/index.test.ts +93 -0
  259. package/src/transforms/__tests__/predicates.test.ts +193 -0
  260. package/src/transforms/__tests__/timeunit.test.ts +129 -0
  261. package/src/transforms/__tests__/window.test.ts +286 -0
  262. package/src/transforms/aggregate.ts +131 -0
  263. package/src/transforms/bin.ts +87 -0
  264. package/src/transforms/calculate.ts +60 -0
  265. package/src/transforms/conditional.ts +46 -0
  266. package/src/transforms/filter.ts +122 -0
  267. package/src/transforms/fold.ts +49 -0
  268. package/src/transforms/index.ts +60 -0
  269. package/src/transforms/predicates.ts +123 -0
  270. package/src/transforms/timeunit.ts +88 -0
  271. package/src/transforms/window.ts +185 -0
@@ -0,0 +1,390 @@
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('preserves data order 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
+ // Data order: Banana, Apple, Cherry
242
+ expect(domain).toEqual(['Banana', 'Apple', 'Cherry']);
243
+ });
244
+
245
+ it('sorts domain ascending when sort is "ascending"', () => {
246
+ const spec = makeBarSpec('ascending');
247
+ const scales = computeScales(spec, chartArea, spec.data);
248
+ const domain = (scales.y!.scale as ScaleBand<string>).domain();
249
+ expect(domain).toEqual(['Apple', 'Banana', 'Cherry']);
250
+ });
251
+
252
+ it('sorts domain descending when sort is "descending"', () => {
253
+ const spec = makeBarSpec('descending');
254
+ const scales = computeScales(spec, chartArea, spec.data);
255
+ const domain = (scales.y!.scale as ScaleBand<string>).domain();
256
+ expect(domain).toEqual(['Cherry', 'Banana', 'Apple']);
257
+ });
258
+
259
+ it('preserves data order when sort is null', () => {
260
+ const spec = makeBarSpec(null);
261
+ const scales = computeScales(spec, chartArea, spec.data);
262
+ const domain = (scales.y!.scale as ScaleBand<string>).domain();
263
+ // Data order: Banana, Apple, Cherry
264
+ expect(domain).toEqual(['Banana', 'Apple', 'Cherry']);
265
+ });
266
+
267
+ it('applies sort to point scales (nominal x on line chart)', () => {
268
+ const spec: NormalizedChartSpec = {
269
+ markType: 'line',
270
+ markDef: { type: 'line' },
271
+ data: [
272
+ { name: 'Zara', score: 10 },
273
+ { name: 'Alice', score: 20 },
274
+ { name: 'Mike', score: 15 },
275
+ ],
276
+ encoding: {
277
+ x: { field: 'name', type: 'nominal', sort: 'descending' },
278
+ y: { field: 'score', type: 'quantitative' },
279
+ },
280
+ chrome: {},
281
+ annotations: [],
282
+ responsive: true,
283
+ theme: {},
284
+ darkMode: 'off',
285
+ labels: { density: 'auto', format: '', prefix: '' },
286
+ watermark: true,
287
+ hiddenSeries: [],
288
+ seriesStyles: {},
289
+ };
290
+
291
+ const scales = computeScales(spec, chartArea, spec.data);
292
+ const domain = (scales.x!.scale as ScalePoint<string>).domain();
293
+ expect(domain).toEqual(['Zara', 'Mike', 'Alice']);
294
+ });
295
+
296
+ it('applies sort to ordinal color scales', () => {
297
+ const spec: NormalizedChartSpec = {
298
+ markType: 'line',
299
+ markDef: { type: 'line' },
300
+ data: [
301
+ { date: '2020-01-01', value: 10, group: 'Gamma' },
302
+ { date: '2020-01-01', value: 20, group: 'Alpha' },
303
+ { date: '2020-01-01', value: 15, group: 'Beta' },
304
+ ],
305
+ encoding: {
306
+ x: { field: 'date', type: 'temporal' },
307
+ y: { field: 'value', type: 'quantitative' },
308
+ color: { field: 'group', type: 'nominal', sort: 'ascending' },
309
+ },
310
+ chrome: {},
311
+ annotations: [],
312
+ responsive: true,
313
+ theme: {},
314
+ darkMode: 'off',
315
+ labels: { density: 'auto', format: '', prefix: '' },
316
+ watermark: true,
317
+ hiddenSeries: [],
318
+ seriesStyles: {},
319
+ };
320
+
321
+ const scales = computeScales(spec, chartArea, spec.data);
322
+ const domain = (scales.color!.scale as ScaleOrdinal<string, string>).domain();
323
+ expect(domain).toEqual(['Alpha', 'Beta', 'Gamma']);
324
+ });
325
+
326
+ it('sorts numerically when values are numeric strings', () => {
327
+ const spec: NormalizedChartSpec = {
328
+ markType: 'bar',
329
+ markDef: { type: 'bar' },
330
+ data: [
331
+ { category: '10', count: 1 },
332
+ { category: '2', count: 2 },
333
+ { category: '1', count: 3 },
334
+ ],
335
+ encoding: {
336
+ x: { field: 'count', type: 'quantitative' },
337
+ y: { field: 'category', type: 'nominal', sort: 'ascending' },
338
+ },
339
+ chrome: {},
340
+ annotations: [],
341
+ responsive: true,
342
+ theme: {},
343
+ darkMode: 'off',
344
+ labels: { density: 'auto', format: '', prefix: '' },
345
+ watermark: true,
346
+ hiddenSeries: [],
347
+ seriesStyles: {},
348
+ };
349
+
350
+ const scales = computeScales(spec, chartArea, spec.data);
351
+ const domain = (scales.y!.scale as ScaleBand<string>).domain();
352
+ // numeric: true in localeCompare means "1" < "2" < "10"
353
+ expect(domain).toEqual(['1', '2', '10']);
354
+ });
355
+
356
+ it('does not sort when explicit scale.domain is provided', () => {
357
+ const spec: NormalizedChartSpec = {
358
+ markType: 'bar',
359
+ markDef: { type: 'bar' },
360
+ data: [
361
+ { category: 'C', count: 10 },
362
+ { category: 'A', count: 20 },
363
+ { category: 'B', count: 30 },
364
+ ],
365
+ encoding: {
366
+ x: { field: 'count', type: 'quantitative' },
367
+ y: {
368
+ field: 'category',
369
+ type: 'nominal',
370
+ sort: 'ascending',
371
+ scale: { domain: ['C', 'B', 'A'] },
372
+ },
373
+ },
374
+ chrome: {},
375
+ annotations: [],
376
+ responsive: true,
377
+ theme: {},
378
+ darkMode: 'off',
379
+ labels: { density: 'auto', format: '', prefix: '' },
380
+ watermark: true,
381
+ hiddenSeries: [],
382
+ seriesStyles: {},
383
+ };
384
+
385
+ const scales = computeScales(spec, chartArea, spec.data);
386
+ const domain = (scales.y!.scale as ScaleBand<string>).domain();
387
+ // Explicit domain takes precedence over sort
388
+ expect(domain).toEqual(['C', 'B', 'A']);
389
+ });
390
+ });
@@ -0,0 +1,215 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { compileChart } from '../compile';
3
+ import { autoColumns, computeFacetGrid } from '../layout/facet';
4
+
5
+ const facetData = [
6
+ { year: '2020', country: 'US', value: 100 },
7
+ { year: '2021', country: 'US', value: 120 },
8
+ { year: '2020', country: 'UK', value: 80 },
9
+ { year: '2021', country: 'UK', value: 90 },
10
+ { year: '2020', country: 'DE', value: 70 },
11
+ { year: '2021', country: 'DE', value: 85 },
12
+ ];
13
+
14
+ const facetSpec = {
15
+ mark: 'line' as const,
16
+ data: facetData,
17
+ encoding: {
18
+ x: { field: 'year', type: 'ordinal' as const },
19
+ y: { field: 'value', type: 'quantitative' as const },
20
+ facet: { field: 'country', type: 'nominal' as const },
21
+ },
22
+ };
23
+
24
+ describe('autoColumns', () => {
25
+ it('returns sqrt-based columns for moderate panel counts', () => {
26
+ expect(autoColumns(4, 800)).toBe(2);
27
+ expect(autoColumns(9, 1200)).toBe(3);
28
+ });
29
+
30
+ it('caps columns by available width', () => {
31
+ expect(autoColumns(9, 400)).toBe(2);
32
+ });
33
+
34
+ it('returns 1 for single panel', () => {
35
+ expect(autoColumns(1, 800)).toBe(1);
36
+ });
37
+
38
+ it('does not exceed panel count', () => {
39
+ expect(autoColumns(2, 2000)).toBe(2);
40
+ });
41
+ });
42
+
43
+ describe('computeFacetGrid', () => {
44
+ it('returns empty result for 0 panels', () => {
45
+ const result = computeFacetGrid(
46
+ [],
47
+ undefined,
48
+ { x: 0, y: 0, width: 600, height: 400 },
49
+ {
50
+ left: 40,
51
+ bottom: 30,
52
+ },
53
+ );
54
+ expect(result.panels).toEqual([]);
55
+ expect(result.columns).toBe(0);
56
+ expect(result.rows).toBe(0);
57
+ });
58
+
59
+ it('computes correct number of panels', () => {
60
+ const result = computeFacetGrid(
61
+ ['A', 'B', 'C', 'D'],
62
+ 2,
63
+ { x: 0, y: 0, width: 600, height: 400 },
64
+ { left: 40, bottom: 30 },
65
+ );
66
+ expect(result.panels).toHaveLength(4);
67
+ expect(result.columns).toBe(2);
68
+ expect(result.rows).toBe(2);
69
+ });
70
+
71
+ it('assigns correct row and col indices', () => {
72
+ const result = computeFacetGrid(
73
+ ['A', 'B', 'C'],
74
+ 2,
75
+ { x: 0, y: 0, width: 600, height: 400 },
76
+ { left: 40, bottom: 30 },
77
+ );
78
+ expect(result.panels[0]).toMatchObject({ key: 'A', row: 0, col: 0 });
79
+ expect(result.panels[1]).toMatchObject({ key: 'B', row: 0, col: 1 });
80
+ expect(result.panels[2]).toMatchObject({ key: 'C', row: 1, col: 0 });
81
+ });
82
+
83
+ it('panels do not overlap', () => {
84
+ const result = computeFacetGrid(
85
+ ['A', 'B', 'C', 'D', 'E', 'F'],
86
+ 3,
87
+ { x: 0, y: 0, width: 800, height: 600 },
88
+ { left: 40, bottom: 30 },
89
+ );
90
+ for (let i = 0; i < result.panels.length; i++) {
91
+ for (let j = i + 1; j < result.panels.length; j++) {
92
+ const a = result.panels[i].area;
93
+ const b = result.panels[j].area;
94
+ const xOverlap = a.x < b.x + b.width && a.x + a.width > b.x;
95
+ const yOverlap = a.y < b.y + b.height && a.y + a.height > b.y;
96
+ expect(xOverlap && yOverlap).toBe(false);
97
+ }
98
+ }
99
+ });
100
+
101
+ it('degrades columns when panels would be too narrow', () => {
102
+ const result = computeFacetGrid(
103
+ ['A', 'B', 'C', 'D'],
104
+ 4,
105
+ { x: 0, y: 0, width: 400, height: 400 },
106
+ { left: 40, bottom: 30 },
107
+ );
108
+ expect(result.columns).toBeLessThan(4);
109
+ for (const panel of result.panels) {
110
+ expect(panel.area.width).toBeGreaterThanOrEqual(0);
111
+ }
112
+ });
113
+
114
+ it('clamps panel height to zero for tiny containers', () => {
115
+ const result = computeFacetGrid(
116
+ ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
117
+ 1,
118
+ { x: 0, y: 0, width: 400, height: 100 },
119
+ { left: 40, bottom: 30 },
120
+ );
121
+ for (const panel of result.panels) {
122
+ expect(panel.area.height).toBeGreaterThanOrEqual(0);
123
+ }
124
+ });
125
+ });
126
+
127
+ describe('compileChart with facet', () => {
128
+ it('populates layout.facet with correct panel count', () => {
129
+ const layout = compileChart(facetSpec, { width: 600, height: 400 });
130
+ expect(layout.facet).toBeDefined();
131
+ expect(layout.facet!.panels).toHaveLength(3);
132
+ expect(layout.facet!.facetField).toBe('country');
133
+ });
134
+
135
+ it('sorts panels ascending by default', () => {
136
+ const layout = compileChart(facetSpec, { width: 600, height: 400 });
137
+ const keys = layout.facet!.panels.map((p) => p.key);
138
+ expect(keys).toEqual(['DE', 'UK', 'US']);
139
+ });
140
+
141
+ it('respects descending sort', () => {
142
+ const spec = {
143
+ ...facetSpec,
144
+ encoding: {
145
+ ...facetSpec.encoding,
146
+ facet: { ...facetSpec.encoding.facet, sort: 'descending' as const },
147
+ },
148
+ };
149
+ const layout = compileChart(spec, { width: 600, height: 400 });
150
+ const keys = layout.facet!.panels.map((p) => p.key);
151
+ expect(keys).toEqual(['US', 'UK', 'DE']);
152
+ });
153
+
154
+ it('preserves data order with sort: null', () => {
155
+ const spec = {
156
+ ...facetSpec,
157
+ encoding: {
158
+ ...facetSpec.encoding,
159
+ facet: { ...facetSpec.encoding.facet, sort: null },
160
+ },
161
+ };
162
+ const layout = compileChart(spec, { width: 600, height: 400 });
163
+ const keys = layout.facet!.panels.map((p) => p.key);
164
+ expect(keys).toEqual(['US', 'UK', 'DE']);
165
+ });
166
+
167
+ it('uses shared scales by default', () => {
168
+ const layout = compileChart(facetSpec, { width: 600, height: 400 });
169
+ expect(layout.facet!.sharedScales).toBe(true);
170
+ });
171
+
172
+ it('supports independent y scales via resolve', () => {
173
+ const spec = {
174
+ ...facetSpec,
175
+ resolve: { scale: { y: 'independent' as const } },
176
+ };
177
+ const layout = compileChart(spec, { width: 600, height: 400 });
178
+ expect(layout.facet!.sharedScales).toBe(false);
179
+ });
180
+
181
+ it('each panel has marks and axes', () => {
182
+ const layout = compileChart(facetSpec, { width: 600, height: 400 });
183
+ for (const panel of layout.facet!.panels) {
184
+ expect(panel.marks.length).toBeGreaterThan(0);
185
+ expect(panel.area.width).toBeGreaterThan(0);
186
+ expect(panel.area.height).toBeGreaterThan(0);
187
+ expect(panel.header.text).toBeTruthy();
188
+ }
189
+ });
190
+
191
+ it('figure-level axes are undefined when faceted', () => {
192
+ const layout = compileChart(facetSpec, { width: 600, height: 400 });
193
+ expect(layout.axes.x).toBeUndefined();
194
+ expect(layout.axes.y).toBeUndefined();
195
+ });
196
+
197
+ it('marks union includes all panel marks', () => {
198
+ const layout = compileChart(facetSpec, { width: 600, height: 400 });
199
+ const panelMarkCount = layout.facet!.panels.reduce((sum, p) => sum + p.marks.length, 0);
200
+ expect(layout.marks.length).toBe(panelMarkCount);
201
+ });
202
+
203
+ it('non-faceted spec produces no facet layout', () => {
204
+ const spec = {
205
+ mark: 'line' as const,
206
+ data: facetData,
207
+ encoding: {
208
+ x: { field: 'year', type: 'ordinal' as const },
209
+ y: { field: 'value', type: 'quantitative' as const },
210
+ },
211
+ };
212
+ const layout = compileChart(spec, { width: 600, height: 400 });
213
+ expect(layout.facet).toBeUndefined();
214
+ });
215
+ });