@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.
Files changed (271) hide show
  1. package/dist/index.d.ts +149 -12
  2. package/dist/index.js +894 -171
  3. package/dist/index.js.map +1 -1
  4. package/package.json +3 -4
  5. package/src/__test-fixtures__/specs.ts +0 -182
  6. package/src/__tests__/__snapshots__/compile-snapshot.test.ts.snap +0 -2339
  7. package/src/__tests__/annotation-behavior.test.ts +0 -852
  8. package/src/__tests__/axes.test.ts +0 -1579
  9. package/src/__tests__/compile-animation.test.ts +0 -161
  10. package/src/__tests__/compile-chart.test.ts +0 -1303
  11. package/src/__tests__/compile-footnotes.test.ts +0 -277
  12. package/src/__tests__/compile-layer.test.ts +0 -969
  13. package/src/__tests__/compile-snapshot.test.ts +0 -156
  14. package/src/__tests__/compound-labels.test.ts +0 -147
  15. package/src/__tests__/dimensions.test.ts +0 -918
  16. package/src/__tests__/encoding-sugar.test.ts +0 -390
  17. package/src/__tests__/facet.test.ts +0 -215
  18. package/src/__tests__/helpers/invariants.ts +0 -299
  19. package/src/__tests__/known-bugs.test.ts +0 -566
  20. package/src/__tests__/layout-fuzz.test.ts +0 -358
  21. package/src/__tests__/layout-invariants.test.ts +0 -535
  22. package/src/__tests__/legend.test.ts +0 -721
  23. package/src/__tests__/mark-keys.test.ts +0 -294
  24. package/src/__tests__/ranking-recipes.test.ts +0 -260
  25. package/src/__tests__/rotation.test.ts +0 -83
  26. package/src/__tests__/scales.test.ts +0 -323
  27. package/src/__tests__/series-search.test.ts +0 -92
  28. package/src/__tests__/spec-sugar.test.ts +0 -801
  29. package/src/__tests__/you-draw-it.test.ts +0 -160
  30. package/src/annotations/__tests__/compute.test.ts +0 -1672
  31. package/src/annotations/__tests__/geometry.test.ts +0 -66
  32. package/src/annotations/__tests__/placement-fuzz.test.ts +0 -256
  33. package/src/annotations/__tests__/placement-invariants.test.ts +0 -345
  34. package/src/annotations/__tests__/placement.test.ts +0 -316
  35. package/src/annotations/__tests__/resolve-text.test.ts +0 -788
  36. package/src/annotations/__tests__/rich-text.test.ts +0 -91
  37. package/src/annotations/__tests__/thinning.test.ts +0 -285
  38. package/src/annotations/collisions.ts +0 -326
  39. package/src/annotations/compute.ts +0 -333
  40. package/src/annotations/constants.ts +0 -162
  41. package/src/annotations/geometry.ts +0 -376
  42. package/src/annotations/move.ts +0 -135
  43. package/src/annotations/placement.ts +0 -636
  44. package/src/annotations/position.ts +0 -131
  45. package/src/annotations/resolve-range.ts +0 -117
  46. package/src/annotations/resolve-refline.ts +0 -157
  47. package/src/annotations/resolve-text.ts +0 -545
  48. package/src/annotations/rich-text.ts +0 -65
  49. package/src/annotations/thinning.ts +0 -190
  50. package/src/barlist/__tests__/compile-barlist.test.ts +0 -200
  51. package/src/barlist/compile-barlist.ts +0 -391
  52. package/src/barlist/types.ts +0 -28
  53. package/src/charts/__tests__/post-process.test.ts +0 -237
  54. package/src/charts/__tests__/registry.test.ts +0 -116
  55. package/src/charts/__tests__/utils.test.ts +0 -195
  56. package/src/charts/_shared/__tests__/density-filter.test.ts +0 -32
  57. package/src/charts/_shared/density-filter.ts +0 -26
  58. package/src/charts/_shared/format-label-value.ts +0 -16
  59. package/src/charts/bar/__tests__/compute.test.ts +0 -934
  60. package/src/charts/bar/__tests__/gradient-orient.test.ts +0 -127
  61. package/src/charts/bar/__tests__/labels.test.ts +0 -292
  62. package/src/charts/bar/compute.ts +0 -539
  63. package/src/charts/bar/index.ts +0 -60
  64. package/src/charts/bar/labels.ts +0 -311
  65. package/src/charts/beeswarm/__tests__/compute.test.ts +0 -280
  66. package/src/charts/beeswarm/__tests__/dodge.test.ts +0 -148
  67. package/src/charts/beeswarm/compute.ts +0 -203
  68. package/src/charts/beeswarm/dodge.ts +0 -155
  69. package/src/charts/beeswarm/index.ts +0 -32
  70. package/src/charts/builtin.ts +0 -75
  71. package/src/charts/calendar/__tests__/compute.test.ts +0 -417
  72. package/src/charts/calendar/compute.ts +0 -319
  73. package/src/charts/calendar/index.ts +0 -32
  74. package/src/charts/column/__tests__/compute.test.ts +0 -540
  75. package/src/charts/column/__tests__/labels.test.ts +0 -179
  76. package/src/charts/column/compute.ts +0 -493
  77. package/src/charts/column/index.ts +0 -53
  78. package/src/charts/column/labels.ts +0 -229
  79. package/src/charts/dot/__tests__/compute.test.ts +0 -380
  80. package/src/charts/dot/__tests__/labels.test.ts +0 -98
  81. package/src/charts/dot/compute.ts +0 -285
  82. package/src/charts/dot/index.ts +0 -62
  83. package/src/charts/dot/labels.ts +0 -121
  84. package/src/charts/line/__tests__/compute.test.ts +0 -1194
  85. package/src/charts/line/__tests__/curves.test.ts +0 -51
  86. package/src/charts/line/__tests__/labels.test.ts +0 -94
  87. package/src/charts/line/__tests__/mark-options.test.ts +0 -175
  88. package/src/charts/line/area.ts +0 -484
  89. package/src/charts/line/compute.ts +0 -356
  90. package/src/charts/line/curves.ts +0 -85
  91. package/src/charts/line/index.ts +0 -176
  92. package/src/charts/line/labels.ts +0 -177
  93. package/src/charts/parliament/__tests__/compute.test.ts +0 -411
  94. package/src/charts/parliament/compute.ts +0 -313
  95. package/src/charts/parliament/index.ts +0 -32
  96. package/src/charts/pie/__tests__/compute.test.ts +0 -384
  97. package/src/charts/pie/__tests__/labels.test.ts +0 -132
  98. package/src/charts/pie/compute.ts +0 -324
  99. package/src/charts/pie/index.ts +0 -49
  100. package/src/charts/pie/labels.ts +0 -143
  101. package/src/charts/post-process.ts +0 -229
  102. package/src/charts/range/__tests__/compute.test.ts +0 -486
  103. package/src/charts/range/compute.ts +0 -273
  104. package/src/charts/range/index.ts +0 -82
  105. package/src/charts/range/labels.ts +0 -169
  106. package/src/charts/rect/__tests__/compute.test.ts +0 -120
  107. package/src/charts/rect/index.ts +0 -127
  108. package/src/charts/registry.ts +0 -72
  109. package/src/charts/rule/index.ts +0 -139
  110. package/src/charts/scatter/__tests__/compute.test.ts +0 -488
  111. package/src/charts/scatter/__tests__/trendline.test.ts +0 -191
  112. package/src/charts/scatter/compute.ts +0 -192
  113. package/src/charts/scatter/index.ts +0 -46
  114. package/src/charts/scatter/trendline.ts +0 -100
  115. package/src/charts/text/__tests__/compute.test.ts +0 -307
  116. package/src/charts/text/index.ts +0 -141
  117. package/src/charts/tick/__tests__/compute.test.ts +0 -75
  118. package/src/charts/tick/index.ts +0 -99
  119. package/src/charts/utils.ts +0 -170
  120. package/src/charts/waffle/__tests__/compute.test.ts +0 -305
  121. package/src/charts/waffle/compute.ts +0 -197
  122. package/src/charts/waffle/index.ts +0 -31
  123. package/src/compile/__tests__/color-scale-range.test.ts +0 -158
  124. package/src/compile/__tests__/color-scale-reverse.test.ts +0 -89
  125. package/src/compile/__tests__/data-clip.test.ts +0 -127
  126. package/src/compile/__tests__/fill-patterns.test.ts +0 -154
  127. package/src/compile/__tests__/size-scale.test.ts +0 -108
  128. package/src/compile/__tests__/watermark-obstacle.test.ts +0 -93
  129. package/src/compile/color-scale-range.ts +0 -101
  130. package/src/compile/data-clip.ts +0 -48
  131. package/src/compile/fill-patterns.ts +0 -74
  132. package/src/compile/layer.ts +0 -626
  133. package/src/compile/size-scale.ts +0 -141
  134. package/src/compile/spec-sugar.ts +0 -758
  135. package/src/compile/watermark-obstacle.ts +0 -54
  136. package/src/compile/you-draw-it.ts +0 -123
  137. package/src/compile.ts +0 -1450
  138. package/src/compiler/__tests__/a11y-warnings.test.ts +0 -148
  139. package/src/compiler/__tests__/animation.test.ts +0 -177
  140. package/src/compiler/__tests__/compile.test.ts +0 -87
  141. package/src/compiler/__tests__/fixtures/vl-parity/axis-null.json +0 -19
  142. package/src/compiler/__tests__/fixtures/vl-parity/color-value.json +0 -20
  143. package/src/compiler/__tests__/fixtures/vl-parity/count-aggregate.json +0 -22
  144. package/src/compiler/__tests__/fixtures/vl-parity/data-values.json +0 -19
  145. package/src/compiler/__tests__/fixtures/vl-parity/layer-axis-null.json +0 -14
  146. package/src/compiler/__tests__/fixtures/vl-parity/layer-count.json +0 -34
  147. package/src/compiler/__tests__/fixtures/vl-parity/layer-data-values.json +0 -22
  148. package/src/compiler/__tests__/fixtures/vl-parity/layer-legend-null.json +0 -41
  149. package/src/compiler/__tests__/fixtures/vl-parity/layer-scheme.json +0 -38
  150. package/src/compiler/__tests__/fixtures/vl-parity/layer-sort.json +0 -28
  151. package/src/compiler/__tests__/fixtures/vl-parity/layer-theta.json +0 -28
  152. package/src/compiler/__tests__/fixtures/vl-parity/layer-title.json +0 -30
  153. package/src/compiler/__tests__/fixtures/vl-parity/layer-value-def.json +0 -29
  154. package/src/compiler/__tests__/fixtures/vl-parity/legend-null.json +0 -32
  155. package/src/compiler/__tests__/fixtures/vl-parity/scheme.json +0 -25
  156. package/src/compiler/__tests__/fixtures/vl-parity/sort-by-value.json +0 -19
  157. package/src/compiler/__tests__/fixtures/vl-parity/theta-arc.json +0 -19
  158. package/src/compiler/__tests__/fixtures/vl-parity/title-subtitle.json +0 -21
  159. package/src/compiler/__tests__/fixtures/vl-parity/width-height.json +0 -23
  160. package/src/compiler/__tests__/keys.test.ts +0 -83
  161. package/src/compiler/__tests__/normalize-highlight.test.ts +0 -103
  162. package/src/compiler/__tests__/normalize.test.ts +0 -399
  163. package/src/compiler/__tests__/sparkline-defaults.test.ts +0 -153
  164. package/src/compiler/__tests__/validate-did-you-mean.test.ts +0 -170
  165. package/src/compiler/__tests__/validate-highlight.test.ts +0 -112
  166. package/src/compiler/__tests__/validate-near-miss.test.ts +0 -251
  167. package/src/compiler/__tests__/validate.test.ts +0 -629
  168. package/src/compiler/__tests__/vl-parity.test.ts +0 -68
  169. package/src/compiler/a11y-warnings.ts +0 -110
  170. package/src/compiler/animation.ts +0 -142
  171. package/src/compiler/index.ts +0 -47
  172. package/src/compiler/keys.ts +0 -55
  173. package/src/compiler/normalize.ts +0 -734
  174. package/src/compiler/sparkline-defaults.ts +0 -138
  175. package/src/compiler/types.ts +0 -241
  176. package/src/compiler/validate.ts +0 -2041
  177. package/src/endpoint-labels/__tests__/compute.test.ts +0 -587
  178. package/src/endpoint-labels/compute.ts +0 -621
  179. package/src/endpoint-labels/constants.ts +0 -54
  180. package/src/endpoint-labels/format.ts +0 -60
  181. package/src/endpoint-labels/predict.ts +0 -139
  182. package/src/format/__tests__/field-format.test.ts +0 -164
  183. package/src/format/field-format.ts +0 -27
  184. package/src/geo/__tests__/compile-map.test.ts +0 -1004
  185. package/src/geo/compile-map.ts +0 -1341
  186. package/src/geo/join.ts +0 -115
  187. package/src/geo/projections.ts +0 -45
  188. package/src/geo/types.ts +0 -26
  189. package/src/graphs/__tests__/community.test.ts +0 -228
  190. package/src/graphs/__tests__/compile-graph.test.ts +0 -396
  191. package/src/graphs/__tests__/encoding.test.ts +0 -415
  192. package/src/graphs/community.ts +0 -92
  193. package/src/graphs/compile-graph.ts +0 -329
  194. package/src/graphs/encoding.ts +0 -356
  195. package/src/graphs/types.ts +0 -106
  196. package/src/index.ts +0 -177
  197. package/src/layout/__tests__/metrics.test.ts +0 -97
  198. package/src/layout/__tests__/scales-highlight.test.ts +0 -192
  199. package/src/layout/axes/rotation.ts +0 -152
  200. package/src/layout/axes/thinning.ts +0 -98
  201. package/src/layout/axes/ticks.ts +0 -409
  202. package/src/layout/axes.ts +0 -757
  203. package/src/layout/dimensions.ts +0 -912
  204. package/src/layout/facet.ts +0 -91
  205. package/src/layout/gridlines.ts +0 -84
  206. package/src/layout/metrics.ts +0 -132
  207. package/src/layout/plan.ts +0 -631
  208. package/src/layout/scales.ts +0 -1053
  209. package/src/layout/shared.ts +0 -44
  210. package/src/legend/__tests__/continuous.test.ts +0 -318
  211. package/src/legend/__tests__/redundancy.test.ts +0 -108
  212. package/src/legend/__tests__/size.test.ts +0 -348
  213. package/src/legend/__tests__/suppression.test.ts +0 -382
  214. package/src/legend/compute.ts +0 -747
  215. package/src/legend/continuous.ts +0 -327
  216. package/src/legend/size.ts +0 -407
  217. package/src/legend/suppression.ts +0 -226
  218. package/src/legend/wrap.ts +0 -117
  219. package/src/sankey/__tests__/compile-sankey.test.ts +0 -458
  220. package/src/sankey/__tests__/layout.test.ts +0 -165
  221. package/src/sankey/__tests__/node-label-wrap.test.ts +0 -114
  222. package/src/sankey/__tests__/node-sort.test.ts +0 -45
  223. package/src/sankey/compile-sankey.ts +0 -706
  224. package/src/sankey/layout.ts +0 -184
  225. package/src/sankey/types.ts +0 -41
  226. package/src/tables/__tests__/bar-column.test.ts +0 -147
  227. package/src/tables/__tests__/category-colors.test.ts +0 -230
  228. package/src/tables/__tests__/compile-table.test.ts +0 -236
  229. package/src/tables/__tests__/format-cells.test.ts +0 -143
  230. package/src/tables/__tests__/heatmap.test.ts +0 -148
  231. package/src/tables/__tests__/pagination.test.ts +0 -78
  232. package/src/tables/__tests__/search.test.ts +0 -94
  233. package/src/tables/__tests__/sort.test.ts +0 -107
  234. package/src/tables/__tests__/sparkline.test.ts +0 -122
  235. package/src/tables/bar-column.ts +0 -94
  236. package/src/tables/category-colors.ts +0 -78
  237. package/src/tables/compile-table.ts +0 -427
  238. package/src/tables/format-cells.ts +0 -115
  239. package/src/tables/heatmap.ts +0 -141
  240. package/src/tables/pagination.ts +0 -46
  241. package/src/tables/search.ts +0 -66
  242. package/src/tables/sort.ts +0 -69
  243. package/src/tables/sparkline.ts +0 -113
  244. package/src/tables/utils.ts +0 -16
  245. package/src/tilemap/__tests__/compile-tilemap.test.ts +0 -576
  246. package/src/tilemap/compile-tilemap.ts +0 -755
  247. package/src/tilemap/layout.ts +0 -172
  248. package/src/tilemap/types.ts +0 -33
  249. package/src/tooltips/__tests__/compute.test.ts +0 -582
  250. package/src/tooltips/compute.ts +0 -700
  251. package/src/transforms/__tests__/aggregate.test.ts +0 -159
  252. package/src/transforms/__tests__/bin.test.ts +0 -88
  253. package/src/transforms/__tests__/calculate.test.ts +0 -146
  254. package/src/transforms/__tests__/conditional.test.ts +0 -127
  255. package/src/transforms/__tests__/filter-relative.test.ts +0 -202
  256. package/src/transforms/__tests__/filter.test.ts +0 -59
  257. package/src/transforms/__tests__/fold.test.ts +0 -79
  258. package/src/transforms/__tests__/index.test.ts +0 -93
  259. package/src/transforms/__tests__/predicates.test.ts +0 -193
  260. package/src/transforms/__tests__/timeunit.test.ts +0 -129
  261. package/src/transforms/__tests__/window.test.ts +0 -286
  262. package/src/transforms/aggregate.ts +0 -131
  263. package/src/transforms/bin.ts +0 -87
  264. package/src/transforms/calculate.ts +0 -60
  265. package/src/transforms/conditional.ts +0 -46
  266. package/src/transforms/filter.ts +0 -122
  267. package/src/transforms/fold.ts +0 -49
  268. package/src/transforms/index.ts +0 -60
  269. package/src/transforms/predicates.ts +0 -123
  270. package/src/transforms/timeunit.ts +0 -88
  271. package/src/transforms/window.ts +0 -185
@@ -1,313 +0,0 @@
1
- /**
2
- * Parliament (hemicycle) chart mark computation.
3
- *
4
- * Seat dots packed into concentric semicircular arcs, party-grouped left to
5
- * right, with a majority-threshold line. Election/legislature results are a
6
- * part-to-whole count (seats per party) that the standard bar/pie machinery
7
- * can't lay out, so this module owns the seat-packing geometry.
8
- *
9
- * Emits existing mark types (no bespoke layout): one `PointMark` per seat,
10
- * one `RuleMarkLayout` for the majority line, and one `TextMarkLayout` for the
11
- * "N to win" label. Riding the existing chart pipeline gets scales, legend,
12
- * tooltips, a11y, dark mode, and animation for free (parliament is axisless,
13
- * so `isAxislessMark` skips axes/gridlines just like arc and waffle).
14
- *
15
- * Seat layout algorithm (deterministic, published approach): pick the smallest
16
- * number of concentric rows R such that N seats fit given a minimum seat-to-row
17
- * gap ratio, distribute seats across rows proportional to each row's arc
18
- * length (outer rows hold more), place each row's seats at even angles across
19
- * the top semicircle (180deg on the left to 0deg on the right), then sort all
20
- * seats left-to-right and assign parties to consecutive blocks in data order.
21
- */
22
-
23
- import type {
24
- DataRow,
25
- Encoding,
26
- LayoutStrategy,
27
- Mark,
28
- MarkAria,
29
- PointMark,
30
- Rect,
31
- ResolvedTheme,
32
- RuleMarkLayout,
33
- TextMarkLayout,
34
- } from '@opendata-ai/openchart-core';
35
- import { formatPercent } from '@opendata-ai/openchart-core';
36
-
37
- import { serializeKeyValue } from '../../compiler/keys';
38
- import type { NormalizedChartSpec } from '../../compiler/types';
39
- import type { ResolvedScales } from '../../layout/scales';
40
- import { getColor } from '../utils';
41
-
42
- // ---------------------------------------------------------------------------
43
- // Constants
44
- // ---------------------------------------------------------------------------
45
-
46
- /** Fraction of the seat pitch used as the gap between adjacent seat dots. */
47
- const SEAT_GAP_FRACTION = 0.4;
48
-
49
- /** Inner-radius fraction of the outer radius (the hollow center of the arc). */
50
- const INNER_RADIUS_FRACTION = 0.4;
51
-
52
- /** Maximum number of concentric rows we will pack into. */
53
- const MAX_ROWS = 40;
54
-
55
- // ---------------------------------------------------------------------------
56
- // Seat-packing geometry
57
- // ---------------------------------------------------------------------------
58
-
59
- /** A single packed seat position (before party assignment). */
60
- interface SeatSlot {
61
- /** Angle in radians measured across the top semicircle (PI = left, 0 = right). */
62
- angle: number;
63
- /** Radius from the hemicycle center. */
64
- radius: number;
65
- /** Row index (0 = innermost). */
66
- row: number;
67
- }
68
-
69
- /**
70
- * Choose the number of concentric rows for `seats` seats.
71
- *
72
- * More seats need more rows to stay legible. We grow the row count until the
73
- * seats-per-innermost-row stays under a density cap derived from the ring
74
- * geometry, capped at MAX_ROWS. Deterministic: same seat count always yields
75
- * the same row count.
76
- */
77
- function chooseRowCount(seats: number): number {
78
- if (seats <= 0) return 0;
79
- // The classic hemicycle heuristic: rows grow ~ with sqrt of seat count so
80
- // dot size stays roughly constant as the assembly grows.
81
- const rows = Math.max(1, Math.round(Math.sqrt(seats / 3)));
82
- return Math.min(MAX_ROWS, rows);
83
- }
84
-
85
- /**
86
- * Pack `seats` seat slots into `rows` concentric semicircular rows.
87
- *
88
- * Seats are distributed across rows proportional to each row's radius (arc
89
- * length), then placed at even angular steps across the top semicircle. The
90
- * returned slots are sorted left-to-right (descending angle) so party blocks
91
- * assign to contiguous seats.
92
- */
93
- function packSeats(seats: number, rows: number): SeatSlot[] {
94
- if (seats <= 0 || rows <= 0) return [];
95
-
96
- // Row radii are evenly spaced from inner to outer in normalized [0,1] units.
97
- // With a single row, place it midway between inner and outer.
98
- const rowRadii: number[] = [];
99
- for (let i = 0; i < rows; i++) {
100
- const t = rows === 1 ? 0.5 : i / (rows - 1);
101
- rowRadii.push(INNER_RADIUS_FRACTION + t * (1 - INNER_RADIUS_FRACTION));
102
- }
103
-
104
- // Distribute seats across rows proportional to radius (arc length), using
105
- // largest-remainder so the counts sum exactly to `seats`.
106
- const radiusSum = rowRadii.reduce((s, r) => s + r, 0);
107
- const rawCounts = rowRadii.map((r) => (r / radiusSum) * seats);
108
- const rowCounts = rawCounts.map((c) => Math.floor(c));
109
- let allocated = rowCounts.reduce((s, c) => s + c, 0);
110
- const remainders = rawCounts
111
- .map((c, i) => ({ rem: c - Math.floor(c), i }))
112
- .sort((a, b) => b.rem - a.rem || a.i - b.i);
113
- for (let k = 0; allocated < seats; k++, allocated++) {
114
- rowCounts[remainders[k % remainders.length].i] += 1;
115
- }
116
-
117
- const slots: SeatSlot[] = [];
118
- for (let row = 0; row < rows; row++) {
119
- const count = rowCounts[row];
120
- if (count <= 0) continue;
121
- const radius = rowRadii[row];
122
- // Place seats at even angular steps from PI (left) to 0 (right). A single
123
- // seat in a row sits at the top (PI/2).
124
- for (let s = 0; s < count; s++) {
125
- const t = count === 1 ? 0.5 : s / (count - 1);
126
- const angle = Math.PI - t * Math.PI;
127
- slots.push({ angle, radius, row });
128
- }
129
- }
130
-
131
- // Sort left-to-right: descending angle (PI first), ties broken outer-row
132
- // first so blocks read cleanly top-to-bottom within a column.
133
- slots.sort((a, b) => b.angle - a.angle || b.radius - a.radius);
134
- return slots;
135
- }
136
-
137
- // ---------------------------------------------------------------------------
138
- // Majority computation
139
- // ---------------------------------------------------------------------------
140
-
141
- /** Resolve the majority-line seat threshold and label from the mark options. */
142
- function resolveMajority(
143
- totalSeats: number,
144
- majorityLine: boolean | { seats?: number; label?: string } | undefined,
145
- ): { seats: number; label: string } | null {
146
- if (majorityLine === false) return null;
147
- const defaultSeats = Math.floor(totalSeats / 2) + 1;
148
- if (majorityLine == null || majorityLine === true) {
149
- return { seats: defaultSeats, label: `${defaultSeats} to win` };
150
- }
151
- // An explicit `seats` must land inside the chamber (1..totalSeats). `{ seats: 0 }`
152
- // or a negative/oversized value would draw the majority marker off the seat
153
- // arc where it reads as nothing, so fall back to the default. The user-facing
154
- // warning for this fires once from normalize (warnParliamentMajorityRange),
155
- // which owns the shared warnings channel; this clamp is the defensive backstop
156
- // for direct compute calls.
157
- let seats = majorityLine.seats ?? defaultSeats;
158
- if (!Number.isFinite(seats) || seats < 1 || seats > totalSeats) {
159
- seats = defaultSeats;
160
- }
161
- return { seats, label: majorityLine.label ?? `${seats} to win` };
162
- }
163
-
164
- // ---------------------------------------------------------------------------
165
- // Public API
166
- // ---------------------------------------------------------------------------
167
-
168
- /**
169
- * Compute parliament (hemicycle) marks from a normalized chart spec.
170
- *
171
- * Categories (parties) come from the color field in data order (matching pie
172
- * and the legend); the seat count from the quantitative y channel (theta
173
- * pre-aliased to y). Emits seat PointMarks in left-to-right order (driving the
174
- * entrance-stagger sweep), then the majority RuleMark and its label TextMark.
175
- */
176
- export function computeParliamentMarks(
177
- spec: NormalizedChartSpec,
178
- scales: ResolvedScales,
179
- chartArea: Rect,
180
- _strategy: LayoutStrategy,
181
- theme: ResolvedTheme,
182
- ): Mark[] {
183
- const encoding = spec.encoding as Encoding;
184
-
185
- const valueChannel = encoding.y ?? encoding.x;
186
- const colorField = encoding.color && 'field' in encoding.color ? encoding.color.field : undefined;
187
- if (!valueChannel || !colorField) return [];
188
-
189
- // Aggregate seats by party, preserving data order (drives seat order, color
190
- // assignment, and the legend, which derives the same order from data).
191
- const partyTotals = new Map<string, number>();
192
- const partyRows = new Map<string, DataRow>();
193
- for (const row of spec.data) {
194
- const party = String(row[colorField] ?? '');
195
- const val = Number(row[valueChannel.field] ?? 0);
196
- if (!Number.isFinite(val) || val < 0) continue;
197
- partyTotals.set(party, (partyTotals.get(party) ?? 0) + Math.round(val));
198
- if (!partyRows.has(party)) partyRows.set(party, row);
199
- }
200
-
201
- const parties = [...partyTotals.keys()];
202
- const totalSeats = parties.reduce((s, p) => s + (partyTotals.get(p) ?? 0), 0);
203
- if (parties.length === 0 || totalSeats <= 0) return [];
204
-
205
- const rows = chooseRowCount(totalSeats);
206
- const slots = packSeats(totalSeats, rows);
207
- if (slots.length === 0) return [];
208
-
209
- // Geometry: fit the hemicycle (a half-disc, so width = 2*R, height = R plus
210
- // one seat radius of headroom) into the chart area and center it.
211
- const seatRadiusOpt = spec.markDef.seatRadius;
212
-
213
- // Angular seat pitch from the densest (innermost) occupied row bounds the dot
214
- // size so seats in the tightest row don't overlap.
215
- const outerRadius = Math.min(chartArea.width / 2, chartArea.height);
216
- const innerR = outerRadius * INNER_RADIUS_FRACTION;
217
- // Seats per innermost row ~ slots on the smallest radius. Derive a max dot
218
- // radius from the innermost row's arc-length per seat.
219
- const innerRowCount = slots.filter((s) => s.row === 0).length || 1;
220
- const innerArcPerSeat = (Math.PI * innerR) / Math.max(1, innerRowCount);
221
- const ringGap = rows > 1 ? (outerRadius - innerR) / (rows - 1) : outerRadius - innerR;
222
- const autoSeatRadius = (Math.min(innerArcPerSeat, ringGap) / 2) * (1 - SEAT_GAP_FRACTION);
223
- const seatRadius =
224
- typeof seatRadiusOpt === 'number' && seatRadiusOpt > 0
225
- ? seatRadiusOpt
226
- : Math.max(1.5, autoSeatRadius);
227
-
228
- const centerX = chartArea.x + chartArea.width / 2;
229
- // Anchor the flat base near the bottom of the area, leaving a seat radius of
230
- // headroom below so the bottom row's dots aren't clipped.
231
- const centerY = chartArea.y + chartArea.height - seatRadius;
232
-
233
- // Assign parties to contiguous seat blocks in data order.
234
- const marks: Mark[] = [];
235
- let slotIndex = 0;
236
- let animationIndex = 0;
237
- for (const party of parties) {
238
- const count = partyTotals.get(party) ?? 0;
239
- if (count <= 0) continue;
240
- const row = partyRows.get(party)!;
241
- const shareStr = totalSeats > 0 ? formatPercent(count / totalSeats) : '0%';
242
- const fill = getColor(scales, party);
243
- const partyKey = serializeKeyValue(party);
244
-
245
- for (let k = 0; k < count && slotIndex < slots.length; k++, slotIndex++, animationIndex++) {
246
- const slot = slots[slotIndex];
247
- // slot.radius is normalized [INNER_RADIUS_FRACTION, 1]; scale to pixels.
248
- const pixelRadius = slot.radius * outerRadius;
249
- const cx = centerX + Math.cos(slot.angle) * pixelRadius;
250
- const cy = centerY - Math.sin(slot.angle) * pixelRadius;
251
-
252
- // One SR stop per party: the first seat carries the label, the rest are
253
- // decorative duplicates (435 seats would drown screen reader users).
254
- const aria: MarkAria =
255
- k === 0
256
- ? { label: `${party}: ${count} seats (${shareStr})` }
257
- : { label: `${party} seat`, decorative: true };
258
-
259
- const seat: PointMark = {
260
- type: 'point',
261
- key: `${partyKey}|${k}`,
262
- cx,
263
- cy,
264
- r: seatRadius,
265
- fill,
266
- stroke: 'none',
267
- strokeWidth: 0,
268
- data: row as Record<string, unknown>,
269
- aria,
270
- animationIndex,
271
- };
272
- marks.push(seat);
273
- }
274
- }
275
-
276
- // Majority-threshold line + label.
277
- const majority = resolveMajority(totalSeats, spec.markDef.majorityLine);
278
- if (majority && majority.seats > 0 && majority.seats <= totalSeats) {
279
- const lineTop = centerY - outerRadius - seatRadius;
280
- const line: RuleMarkLayout = {
281
- type: 'rule',
282
- key: 'oc-majority-line',
283
- x1: centerX,
284
- y1: lineTop,
285
- x2: centerX,
286
- y2: centerY + seatRadius,
287
- stroke: theme.colors.text,
288
- strokeWidth: 1.5,
289
- strokeDasharray: '4 3',
290
- opacity: 0.7,
291
- data: { majority: majority.seats } as Record<string, unknown>,
292
- aria: { label: `Majority threshold: ${majority.seats} seats`, decorative: true },
293
- };
294
- marks.push(line);
295
-
296
- const label: TextMarkLayout = {
297
- type: 'textMark',
298
- key: 'oc-majority-label',
299
- x: centerX,
300
- y: Math.max(chartArea.y + theme.fonts.sizes.small, lineTop - 6),
301
- text: majority.label,
302
- fill: theme.colors.text,
303
- fontSize: theme.fonts.sizes.small,
304
- fontWeight: theme.fonts.weights.semibold,
305
- textAnchor: 'middle',
306
- data: { majority: majority.seats } as Record<string, unknown>,
307
- aria: { label: `${majority.seats} seats needed for a majority`, decorative: true },
308
- };
309
- marks.push(label);
310
- }
311
-
312
- return marks;
313
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Parliament (hemicycle) chart module.
3
- *
4
- * Exports the parliament renderer. Parliament marks emit existing mark types
5
- * (PointMark seats + a RuleMarkLayout majority line + a TextMarkLayout label),
6
- * so they render through the existing point/rule/text mark paths with no
7
- * bespoke renderer or layout type.
8
- */
9
-
10
- import type { Mark } from '@opendata-ai/openchart-core';
11
- import type { ChartRenderer } from '../registry';
12
- import { computeParliamentMarks } from './compute';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Parliament chart renderer
16
- // ---------------------------------------------------------------------------
17
-
18
- /**
19
- * Parliament chart renderer.
20
- *
21
- * Produces one seat dot per seat (party-grouped, left to right) plus the
22
- * majority-threshold line and its label.
23
- */
24
- export const parliamentRenderer: ChartRenderer = (spec, scales, chartArea, strategy, theme) => {
25
- return computeParliamentMarks(spec, scales, chartArea, strategy, theme) as Mark[];
26
- };
27
-
28
- // ---------------------------------------------------------------------------
29
- // Public exports
30
- // ---------------------------------------------------------------------------
31
-
32
- export { computeParliamentMarks } from './compute';