eleventy-plugin-uncharted 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -229,14 +229,16 @@ The chart automatically calculates the range from the maximum positive stack to
229
229
  | `titleY` | string | Y-axis title (scatter only, defaults to column name) |
230
230
  | `legend` | array | Custom legend labels |
231
231
  | `center` | object | Donut center content (`value`, `label`) |
232
+ | `showPercentages` | boolean | Show percentages instead of values in donut legend |
232
233
  | `animate` | boolean | Override global animation setting |
233
234
  | `format` | object | Number formatting options (see Value Formatting) |
235
+ | `rotateLabels` | boolean | Rotate X-axis labels vertically (stacked-column, dot) |
234
236
 
235
237
  ## Styling
236
238
 
237
239
  ### CSS Custom Properties
238
240
 
239
- Override the default color palette:
241
+ Override the default color palette and sizing:
240
242
 
241
243
  ```css
242
244
  :root {
@@ -249,6 +251,29 @@ Override the default color palette:
249
251
  --chart-color-7: #009688;
250
252
  --chart-color-8: #78909c;
251
253
  --chart-bg: rgba(128, 128, 128, 0.15);
254
+ --chart-height: 12rem; /* Height of bar/column/dot/scatter charts */
255
+ --chart-column-width: 1rem; /* Min width per column */
256
+ --chart-donut-size: 12rem; /* Donut chart diameter */
257
+ --chart-donut-thickness: 2.5rem; /* Donut ring thickness */
258
+ }
259
+ ```
260
+
261
+ ### Per-Chart Styling
262
+
263
+ Each chart gets a class based on its ID for targeted styling:
264
+
265
+ ```yaml
266
+ charts:
267
+ sales-growth:
268
+ type: stacked-column
269
+ file: charts/sales.csv
270
+ ```
271
+
272
+ ```css
273
+ /* Target this specific chart */
274
+ .chart-sales-growth {
275
+ --chart-height: 16rem;
276
+ --chart-color-1: #ff6b6b;
252
277
  }
253
278
  ```
254
279
 
package/css/uncharted.css CHANGED
@@ -26,10 +26,11 @@
26
26
  /* Spacing and sizing */
27
27
  --chart-gap: 0.5rem;
28
28
  --chart-bar-height: 1.5rem;
29
- --chart-column-width: 2.5rem;
29
+ --chart-column-width: 1rem;
30
30
  --chart-donut-size: 12rem;
31
31
  --chart-donut-thickness: 2.5rem;
32
32
  --chart-dot-size: 0.75rem;
33
+ --chart-height: 12rem;
33
34
  }
34
35
 
35
36
  /* ==========================================================================
@@ -131,7 +132,7 @@
131
132
  align-items: flex-end;
132
133
  min-width: 2rem;
133
134
  box-sizing: border-box;
134
- height: 12rem;
135
+ height: var(--chart-height);
135
136
  padding-top: 0.5rem;
136
137
  padding-bottom: 0;
137
138
  }
@@ -247,14 +248,31 @@
247
248
  align-items: stretch;
248
249
  }
249
250
 
250
- .chart-stacked-column .chart-columns {
251
- display: flex;
252
- gap: var(--chart-gap);
253
- align-items: stretch;
254
- height: 12rem;
251
+ .chart-stacked-column .chart-scroll {
255
252
  flex: 1;
253
+ overflow-x: auto;
254
+ overflow-y: visible;
255
+ position: relative;
256
+ }
257
+
258
+ .chart-stacked-column .chart-scroll::before {
259
+ content: '';
260
+ position: sticky;
261
+ left: 0;
262
+ display: block;
263
+ height: var(--chart-height);
264
+ margin-bottom: calc(-1 * var(--chart-height));
256
265
  background: var(--chart-bg);
257
266
  border-radius: 3px;
267
+ pointer-events: none;
268
+ }
269
+
270
+ .chart-stacked-column .chart-columns {
271
+ position: relative;
272
+ display: flex;
273
+ gap: 0.25rem;
274
+ align-items: stretch;
275
+ height: var(--chart-height);
258
276
  padding: 0.5rem 0.5rem 0 0.5rem;
259
277
  box-sizing: border-box;
260
278
  }
@@ -278,37 +296,69 @@
278
296
 
279
297
  .chart-stacked-column .column-labels {
280
298
  display: flex;
281
- gap: var(--chart-gap);
299
+ gap: 0.25rem;
282
300
  padding: 0.25rem 0.5rem 0;
283
- margin-left: 2.5rem;
284
301
  }
285
302
 
286
303
  .chart-stacked-column .column-label {
287
304
  flex: 1;
288
305
  min-width: var(--chart-column-width);
289
306
  max-width: calc(var(--chart-column-width) * 2);
290
- font-size: 0.75rem;
307
+ font-size: 0.7rem;
308
+ opacity: 0.6;
291
309
  text-align: center;
292
310
  white-space: nowrap;
293
311
  overflow: hidden;
294
312
  text-overflow: ellipsis;
295
313
  }
296
314
 
315
+ /* Rotated column labels (opt-in via rotateLabels config) */
316
+ .chart-stacked-column.rotate-labels .column-labels {
317
+ padding-top: 0.5rem;
318
+ align-items: flex-start;
319
+ }
320
+
321
+ .chart-stacked-column.rotate-labels .column-label {
322
+ writing-mode: vertical-rl;
323
+ transform: rotate(180deg);
324
+ display: flex;
325
+ align-items: center;
326
+ overflow: visible;
327
+ text-overflow: clip;
328
+ }
329
+
297
330
  /* ==========================================================================
298
331
  Donut Chart
299
332
  ========================================================================== */
300
333
 
301
334
  .chart-donut {
302
335
  display: flex;
303
- flex-direction: column;
336
+ flex-direction: row;
337
+ flex-wrap: wrap;
304
338
  align-items: flex-start;
339
+ gap: 1rem;
340
+ --donut-1: var(--chart-color-1);
341
+ --donut-2: var(--chart-color-2);
342
+ --donut-3: var(--chart-color-3);
343
+ --donut-4: var(--chart-color-4);
344
+ --donut-5: var(--chart-color-5);
345
+ --donut-6: var(--chart-color-6);
346
+ --donut-7: var(--chart-color-7);
347
+ --donut-8: var(--chart-color-8);
348
+ --donut-9: var(--chart-color-9);
349
+ --donut-10: var(--chart-color-10);
350
+ --donut-11: var(--chart-color-11);
351
+ --donut-12: var(--chart-color-12);
352
+ }
353
+
354
+ .chart-donut > .chart-title {
355
+ flex-basis: 100%;
305
356
  }
306
357
 
307
358
  .chart-donut .donut-container {
308
359
  position: relative;
309
360
  display: flex;
310
361
  justify-content: center;
311
- margin-bottom: 1rem;
312
362
  }
313
363
 
314
364
  .chart-donut .donut-ring {
@@ -364,6 +414,20 @@
364
414
  min-width: 10rem;
365
415
  }
366
416
 
417
+ /* Donut legend uses same variables as gradient for consistent overrides */
418
+ .chart-donut .chart-color-1 { --color: var(--donut-1, var(--chart-color-1)); }
419
+ .chart-donut .chart-color-2 { --color: var(--donut-2, var(--chart-color-2)); }
420
+ .chart-donut .chart-color-3 { --color: var(--donut-3, var(--chart-color-3)); }
421
+ .chart-donut .chart-color-4 { --color: var(--donut-4, var(--chart-color-4)); }
422
+ .chart-donut .chart-color-5 { --color: var(--donut-5, var(--chart-color-5)); }
423
+ .chart-donut .chart-color-6 { --color: var(--donut-6, var(--chart-color-6)); }
424
+ .chart-donut .chart-color-7 { --color: var(--donut-7, var(--chart-color-7)); }
425
+ .chart-donut .chart-color-8 { --color: var(--donut-8, var(--chart-color-8)); }
426
+ .chart-donut .chart-color-9 { --color: var(--donut-9, var(--chart-color-9)); }
427
+ .chart-donut .chart-color-10 { --color: var(--donut-10, var(--chart-color-10)); }
428
+ .chart-donut .chart-color-11 { --color: var(--donut-11, var(--chart-color-11)); }
429
+ .chart-donut .chart-color-12 { --color: var(--donut-12, var(--chart-color-12)); }
430
+
367
431
  .chart-donut .chart-legend-item .legend-label {
368
432
  flex: 1;
369
433
  }
@@ -376,13 +440,29 @@
376
440
  align-items: stretch;
377
441
  }
378
442
 
379
- .chart-dot .dot-chart {
380
- height: 12rem;
443
+ .chart-dot .chart-scroll {
444
+ flex: 1;
445
+ overflow-x: auto;
446
+ overflow-y: visible;
447
+ position: relative;
448
+ }
449
+
450
+ .chart-dot .chart-scroll::before {
451
+ content: '';
452
+ position: sticky;
453
+ left: 0;
454
+ display: block;
455
+ height: var(--chart-height);
456
+ margin-bottom: calc(-1 * var(--chart-height));
381
457
  background: var(--chart-bg);
382
458
  border-radius: 3px;
383
- flex: 1;
384
- box-sizing: border-box;
459
+ pointer-events: none;
460
+ }
461
+
462
+ .chart-dot .dot-chart {
385
463
  position: relative;
464
+ height: var(--chart-height);
465
+ box-sizing: border-box;
386
466
  }
387
467
 
388
468
  /* Inner field sized to content area - dots position relative to this */
@@ -425,7 +505,6 @@
425
505
  gap: 6px;
426
506
  padding: 0 0.5rem;
427
507
  margin-top: 0.5rem;
428
- margin-left: calc(2rem + 0.5rem); /* y-axis width + gap */
429
508
  }
430
509
 
431
510
  .chart-dot .dot-label {
@@ -435,6 +514,21 @@
435
514
  min-width: 1.5rem;
436
515
  }
437
516
 
517
+ /* Rotated dot labels (opt-in via rotateLabels config) */
518
+ .chart-dot.rotate-labels .dot-labels {
519
+ padding-top: 0.5rem;
520
+ align-items: flex-start;
521
+ }
522
+
523
+ .chart-dot.rotate-labels .dot-label {
524
+ writing-mode: vertical-rl;
525
+ transform: rotate(180deg);
526
+ display: flex;
527
+ align-items: center;
528
+ overflow: visible;
529
+ text-overflow: clip;
530
+ }
531
+
438
532
  /* ==========================================================================
439
533
  Scatter Chart (Continuous X and Y axes)
440
534
  ========================================================================== */
@@ -451,7 +545,7 @@
451
545
 
452
546
  .chart-scatter .dot-area {
453
547
  position: relative;
454
- height: 12rem;
548
+ height: var(--chart-height);
455
549
  background: var(--chart-bg);
456
550
  border-radius: 3px;
457
551
  box-sizing: border-box;
@@ -490,41 +584,13 @@
490
584
  ========================================================================== */
491
585
 
492
586
  /* Row/column index for staggered delays */
493
- .bar-row:nth-child(1) { --row-index: 0; }
494
- .bar-row:nth-child(2) { --row-index: 1; }
495
- .bar-row:nth-child(3) { --row-index: 2; }
496
- .bar-row:nth-child(4) { --row-index: 3; }
497
- .bar-row:nth-child(5) { --row-index: 4; }
498
- .bar-row:nth-child(6) { --row-index: 5; }
499
- .bar-row:nth-child(7) { --row-index: 6; }
500
- .bar-row:nth-child(8) { --row-index: 7; }
501
-
502
- .column-track:nth-child(1) { --col-index: 0; }
503
- .column-track:nth-child(2) { --col-index: 1; }
504
- .column-track:nth-child(3) { --col-index: 2; }
505
- .column-track:nth-child(4) { --col-index: 3; }
506
- .column-track:nth-child(5) { --col-index: 4; }
507
- .column-track:nth-child(6) { --col-index: 5; }
508
- .column-track:nth-child(7) { --col-index: 6; }
509
- .column-track:nth-child(8) { --col-index: 7; }
510
- .column-track:nth-child(9) { --col-index: 8; }
511
- .column-track:nth-child(10) { --col-index: 9; }
512
- .column-track:nth-child(11) { --col-index: 10; }
513
- .column-track:nth-child(12) { --col-index: 11; }
514
-
515
- .dot-col:nth-child(1) { --col-index: 0; }
516
- .dot-col:nth-child(2) { --col-index: 1; }
517
- .dot-col:nth-child(3) { --col-index: 2; }
518
- .dot-col:nth-child(4) { --col-index: 3; }
519
- .dot-col:nth-child(5) { --col-index: 4; }
520
- .dot-col:nth-child(6) { --col-index: 5; }
521
- .dot-col:nth-child(7) { --col-index: 6; }
522
- .dot-col:nth-child(8) { --col-index: 7; }
587
+ /* Row, column, and dot indices set via inline styles in renderers */
523
588
 
524
589
  /* Bar chart: clip-path on fills wrapper, reveals left-to-right */
525
590
  .chart-animate .bar-fills {
591
+ clip-path: inset(0 100% 0 0);
526
592
  animation: bar-reveal 1s cubic-bezier(0.25, 1, 0.5, 1) forwards;
527
- animation-delay: calc(var(--row-index, 0) * 0.08s);
593
+ animation-delay: calc(var(--row-index, 0) * var(--delay-step, 0.08s));
528
594
  }
529
595
 
530
596
  @keyframes bar-reveal {
@@ -534,8 +600,9 @@
534
600
 
535
601
  /* Column chart: clip-path on track, reveals bottom-to-top */
536
602
  .chart-animate .column-track {
603
+ clip-path: inset(100% 0 0 0);
537
604
  animation: column-reveal 0.6s cubic-bezier(0.25, 1, 0.5, 1) forwards;
538
- animation-delay: calc(var(--col-index, 0) * 0.05s);
605
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.05s));
539
606
  }
540
607
 
541
608
  @keyframes column-reveal {
@@ -547,7 +614,7 @@
547
614
  .chart-animate.has-negative-y .column-track {
548
615
  --zero-from-top: calc(100% - var(--zero-position, 0%));
549
616
  animation: column-expand-from-zero 0.6s cubic-bezier(0.25, 1, 0.5, 1) forwards;
550
- animation-delay: calc(var(--col-index, 0) * 0.05s);
617
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.05s));
551
618
  clip-path: polygon(
552
619
  0% var(--zero-from-top),
553
620
  100% var(--zero-from-top),
@@ -573,7 +640,7 @@
573
640
  /* Dot chart: dots rise from bottom with staggered delays */
574
641
  .chart-animate.chart-dot .dot {
575
642
  animation: dot-rise 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
576
- animation-delay: calc(var(--col-index, 0) * 0.08s);
643
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.08s));
577
644
  opacity: 0;
578
645
  }
579
646
 
@@ -593,7 +660,7 @@
593
660
  /* Dot chart with negatives: dots move from zero axis */
594
661
  .chart-animate.chart-dot.has-negative-y .dot {
595
662
  animation: dot-from-zero 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
596
- animation-delay: calc(var(--col-index, 0) * 0.08s);
663
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.08s));
597
664
  opacity: 0;
598
665
  }
599
666
 
@@ -101,6 +101,7 @@ export default function(eleventyConfig, options = {}) {
101
101
  const animate = chartConfig.animate ?? globalAnimate;
102
102
  return renderer({
103
103
  ...chartConfig,
104
+ id: chartId,
104
105
  data,
105
106
  animate
106
107
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eleventy-plugin-uncharted",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "An Eleventy plugin that renders CSS-based charts from CSV data using shortcodes",
5
5
  "main": "eleventy.config.js",
6
6
  "type": "module",
@@ -12,10 +12,11 @@ import { formatNumber } from '../formatters.js';
12
12
  * @param {string|number} [config.center.value] - Value to show in center (use "total" for auto-calculated)
13
13
  * @param {string} [config.center.label] - Label below the value
14
14
  * @param {boolean} [config.animate] - Enable animations
15
+ * @param {boolean} [config.showPercentages] - Show percentages instead of values in legend
15
16
  * @returns {string} - HTML string
16
17
  */
17
18
  export function renderDonut(config) {
18
- const { title, subtitle, data, legend, center, animate, format } = config;
19
+ const { title, subtitle, data, legend, center, animate, format, id, showPercentages } = config;
19
20
 
20
21
  if (!data || data.length === 0) {
21
22
  return `<!-- Donut chart: no data provided -->`;
@@ -58,13 +59,15 @@ export function renderDonut(config) {
58
59
  const startAngle = currentAngle;
59
60
  const endAngle = currentAngle + percentage;
60
61
 
61
- gradientStops.push(`var(--chart-color-${i + 1}) ${startAngle.toFixed(2)}% ${endAngle.toFixed(2)}%`);
62
+ // Use segment-specific variable (defaults set in CSS)
63
+ gradientStops.push(`var(--donut-${i + 1}) ${startAngle.toFixed(2)}% ${endAngle.toFixed(2)}%`);
62
64
  currentAngle = endAngle;
63
65
  });
64
66
 
65
67
  const gradient = `conic-gradient(${gradientStops.join(', ')})`;
66
68
 
67
- let html = `<figure class="chart chart-donut${animateClass}">`;
69
+ const idClass = id ? ` chart-${id}` : '';
70
+ let html = `<figure class="chart chart-donut${animateClass}${idClass}">`;
68
71
 
69
72
  if (title) {
70
73
  html += `<figcaption class="chart-title">${escapeHtml(title)}`;
@@ -94,17 +97,22 @@ export function renderDonut(config) {
94
97
  html += `</div>`;
95
98
  html += `</div>`;
96
99
 
97
- // Legend with percentages
100
+ // Legend with values (or percentages if showPercentages is true)
98
101
  const legendLabels = legend ?? segments.map(s => s.label);
99
102
  html += `<ul class="chart-legend">`;
100
103
  segments.forEach((segment, i) => {
101
104
  const label = legendLabels[i] ?? segment.label;
102
- const percentage = ((segment.value / total) * 100).toFixed(1);
105
+ let displayValue;
106
+ if (showPercentages) {
107
+ displayValue = ((segment.value / total) * 100).toFixed(1) + '%';
108
+ } else {
109
+ displayValue = formatNumber(segment.value, format) || segment.value;
110
+ }
103
111
  const colorClass = `chart-color-${i + 1}`;
104
112
  const seriesClass = `chart-series-${slugify(segment.label)}`;
105
113
  html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">`;
106
114
  html += `<span class="legend-label">${escapeHtml(label)}</span>`;
107
- html += `<span class="legend-value">${percentage}%</span>`;
115
+ html += `<span class="legend-value">${escapeHtml(String(displayValue))}</span>`;
108
116
  html += `</li>`;
109
117
  });
110
118
  html += `</ul>`;
@@ -15,7 +15,7 @@ import { formatNumber } from '../formatters.js';
15
15
  * @returns {string} - HTML string
16
16
  */
17
17
  export function renderDot(config) {
18
- const { title, subtitle, data, max, min, legend, animate, format } = config;
18
+ const { title, subtitle, data, max, min, legend, animate, format, id, rotateLabels } = config;
19
19
 
20
20
  if (!data || data.length === 0) {
21
21
  return `<!-- Dot chart: no data provided -->`;
@@ -45,7 +45,9 @@ export function renderDot(config) {
45
45
  const zeroPct = hasNegativeY ? ((0 - minValue) / range) * 100 : 0;
46
46
 
47
47
  const negativeClass = hasNegativeY ? ' has-negative-y' : '';
48
- let html = `<figure class="chart chart-dot${animateClass}${negativeClass}">`;
48
+ const idClass = id ? ` chart-${id}` : '';
49
+ const rotateClass = rotateLabels ? ' rotate-labels' : '';
50
+ let html = `<figure class="chart chart-dot${animateClass}${negativeClass}${idClass}${rotateClass}">`;
49
51
 
50
52
  if (title) {
51
53
  html += `<figcaption class="chart-title">${escapeHtml(title)}`;
@@ -78,15 +80,23 @@ export function renderDot(config) {
78
80
  html += `<span class="axis-label">${formatNumber(minValue, format) || minValue}</span>`;
79
81
  html += `</div>`;
80
82
 
81
- const zeroStyle = hasNegativeY ? ` style="--zero-position: ${zeroPct.toFixed(2)}%"` : '';
82
- html += `<div class="dot-chart"${zeroStyle}>`;
83
+ // Scroll wrapper for chart + labels
84
+ html += `<div class="chart-scroll">`;
85
+
86
+ // Calculate delay step to cap total stagger at 1s
87
+ const maxStagger = 1; // seconds
88
+ const defaultDelay = 0.08; // seconds
89
+ const delayStep = data.length > 1 ? Math.min(defaultDelay, maxStagger / (data.length - 1)) : 0;
90
+ const styleVars = [`--delay-step: ${delayStep.toFixed(3)}s`];
91
+ if (hasNegativeY) styleVars.push(`--zero-position: ${zeroPct.toFixed(2)}%`);
92
+ html += `<div class="dot-chart" style="${styleVars.join('; ')}">`;
83
93
  html += `<div class="dot-field">`;
84
94
 
85
95
  // Each row becomes a column with dots for each series
86
- data.forEach(row => {
96
+ data.forEach((row, colIndex) => {
87
97
  const label = row[labelKey] ?? '';
88
98
 
89
- html += `<div class="dot-col">`;
99
+ html += `<div class="dot-col" style="--col-index: ${colIndex}">`;
90
100
 
91
101
  seriesKeys.forEach((key, i) => {
92
102
  const val = row[key];
@@ -98,14 +108,13 @@ export function renderDot(config) {
98
108
 
99
109
  html += `<div class="dot ${colorClass} ${seriesClass}" `;
100
110
  html += `style="--value: ${yPct.toFixed(2)}%" `;
101
- html += `title="${escapeHtml(label)}: ${formatNumber(value, format) || value} ${escapeHtml(tooltipLabel)}"`;
111
+ html += `title="${escapeHtml(tooltipLabel)}: ${formatNumber(value, format) || value}"`;
102
112
  html += `></div>`;
103
113
  });
104
114
 
105
115
  html += `</div>`;
106
116
  });
107
117
 
108
- html += `</div>`;
109
118
  html += `</div>`;
110
119
  html += `</div>`;
111
120
 
@@ -117,6 +126,8 @@ export function renderDot(config) {
117
126
  });
118
127
  html += `</div>`;
119
128
 
129
+ html += `</div>`; // close chart-scroll
130
+ html += `</div>`; // close chart-body
120
131
  html += `</figure>`;
121
132
 
122
133
  return html;
@@ -18,7 +18,7 @@ import { formatNumber } from '../formatters.js';
18
18
  * @returns {string} - HTML string
19
19
  */
20
20
  export function renderScatter(config) {
21
- const { title, subtitle, data, maxX, maxY, minX, minY, legend, animate, format, titleX, titleY } = config;
21
+ const { title, subtitle, data, maxX, maxY, minX, minY, legend, animate, format, titleX, titleY, id } = config;
22
22
 
23
23
  // Handle nested X/Y format for scatter charts
24
24
  const fmtX = format?.x || format || {};
@@ -77,7 +77,8 @@ export function renderScatter(config) {
77
77
  const seriesIndex = new Map(seriesList.map((s, i) => [s, i]));
78
78
 
79
79
  const negativeClasses = (hasNegativeX ? ' has-negative-x' : '') + (hasNegativeY ? ' has-negative-y' : '');
80
- let html = `<figure class="chart chart-scatter${animateClass}${negativeClasses}">`;
80
+ const idClass = id ? ` chart-${id}` : '';
81
+ let html = `<figure class="chart chart-scatter${animateClass}${negativeClasses}${idClass}">`;
81
82
 
82
83
  if (title) {
83
84
  html += `<figcaption class="chart-title">${escapeHtml(title)}`;
@@ -13,7 +13,7 @@ import { formatNumber } from '../formatters.js';
13
13
  * @returns {string} - HTML string
14
14
  */
15
15
  export function renderStackedBar(config) {
16
- const { title, subtitle, data, max, legend, animate, format } = config;
16
+ const { title, subtitle, data, max, legend, animate, format, id } = config;
17
17
 
18
18
  if (!data || data.length === 0) {
19
19
  return `<!-- Stacked bar chart: no data provided -->`;
@@ -34,7 +34,8 @@ export function renderStackedBar(config) {
34
34
  }, 0);
35
35
  }));
36
36
 
37
- let html = `<figure class="chart chart-stacked-bar${animateClass}">`;
37
+ const idClass = id ? ` chart-${id}` : '';
38
+ let html = `<figure class="chart chart-stacked-bar${animateClass}${idClass}">`;
38
39
 
39
40
  if (title) {
40
41
  html += `<figcaption class="chart-title">${escapeHtml(title)}`;
@@ -56,9 +57,13 @@ export function renderStackedBar(config) {
56
57
  html += `</ul>`;
57
58
  }
58
59
 
59
- html += `<div class="chart-bars">`;
60
+ // Calculate delay step to cap total stagger at 1s
61
+ const maxStagger = 1; // seconds
62
+ const defaultDelay = 0.08; // seconds
63
+ const delayStep = data.length > 1 ? Math.min(defaultDelay, maxStagger / (data.length - 1)) : 0;
64
+ html += `<div class="chart-bars" style="--delay-step: ${delayStep.toFixed(3)}s">`;
60
65
 
61
- data.forEach(row => {
66
+ data.forEach((row, rowIndex) => {
62
67
  const label = row[labelKey] ?? '';
63
68
  const values = seriesKeys.map(key => {
64
69
  const val = row[key];
@@ -68,7 +73,7 @@ export function renderStackedBar(config) {
68
73
  const percentages = calculatePercentages(values, calculatedMax);
69
74
  const seriesLabels = legendLabels ?? seriesKeys;
70
75
 
71
- html += `<div class="bar-row">`;
76
+ html += `<div class="bar-row" style="--row-index: ${rowIndex}">`;
72
77
  html += `<span class="bar-label">${escapeHtml(label)}</span>`;
73
78
  html += `<div class="bar-track">`;
74
79
  html += `<div class="bar-fills" title="${escapeHtml(label)}: ${formatNumber(total, format) || total}">`;
@@ -14,7 +14,7 @@ import { formatNumber } from '../formatters.js';
14
14
  * @returns {string} - HTML string
15
15
  */
16
16
  export function renderStackedColumn(config) {
17
- const { title, subtitle, data, max, min, legend, animate, format } = config;
17
+ const { title, subtitle, data, max, min, legend, animate, format, id, rotateLabels } = config;
18
18
 
19
19
  if (!data || data.length === 0) {
20
20
  return `<!-- Stacked column chart: no data provided -->`;
@@ -55,7 +55,9 @@ export function renderStackedColumn(config) {
55
55
  const zeroPct = hasNegativeY ? ((0 - minValue) / range) * 100 : 0;
56
56
 
57
57
  const negativeClass = hasNegativeY ? ' has-negative-y' : '';
58
- let html = `<figure class="chart chart-stacked-column${animateClass}${negativeClass}">`;
58
+ const idClass = id ? ` chart-${id}` : '';
59
+ const rotateClass = rotateLabels ? ' rotate-labels' : '';
60
+ let html = `<figure class="chart chart-stacked-column${animateClass}${negativeClass}${idClass}${rotateClass}">`;
59
61
 
60
62
  if (title) {
61
63
  html += `<figcaption class="chart-title">${escapeHtml(title)}`;
@@ -89,12 +91,20 @@ export function renderStackedColumn(config) {
89
91
  html += `<span class="axis-label">${formatNumber(minLabelY, format) || minLabelY}</span>`;
90
92
  html += `</div>`;
91
93
 
92
- const columnsStyle = hasNegativeY ? ` style="--zero-position: ${zeroPct.toFixed(2)}%"` : '';
93
- html += `<div class="chart-columns"${columnsStyle}>`;
94
+ // Scroll wrapper for columns + labels
95
+ html += `<div class="chart-scroll">`;
94
96
 
95
- data.forEach(row => {
97
+ // Calculate delay step to cap total stagger at 1s
98
+ const maxStagger = 1; // seconds
99
+ const defaultDelay = 0.05; // seconds
100
+ const delayStep = data.length > 1 ? Math.min(defaultDelay, maxStagger / (data.length - 1)) : 0;
101
+ const styleVars = [`--delay-step: ${delayStep.toFixed(3)}s`];
102
+ if (hasNegativeY) styleVars.push(`--zero-position: ${zeroPct.toFixed(2)}%`);
103
+ html += `<div class="chart-columns" style="${styleVars.join('; ')}">`;
104
+
105
+ data.forEach((row, colIndex) => {
96
106
  const label = row[labelKey] ?? '';
97
- html += `<div class="column-track" title="${escapeHtml(label)}">`;
107
+ html += `<div class="column-track" style="--col-index: ${colIndex}" title="${escapeHtml(label)}">`;
98
108
 
99
109
  if (hasNegativeY) {
100
110
  // Build segments first to identify stack ends
@@ -170,7 +180,6 @@ export function renderStackedColumn(config) {
170
180
  html += `</div>`;
171
181
  });
172
182
 
173
- html += `</div>`;
174
183
  html += `</div>`;
175
184
 
176
185
  // X-axis labels
@@ -180,6 +189,9 @@ export function renderStackedColumn(config) {
180
189
  html += `<span class="column-label">${escapeHtml(label)}</span>`;
181
190
  });
182
191
  html += `</div>`;
192
+
193
+ html += `</div>`; // close chart-scroll
194
+ html += `</div>`; // close chart-body
183
195
  html += `</figure>`;
184
196
 
185
197
  return html;