@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,319 +0,0 @@
1
- /**
2
- * Calendar heatmap mark computation.
3
- *
4
- * GitHub-style layout: weeks as columns, weekdays as rows, month labels
5
- * above each band, Mon/Wed/Fri weekday labels on the left. Multi-year data
6
- * partitions into one full-year band per calendar year, stacked vertically
7
- * with a year label per band; every band shares the figure's single color
8
- * scale (built once from the full dataset by computeScales).
9
- *
10
- * The calendar owns its geometry: no positional scales or axes. All date
11
- * math runs in UTC (d3-time utc* intervals) so the layout is deterministic
12
- * regardless of the host machine's timezone -- ISO date strings like
13
- * "2024-01-15" parse as UTC midnight, and a local-time week interval would
14
- * shift them across day boundaries in western timezones.
15
- *
16
- * Days inside a band's year with no data row (or a non-numeric value)
17
- * render as empty cells filled with the theme's achromatic surface tint
18
- * (colors.annotationFill), visually distinct from the chromatic scale
19
- * minimum in both light and dark modes. Days padding the first/last week
20
- * but belonging to a neighboring year render nothing.
21
- */
22
-
23
- import type {
24
- Encoding,
25
- Mark,
26
- MarkAria,
27
- Rect,
28
- RectMark,
29
- ResolvedTheme,
30
- TextMarkLayout,
31
- } from '@opendata-ai/openchart-core';
32
- import { defaultNumberFormatter, formatDate } from '@opendata-ai/openchart-core';
33
- import { type CountableTimeInterval, utcDay, utcMonday, utcSunday } from 'd3-time';
34
-
35
- import type { NormalizedChartSpec } from '../../compiler/types';
36
- import type { ResolvedScales } from '../../layout/scales';
37
- import { getSequentialColor } from '../utils';
38
-
39
- // ---------------------------------------------------------------------------
40
- // Constants
41
- // ---------------------------------------------------------------------------
42
-
43
- /**
44
- * Minimum day-cell size in pixels (the "never shrink marks below legibility"
45
- * floor, extending the never-shrink-text house rule to marks). When the
46
- * container is too narrow for 53 legible columns the grid keeps this floor
47
- * and overflows the chart area instead of shrinking further; hosts that need
48
- * every column visible at phone widths wrap the figure in a horizontal
49
- * scroll container.
50
- */
51
- export const MIN_CELL_SIZE = 7;
52
-
53
- /** Maximum grid step (cell + gap) so huge containers stay dense. */
54
- const MAX_STEP = 20;
55
-
56
- /** Minimum grid step: the cell floor plus a 1px gap. */
57
- const MIN_STEP = MIN_CELL_SIZE + 1;
58
-
59
- /** Vertical gap between stacked year bands. */
60
- const BAND_GAP = 18;
61
-
62
- /** Gap between the weekday labels and the first week column. */
63
- const WEEKDAY_LABEL_GAP = 6;
64
-
65
- /** Default corner radius for day cells (markDef.cellRadius overrides). */
66
- const DEFAULT_CELL_RADIUS = 1;
67
-
68
- /** Weekday labels shown on the left (day-of-week numbers, 0 = Sunday). */
69
- const WEEKDAY_LABELS: Array<{ dow: number; label: string }> = [
70
- { dow: 1, label: 'Mon' },
71
- { dow: 3, label: 'Wed' },
72
- { dow: 5, label: 'Fri' },
73
- ];
74
-
75
- /** Abbreviated month labels, January first. */
76
- const MONTH_LABELS = [
77
- 'Jan',
78
- 'Feb',
79
- 'Mar',
80
- 'Apr',
81
- 'May',
82
- 'Jun',
83
- 'Jul',
84
- 'Aug',
85
- 'Sep',
86
- 'Oct',
87
- 'Nov',
88
- 'Dec',
89
- ];
90
-
91
- // ---------------------------------------------------------------------------
92
- // Date helpers
93
- // ---------------------------------------------------------------------------
94
-
95
- /** Parse a raw field value to a UTC-floored day, or null when unparseable. */
96
- export function parseUtcDay(value: unknown): Date | null {
97
- if (value == null) return null;
98
- const date = value instanceof Date ? value : new Date(value as string | number);
99
- if (Number.isNaN(date.getTime())) return null;
100
- return utcDay.floor(date);
101
- }
102
-
103
- /** ISO yyyy-mm-dd key for a UTC day. */
104
- function isoDayKey(day: Date): string {
105
- return day.toISOString().slice(0, 10);
106
- }
107
-
108
- /** Week interval for a weekStart option. */
109
- function weekInterval(weekStart: 'monday' | 'sunday'): CountableTimeInterval {
110
- return weekStart === 'sunday' ? utcSunday : utcMonday;
111
- }
112
-
113
- /** Weekday row index (0..6) for a UTC day under a weekStart option. */
114
- export function weekdayRow(day: Date, weekStart: 'monday' | 'sunday'): number {
115
- const startDow = weekStart === 'sunday' ? 0 : 1;
116
- return (day.getUTCDay() - startDow + 7) % 7;
117
- }
118
-
119
- // ---------------------------------------------------------------------------
120
- // Public API
121
- // ---------------------------------------------------------------------------
122
-
123
- /**
124
- * Compute calendar heatmap marks from a normalized chart spec.
125
- *
126
- * Emits one RectMark per day of every calendar year present in the data
127
- * (days without data get the empty-cell surface tint) plus TextMarkLayout
128
- * marks for month, weekday, and (multi-year only) year labels.
129
- */
130
- export function computeCalendarMarks(
131
- spec: NormalizedChartSpec,
132
- scales: ResolvedScales,
133
- chartArea: Rect,
134
- theme: ResolvedTheme,
135
- ): Mark[] {
136
- const encoding = spec.encoding as Encoding;
137
- const xEnc = encoding.x;
138
- const colorEnc = encoding.color && 'field' in encoding.color ? encoding.color : undefined;
139
- if (!xEnc || !colorEnc) return [];
140
-
141
- const weekStart = spec.markDef.weekStart ?? 'monday';
142
- const week = weekInterval(weekStart);
143
- const cellRadius = spec.markDef.cellRadius ?? DEFAULT_CELL_RADIUS;
144
-
145
- // Index rows by UTC day. Unparseable dates and non-numeric values were
146
- // rejected (or warned about) at validation; anything that still slips
147
- // through renders as an empty cell rather than corrupting the grid.
148
- const dayData = new Map<string, { row: Record<string, unknown>; value: number }>();
149
- const years = new Set<number>();
150
- for (const row of spec.data) {
151
- const day = parseUtcDay(row[xEnc.field]);
152
- if (!day) continue;
153
- years.add(day.getUTCFullYear());
154
- const value = Number(row[colorEnc.field]);
155
- if (!Number.isFinite(value)) continue;
156
- dayData.set(isoDayKey(day), { row: row as Record<string, unknown>, value });
157
- }
158
- if (years.size === 0) return [];
159
-
160
- const sortedYears = Array.from(years).sort((a, b) => a - b);
161
-
162
- // Per-band week-column counts. A band always spans its full calendar year
163
- // (Jan 1 through Dec 31), so partial-year data keeps the familiar 53-column
164
- // silhouette and missing days read as gaps rather than a shifted grid.
165
- const bandColumns = sortedYears.map((year) => {
166
- const firstWeek = week.floor(new Date(Date.UTC(year, 0, 1)));
167
- const lastDay = new Date(Date.UTC(year, 11, 31));
168
- return week.count(firstWeek, lastDay) + 1;
169
- });
170
- const maxColumns = Math.max(...bandColumns);
171
-
172
- // -------------------------------------------------------------------------
173
- // Geometry
174
- // -------------------------------------------------------------------------
175
- const labelFont = theme.fonts.sizes.small;
176
- const gutter = Math.ceil(labelFont * 2.6) + WEEKDAY_LABEL_GAP;
177
- const monthRow = Math.ceil(labelFont * 1.3) + 2;
178
- const bands = sortedYears.length;
179
- const showYearLabels = bands > 1;
180
-
181
- const availWidth = Math.max(0, chartArea.width - gutter);
182
- const stepFromWidth = availWidth / maxColumns;
183
- const availHeight = Math.max(0, chartArea.height - bands * monthRow - (bands - 1) * BAND_GAP);
184
- const stepFromHeight = availHeight / (7 * bands);
185
- // Square cells only: the calendar idiom reads as a grid of days, and
186
- // wide cells would misalign weekday rows across stacked year bands. At
187
- // narrow widths the step clamps to the MIN_CELL_SIZE floor instead of
188
- // going sub-legible (the grid then overflows to the right).
189
- const step = Math.min(
190
- MAX_STEP,
191
- Math.max(MIN_STEP, Math.floor(Math.min(stepFromWidth, stepFromHeight))),
192
- );
193
- const cellGap = step >= 14 ? 3 : step >= 10 ? 2 : 1;
194
- const cell = step - cellGap;
195
-
196
- const gridWidth = maxColumns * step - cellGap;
197
- const bandHeight = monthRow + 7 * step - cellGap;
198
- const totalHeight = bands * bandHeight + (bands - 1) * BAND_GAP;
199
- const originX = chartArea.x + gutter + Math.max(0, (availWidth - gridWidth) / 2);
200
- const originY = chartArea.y + Math.max(0, (chartArea.height - totalHeight) / 2);
201
-
202
- // Month label thinning: when four week columns can't fit a three-letter
203
- // label, keep every other month so labels never collide.
204
- const monthLabelWidth = labelFont * 2.2;
205
- const monthLabelEvery = step * 4.34 < monthLabelWidth + 4 ? 2 : 1;
206
-
207
- const labelBaseline = labelFont; // baseline offset from the band top
208
-
209
- const cellMarks: RectMark[] = [];
210
- const labelMarks: TextMarkLayout[] = [];
211
-
212
- const labelAria: MarkAria = { decorative: true };
213
- const labelStyle = {
214
- fill: theme.colors.axis,
215
- fontSize: labelFont,
216
- fontWeight: theme.fonts.weights.normal,
217
- fontFamily: theme.fonts.family,
218
- };
219
-
220
- // -------------------------------------------------------------------------
221
- // Bands
222
- // -------------------------------------------------------------------------
223
- const calendarFmt = defaultNumberFormatter();
224
- for (let b = 0; b < bands; b++) {
225
- const year = sortedYears[b];
226
- const bandTop = originY + b * (bandHeight + BAND_GAP);
227
- const gridTop = bandTop + monthRow;
228
- const yearStart = new Date(Date.UTC(year, 0, 1));
229
- const yearEnd = new Date(Date.UTC(year + 1, 0, 1));
230
- const firstWeek = week.floor(yearStart);
231
-
232
- // Year label (multi-year only), in the weekday gutter column.
233
- if (showYearLabels) {
234
- labelMarks.push({
235
- type: 'textMark',
236
- x: chartArea.x,
237
- y: bandTop + labelBaseline,
238
- text: String(year),
239
- ...labelStyle,
240
- fill: theme.colors.text,
241
- fontWeight: theme.fonts.weights.semibold,
242
- textAnchor: 'start',
243
- data: {},
244
- aria: labelAria,
245
- });
246
- }
247
-
248
- // Month labels above the grid, at the column containing the 1st.
249
- for (let m = 0; m < 12; m += monthLabelEvery) {
250
- const col = week.count(firstWeek, new Date(Date.UTC(year, m, 1)));
251
- labelMarks.push({
252
- type: 'textMark',
253
- x: originX + col * step,
254
- y: bandTop + labelBaseline,
255
- text: MONTH_LABELS[m],
256
- ...labelStyle,
257
- textAnchor: 'start',
258
- data: {},
259
- aria: labelAria,
260
- });
261
- }
262
-
263
- // Weekday labels (Mon/Wed/Fri) left of the grid, centered on their row.
264
- for (const { dow, label } of WEEKDAY_LABELS) {
265
- const row = (dow - (weekStart === 'sunday' ? 0 : 1) + 7) % 7;
266
- labelMarks.push({
267
- type: 'textMark',
268
- x: originX - WEEKDAY_LABEL_GAP,
269
- y: gridTop + row * step + cell / 2 + labelFont * 0.35,
270
- text: label,
271
- ...labelStyle,
272
- textAnchor: 'end',
273
- data: {},
274
- aria: labelAria,
275
- });
276
- }
277
-
278
- // Day cells: every day of the calendar year.
279
- for (const day of utcDay.range(yearStart, yearEnd)) {
280
- const col = week.count(firstWeek, day);
281
- const row = weekdayRow(day, weekStart);
282
- const key = isoDayKey(day);
283
- const datum = dayData.get(key);
284
- const dateLabel = formatDate(day, undefined, 'day');
285
-
286
- if (datum) {
287
- cellMarks.push({
288
- type: 'rect',
289
- key,
290
- x: originX + col * step,
291
- y: gridTop + row * step,
292
- width: cell,
293
- height: cell,
294
- fill: getSequentialColor(scales, datum.value),
295
- cornerRadius: cellRadius,
296
- data: datum.row,
297
- aria: {
298
- label: `${dateLabel}, ${colorEnc.field}: ${calendarFmt(datum.value)}`,
299
- },
300
- });
301
- } else {
302
- cellMarks.push({
303
- type: 'rect',
304
- key,
305
- x: originX + col * step,
306
- y: gridTop + row * step,
307
- width: cell,
308
- height: cell,
309
- fill: theme.colors.annotationFill,
310
- cornerRadius: cellRadius,
311
- data: {},
312
- aria: { decorative: true },
313
- });
314
- }
315
- }
316
- }
317
-
318
- return [...cellMarks, ...labelMarks];
319
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Calendar heatmap chart module.
3
- *
4
- * Exports the calendar renderer and computation function. Calendars emit
5
- * RectMark day cells plus decorative TextMarkLayout month/weekday/year
6
- * labels; there are no positional scales or axes (the compile pipeline
7
- * suppresses them, arc precedent). The quantitative color encoding drives
8
- * both the cell fills and the default-on continuous color legend.
9
- */
10
-
11
- import type { ChartRenderer } from '../registry';
12
- import { computeCalendarMarks } from './compute';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Calendar chart renderer
16
- // ---------------------------------------------------------------------------
17
-
18
- /**
19
- * Calendar heatmap renderer.
20
- *
21
- * Produces one rect per day of each calendar year in the data, plus text
22
- * marks for the month/weekday/year labels.
23
- */
24
- export const calendarRenderer: ChartRenderer = (spec, scales, chartArea, _strategy, theme) => {
25
- return computeCalendarMarks(spec, scales, chartArea, theme);
26
- };
27
-
28
- // ---------------------------------------------------------------------------
29
- // Public exports
30
- // ---------------------------------------------------------------------------
31
-
32
- export { computeCalendarMarks, MIN_CELL_SIZE, parseUtcDay, weekdayRow } from './compute';