photon-grid-core 0.0.2 → 1.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 (201) hide show
  1. package/README.md +304 -304
  2. package/chart/chart-config/chart-controls.d.ts +49 -0
  3. package/chart/chart-config/chart-controls.d.ts.map +1 -0
  4. package/chart/chart-config/chart-controls.js +229 -0
  5. package/chart/chart-config/chart-controls.js.map +1 -0
  6. package/chart/chart-config/chart-tool-panel.d.ts +86 -0
  7. package/chart/chart-config/chart-tool-panel.d.ts.map +1 -0
  8. package/chart/chart-config/chart-tool-panel.js +402 -0
  9. package/chart/chart-config/chart-tool-panel.js.map +1 -0
  10. package/chart/chart-panel.d.ts +97 -0
  11. package/chart/chart-panel.d.ts.map +1 -1
  12. package/chart/chart-panel.js +211 -58
  13. package/chart/chart-panel.js.map +1 -1
  14. package/chart/chart-renderer.d.ts +97 -0
  15. package/chart/chart-renderer.d.ts.map +1 -1
  16. package/chart/chart-renderer.js +335 -72
  17. package/chart/chart-renderer.js.map +1 -1
  18. package/chart/chart-theme.d.ts +38 -0
  19. package/chart/chart-theme.d.ts.map +1 -0
  20. package/chart/chart-theme.js +47 -0
  21. package/chart/chart-theme.js.map +1 -0
  22. package/chart/model/chart-model-mapper.d.ts +38 -0
  23. package/chart/model/chart-model-mapper.d.ts.map +1 -0
  24. package/chart/model/chart-model-mapper.js +128 -0
  25. package/chart/model/chart-model-mapper.js.map +1 -0
  26. package/chart/model/chart-model.d.ts +153 -0
  27. package/chart/model/chart-model.d.ts.map +1 -0
  28. package/chart/model/chart-model.js +79 -0
  29. package/chart/model/chart-model.js.map +1 -0
  30. package/chart/range-chart-controller.d.ts +68 -0
  31. package/chart/range-chart-controller.d.ts.map +1 -0
  32. package/chart/range-chart-controller.js +209 -0
  33. package/chart/range-chart-controller.js.map +1 -0
  34. package/chart/range-chart-service.d.ts +78 -0
  35. package/chart/range-chart-service.d.ts.map +1 -0
  36. package/chart/range-chart-service.js +162 -0
  37. package/chart/range-chart-service.js.map +1 -0
  38. package/core/grid-api.d.ts +20 -0
  39. package/core/grid-api.d.ts.map +1 -1
  40. package/core/grid-api.js +41 -0
  41. package/core/grid-api.js.map +1 -1
  42. package/core/grid-context.d.ts +7 -0
  43. package/core/grid-context.d.ts.map +1 -1
  44. package/core/grid-core.d.ts.map +1 -1
  45. package/core/grid-core.js +21 -9
  46. package/core/grid-core.js.map +1 -1
  47. package/icons/icon-sets/core-icons.js +1 -1
  48. package/icons/icon-sets/core-icons.js.map +1 -1
  49. package/index.d.ts +1 -0
  50. package/index.d.ts.map +1 -1
  51. package/package.json +57 -70
  52. package/renderer/body-renderer.d.ts.map +1 -1
  53. package/renderer/body-renderer.js +21 -4
  54. package/renderer/body-renderer.js.map +1 -1
  55. package/renderer/cell-renderer.js +1 -1
  56. package/renderer/cell-renderer.js.map +1 -1
  57. package/renderer/column-menu.js +24 -25
  58. package/renderer/column-menu.js.map +1 -1
  59. package/renderer/column-style-manager.d.ts.map +1 -1
  60. package/renderer/column-style-manager.js +11 -2
  61. package/renderer/column-style-manager.js.map +1 -1
  62. package/renderer/grid-renderer.js +1 -1
  63. package/renderer/grid-renderer.js.map +1 -1
  64. package/renderer/header-renderer.d.ts.map +1 -1
  65. package/renderer/header-renderer.js +17 -3
  66. package/renderer/header-renderer.js.map +1 -1
  67. package/renderer/row-drag-renderer.d.ts +18 -0
  68. package/renderer/row-drag-renderer.d.ts.map +1 -1
  69. package/renderer/row-drag-renderer.js +53 -0
  70. package/renderer/row-drag-renderer.js.map +1 -1
  71. package/styles/base/cells.css.d.ts +9 -0
  72. package/styles/base/cells.css.d.ts.map +1 -0
  73. package/styles/base/cells.css.js +233 -0
  74. package/styles/base/cells.css.js.map +1 -0
  75. package/styles/base/chart-config.css.d.ts +10 -0
  76. package/styles/base/chart-config.css.d.ts.map +1 -0
  77. package/styles/base/chart-config.css.js +126 -0
  78. package/styles/base/chart-config.css.js.map +1 -0
  79. package/styles/base/chart-controls.css.d.ts +7 -0
  80. package/styles/base/chart-controls.css.d.ts.map +1 -0
  81. package/styles/base/chart-controls.css.js +165 -0
  82. package/styles/base/chart-controls.css.js.map +1 -0
  83. package/styles/base/chart-panel.css.d.ts +9 -0
  84. package/styles/base/chart-panel.css.d.ts.map +1 -0
  85. package/styles/base/chart-panel.css.js +148 -0
  86. package/styles/base/chart-panel.css.js.map +1 -0
  87. package/styles/base/column-context-menu.css.d.ts +9 -0
  88. package/styles/base/column-context-menu.css.d.ts.map +1 -0
  89. package/styles/base/column-context-menu.css.js +153 -0
  90. package/styles/base/column-context-menu.css.js.map +1 -0
  91. package/styles/base/column-group-header.css.d.ts +9 -0
  92. package/styles/base/column-group-header.css.d.ts.map +1 -0
  93. package/styles/base/column-group-header.css.js +270 -0
  94. package/styles/base/column-group-header.css.js.map +1 -0
  95. package/styles/base/context-menu.css.d.ts +9 -0
  96. package/styles/base/context-menu.css.d.ts.map +1 -0
  97. package/styles/base/context-menu.css.js +103 -0
  98. package/styles/base/context-menu.css.js.map +1 -0
  99. package/styles/base/editors.css.d.ts +9 -0
  100. package/styles/base/editors.css.d.ts.map +1 -0
  101. package/styles/base/editors.css.js +291 -0
  102. package/styles/base/editors.css.js.map +1 -0
  103. package/styles/base/filter.css.d.ts +9 -0
  104. package/styles/base/filter.css.d.ts.map +1 -0
  105. package/styles/base/filter.css.js +312 -0
  106. package/styles/base/filter.css.js.map +1 -0
  107. package/styles/base/footer.css.d.ts +9 -0
  108. package/styles/base/footer.css.d.ts.map +1 -0
  109. package/styles/base/footer.css.js +78 -0
  110. package/styles/base/footer.css.js.map +1 -0
  111. package/styles/base/group-bar-search.css.d.ts +9 -0
  112. package/styles/base/group-bar-search.css.d.ts.map +1 -0
  113. package/styles/base/group-bar-search.css.js +86 -0
  114. package/styles/base/group-bar-search.css.js.map +1 -0
  115. package/styles/base/group-drop-zone.css.d.ts +9 -0
  116. package/styles/base/group-drop-zone.css.d.ts.map +1 -0
  117. package/styles/base/group-drop-zone.css.js +173 -0
  118. package/styles/base/group-drop-zone.css.js.map +1 -0
  119. package/styles/base/header.css.d.ts +9 -0
  120. package/styles/base/header.css.d.ts.map +1 -0
  121. package/styles/base/header.css.js +206 -0
  122. package/styles/base/header.css.js.map +1 -0
  123. package/styles/base/master-detail.css.d.ts +9 -0
  124. package/styles/base/master-detail.css.d.ts.map +1 -0
  125. package/styles/base/master-detail.css.js +202 -0
  126. package/styles/base/master-detail.css.js.map +1 -0
  127. package/styles/base/misc.css.d.ts +9 -0
  128. package/styles/base/misc.css.d.ts.map +1 -0
  129. package/styles/base/misc.css.js +65 -0
  130. package/styles/base/misc.css.js.map +1 -0
  131. package/styles/base/panels.css.d.ts +9 -0
  132. package/styles/base/panels.css.d.ts.map +1 -0
  133. package/styles/base/panels.css.js +162 -0
  134. package/styles/base/panels.css.js.map +1 -0
  135. package/styles/base/photon-ai.css.d.ts +9 -0
  136. package/styles/base/photon-ai.css.d.ts.map +1 -0
  137. package/styles/base/photon-ai.css.js +200 -0
  138. package/styles/base/photon-ai.css.js.map +1 -0
  139. package/styles/base/root.css.d.ts +9 -0
  140. package/styles/base/root.css.d.ts.map +1 -0
  141. package/styles/base/root.css.js +56 -0
  142. package/styles/base/root.css.js.map +1 -0
  143. package/styles/base/row-drag.css.d.ts +9 -0
  144. package/styles/base/row-drag.css.d.ts.map +1 -0
  145. package/styles/base/row-drag.css.js +93 -0
  146. package/styles/base/row-drag.css.js.map +1 -0
  147. package/styles/base/row-group.css.d.ts +9 -0
  148. package/styles/base/row-group.css.d.ts.map +1 -0
  149. package/styles/base/row-group.css.js +79 -0
  150. package/styles/base/row-group.css.js.map +1 -0
  151. package/styles/base/rows.css.d.ts +9 -0
  152. package/styles/base/rows.css.d.ts.map +1 -0
  153. package/styles/base/rows.css.js +83 -0
  154. package/styles/base/rows.css.js.map +1 -0
  155. package/styles/base/scrollbars.css.d.ts +9 -0
  156. package/styles/base/scrollbars.css.d.ts.map +1 -0
  157. package/styles/base/scrollbars.css.js +56 -0
  158. package/styles/base/scrollbars.css.js.map +1 -0
  159. package/styles/base/sparkline.css.d.ts +9 -0
  160. package/styles/base/sparkline.css.d.ts.map +1 -0
  161. package/styles/base/sparkline.css.js +109 -0
  162. package/styles/base/sparkline.css.js.map +1 -0
  163. package/styles/base/tooltip.css.d.ts +9 -0
  164. package/styles/base/tooltip.css.d.ts.map +1 -0
  165. package/styles/base/tooltip.css.js +39 -0
  166. package/styles/base/tooltip.css.js.map +1 -0
  167. package/styles/base/tree.css.d.ts +9 -0
  168. package/styles/base/tree.css.d.ts.map +1 -0
  169. package/styles/base/tree.css.js +78 -0
  170. package/styles/base/tree.css.js.map +1 -0
  171. package/styles/base-styles.d.ts.map +1 -1
  172. package/styles/base-styles.js +60 -3026
  173. package/styles/base-styles.js.map +1 -1
  174. package/styles/themes/theme-alpine.d.ts +1 -1
  175. package/styles/themes/theme-alpine.d.ts.map +1 -1
  176. package/styles/themes/theme-alpine.js +1 -1
  177. package/styles/themes/theme-balham.d.ts +1 -1
  178. package/styles/themes/theme-balham.d.ts.map +1 -1
  179. package/styles/themes/theme-balham.js +0 -1
  180. package/styles/themes/theme-balham.js.map +1 -1
  181. package/styles/themes/theme-dark.d.ts +1 -1
  182. package/styles/themes/theme-dark.d.ts.map +1 -1
  183. package/styles/themes/theme-dark.js +1 -1
  184. package/styles/themes/theme-material.d.ts +1 -1
  185. package/styles/themes/theme-material.d.ts.map +1 -1
  186. package/styles/themes/theme-material.js +0 -1
  187. package/styles/themes/theme-material.js.map +1 -1
  188. package/styles/themes/theme-quartz.d.ts +1 -1
  189. package/styles/themes/theme-quartz.d.ts.map +1 -1
  190. package/styles/themes/theme-quartz.js +1 -1
  191. package/types/event.types.d.ts +29 -0
  192. package/types/event.types.d.ts.map +1 -1
  193. package/types/event.types.js +1 -0
  194. package/types/event.types.js.map +1 -1
  195. package/types/grid.types.d.ts +51 -0
  196. package/types/grid.types.d.ts.map +1 -1
  197. package/LICENSE +0 -1
  198. package/chart/chart-analyzer.d.ts +0 -12
  199. package/chart/chart-analyzer.d.ts.map +0 -1
  200. package/chart/chart-analyzer.js +0 -77
  201. package/chart/chart-analyzer.js.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { resolveChartTheme } from './chart-theme';
1
2
  const DEFAULTS = {
2
3
  type: 'column-grouped',
3
4
  width: 600,
@@ -9,23 +10,55 @@ const DEFAULTS = {
9
10
  barWidth: 0.75,
10
11
  lineWidth: 2,
11
12
  smooth: false,
12
- fontFamily: 'system-ui, sans-serif',
13
+ fontFamily: '',
13
14
  fontSize: 12,
14
- textColor: '#374151',
15
- gridColor: '#e5e7eb',
15
+ textColor: '',
16
+ gridColor: '',
16
17
  backgroundColor: 'transparent',
17
18
  animationDuration: 400,
19
+ title: '',
20
+ titleColor: '',
21
+ titleFontSize: 15,
22
+ titleAlign: 'left',
23
+ subtitle: '',
24
+ subtitleColor: '',
25
+ subtitleFontSize: 12,
26
+ subtitleAlign: 'left',
27
+ legendPosition: 'bottom',
28
+ xAxisTitle: '',
29
+ yAxisTitle: '',
30
+ axisTitleColor: '',
31
+ axisLabelColor: '',
32
+ axisLineColor: '',
33
+ showXTicks: true,
34
+ showYTicks: true,
35
+ showXLabels: true,
36
+ showYLabels: true,
37
+ seriesColors: {},
38
+ strokeWidth: 0,
39
+ fillOpacity: 0,
40
+ compact: false,
18
41
  };
19
42
  const APEX_COLORS = [
20
43
  '#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0',
21
44
  '#3F51B5', '#03A9F4', '#4CAF50', '#F9CE1D', '#FF9800',
22
45
  ];
23
- function assignColors(data) {
46
+ /**
47
+ * Assigns a concrete color to every dataset. Precedence: an explicit override
48
+ * in `seriesColors` (keyed by the dataset label), then a color already on the
49
+ * dataset, then the theme `palette` cycled by index.
50
+ *
51
+ * @param data - Source chart data (datasets may lack colors).
52
+ * @param palette - Theme-resolved series palette.
53
+ * @param seriesColors - Per-label color overrides from the chart model.
54
+ */
55
+ function assignColors(data, palette, seriesColors) {
56
+ const pal = palette.length > 0 ? palette : APEX_COLORS;
24
57
  return {
25
58
  labels: data.labels,
26
59
  datasets: data.datasets.map((ds, i) => ({
27
60
  ...ds,
28
- color: ds.color ?? APEX_COLORS[i % APEX_COLORS.length],
61
+ color: seriesColors[ds.label] ?? ds.color ?? pal[i % pal.length],
29
62
  })),
30
63
  };
31
64
  }
@@ -45,6 +78,12 @@ export class ChartRenderer {
45
78
  this.seriesScales = [];
46
79
  /** Active RAF IDs for per-series toggle animations, keyed by dataset index. */
47
80
  this.seriesToggleRafs = new Map();
81
+ /**
82
+ * Theme colors/palette resolved once per {@link render} / {@link toggleSeries}
83
+ * call and reused across every animation and hover frame — never re-resolved
84
+ * per frame (that would force layout via `getComputedStyle`).
85
+ */
86
+ this.theme = null;
48
87
  this.canvas = canvas;
49
88
  const ctx = canvas.getContext('2d');
50
89
  if (!ctx)
@@ -52,11 +91,30 @@ export class ChartRenderer {
52
91
  this.ctx = ctx;
53
92
  this.attachEvents();
54
93
  }
94
+ /**
95
+ * Resolves theme tokens once and back-fills any color/font option the caller
96
+ * left blank (empty string) with the theme value. Explicit overrides win over
97
+ * theme defaults. Mutates `options` in place and caches the resolved theme.
98
+ */
99
+ prepareOptions(options) {
100
+ const theme = resolveChartTheme(this.canvas);
101
+ this.theme = theme;
102
+ if (!options.fontFamily)
103
+ options.fontFamily = theme.fontFamily;
104
+ options.textColor = options.axisLabelColor || options.textColor || theme.textColor;
105
+ options.gridColor = options.axisLineColor || options.gridColor || theme.gridColor;
106
+ }
107
+ /** Theme palette color for a given series/slice index, cycling as needed. */
108
+ paletteColor(i) {
109
+ const pal = this.theme?.palette ?? APEX_COLORS;
110
+ return pal[i % pal.length];
111
+ }
55
112
  render(data, opts = { type: 'column-grouped' }) {
56
113
  const options = { ...DEFAULTS, ...opts };
57
114
  this.canvas.width = options.width;
58
115
  this.canvas.height = options.height;
59
- const coloredData = assignColors(data);
116
+ this.prepareOptions(options);
117
+ const coloredData = assignColors(data, this.theme.palette, options.seriesColors);
60
118
  this.lastData = coloredData;
61
119
  this.lastOptions = options;
62
120
  // Full re-render: reset every series to fully visible
@@ -98,7 +156,8 @@ export class ChartRenderer {
98
156
  */
99
157
  toggleSeries(index, toVisible, data, opts = { type: 'column-grouped' }) {
100
158
  const options = { ...DEFAULTS, ...opts };
101
- const coloredData = assignColors(data);
159
+ this.prepareOptions(options);
160
+ const coloredData = assignColors(data, this.theme.palette, options.seriesColors);
102
161
  this.lastData = coloredData;
103
162
  this.lastOptions = options;
104
163
  while (this.seriesScales.length < coloredData.datasets.length)
@@ -276,7 +335,7 @@ export class ChartRenderer {
276
335
  const { ctx } = this;
277
336
  const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
278
337
  const plotLeft = 80;
279
- const plotTop = 10;
338
+ const plotTop = 10 + this.titleBandHeight(opts);
280
339
  const plotRight = opts.width - 20;
281
340
  const plotBottom = opts.height - legendH - 30;
282
341
  const plotH = plotBottom - plotTop;
@@ -420,12 +479,60 @@ export class ChartRenderer {
420
479
  };
421
480
  this.rafId = requestAnimationFrame(tick);
422
481
  }
482
+ /**
483
+ * Vertical space (px) reserved at the top of the canvas for the title and
484
+ * subtitle bands. Returns 0 when neither is set so no space is wasted.
485
+ */
486
+ titleBandHeight(opts) {
487
+ let h = 0;
488
+ if (opts.title)
489
+ h += opts.titleFontSize + 8;
490
+ if (opts.subtitle)
491
+ h += opts.subtitleFontSize + 6;
492
+ return h > 0 ? h + 6 : 0;
493
+ }
494
+ /** Whether a shared series legend should be drawn. */
495
+ legendActive(opts, data) {
496
+ return opts.showLegend && data.datasets.length > 1;
497
+ }
498
+ /**
499
+ * Whether the legend may be placed on a side (left/right). Only cartesian
500
+ * charts (which route layout through {@link getPlotArea}) reserve side space;
501
+ * pie/polar/funnel/bar keep the legend on the bottom to avoid overlap.
502
+ */
503
+ supportsSideLegend(opts) {
504
+ const type = opts.type;
505
+ return type.startsWith('column') || type === 'line' || type === 'area' || type === 'scatter';
506
+ }
507
+ /** Effective legend position, clamped to bottom for non-cartesian charts. */
508
+ effectiveLegendPosition(opts) {
509
+ if (!this.supportsSideLegend(opts))
510
+ return 'bottom';
511
+ return opts.legendPosition;
512
+ }
423
513
  getPlotArea(opts, data) {
424
- const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
425
- const plotLeft = 65;
426
- const plotTop = 20;
427
- const plotRight = opts.width - 20;
428
- const plotBottom = opts.height - legendH - 36;
514
+ const legendOn = this.legendActive(opts, data);
515
+ const pos = this.effectiveLegendPosition(opts);
516
+ const band = ChartRenderer.LEGEND_BAND;
517
+ const side = ChartRenderer.LEGEND_SIDE;
518
+ const titleH = this.titleBandHeight(opts);
519
+ const xTitleH = opts.xAxisTitle ? opts.fontSize + 10 : 0;
520
+ const yTitleW = opts.yAxisTitle ? opts.fontSize + 12 : 0;
521
+ // Compact previews use a uniform padding gutter so the plot fills the canvas.
522
+ let plotTop = opts.compact ? opts.padding + titleH : 20 + titleH;
523
+ let plotBottom = opts.compact ? opts.height - opts.padding : opts.height - 36 - xTitleH;
524
+ let plotLeft = opts.compact ? opts.padding : 65 + yTitleW;
525
+ let plotRight = opts.width - (opts.compact ? opts.padding : 20);
526
+ if (legendOn) {
527
+ if (pos === 'top')
528
+ plotTop += band;
529
+ else if (pos === 'bottom')
530
+ plotBottom -= band;
531
+ else if (pos === 'left')
532
+ plotLeft += side;
533
+ else if (pos === 'right')
534
+ plotRight -= side;
535
+ }
429
536
  return {
430
537
  plotLeft,
431
538
  plotTop,
@@ -433,7 +540,7 @@ export class ChartRenderer {
433
540
  plotBottom,
434
541
  plotW: plotRight - plotLeft,
435
542
  plotH: plotBottom - plotTop,
436
- legendH,
543
+ legendH: legendOn && (pos === 'top' || pos === 'bottom') ? band : 0,
437
544
  };
438
545
  }
439
546
  draw(data, options, progress = 1) {
@@ -489,10 +596,81 @@ export class ChartRenderer {
489
596
  this.drawColumnGrouped(data, options, progress);
490
597
  break;
491
598
  }
492
- if (options.showLegend && data.datasets.length > 1) {
599
+ if (this.supportsSideLegend(options)) {
600
+ this.drawAxisTitles(options);
601
+ }
602
+ this.drawTitleBlock(options);
603
+ if (this.legendActive(options, data)) {
493
604
  this.drawLegend(data, options);
494
605
  }
495
606
  }
607
+ /**
608
+ * Draws the chart title and subtitle in the reserved top band, honoring the
609
+ * configured color (falling back to theme) and alignment. No-op when both are
610
+ * empty.
611
+ */
612
+ drawTitleBlock(opts) {
613
+ if (!opts.title && !opts.subtitle)
614
+ return;
615
+ const { ctx } = this;
616
+ const theme = this.theme;
617
+ ctx.save();
618
+ ctx.textBaseline = 'alphabetic';
619
+ let y = 20;
620
+ if (opts.title) {
621
+ ctx.font = `600 ${opts.titleFontSize}px ${opts.fontFamily}`;
622
+ ctx.fillStyle = opts.titleColor || theme?.textColor || opts.textColor;
623
+ const x = this.alignedX(opts.titleAlign, opts.width);
624
+ ctx.textAlign = opts.titleAlign;
625
+ ctx.fillText(opts.title, x, y);
626
+ y += opts.titleFontSize + 4;
627
+ }
628
+ if (opts.subtitle) {
629
+ ctx.font = `${opts.subtitleFontSize}px ${opts.fontFamily}`;
630
+ ctx.fillStyle = opts.subtitleColor || theme?.mutedColor || opts.textColor;
631
+ const x = this.alignedX(opts.subtitleAlign, opts.width);
632
+ ctx.textAlign = opts.subtitleAlign;
633
+ ctx.fillText(opts.subtitle, x, y + opts.subtitleFontSize);
634
+ }
635
+ ctx.restore();
636
+ }
637
+ /** X coordinate for a given horizontal alignment across the canvas width. */
638
+ alignedX(align, width) {
639
+ if (align === 'center')
640
+ return width / 2;
641
+ if (align === 'right')
642
+ return width - 20;
643
+ return 20;
644
+ }
645
+ /**
646
+ * Draws the x- and y-axis titles (cartesian charts only). The y-axis title is
647
+ * rotated −90° and centered along the left margin; the x-axis title sits below
648
+ * the category labels.
649
+ */
650
+ drawAxisTitles(opts) {
651
+ if (!opts.xAxisTitle && !opts.yAxisTitle)
652
+ return;
653
+ const { ctx } = this;
654
+ const color = opts.axisTitleColor || this.theme?.textColor || opts.textColor;
655
+ ctx.save();
656
+ ctx.fillStyle = color;
657
+ ctx.font = `600 ${opts.fontSize}px ${opts.fontFamily}`;
658
+ if (opts.xAxisTitle) {
659
+ ctx.textAlign = 'center';
660
+ ctx.textBaseline = 'bottom';
661
+ ctx.fillText(opts.xAxisTitle, opts.width / 2, opts.height - 4);
662
+ }
663
+ if (opts.yAxisTitle) {
664
+ ctx.save();
665
+ ctx.translate(14, opts.height / 2);
666
+ ctx.rotate(-Math.PI / 2);
667
+ ctx.textAlign = 'center';
668
+ ctx.textBaseline = 'top';
669
+ ctx.fillText(opts.yAxisTitle, 0, 0);
670
+ ctx.restore();
671
+ }
672
+ ctx.restore();
673
+ }
496
674
  // ── Column Grouped ──────────────────────────────────────────────────────────
497
675
  drawColumnGrouped(data, opts, progress) {
498
676
  const { ctx } = this;
@@ -559,28 +737,43 @@ export class ChartRenderer {
559
737
  drawColumnStacked(data, opts, progress) {
560
738
  const { ctx } = this;
561
739
  const { plotLeft, plotTop, plotBottom, plotRight, plotW, plotH } = this.getPlotArea(opts, data);
740
+ // Match the grouped chart: with multiple series, scale each series to its own
741
+ // maximum so a small-scale series (e.g. Age next to Salary) stays visible
742
+ // instead of collapsing to a sub-pixel sliver. Each series then occupies an
743
+ // equal band of the plot height. Falls back to true absolute stacking for a
744
+ // single series.
745
+ const nD = data.datasets.length;
746
+ const usePerSeries = nD > 1;
747
+ const seriesMaxes = data.datasets.map((ds) => this.niceMax(Math.max(...ds.data, 0)));
562
748
  const stackTotals = data.labels.map((_, gi) => data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0));
563
- const rawMax = Math.max(...stackTotals, 0);
564
- const maxVal = this.niceMax(rawMax);
565
- if (opts.showGrid)
566
- this.drawGridLines(plotLeft, plotTop, plotW, plotH, maxVal, opts);
567
- else
749
+ const maxVal = this.niceMax(Math.max(...stackTotals, 0));
750
+ if (opts.showGrid) {
751
+ if (usePerSeries)
752
+ this.drawGridLinesRelative(plotLeft, plotTop, plotW, plotH, opts);
753
+ else
754
+ this.drawGridLines(plotLeft, plotTop, plotW, plotH, maxVal, opts);
755
+ }
756
+ else {
568
757
  this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
758
+ }
569
759
  const nGroups = data.labels.length;
570
760
  const groupWidth = plotW / Math.max(nGroups, 1);
571
761
  const barW = groupWidth * opts.barWidth;
762
+ const band = plotH / Math.max(nD, 1);
572
763
  const labelStep = this.getLabelStep(nGroups, plotW, opts.fontSize);
573
764
  const labelAlpha = Math.min(1, progress * 2.5);
574
765
  for (let gi = 0; gi < nGroups; gi++) {
575
766
  const barX = plotLeft + gi * groupWidth + (groupWidth - barW) / 2;
576
767
  let currentY = plotBottom;
577
- for (let di = 0; di < data.datasets.length; di++) {
768
+ for (let di = 0; di < nD; di++) {
578
769
  const value = data.datasets[di].data[gi] ?? 0;
579
- const segH = (value / maxVal) * plotH * progress;
770
+ const segH = (usePerSeries
771
+ ? (value / (seriesMaxes[di] || 1)) * band
772
+ : (value / maxVal) * plotH) * progress;
580
773
  const segY = currentY - segH;
581
774
  ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
582
775
  ctx.beginPath();
583
- if (di === data.datasets.length - 1) {
776
+ if (di === nD - 1) {
584
777
  ctx.roundRect(barX, segY, Math.max(barW, 1), Math.max(segH, 0), [3, 3, 0, 0]);
585
778
  }
586
779
  else {
@@ -607,22 +800,30 @@ export class ChartRenderer {
607
800
  this.drawGridLines(plotLeft, plotTop, plotW, plotH, 100, opts);
608
801
  else
609
802
  this.drawAxes(plotLeft, plotTop, plotRight, plotBottom, opts);
803
+ // Weight each series by its own maximum so a small-scale series keeps a
804
+ // visible share of the 100% bar instead of rounding to ~0%.
805
+ const nD = data.datasets.length;
806
+ const usePerSeries = nD > 1;
807
+ const seriesMaxes = data.datasets.map((ds) => this.niceMax(Math.max(...ds.data, 0)));
610
808
  const nGroups = data.labels.length;
611
809
  const groupWidth = plotW / Math.max(nGroups, 1);
612
810
  const barW = groupWidth * opts.barWidth;
613
811
  const labelStep = this.getLabelStep(nGroups, plotW, opts.fontSize);
614
812
  const labelAlpha = Math.min(1, progress * 2.5);
615
813
  for (let gi = 0; gi < nGroups; gi++) {
616
- const total = data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0) || 1;
814
+ const shares = data.datasets.map((ds, di) => {
815
+ const value = ds.data[gi] ?? 0;
816
+ return usePerSeries ? value / (seriesMaxes[di] || 1) : value;
817
+ });
818
+ const total = shares.reduce((sum, s) => sum + s, 0) || 1;
617
819
  const barX = plotLeft + gi * groupWidth + (groupWidth - barW) / 2;
618
820
  let currentY = plotBottom;
619
- for (let di = 0; di < data.datasets.length; di++) {
620
- const value = data.datasets[di].data[gi] ?? 0;
621
- const segH = (value / total) * plotH * progress;
821
+ for (let di = 0; di < nD; di++) {
822
+ const segH = (shares[di] / total) * plotH * progress;
622
823
  const segY = currentY - segH;
623
824
  ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
624
825
  ctx.beginPath();
625
- if (di === data.datasets.length - 1) {
826
+ if (di === nD - 1) {
626
827
  ctx.roundRect(barX, segY, Math.max(barW, 1), Math.max(segH, 0), [3, 3, 0, 0]);
627
828
  }
628
829
  else {
@@ -645,11 +846,11 @@ export class ChartRenderer {
645
846
  drawBarGrouped(data, opts, progress) {
646
847
  const { ctx } = this;
647
848
  const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
648
- const labelAreaW = 80;
849
+ const labelAreaW = opts.compact ? opts.padding : 80;
649
850
  const plotLeft = labelAreaW;
650
- const plotTop = 10;
651
- const plotRight = opts.width - 20;
652
- const plotBottom = opts.height - legendH - 30;
851
+ const plotTop = (opts.compact ? opts.padding : 10) + this.titleBandHeight(opts);
852
+ const plotRight = opts.width - (opts.compact ? opts.padding : 20);
853
+ const plotBottom = opts.height - legendH - (opts.compact ? opts.padding : 30);
653
854
  const plotW = plotRight - plotLeft;
654
855
  const plotH = plotBottom - plotTop;
655
856
  const nDatasets = data.datasets.length;
@@ -723,13 +924,16 @@ export class ChartRenderer {
723
924
  drawBarStacked(data, opts, progress) {
724
925
  const { ctx } = this;
725
926
  const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
726
- const labelAreaW = 80;
927
+ const labelAreaW = opts.compact ? opts.padding : 80;
727
928
  const plotLeft = labelAreaW;
728
- const plotTop = 10;
729
- const plotRight = opts.width - 20;
730
- const plotBottom = opts.height - legendH - 30;
929
+ const plotTop = (opts.compact ? opts.padding : 10) + this.titleBandHeight(opts);
930
+ const plotRight = opts.width - (opts.compact ? opts.padding : 20);
931
+ const plotBottom = opts.height - legendH - (opts.compact ? opts.padding : 30);
731
932
  const plotW = plotRight - plotLeft;
732
933
  const plotH = plotBottom - plotTop;
934
+ const nD = data.datasets.length;
935
+ const usePerSeries = nD > 1;
936
+ const seriesMaxes = data.datasets.map((ds) => this.niceMax(Math.max(...ds.data, 0)));
733
937
  const stackTotals = data.labels.map((_, gi) => data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0));
734
938
  const rawMax = Math.max(...stackTotals, 0);
735
939
  const maxVal = this.niceMax(rawMax);
@@ -748,7 +952,9 @@ export class ChartRenderer {
748
952
  ctx.fillStyle = opts.textColor;
749
953
  ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
750
954
  ctx.textAlign = 'center';
751
- ctx.fillText(this.formatNum((maxVal * i) / steps), x, plotBottom + 14);
955
+ // Per-series normalization uses a relative (0–100%) axis, like grouped.
956
+ const xLabel = usePerSeries ? `${Math.round((100 * i) / steps)}%` : this.formatNum((maxVal * i) / steps);
957
+ ctx.fillText(xLabel, x, plotBottom + 14);
752
958
  }
753
959
  ctx.setLineDash([]);
754
960
  ctx.strokeStyle = opts.gridColor;
@@ -768,16 +974,19 @@ export class ChartRenderer {
768
974
  const nGroups = data.labels.length;
769
975
  const groupH = plotH / Math.max(nGroups, 1);
770
976
  const barH = groupH * opts.barWidth;
977
+ const band = plotW / Math.max(nD, 1);
771
978
  const labelStep = this.getLabelStep(nGroups, plotH, opts.fontSize);
772
979
  for (let gi = 0; gi < nGroups; gi++) {
773
980
  const barY = plotTop + gi * groupH + (groupH - barH) / 2;
774
981
  let currentX = plotLeft;
775
- for (let di = 0; di < data.datasets.length; di++) {
982
+ for (let di = 0; di < nD; di++) {
776
983
  const value = data.datasets[di].data[gi] ?? 0;
777
- const segW = (value / maxVal) * plotW * progress;
984
+ const segW = (usePerSeries
985
+ ? (value / (seriesMaxes[di] || 1)) * band
986
+ : (value / maxVal) * plotW) * progress;
778
987
  ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
779
988
  ctx.beginPath();
780
- if (di === data.datasets.length - 1) {
989
+ if (di === nD - 1) {
781
990
  ctx.roundRect(currentX, barY, Math.max(segW, 0), Math.max(barH, 1), [0, 3, 3, 0]);
782
991
  }
783
992
  else {
@@ -800,11 +1009,11 @@ export class ChartRenderer {
800
1009
  drawBar100Stacked(data, opts, progress) {
801
1010
  const { ctx } = this;
802
1011
  const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
803
- const labelAreaW = 80;
1012
+ const labelAreaW = opts.compact ? opts.padding : 80;
804
1013
  const plotLeft = labelAreaW;
805
- const plotTop = 10;
806
- const plotRight = opts.width - 20;
807
- const plotBottom = opts.height - legendH - 30;
1014
+ const plotTop = (opts.compact ? opts.padding : 10) + this.titleBandHeight(opts);
1015
+ const plotRight = opts.width - (opts.compact ? opts.padding : 20);
1016
+ const plotBottom = opts.height - legendH - (opts.compact ? opts.padding : 30);
808
1017
  const plotW = plotRight - plotLeft;
809
1018
  const plotH = plotBottom - plotTop;
810
1019
  const labelAlpha = Math.min(1, progress * 2.5);
@@ -843,13 +1052,21 @@ export class ChartRenderer {
843
1052
  const groupH = plotH / Math.max(nGroups, 1);
844
1053
  const barH = groupH * opts.barWidth;
845
1054
  const labelStep = this.getLabelStep(nGroups, plotH, opts.fontSize);
1055
+ // Weight each series by its own maximum so a small-scale series keeps a
1056
+ // visible share of the 100% bar instead of rounding to ~0%.
1057
+ const nD = data.datasets.length;
1058
+ const usePerSeries = nD > 1;
1059
+ const seriesMaxes = data.datasets.map((ds) => this.niceMax(Math.max(...ds.data, 0)));
846
1060
  for (let gi = 0; gi < nGroups; gi++) {
847
- const total = data.datasets.reduce((sum, ds) => sum + (ds.data[gi] ?? 0), 0) || 1;
1061
+ const shares = data.datasets.map((ds, di) => {
1062
+ const value = ds.data[gi] ?? 0;
1063
+ return usePerSeries ? value / (seriesMaxes[di] || 1) : value;
1064
+ });
1065
+ const total = shares.reduce((sum, s) => sum + s, 0) || 1;
848
1066
  const barY = plotTop + gi * groupH + (groupH - barH) / 2;
849
1067
  let currentX = plotLeft;
850
- for (let di = 0; di < data.datasets.length; di++) {
851
- const value = data.datasets[di].data[gi] ?? 0;
852
- const segW = (value / total) * plotW * progress;
1068
+ for (let di = 0; di < nD; di++) {
1069
+ const segW = (shares[di] / total) * plotW * progress;
853
1070
  ctx.fillStyle = data.datasets[di].color ?? APEX_COLORS[di % APEX_COLORS.length];
854
1071
  ctx.beginPath();
855
1072
  if (di === data.datasets.length - 1) {
@@ -892,7 +1109,7 @@ export class ChartRenderer {
892
1109
  ctx.rect(plotLeft, plotTop, plotW, plotH + 1);
893
1110
  ctx.clip();
894
1111
  ctx.strokeStyle = ds.color ?? APEX_COLORS[0];
895
- ctx.lineWidth = opts.lineWidth;
1112
+ ctx.lineWidth = opts.strokeWidth > 0 ? opts.strokeWidth : opts.lineWidth;
896
1113
  ctx.lineCap = 'round';
897
1114
  ctx.lineJoin = 'round';
898
1115
  ctx.beginPath();
@@ -960,10 +1177,10 @@ export class ChartRenderer {
960
1177
  ctx.lineTo(getX(Math.min(visibleCount, nPoints) - 1), plotBottom);
961
1178
  ctx.lineTo(getX(0), plotBottom);
962
1179
  ctx.closePath();
963
- ctx.fillStyle = color + '26';
1180
+ ctx.fillStyle = color + this.alphaHex(opts.fillOpacity > 0 ? opts.fillOpacity : 0.15);
964
1181
  ctx.fill();
965
1182
  ctx.strokeStyle = color;
966
- ctx.lineWidth = opts.lineWidth;
1183
+ ctx.lineWidth = opts.strokeWidth > 0 ? opts.strokeWidth : opts.lineWidth;
967
1184
  ctx.lineCap = 'round';
968
1185
  ctx.lineJoin = 'round';
969
1186
  ctx.beginPath();
@@ -994,15 +1211,15 @@ export class ChartRenderer {
994
1211
  const { ctx } = this;
995
1212
  const legendH = opts.showLegend ? 40 : 0;
996
1213
  const cx = opts.width / 2;
997
- const cy = (opts.height - legendH) / 2;
998
- const radius = Math.min(opts.width, opts.height - legendH) / 2 - 20;
1214
+ const cy = this.titleBandHeight(opts) + (opts.height - legendH - this.titleBandHeight(opts)) / 2;
1215
+ const radius = Math.min(opts.width, opts.height - legendH - this.titleBandHeight(opts)) / 2 - (opts.compact ? opts.padding : 20);
999
1216
  const innerRadius = isDoughnut ? radius * 0.5 : 0;
1000
1217
  const values = data.datasets[0]?.data ?? [];
1001
1218
  const total = values.reduce((a, b) => a + b, 0) || 1;
1002
1219
  let startAngle = -Math.PI / 2;
1003
1220
  for (let i = 0; i < values.length; i++) {
1004
1221
  const sliceAngle = (values[i] / total) * Math.PI * 2 * progress;
1005
- const color = APEX_COLORS[i % APEX_COLORS.length];
1222
+ const color = this.paletteColor(i);
1006
1223
  ctx.beginPath();
1007
1224
  ctx.moveTo(cx, cy);
1008
1225
  ctx.arc(cx, cy, radius, startAngle, startAngle + sliceAngle);
@@ -1090,8 +1307,8 @@ export class ChartRenderer {
1090
1307
  const { ctx } = this;
1091
1308
  const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
1092
1309
  const cx = opts.width / 2;
1093
- const cy = (opts.height - legendH) / 2;
1094
- const radius = Math.min(opts.width, opts.height - legendH) / 2 - 30;
1310
+ const cy = this.titleBandHeight(opts) + (opts.height - legendH - this.titleBandHeight(opts)) / 2;
1311
+ const radius = Math.min(opts.width, opts.height - legendH - this.titleBandHeight(opts)) / 2 - (opts.compact ? opts.padding : 30);
1095
1312
  const labels = data.labels;
1096
1313
  const nSpokes = labels.length;
1097
1314
  if (nSpokes < 3)
@@ -1164,12 +1381,12 @@ export class ChartRenderer {
1164
1381
  const maxVal = Math.max(...values, 0) || 1;
1165
1382
  const n = labels.length;
1166
1383
  const legendH = opts.showLegend && data.datasets.length > 1 ? 40 : 0;
1167
- const labelAreaW = 90;
1384
+ const labelAreaW = opts.compact ? opts.padding : 90;
1168
1385
  const plotLeft = labelAreaW;
1169
- const plotRight = opts.width - 20;
1386
+ const plotRight = opts.width - (opts.compact ? opts.padding : 20);
1170
1387
  const plotWidth = plotRight - plotLeft;
1171
- const plotTop = 10;
1172
- const plotBottom = opts.height - legendH - 10;
1388
+ const plotTop = (opts.compact ? opts.padding : 10) + this.titleBandHeight(opts);
1389
+ const plotBottom = opts.height - legendH - (opts.compact ? opts.padding : 10);
1173
1390
  const plotH = plotBottom - plotTop;
1174
1391
  const itemH = Math.max((plotH / Math.max(n, 1)) - 2, 1);
1175
1392
  const cx = plotLeft + plotWidth / 2;
@@ -1179,7 +1396,7 @@ export class ChartRenderer {
1179
1396
  const barW = pct * plotWidth * progress;
1180
1397
  const barX = cx - barW / 2;
1181
1398
  const barY = plotTop + i * (itemH + 2);
1182
- const color = APEX_COLORS[i % APEX_COLORS.length];
1399
+ const color = this.paletteColor(i);
1183
1400
  ctx.fillStyle = color;
1184
1401
  ctx.beginPath();
1185
1402
  ctx.roundRect(barX, barY, Math.max(barW, 0), itemH, 3);
@@ -1210,10 +1427,12 @@ export class ChartRenderer {
1210
1427
  ctx.moveTo(left, y);
1211
1428
  ctx.lineTo(left + width, y);
1212
1429
  ctx.stroke();
1213
- ctx.fillStyle = opts.textColor;
1214
- ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1215
- ctx.textAlign = 'right';
1216
- ctx.fillText(this.formatNum((maxVal * i) / steps), left - 6, y + 4);
1430
+ if (opts.showYLabels) {
1431
+ ctx.fillStyle = opts.textColor;
1432
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1433
+ ctx.textAlign = 'right';
1434
+ ctx.fillText(this.formatNum((maxVal * i) / steps), left - 6, y + 4);
1435
+ }
1217
1436
  }
1218
1437
  ctx.setLineDash([]);
1219
1438
  ctx.strokeStyle = opts.gridColor;
@@ -1243,10 +1462,12 @@ export class ChartRenderer {
1243
1462
  ctx.moveTo(left, y);
1244
1463
  ctx.lineTo(left + width, y);
1245
1464
  ctx.stroke();
1246
- ctx.fillStyle = opts.textColor;
1247
- ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1248
- ctx.textAlign = 'right';
1249
- ctx.fillText(`${Math.round((100 * i) / steps)}%`, left - 6, y + 4);
1465
+ if (opts.showYLabels) {
1466
+ ctx.fillStyle = opts.textColor;
1467
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1468
+ ctx.textAlign = 'right';
1469
+ ctx.fillText(`${Math.round((100 * i) / steps)}%`, left - 6, y + 4);
1470
+ }
1250
1471
  }
1251
1472
  ctx.setLineDash([]);
1252
1473
  ctx.strokeStyle = opts.gridColor;
@@ -1262,8 +1483,18 @@ export class ChartRenderer {
1262
1483
  }
1263
1484
  // ── Legend ──────────────────────────────────────────────────────────────────
1264
1485
  drawLegend(data, opts) {
1486
+ const position = this.effectiveLegendPosition(opts);
1487
+ if (position === 'left' || position === 'right') {
1488
+ this.drawLegendVertical(data, opts, position);
1489
+ }
1490
+ else {
1491
+ this.drawLegendHorizontal(data, opts, position);
1492
+ }
1493
+ }
1494
+ /** Horizontal legend row along the top or bottom edge. */
1495
+ drawLegendHorizontal(data, opts, position) {
1265
1496
  const { ctx } = this;
1266
- const y = opts.height - 20;
1497
+ const y = position === 'top' ? this.titleBandHeight(opts) + 22 : opts.height - 16;
1267
1498
  const totalItems = data.datasets.length;
1268
1499
  const itemWidth = opts.width / totalItems;
1269
1500
  ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
@@ -1271,8 +1502,29 @@ export class ChartRenderer {
1271
1502
  ctx.textBaseline = 'middle';
1272
1503
  for (let i = 0; i < totalItems; i++) {
1273
1504
  const x = i * itemWidth + (itemWidth - 80) / 2;
1274
- const color = data.datasets[i].color ?? APEX_COLORS[i % APEX_COLORS.length];
1275
- ctx.fillStyle = color;
1505
+ ctx.fillStyle = data.datasets[i].color ?? this.paletteColor(i);
1506
+ ctx.beginPath();
1507
+ ctx.roundRect(x, y - 5, 12, 10, 2);
1508
+ ctx.fill();
1509
+ ctx.fillStyle = opts.textColor;
1510
+ ctx.fillText(this.truncate(data.datasets[i].label, 14), x + 16, y);
1511
+ }
1512
+ ctx.textBaseline = 'alphabetic';
1513
+ }
1514
+ /** Vertical legend stack along the left or right edge (cartesian charts only). */
1515
+ drawLegendVertical(data, opts, position) {
1516
+ const { ctx } = this;
1517
+ const rowH = opts.fontSize + 10;
1518
+ const totalItems = data.datasets.length;
1519
+ const blockH = totalItems * rowH;
1520
+ const startY = Math.max(this.titleBandHeight(opts) + 24, (opts.height - blockH) / 2);
1521
+ const x = position === 'left' ? 12 : opts.width - ChartRenderer.LEGEND_SIDE + 12;
1522
+ ctx.font = `${opts.fontSize}px ${opts.fontFamily}`;
1523
+ ctx.textAlign = 'left';
1524
+ ctx.textBaseline = 'middle';
1525
+ for (let i = 0; i < totalItems; i++) {
1526
+ const y = startY + i * rowH + rowH / 2;
1527
+ ctx.fillStyle = data.datasets[i].color ?? this.paletteColor(i);
1276
1528
  ctx.beginPath();
1277
1529
  ctx.roundRect(x, y - 5, 12, 10, 2);
1278
1530
  ctx.fill();
@@ -1317,6 +1569,15 @@ export class ChartRenderer {
1317
1569
  truncate(str, max) {
1318
1570
  return str.length > max ? str.slice(0, max - 1) + '…' : str;
1319
1571
  }
1572
+ /**
1573
+ * Converts a 0–1 opacity into a two-digit hex alpha suffix for `#rrggbb` +
1574
+ * `aa` color strings. Clamped to the valid range so out-of-bounds model values
1575
+ * never produce a malformed color.
1576
+ */
1577
+ alphaHex(opacity) {
1578
+ const clamped = Math.max(0, Math.min(1, opacity));
1579
+ return Math.round(clamped * 255).toString(16).padStart(2, '0');
1580
+ }
1320
1581
  formatNum(n) {
1321
1582
  if (n >= 1e6)
1322
1583
  return (n / 1e6).toFixed(n % 1e6 === 0 ? 0 : 1) + 'M';
@@ -1327,4 +1588,6 @@ export class ChartRenderer {
1327
1588
  return String(Math.round(n));
1328
1589
  }
1329
1590
  }
1591
+ ChartRenderer.LEGEND_BAND = 40;
1592
+ ChartRenderer.LEGEND_SIDE = 120;
1330
1593
  //# sourceMappingURL=chart-renderer.js.map