@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,540 @@
1
+ import type { LayoutStrategy, Rect } from '@opendata-ai/openchart-core';
2
+ import { buildD3Formatter } from '@opendata-ai/openchart-core';
3
+ import { describe, expect, it } from 'vitest';
4
+ import type { NormalizedChartSpec } from '../../../compiler/types';
5
+ import { computeScales } from '../../../layout/scales';
6
+ import { computeColumnMarks } from '../compute';
7
+ import { computeColumnLabels } from '../labels';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Shared fixtures
11
+ // ---------------------------------------------------------------------------
12
+
13
+ const chartArea: Rect = { x: 50, y: 20, width: 500, height: 300 };
14
+
15
+ const fullStrategy: LayoutStrategy = {
16
+ labelMode: 'all',
17
+ legendPosition: 'right',
18
+ annotationPosition: 'inline',
19
+ axisLabelDensity: 'full',
20
+ };
21
+
22
+ function makeSimpleColumnSpec(): NormalizedChartSpec {
23
+ return {
24
+ markType: 'bar',
25
+ markDef: { type: 'bar', orient: 'vertical' },
26
+ data: [
27
+ { month: 'Jan', sales: 120 },
28
+ { month: 'Feb', sales: 80 },
29
+ { month: 'Mar', sales: 150 },
30
+ { month: 'Apr', sales: 200 },
31
+ ],
32
+ encoding: {
33
+ x: { field: 'month', type: 'nominal' },
34
+ y: { field: 'sales', type: 'quantitative' },
35
+ },
36
+ chrome: {},
37
+ annotations: [],
38
+ responsive: true,
39
+ theme: {},
40
+ darkMode: 'off',
41
+ labels: { density: 'auto', format: '' },
42
+ };
43
+ }
44
+
45
+ function makeGroupedColumnSpec(): NormalizedChartSpec {
46
+ return {
47
+ markType: 'bar',
48
+ markDef: { type: 'bar', orient: 'vertical' },
49
+ data: [
50
+ { month: 'Jan', sales: 120, region: 'North' },
51
+ { month: 'Jan', sales: 80, region: 'South' },
52
+ { month: 'Feb', sales: 90, region: 'North' },
53
+ { month: 'Feb', sales: 110, region: 'South' },
54
+ { month: 'Mar', sales: 150, region: 'North' },
55
+ { month: 'Mar', sales: 130, region: 'South' },
56
+ ],
57
+ encoding: {
58
+ x: { field: 'month', type: 'nominal' },
59
+ // Grouped is opt-out since v8 (stacked is now the default): stamp
60
+ // stack: null explicitly so this fixture stays grouped.
61
+ y: { field: 'sales', type: 'quantitative', stack: null },
62
+ color: { field: 'region', type: 'nominal' },
63
+ },
64
+ chrome: {},
65
+ annotations: [],
66
+ responsive: true,
67
+ theme: {},
68
+ darkMode: 'off',
69
+ labels: { density: 'auto', format: '' },
70
+ };
71
+ }
72
+
73
+ function makeNegativeColumnSpec(): NormalizedChartSpec {
74
+ return {
75
+ markType: 'bar',
76
+ markDef: { type: 'bar', orient: 'vertical' },
77
+ data: [
78
+ { quarter: 'Q1', growth: 5 },
79
+ { quarter: 'Q2', growth: -3 },
80
+ { quarter: 'Q3', growth: 8 },
81
+ { quarter: 'Q4', growth: -1 },
82
+ ],
83
+ encoding: {
84
+ x: { field: 'quarter', type: 'nominal' },
85
+ y: { field: 'growth', type: 'quantitative' },
86
+ },
87
+ chrome: {},
88
+ annotations: [],
89
+ responsive: true,
90
+ theme: {},
91
+ darkMode: 'off',
92
+ labels: { density: 'auto', format: '' },
93
+ };
94
+ }
95
+
96
+ // ---------------------------------------------------------------------------
97
+ // computeColumnMarks tests
98
+ // ---------------------------------------------------------------------------
99
+
100
+ describe('computeColumnMarks', () => {
101
+ describe('simple columns', () => {
102
+ it('produces one RectMark per data row', () => {
103
+ const spec = makeSimpleColumnSpec();
104
+ const scales = computeScales(spec, chartArea, spec.data);
105
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
106
+
107
+ expect(marks).toHaveLength(4);
108
+ expect(marks.every((m) => m.type === 'rect')).toBe(true);
109
+ });
110
+
111
+ it('columns have positive width and height', () => {
112
+ const spec = makeSimpleColumnSpec();
113
+ const scales = computeScales(spec, chartArea, spec.data);
114
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
115
+
116
+ for (const mark of marks) {
117
+ expect(mark.width).toBeGreaterThan(0);
118
+ expect(mark.height).toBeGreaterThan(0);
119
+ }
120
+ });
121
+
122
+ it('taller columns correspond to larger values', () => {
123
+ const spec = makeSimpleColumnSpec();
124
+ const scales = computeScales(spec, chartArea, spec.data);
125
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
126
+
127
+ // Apr (200) should be taller than Feb (80)
128
+ const apr = marks.find((m) => m.aria.label.includes('Apr'))!;
129
+ const feb = marks.find((m) => m.aria.label.includes('Feb'))!;
130
+ expect(apr.height).toBeGreaterThan(feb.height);
131
+ });
132
+
133
+ it('higher values have lower y positions (SVG coordinates)', () => {
134
+ const spec = makeSimpleColumnSpec();
135
+ const scales = computeScales(spec, chartArea, spec.data);
136
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
137
+
138
+ const apr = marks.find((m) => m.aria.label.includes('Apr'))!;
139
+ const feb = marks.find((m) => m.aria.label.includes('Feb'))!;
140
+ // Apr (200) should start higher (lower y) than Feb (80)
141
+ expect(apr.y).toBeLessThan(feb.y);
142
+ });
143
+
144
+ it('each column has an aria label with category and value', () => {
145
+ const spec = makeSimpleColumnSpec();
146
+ const scales = computeScales(spec, chartArea, spec.data);
147
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
148
+
149
+ expect(marks[0].aria.label).toContain('Jan');
150
+ expect(marks[0].aria.label).toContain('120');
151
+ });
152
+ });
153
+
154
+ describe('stacked columns (stack: zero)', () => {
155
+ function makeStackedColumnSpec(): NormalizedChartSpec {
156
+ const spec = makeGroupedColumnSpec();
157
+ (spec.encoding.y as { stack?: string }).stack = 'zero';
158
+ return spec;
159
+ }
160
+
161
+ it('produces marks for all data rows', () => {
162
+ const spec = makeStackedColumnSpec();
163
+ const scales = computeScales(spec, chartArea, spec.data);
164
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
165
+
166
+ // 3 categories * 2 groups = 6
167
+ expect(marks).toHaveLength(6);
168
+ });
169
+
170
+ it('stacked segments within a category have different colors', () => {
171
+ const spec = makeStackedColumnSpec();
172
+ const scales = computeScales(spec, chartArea, spec.data);
173
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
174
+
175
+ const janMarks = marks.filter((m) => m.aria.label.includes('Jan'));
176
+ expect(janMarks).toHaveLength(2);
177
+ expect(janMarks[0].fill).not.toBe(janMarks[1].fill);
178
+ });
179
+
180
+ it('stacked columns within a category share the same x position', () => {
181
+ const spec = makeStackedColumnSpec();
182
+ const scales = computeScales(spec, chartArea, spec.data);
183
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
184
+
185
+ const janNorth = marks.find(
186
+ (m) => m.aria.label.includes('Jan') && m.aria.label.includes('North'),
187
+ )!;
188
+ const janSouth = marks.find(
189
+ (m) => m.aria.label.includes('Jan') && m.aria.label.includes('South'),
190
+ )!;
191
+
192
+ // Stacked columns share the same x position
193
+ expect(janNorth.x).toBe(janSouth.x);
194
+ // But have different y positions (stacked vertically)
195
+ expect(janNorth.y).not.toBe(janSouth.y);
196
+ });
197
+ });
198
+
199
+ describe('grouped columns (stack: null)', () => {
200
+ it('produces marks for all data rows', () => {
201
+ const spec = makeGroupedColumnSpec();
202
+ const scales = computeScales(spec, chartArea, spec.data);
203
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
204
+
205
+ expect(marks).toHaveLength(6);
206
+ });
207
+
208
+ it('grouped columns within a category have different x positions', () => {
209
+ const spec = makeGroupedColumnSpec();
210
+ const scales = computeScales(spec, chartArea, spec.data);
211
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
212
+
213
+ const janNorth = marks.find(
214
+ (m) => m.aria.label.includes('Jan') && m.aria.label.includes('North'),
215
+ )!;
216
+ const janSouth = marks.find(
217
+ (m) => m.aria.label.includes('Jan') && m.aria.label.includes('South'),
218
+ )!;
219
+
220
+ expect(janNorth.x).not.toBe(janSouth.x);
221
+ });
222
+
223
+ it('grouped columns have subdivided widths (vs stacked full bandwidth)', () => {
224
+ const groupedSpec = makeGroupedColumnSpec();
225
+ const groupedScales = computeScales(groupedSpec, chartArea, groupedSpec.data);
226
+ const groupedMarks = computeColumnMarks(groupedSpec, groupedScales, chartArea, fullStrategy);
227
+
228
+ // With 2 groups, each sub-column should be narrower than full bandwidth
229
+ const stackedSpec = makeGroupedColumnSpec();
230
+ (stackedSpec.encoding.y as { stack?: string }).stack = 'zero';
231
+ const stackedScales = computeScales(stackedSpec, chartArea, stackedSpec.data);
232
+ const stackedMarks = computeColumnMarks(stackedSpec, stackedScales, chartArea, fullStrategy);
233
+
234
+ expect(groupedMarks[0].width).toBeLessThan(stackedMarks[0].width);
235
+ expect(groupedMarks[0].width).toBeGreaterThan(0);
236
+ });
237
+
238
+ it('grouped columns have cornerRadius 2 and no stackGroup', () => {
239
+ const spec = makeGroupedColumnSpec();
240
+ const scales = computeScales(spec, chartArea, spec.data);
241
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
242
+
243
+ for (const mark of marks) {
244
+ expect(mark.cornerRadius).toBe(2);
245
+ expect(mark.stackGroup).toBeUndefined();
246
+ }
247
+ });
248
+
249
+ it('scale domain covers max individual value, not stacked sum', () => {
250
+ const spec = makeGroupedColumnSpec();
251
+ const scales = computeScales(spec, chartArea, spec.data);
252
+
253
+ // Max individual value is 150 (Mar North), not 280 (Mar stacked sum)
254
+ const yScale = scales.y!.scale;
255
+ const domain = yScale.domain() as number[];
256
+ expect(domain[1]).toBeLessThanOrEqual(170); // some nice rounding above 150
257
+ });
258
+ });
259
+
260
+ describe('negative values', () => {
261
+ it('negative columns extend downward from baseline', () => {
262
+ const spec = makeNegativeColumnSpec();
263
+ const scales = computeScales(spec, chartArea, spec.data);
264
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
265
+
266
+ const q2 = marks.find((m) => m.aria.label.includes('Q2'))!;
267
+ const q1 = marks.find((m) => m.aria.label.includes('Q1'))!;
268
+
269
+ // Negative value column should start at a higher y (lower on page = baseline)
270
+ expect(q2.y).toBeGreaterThan(q1.y);
271
+ });
272
+
273
+ it('all columns have positive height', () => {
274
+ const spec = makeNegativeColumnSpec();
275
+ const scales = computeScales(spec, chartArea, spec.data);
276
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
277
+
278
+ for (const mark of marks) {
279
+ expect(mark.height).toBeGreaterThan(0);
280
+ }
281
+ });
282
+ });
283
+
284
+ describe('edge cases', () => {
285
+ it('returns empty array when no y encoding', () => {
286
+ const spec: NormalizedChartSpec = {
287
+ markType: 'bar',
288
+ markDef: { type: 'bar', orient: 'vertical' },
289
+ data: [{ month: 'Jan', sales: 100 }],
290
+ encoding: {
291
+ x: { field: 'month', type: 'nominal' },
292
+ },
293
+ chrome: {},
294
+ annotations: [],
295
+ responsive: true,
296
+ theme: {},
297
+ darkMode: 'off',
298
+ labels: { density: 'auto', format: '' },
299
+ };
300
+ const scales = computeScales(spec, chartArea, spec.data);
301
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
302
+ expect(marks).toHaveLength(0);
303
+ });
304
+
305
+ it('returns empty array for empty data', () => {
306
+ const spec: NormalizedChartSpec = {
307
+ markType: 'bar',
308
+ markDef: { type: 'bar', orient: 'vertical' },
309
+ data: [],
310
+ encoding: {
311
+ x: { field: 'month', type: 'nominal' },
312
+ y: { field: 'sales', type: 'quantitative' },
313
+ },
314
+ chrome: {},
315
+ annotations: [],
316
+ responsive: true,
317
+ theme: {},
318
+ darkMode: 'off',
319
+ labels: { density: 'auto', format: '' },
320
+ };
321
+ const scales = computeScales(spec, chartArea, spec.data);
322
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
323
+ expect(marks).toHaveLength(0);
324
+ });
325
+ });
326
+ });
327
+
328
+ // ---------------------------------------------------------------------------
329
+ // computeColumnLabels tests
330
+ // ---------------------------------------------------------------------------
331
+
332
+ describe('computeColumnLabels', () => {
333
+ it('produces one label per column mark', () => {
334
+ const spec = makeSimpleColumnSpec();
335
+ const scales = computeScales(spec, chartArea, spec.data);
336
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
337
+ const labels = computeColumnLabels(marks, chartArea);
338
+
339
+ expect(labels).toHaveLength(marks.length);
340
+ });
341
+
342
+ it('labels contain the value text', () => {
343
+ const spec = makeSimpleColumnSpec();
344
+ const scales = computeScales(spec, chartArea, spec.data);
345
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
346
+ const labels = computeColumnLabels(marks, chartArea);
347
+
348
+ const texts = labels.map((l) => l.text);
349
+ expect(texts).toContain('120');
350
+ expect(texts).toContain('200');
351
+ });
352
+
353
+ it('applies d3 label format string', () => {
354
+ const spec = makeSimpleColumnSpec();
355
+ const scales = computeScales(spec, chartArea, spec.data);
356
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
357
+ const labels = computeColumnLabels(marks, chartArea, 'auto', buildD3Formatter('$,.0f'));
358
+
359
+ const texts = labels.map((l) => l.text);
360
+ expect(texts).toContain('$120');
361
+ expect(texts).toContain('$200');
362
+ });
363
+
364
+ it('applies format with trailing zero trim (~)', () => {
365
+ const spec: NormalizedChartSpec = {
366
+ markType: 'bar',
367
+ markDef: { type: 'bar', orient: 'vertical' },
368
+ data: [
369
+ { company: 'A', cap: 3.1 },
370
+ { company: 'B', cap: 2.85 },
371
+ ],
372
+ encoding: {
373
+ x: { field: 'company', type: 'nominal' },
374
+ y: { field: 'cap', type: 'quantitative' },
375
+ },
376
+ chrome: {},
377
+ annotations: [],
378
+ responsive: true,
379
+ theme: {},
380
+ darkMode: 'off',
381
+ labels: { density: 'all', format: '$,.2~f' },
382
+ };
383
+ const scales = computeScales(spec, chartArea, spec.data);
384
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
385
+ const labels = computeColumnLabels(marks, chartArea, 'all', buildD3Formatter('$,.2~f'));
386
+
387
+ const texts = labels.map((l) => l.text);
388
+ expect(texts).toContain('$3.1');
389
+ expect(texts).toContain('$2.85');
390
+ });
391
+
392
+ it('applies format with literal alpha suffix (e.g. "T")', () => {
393
+ const spec: NormalizedChartSpec = {
394
+ markType: 'bar',
395
+ markDef: { type: 'bar', orient: 'vertical' },
396
+ data: [
397
+ { company: 'Apple', cap: 3.75 },
398
+ { company: 'Meta', cap: 1.63 },
399
+ ],
400
+ encoding: {
401
+ x: { field: 'company', type: 'nominal' },
402
+ y: { field: 'cap', type: 'quantitative' },
403
+ },
404
+ chrome: {},
405
+ annotations: [],
406
+ responsive: true,
407
+ theme: {},
408
+ darkMode: 'off',
409
+ labels: { density: 'all', format: '$,.2~fT' },
410
+ };
411
+ const scales = computeScales(spec, chartArea, spec.data);
412
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
413
+ const labels = computeColumnLabels(marks, chartArea, 'all', buildD3Formatter('$,.2~fT'));
414
+
415
+ const texts = labels.map((l) => l.text);
416
+ expect(texts).toContain('$3.75T');
417
+ expect(texts).toContain('$1.63T');
418
+ });
419
+
420
+ it('applies format with non-alpha suffix (e.g. "%")', () => {
421
+ const spec = makeSimpleColumnSpec();
422
+ const scales = computeScales(spec, chartArea, spec.data);
423
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
424
+ const labels = computeColumnLabels(marks, chartArea, 'auto', buildD3Formatter('.0f%'));
425
+
426
+ const texts = labels.map((l) => l.text);
427
+ expect(texts).toContain('120%');
428
+ expect(texts).toContain('200%');
429
+ });
430
+ });
431
+
432
+ // ---------------------------------------------------------------------------
433
+ // Stack mode tests
434
+ // ---------------------------------------------------------------------------
435
+
436
+ describe('stack modes', () => {
437
+ function makeStackedSpec(
438
+ stackMode: boolean | 'zero' | 'normalize' | 'center' | null | undefined,
439
+ ): NormalizedChartSpec {
440
+ return {
441
+ markType: 'bar',
442
+ markDef: { type: 'bar', orient: 'vertical' },
443
+ data: [
444
+ { cat: 'A', val: 30, grp: 'X' },
445
+ { cat: 'A', val: 70, grp: 'Y' },
446
+ { cat: 'B', val: 40, grp: 'X' },
447
+ { cat: 'B', val: 60, grp: 'Y' },
448
+ ],
449
+ encoding: {
450
+ x: { field: 'cat', type: 'nominal' },
451
+ y: { field: 'val', type: 'quantitative', stack: stackMode },
452
+ color: { field: 'grp', type: 'nominal' },
453
+ },
454
+ chrome: {},
455
+ annotations: [],
456
+ responsive: true,
457
+ theme: {},
458
+ darkMode: 'off',
459
+ labels: { density: 'auto', format: '' },
460
+ };
461
+ }
462
+
463
+ it('normalize: produces marks whose stacked fractions sum to ~1 per category', () => {
464
+ const spec = makeStackedSpec('normalize');
465
+ const scales = computeScales(spec, chartArea, spec.data);
466
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
467
+
468
+ expect(marks.length).toBe(4);
469
+
470
+ // Group by category (stackGroup) and verify normalized heights
471
+ const catA = marks.filter((m) => m.stackGroup === 'A');
472
+ const catB = marks.filter((m) => m.stackGroup === 'B');
473
+ expect(catA).toHaveLength(2);
474
+ expect(catB).toHaveLength(2);
475
+
476
+ // The y scale domain is [0, 1] for normalize. Verify marks don't overlap
477
+ // and each category's marks span the full [0, 1] range when mapped back.
478
+ // Category A: 30/(30+70)=0.3, 70/(30+70)=0.7
479
+ // Category B: 40/(40+60)=0.4, 60/(40+60)=0.6
480
+ // All marks should have non-zero height
481
+ for (const mark of marks) {
482
+ expect(mark.height).toBeGreaterThan(0);
483
+ }
484
+ });
485
+
486
+ it('center: produces marks with symmetric offsets around zero', () => {
487
+ const spec = makeStackedSpec('center');
488
+ const scales = computeScales(spec, chartArea, spec.data);
489
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
490
+
491
+ expect(marks.length).toBe(4);
492
+
493
+ // All marks should have non-zero height
494
+ for (const mark of marks) {
495
+ expect(mark.height).toBeGreaterThan(0);
496
+ }
497
+ });
498
+
499
+ it('existing zero mode still works correctly', () => {
500
+ const spec = makeStackedSpec('zero');
501
+ const scales = computeScales(spec, chartArea, spec.data);
502
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
503
+
504
+ expect(marks.length).toBe(4);
505
+ // All stacked marks should have stackGroup set
506
+ for (const mark of marks) {
507
+ expect(mark.stackGroup).toBeDefined();
508
+ }
509
+ });
510
+
511
+ it('null/false disables stacking (grouped mode)', () => {
512
+ const specNull = makeStackedSpec(null);
513
+ const scalesNull = computeScales(specNull, chartArea, specNull.data);
514
+ const marksNull = computeColumnMarks(specNull, scalesNull, chartArea, fullStrategy);
515
+
516
+ // Grouped mode: no stackGroup
517
+ for (const mark of marksNull) {
518
+ expect(mark.stackGroup).toBeUndefined();
519
+ }
520
+
521
+ const specFalse = makeStackedSpec(false);
522
+ const scalesFalse = computeScales(specFalse, chartArea, specFalse.data);
523
+ const marksFalse = computeColumnMarks(specFalse, scalesFalse, chartArea, fullStrategy);
524
+
525
+ for (const mark of marksFalse) {
526
+ expect(mark.stackGroup).toBeUndefined();
527
+ }
528
+ });
529
+
530
+ it('undefined (omitted) stacks by default (v8 VL-aligned default)', () => {
531
+ const spec = makeStackedSpec(undefined);
532
+ const scales = computeScales(spec, chartArea, spec.data);
533
+ const marks = computeColumnMarks(spec, scales, chartArea, fullStrategy);
534
+
535
+ expect(marks.length).toBe(4);
536
+ for (const mark of marks) {
537
+ expect(mark.stackGroup).toBeDefined();
538
+ }
539
+ });
540
+ });