@svgrid/grid 3.0.2 → 3.0.4

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 (281) hide show
  1. package/README.md +2 -1
  2. package/dist/GridMenus.svelte +44 -2
  3. package/dist/SvChartMenu.svelte +69 -0
  4. package/dist/SvChartMenu.svelte.d.ts +11 -0
  5. package/dist/SvChartPanes.svelte +113 -0
  6. package/dist/SvChartPanes.svelte.d.ts +20 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +110 -39
  8. package/dist/SvGrid.controller.svelte.js +655 -200
  9. package/dist/SvGrid.css +105 -2
  10. package/dist/SvGrid.svelte +386 -514
  11. package/dist/SvGrid.types.d.ts +210 -9
  12. package/dist/SvGridCellEditor.svelte +17 -0
  13. package/dist/SvGridChart.svelte +2257 -329
  14. package/dist/SvGridChart.svelte.d.ts +6 -106
  15. package/dist/SvGridChart.types.d.ts +338 -0
  16. package/dist/SvGridChart.types.js +1 -0
  17. package/dist/SvGridChartBuilder.svelte +323 -0
  18. package/dist/SvGridChartBuilder.svelte.d.ts +34 -0
  19. package/dist/SvGridChartPanel.svelte +221 -275
  20. package/dist/SvGridChartPickers.svelte +417 -0
  21. package/dist/SvGridChartPickers.svelte.d.ts +70 -0
  22. package/dist/SvGridChartView.svelte +13 -14
  23. package/dist/SvModal.svelte +7 -1
  24. package/dist/SvModal.svelte.d.ts +5 -0
  25. package/dist/ai.d.ts +40 -8
  26. package/dist/ai.js +135 -24
  27. package/dist/aria.d.ts +11 -0
  28. package/dist/build-api.js +108 -22
  29. package/dist/cdn/GridMenus-7kbpnnBW.js +635 -0
  30. package/dist/cdn/GridMenus-BL7ZgQvU.js +636 -0
  31. package/dist/cdn/SvChartMenu-Bl6PBbkT.js +58 -0
  32. package/dist/cdn/SvChartMenu-FBSMINA6.js +59 -0
  33. package/dist/cdn/{SvDateRangeInput-DMLKmGEc.js → SvDateRangeInput-BeU_TkbQ.js} +15 -14
  34. package/dist/cdn/{SvDateRangeInput-CaOuMs8O.js → SvDateRangeInput-yX8vzleW.js} +38 -37
  35. package/dist/cdn/{SvDateTimePicker-sonaH0oh.js → SvDateTimePicker-DQwt4UAs.js} +139 -180
  36. package/dist/cdn/SvDateTimePicker-vNU6bZ-q.js +775 -0
  37. package/dist/cdn/{SvGridCellEditor-CjEJMJKc.js → SvGridCellEditor-B1p-vCK5.js} +188 -178
  38. package/dist/cdn/{SvGridCellEditor-BWnTi2N7.js → SvGridCellEditor-D_0q4xAu.js} +98 -88
  39. package/dist/cdn/SvGridChart-C3EWAZaM.js +3574 -0
  40. package/dist/cdn/SvGridChart-DzLkSwCH.js +3572 -0
  41. package/dist/cdn/SvGridChartBuilder-BE2T1ykB.js +783 -0
  42. package/dist/cdn/SvGridChartBuilder-CfII62sZ.js +784 -0
  43. package/dist/cdn/SvGridChartPanel-CCX5_Wgd.js +675 -0
  44. package/dist/cdn/SvGridChartPanel-xf4sWVTo.js +699 -0
  45. package/dist/cdn/SvGridChartView-CfuXmY5I.js +48 -0
  46. package/dist/cdn/SvGridChartView-DYwabQWj.js +47 -0
  47. package/dist/cdn/SvMenuList-CLN8OuIK.js +452 -0
  48. package/dist/cdn/SvMenuList-DkGhNKjf.js +451 -0
  49. package/dist/cdn/SvModal-BAE-pjZX.js +396 -0
  50. package/dist/cdn/SvModal-CIZWCcad.js +397 -0
  51. package/dist/cdn/chart-Dz7SqMXH.js +4341 -0
  52. package/dist/cdn/chart-export-BXqueeWJ.js +135 -0
  53. package/dist/cdn/chart-export-pdf-DONFUnNY.js +110 -0
  54. package/dist/cdn/chart-panel-messages-CUbf2R4i.js +1052 -0
  55. package/dist/cdn/chart-panel-messages-CmNrMdsr.js +1053 -0
  56. package/dist/cdn/chart-summary-BJW_tg_X.js +138 -0
  57. package/dist/cdn/chart-trend-CaN9mDEV.js +50 -0
  58. package/dist/cdn/chart-validate-HcOVFAfJ.js +188 -0
  59. package/dist/cdn/{column-resize-DsfNXMom.js → column-resize-DpmOLfRp.js} +3 -3
  60. package/dist/cdn/{date-format-BnnHlqGw.js → date-format-CtquV-p3.js} +583 -603
  61. package/dist/cdn/{date-format-BNii4zeD.js → date-format-D6KFzU_W.js} +347 -367
  62. package/dist/cdn/dismissable-DAHetSNk.js +44 -0
  63. package/dist/cdn/editor-contract-LXgQJAgd.js +22 -0
  64. package/dist/cdn/focus-trap-BBOQYBma.js +65 -0
  65. package/dist/cdn/{row-resize-BRcimkUT.js → row-resize-niQCp040.js} +44 -36
  66. package/dist/cdn/{src-DNbz94Ct.js → src-BKhZ6eXd.js} +10227 -9787
  67. package/dist/cdn/{src-BJ2opd0j.js → src-V1uu8iE9.js} +6568 -6128
  68. package/dist/cdn/svgrid.js +26 -15
  69. package/dist/cdn/svgrid.svelte-external.js +26 -15
  70. package/dist/chart-axes.d.ts +53 -0
  71. package/dist/chart-axes.js +351 -0
  72. package/dist/chart-cartesian.d.ts +88 -0
  73. package/dist/chart-cartesian.js +1862 -0
  74. package/dist/chart-decimate.d.ts +51 -0
  75. package/dist/chart-decimate.js +199 -0
  76. package/dist/chart-export-pdf.d.ts +47 -0
  77. package/dist/chart-export-pdf.js +187 -0
  78. package/dist/chart-export.d.ts +2 -1
  79. package/dist/chart-export.js +18 -3
  80. package/dist/chart-financial.d.ts +91 -0
  81. package/dist/chart-financial.js +175 -0
  82. package/dist/chart-flow.d.ts +10 -0
  83. package/dist/chart-flow.js +298 -0
  84. package/dist/chart-format.d.ts +18 -0
  85. package/dist/chart-format.js +65 -0
  86. package/dist/chart-grid.d.ts +8 -0
  87. package/dist/chart-grid.js +190 -0
  88. package/dist/chart-hierarchy.d.ts +17 -0
  89. package/dist/chart-hierarchy.js +146 -0
  90. package/dist/chart-indicators.d.ts +115 -0
  91. package/dist/chart-indicators.js +389 -0
  92. package/dist/chart-messages.d.ts +95 -0
  93. package/dist/chart-messages.js +92 -0
  94. package/dist/chart-motion.d.ts +16 -0
  95. package/dist/chart-motion.js +141 -0
  96. package/dist/chart-panel-messages.d.ts +265 -0
  97. package/dist/chart-panel-messages.js +285 -0
  98. package/dist/chart-pivot.d.ts +87 -0
  99. package/dist/chart-pivot.js +156 -0
  100. package/dist/chart-polar.d.ts +25 -0
  101. package/dist/chart-polar.js +700 -0
  102. package/dist/chart-samples.d.ts +16 -0
  103. package/dist/chart-samples.js +153 -0
  104. package/dist/chart-scale.d.ts +113 -0
  105. package/dist/chart-scale.js +405 -0
  106. package/dist/chart-stats.d.ts +111 -0
  107. package/dist/chart-stats.js +419 -0
  108. package/dist/chart-stream.d.ts +45 -0
  109. package/dist/chart-stream.js +91 -0
  110. package/dist/chart-summary.d.ts +27 -0
  111. package/dist/chart-summary.js +205 -0
  112. package/dist/chart-sync.svelte.d.ts +32 -0
  113. package/dist/chart-sync.svelte.js +23 -0
  114. package/dist/chart-table.d.ts +35 -0
  115. package/dist/chart-table.js +165 -0
  116. package/dist/chart-trend.d.ts +19 -0
  117. package/dist/chart-trend.js +61 -0
  118. package/dist/chart-types.d.ts +1456 -0
  119. package/dist/chart-types.js +6 -0
  120. package/dist/chart-validate.d.ts +43 -0
  121. package/dist/chart-validate.js +272 -0
  122. package/dist/chart-zoom.d.ts +40 -0
  123. package/dist/chart-zoom.js +145 -0
  124. package/dist/chart.d.ts +186 -865
  125. package/dist/chart.js +460 -2204
  126. package/dist/clipboard.d.ts +1 -0
  127. package/dist/clipboard.js +137 -18
  128. package/dist/column-resize.d.ts +3 -0
  129. package/dist/column-resize.js +4 -1
  130. package/dist/columns.js +3 -0
  131. package/dist/command-context.d.ts +19 -0
  132. package/dist/command-context.js +116 -0
  133. package/dist/core.d.ts +7 -0
  134. package/dist/createPopoverSelect.svelte.d.ts +2 -2
  135. package/dist/editing.d.ts +2 -8
  136. package/dist/editing.js +262 -33
  137. package/dist/fill-patterns.d.ts +0 -5
  138. package/dist/fill-patterns.js +56 -2
  139. package/dist/grid-messages.d.ts +10 -3
  140. package/dist/grid-messages.js +5 -0
  141. package/dist/history.d.ts +83 -0
  142. package/dist/history.js +133 -0
  143. package/dist/index.d.ts +24 -6
  144. package/dist/index.js +20 -2
  145. package/dist/keyboard-handlers.js +88 -31
  146. package/dist/keyboard.d.ts +55 -0
  147. package/dist/keyboard.js +132 -0
  148. package/dist/menus.d.ts +3 -1
  149. package/dist/menus.js +7 -2
  150. package/dist/merges.d.ts +97 -0
  151. package/dist/merges.js +147 -0
  152. package/dist/row-resize.d.ts +3 -0
  153. package/dist/row-resize.js +14 -0
  154. package/dist/scroll-sync.d.ts +1 -0
  155. package/dist/scroll-sync.js +24 -0
  156. package/dist/selection.d.ts +16 -0
  157. package/dist/selection.js +108 -16
  158. package/dist/shortcut-registry.d.ts +145 -0
  159. package/dist/shortcut-registry.js +44 -0
  160. package/dist/svgrid-wrapper.types.d.ts +213 -22
  161. package/dist/virtualization/virtualizer.js +38 -17
  162. package/package.json +6 -1
  163. package/src/GridMenus.svelte +44 -2
  164. package/src/SvChartMenu.svelte +69 -0
  165. package/src/SvChartPanes.svelte +113 -0
  166. package/src/SvChartPanes.test.ts +66 -0
  167. package/src/SvGrid.controller.svelte.ts +629 -172
  168. package/src/SvGrid.css +105 -2
  169. package/src/SvGrid.svelte +386 -514
  170. package/src/SvGrid.types.ts +188 -8
  171. package/src/SvGridCellEditor.svelte +17 -0
  172. package/src/SvGridChart.svelte +2257 -329
  173. package/src/SvGridChart.test.ts +1419 -1
  174. package/src/SvGridChart.types.ts +336 -0
  175. package/src/SvGridChartBuilder.svelte +323 -0
  176. package/src/SvGridChartPanel.svelte +221 -275
  177. package/src/SvGridChartPickers.svelte +417 -0
  178. package/src/SvGridChartView.svelte +13 -14
  179. package/src/SvModal.svelte +7 -1
  180. package/src/SvModal.test.ts +9 -0
  181. package/src/ai.test.ts +57 -2
  182. package/src/ai.ts +166 -22
  183. package/src/aria.d.ts +11 -0
  184. package/src/build-api.coverage.test.ts +184 -0
  185. package/src/build-api.ts +107 -18
  186. package/src/chart-axes.test.ts +573 -0
  187. package/src/chart-axes.ts +342 -0
  188. package/src/chart-cartesian.ts +1863 -0
  189. package/src/chart-decimate.test.ts +249 -0
  190. package/src/chart-decimate.ts +186 -0
  191. package/src/chart-export-pdf.test.ts +91 -0
  192. package/src/chart-export-pdf.ts +207 -0
  193. package/src/chart-export.test.ts +14 -0
  194. package/src/chart-export.ts +13 -3
  195. package/src/chart-financial.ts +208 -0
  196. package/src/chart-flow.ts +286 -0
  197. package/src/chart-format.ts +67 -0
  198. package/src/chart-grid.ts +179 -0
  199. package/src/chart-hierarchy.ts +135 -0
  200. package/src/chart-indicators.test.ts +443 -0
  201. package/src/chart-indicators.ts +384 -0
  202. package/src/chart-messages.test.ts +21 -0
  203. package/src/chart-messages.ts +184 -0
  204. package/src/chart-motion.test.ts +71 -0
  205. package/src/chart-motion.ts +148 -0
  206. package/src/chart-panel-messages.test.ts +29 -0
  207. package/src/chart-panel-messages.ts +550 -0
  208. package/src/chart-pivot.test.ts +54 -0
  209. package/src/chart-pivot.ts +225 -0
  210. package/src/chart-polar.ts +674 -0
  211. package/src/chart-samples.ts +106 -0
  212. package/src/chart-scale.ts +394 -0
  213. package/src/chart-series-types.test.ts +714 -0
  214. package/src/chart-stats.ts +409 -0
  215. package/src/chart-stream.test.ts +75 -0
  216. package/src/chart-stream.ts +114 -0
  217. package/src/chart-summary.test.ts +90 -0
  218. package/src/chart-summary.ts +197 -0
  219. package/src/chart-sync.svelte.ts +41 -0
  220. package/src/chart-table.test.ts +76 -0
  221. package/src/chart-table.ts +178 -0
  222. package/src/chart-trend.ts +47 -0
  223. package/src/chart-types.ts +1309 -0
  224. package/src/chart-validate.test.ts +100 -0
  225. package/src/chart-validate.ts +247 -0
  226. package/src/chart-zoom.test.ts +137 -0
  227. package/src/chart-zoom.ts +124 -0
  228. package/src/chart.coverage.test.ts +81 -0
  229. package/src/chart.test.ts +56 -3
  230. package/src/chart.ts +557 -2784
  231. package/src/clipboard.test.ts +151 -0
  232. package/src/clipboard.ts +145 -16
  233. package/src/column-resize.ts +7 -1
  234. package/src/columns.ts +2 -0
  235. package/src/command-context.test.ts +200 -0
  236. package/src/command-context.ts +137 -0
  237. package/src/core.ts +7 -0
  238. package/src/editing.test.ts +281 -3
  239. package/src/editing.ts +256 -38
  240. package/src/fill-patterns.test.ts +35 -0
  241. package/src/fill-patterns.ts +56 -2
  242. package/src/grid-messages.ts +16 -3
  243. package/src/history.test.ts +196 -0
  244. package/src/history.ts +162 -0
  245. package/src/icon-seam.test.ts +3 -2
  246. package/src/index.ts +150 -2
  247. package/src/keyboard-handlers.coverage.test.ts +126 -6
  248. package/src/keyboard-handlers.ts +91 -30
  249. package/src/keyboard-shortcuts.seam.test.ts +200 -0
  250. package/src/keyboard.test.ts +124 -1
  251. package/src/keyboard.ts +142 -0
  252. package/src/menus.test.ts +26 -0
  253. package/src/menus.ts +10 -3
  254. package/src/merges.test.ts +112 -0
  255. package/src/merges.ts +168 -0
  256. package/src/pivot.grid.test.ts +40 -5
  257. package/src/row-resize.test.ts +44 -0
  258. package/src/row-resize.ts +17 -0
  259. package/src/scroll-sync.test.ts +42 -1
  260. package/src/scroll-sync.ts +25 -0
  261. package/src/selection.multi-range.test.ts +34 -0
  262. package/src/selection.test.ts +30 -2
  263. package/src/selection.ts +111 -19
  264. package/src/shortcut-registry.test.ts +97 -0
  265. package/src/shortcut-registry.ts +181 -0
  266. package/src/svgrid-wrapper.types.ts +184 -13
  267. package/src/svgrid.behavior.test.ts +135 -0
  268. package/src/svgrid.charting.test.ts +513 -3
  269. package/src/svgrid.context-menu.test.ts +31 -0
  270. package/src/svgrid.new-features.wrapper.test.ts +10 -3
  271. package/src/virtualization/virtualizer.ts +39 -17
  272. package/dist/cdn/GridMenus-DyFA-QxA.js +0 -602
  273. package/dist/cdn/GridMenus-M2XP9t0_.js +0 -603
  274. package/dist/cdn/SvDateTimePicker-CCbDZNZB.js +0 -816
  275. package/dist/cdn/SvGridChart-DAb9RbRs.js +0 -1480
  276. package/dist/cdn/SvGridChart-Yl-IXZFO.js +0 -1481
  277. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +0 -834
  278. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +0 -810
  279. package/dist/cdn/SvGridChartView-CxfakxR0.js +0 -56
  280. package/dist/cdn/SvGridChartView-Dm_YEx9L.js +0 -55
  281. package/dist/cdn/chart-CKJRCPOk.js +0 -1652
@@ -399,6 +399,29 @@ describe("buildChart: waterfall", () => {
399
399
  // a connector line exists between bar tops
400
400
  expect(g.lines[0]!.path).toContain("M");
401
401
  });
402
+ it("tilts its labels when the widest does not fit its slot, with the angle the renderer needs", () => {
403
+ const cats = ["Revenue", "Cost of sales", "Gross profit", "R&D", "Sales & marketing", "G&A", "Operating income", "Tax", "Net income"];
404
+ const wide = buildChart({ type: "waterfall", categories: cats, series: [{ label: "s", values: cats.map(() => 1) }], width: 1400, height: 300 });
405
+ expect(wide.xLabelRotated).toBe(false);
406
+ const narrow = buildChart({ type: "waterfall", categories: cats, series: [{ label: "s", values: cats.map(() => 1) }], width: 700, height: 300 });
407
+ expect(narrow.xLabelRotated).toBe(true);
408
+ expect(narrow.xLabelAngle).toBe(-40);
409
+ const pinned = buildChart({ type: "waterfall", categories: cats, series: [{ label: "s", values: cats.map(() => 1) }], width: 700, height: 300, xAxis: { labelRotation: 0 } });
410
+ expect(pinned.xLabelRotated).toBe(false);
411
+ });
412
+ it("a total with a value opens the bridge at that level; a total with 0 reads the running sum", () => {
413
+ const g = buildChart({
414
+ type: "waterfall",
415
+ categories: ["Revenue", "Cost", "Gross", "Tax", "Net"],
416
+ series: [{ label: "s", values: [4300, -1840, 0, -180, 0] }],
417
+ waterfallTotals: [true, false, true, false, true],
418
+ });
419
+ expect(g.bars.map((b) => b.value)).toEqual([4300, -1840, 2460, -180, 2280]);
420
+ // The opening bar rises from zero; the cost hangs from its top.
421
+ const [revenue, cost] = g.bars;
422
+ expect(revenue!.y).toBeLessThan(cost!.y + cost!.h);
423
+ expect(cost!.y).toBeCloseTo(revenue!.y, 0);
424
+ });
402
425
  it("colors positive/negative/total via waterfallColors overrides", () => {
403
426
  const g = buildChart({
404
427
  type: "waterfall",
@@ -437,6 +460,21 @@ describe("buildChart: funnel", () => {
437
460
  });
438
461
  });
439
462
 
463
+ describe("buildChart: colour legends", () => {
464
+ it("round their steps to the precision the range earns", () => {
465
+ const counts = buildChart({
466
+ type: "calendar", categories: [], series: [],
467
+ calendarValues: [{ date: "2026-01-05", value: 20 }, { date: "2026-01-06", value: 314 }, { date: "2026-01-07", value: 150 }],
468
+ });
469
+ expect(counts.calendarLegend.map((l) => l.label)).toEqual(["20", "94", "167", "241", "314"]);
470
+ const ratios = buildChart({
471
+ type: "heatmap", categories: ["a", "b"],
472
+ series: [{ label: "r1", values: [0.1, 0.5] }, { label: "r2", values: [0.3, 0.9] }],
473
+ });
474
+ expect(ratios.heatmapLegend.map((l) => l.value)).toEqual([0.1, 0.3, 0.5, 0.7, 0.9]);
475
+ });
476
+ });
477
+
440
478
  describe("buildChart: radar", () => {
441
479
  it("one polygon per series + axis spokes + rings", () => {
442
480
  const g = buildChart({
@@ -459,6 +497,27 @@ describe("buildChart: radar", () => {
459
497
  buildChart({ type: "radar", categories: ["A"], series: [] }).radarAxes,
460
498
  ).toHaveLength(0);
461
499
  });
500
+ it("yAxis.min / max pin the rim, and a value past it sits on the rim", () => {
501
+ const spec = {
502
+ type: "radar" as const,
503
+ categories: ["A", "B", "C", "D"],
504
+ series: [{ label: "s", values: [50, 50, 50, 50] }],
505
+ };
506
+ const free = buildChart(spec);
507
+ const pinned = buildChart({ ...spec, yAxis: { min: 0, max: 100 } });
508
+ const c = pinned.radarCenter!;
509
+ // Unpinned, 50 is the data maximum and sits on the rim; pinned to 100 it sits halfway.
510
+ expect(free.radarSeries[0]!.points[0]!.y).toBeCloseTo(c.cy - c.r, 0);
511
+ expect(pinned.radarSeries[0]!.points[0]!.y).toBeCloseTo(c.cy - c.r / 2, 0);
512
+ expect(pinned.radarRings.at(-1)).toBe(100);
513
+ // Past the rim clamps to it rather than leaving the dial.
514
+ const over = buildChart({ ...spec, yAxis: { min: 0, max: 40 } });
515
+ expect(over.radarSeries[0]!.points[0]!.y).toBeCloseTo(c.cy - c.r, 0);
516
+ // A pinned minimum lifts the floor: the value at the minimum is the centre.
517
+ const floor = buildChart({ ...spec, series: [{ label: "s", values: [20, 60, 20, 60] }], yAxis: { min: 20, max: 60 } });
518
+ expect(floor.radarSeries[0]!.points[0]!.x).toBeCloseTo(c.cx, 0);
519
+ expect(floor.radarSeries[0]!.points[0]!.y).toBeCloseTo(c.cy, 0);
520
+ });
462
521
  it("guards against an all-zero max (vMax defaults to 1)", () => {
463
522
  const g = buildChart({
464
523
  type: "radar",
@@ -493,6 +552,19 @@ describe("buildChart: calendar heatmap", () => {
493
552
  expect(blank!.color).toBe("transparent");
494
553
  expect(g.calendarMonthTicks.length).toBeGreaterThanOrEqual(1);
495
554
  });
555
+ it("labels only the months inside the range: the padding days of the next year get no tick", () => {
556
+ // 2026 ends on a Thursday; the grid pads to Saturday 2 Jan 2027.
557
+ const g = buildChart({
558
+ type: "calendar", categories: [], series: [],
559
+ calendarValues: [{ date: "2026-12-30", value: 1 }],
560
+ calendarStart: "2026-01-01", calendarEnd: "2026-12-31",
561
+ });
562
+ expect(g.calendarMonthTicks).toHaveLength(12);
563
+ expect(g.calendarMonthTicks[0]!.label).toBe(new Date("2026-01-01T00:00:00Z").toLocaleDateString(undefined, { month: "short" }));
564
+ expect(g.calendarMonthTicks[11]!.label).toBe(new Date("2026-12-01T00:00:00Z").toLocaleDateString(undefined, { month: "short" }));
565
+ // The padding cells exist but are blank.
566
+ expect(g.calendarCells.filter((c) => c.date > "2026-12-31").every((c) => !c.defined)).toBe(true);
567
+ });
496
568
  it("honours explicit calendarStart even with no values", () => {
497
569
  const g = buildChart({
498
570
  type: "calendar",
@@ -628,6 +700,15 @@ describe("buildChart: heatmap", () => {
628
700
  expect(g.heatmapRowTicks.map((t) => t.label)).toEqual(["North", "South"]);
629
701
  expect(g.heatmapColTicks.map((t) => t.label)).toEqual(["Q1", "Q2", "Q3"]);
630
702
  expect(g.heatmapLegend).toHaveLength(5);
703
+ // Column labels thin out when the cells get narrower than a label: 24
704
+ // hours in 520px is about 15px a cell, so every fourth one is written.
705
+ const hours = Array.from({ length: 24 }, (_, h) => `${String(h).padStart(2, "0")}:00`);
706
+ const dense = buildChart({ type: "heatmap", categories: hours, series: [{ label: "Mon", values: hours.map((_, i) => i) }] });
707
+ expect(dense.heatmapCells).toHaveLength(24);
708
+ expect(dense.heatmapColTicks.length).toBeLessThan(24);
709
+ expect(dense.heatmapColTicks[0]!.label).toBe("00:00");
710
+ const gap = dense.heatmapColTicks[1]!.x - dense.heatmapColTicks[0]!.x;
711
+ expect(gap).toBeGreaterThanOrEqual(60);
631
712
  for (const c of g.heatmapCells) {
632
713
  expect(["#0f172a", "#ffffff"]).toContain(c.textColor);
633
714
  }
package/src/chart.test.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { buildChart, rowsToChartSpec, niceScale, sliceChartWindow, specToTreemap, specToCalendar, specToSankey, rowsToScatterSpec, rowsToGaugeSpec, boxStats, rowsToBoxSpec, chartScales } from './chart'
2
+ import { buildChart, rowsToChartSpec, niceScale, sliceChartWindow, specToTreemap, specToCalendar, specToSankey, rowsToScatterSpec, rowsToGaugeSpec, boxStats, rowsToBoxSpec, chartScales, layoutDataLabels } from './chart'
3
3
  import type { ChartSpec } from './chart'
4
4
 
5
5
  describe('niceScale', () => {
@@ -116,9 +116,37 @@ describe('buildChart', () => {
116
116
  expect(g.donut!.total).toBe(10)
117
117
  })
118
118
 
119
- it('rotates x labels when there are many / long categories', () => {
120
- const many = buildChart({ type: 'bar', categories: Array.from({ length: 12 }, (_, i) => `Cat ${i}`), series: [{ label: 's', values: Array(12).fill(1) }] })
119
+ it('data labels on a 100% chart read the share, the way the axis does', () => {
120
+ const spec: ChartSpec = {
121
+ type: 'bar',
122
+ stacked100: true,
123
+ categories: ['Q1', 'Q2'],
124
+ series: [
125
+ { label: 'a', values: [25, 60] },
126
+ { label: 'b', values: [75, 40] },
127
+ ],
128
+ valueFormat: 'currency',
129
+ width: 400,
130
+ height: 200,
131
+ }
132
+ const geo = buildChart(spec)
133
+ const labels = layoutDataLabels(geo, { show: true, placement: 'inside', hideOverlap: false }, (v) => `$${v}`, { stacked: true, share: true })
134
+ expect(labels.map((l) => l.text)).toEqual(['25%', '60%', '75%', '40%'])
135
+ // Without the share flag the raw values stay, formatted the chart's way.
136
+ const raw = layoutDataLabels(geo, { show: true, placement: 'inside', hideOverlap: false }, (v) => `$${v}`, { stacked: true })
137
+ expect(raw.map((l) => l.text)).toEqual(['$25', '$60', '$75', '$40'])
138
+ // A formatter still receives the raw value.
139
+ const custom = layoutDataLabels(geo, { show: true, placement: 'inside', hideOverlap: false, formatter: (v) => `v${v}` }, String, { stacked: true, share: true })
140
+ expect(custom.map((l) => l.text)).toEqual(['v25', 'v60', 'v75', 'v40'])
141
+ })
142
+
143
+ it('rotates x labels when the widest one does not fit its slot, and only then', () => {
144
+ const cats = Array.from({ length: 12 }, (_, i) => `Cat ${i}`)
145
+ const many = buildChart({ type: 'bar', categories: cats, series: [{ label: 's', values: Array(12).fill(1) }] })
121
146
  expect(many.xLabelRotated).toBe(true)
147
+ // The same twelve labels on a chart wide enough for them stay upright.
148
+ const wide = buildChart({ type: 'bar', categories: cats, width: 1100, series: [{ label: 's', values: Array(12).fill(1) }] })
149
+ expect(wide.xLabelRotated).toBe(false)
122
150
  const few = buildChart({ type: 'bar', categories: ['A', 'B'], series: [{ label: 's', values: [1, 2] }] })
123
151
  expect(few.xLabelRotated).toBe(false)
124
152
  })
@@ -317,6 +345,31 @@ describe('rowsToChartSpec', () => {
317
345
  expect(spec.series[0]!.values).toEqual([100, 50, 50])
318
346
  })
319
347
 
348
+ it('topN folds the tail INTO a category already named "Other" instead of duplicating it', () => {
349
+ // A real "Other" bucket in the data (a browser share's long tail) used
350
+ // to come out beside the folded one: two categories with one name, which
351
+ // is a duplicate key the renderer throws on.
352
+ const many = [
353
+ { cat: 'A', v: 100, id: 1 },
354
+ { cat: 'Other', v: 60, id: 2 },
355
+ { cat: 'B', v: 50, id: 3 },
356
+ { cat: 'C', v: 30, id: 4 },
357
+ { cat: 'D', v: 20, id: 5 },
358
+ ]
359
+ // "Other" is second by value and would have made the top 3 on its own.
360
+ const spec = rowsToChartSpec(many, { type: 'bar', category: 'cat', value: 'v', topN: 3, idField: 'id' })
361
+ expect(spec.categories).toEqual(['A', 'B', 'Other'])
362
+ expect(spec.series[0]!.values).toEqual([100, 50, 60 + 30 + 20])
363
+ expect(spec.series[0]!.rowIds).toEqual([[1], [3], [2, 4, 5]])
364
+ expect(new Set(spec.categories).size).toBe(spec.categories.length)
365
+ // Below the cut it is folded the same way, and a custom label is honoured.
366
+ const low = rowsToChartSpec(many.map((r) => (r.cat === 'Other' ? { ...r, v: 5 } : r)), { type: 'bar', category: 'cat', value: 'v', topN: 3 })
367
+ expect(low.categories).toEqual(['A', 'B', 'C', 'Other'])
368
+ expect(low.series[0]!.values).toEqual([100, 50, 30, 25])
369
+ const rest = rowsToChartSpec(many, { type: 'bar', category: 'cat', value: 'v', topN: 3, otherLabel: 'Rest' })
370
+ expect(rest.categories).toEqual(['A', 'Other', 'B', 'Rest'])
371
+ })
372
+
320
373
  it('stacked100 flag passes through to the spec', () => {
321
374
  const spec = rowsToChartSpec(rows, { type: 'bar', category: 'region', value: ['revenue', 'cost'], stacked100: true })
322
375
  expect(spec.stacked100).toBe(true)