eleventy-plugin-uncharted 0.2.0 → 0.3.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.
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,38 @@ 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: 20rem; /* Donut chart max diameter */
257
+ --chart-donut-hole: 30%; /* Donut hole size (percentage of diameter) */
258
+ }
259
+ ```
260
+
261
+ ### Responsive Donut Charts
262
+
263
+ Donut charts automatically adapt to their container using CSS container queries:
264
+
265
+ - **Narrow containers**: Donut on top, legend wraps below horizontally
266
+ - **Wide containers** (24rem+): Donut and legend side by side
267
+
268
+ The donut scales to 80% of container width (minimum 8rem, maximum `--chart-donut-size`).
269
+
270
+ ### Per-Chart Styling
271
+
272
+ Each chart gets a class based on its ID for targeted styling:
273
+
274
+ ```yaml
275
+ charts:
276
+ sales-growth:
277
+ type: stacked-column
278
+ file: charts/sales.csv
279
+ ```
280
+
281
+ ```css
282
+ /* Target this specific chart */
283
+ .chart-sales-growth {
284
+ --chart-height: 16rem;
285
+ --chart-color-1: #ff6b6b;
252
286
  }
253
287
  ```
254
288
 
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;
30
- --chart-donut-size: 12rem;
31
- --chart-donut-thickness: 2.5rem;
29
+ --chart-column-width: 1rem;
30
+ --chart-donut-size: 20rem;
31
+ --chart-donut-hole: 30%;
32
32
  --chart-dot-size: 0.75rem;
33
+ --chart-height: 12rem;
33
34
  }
34
35
 
35
36
  /* ==========================================================================
@@ -54,8 +55,10 @@
54
55
  ========================================================================== */
55
56
 
56
57
  .chart {
57
- margin: 1.5rem 0;
58
58
  font-family: inherit;
59
+ display: flex;
60
+ flex-direction: column;
61
+ flex: 1;
59
62
  }
60
63
 
61
64
  .chart-title {
@@ -111,7 +114,7 @@
111
114
 
112
115
  .chart-legend-item .legend-value {
113
116
  opacity: 0.7;
114
- margin-left: 0.25rem;
117
+ margin-left: 0.125rem;
115
118
  }
116
119
 
117
120
  /* ==========================================================================
@@ -121,6 +124,8 @@
121
124
  .chart-body {
122
125
  display: flex;
123
126
  gap: 0.5rem;
127
+ flex: 1;
128
+ min-height: var(--chart-height);
124
129
  }
125
130
 
126
131
  .chart-y-axis {
@@ -131,7 +136,7 @@
131
136
  align-items: flex-end;
132
137
  min-width: 2rem;
133
138
  box-sizing: border-box;
134
- height: 12rem;
139
+ min-height: var(--chart-height);
135
140
  padding-top: 0.5rem;
136
141
  padding-bottom: 0;
137
142
  }
@@ -201,8 +206,10 @@
201
206
  .chart-stacked-bar .chart-bars {
202
207
  display: grid;
203
208
  grid-template-columns: auto 1fr auto;
204
- gap: var(--chart-gap) 0.75rem;
205
- align-items: center;
209
+ grid-auto-rows: 1fr;
210
+ gap: clamp(0.375rem, 1.5cqi, 1rem) 0.75rem;
211
+ align-items: stretch;
212
+ min-height: var(--chart-height);
206
213
  }
207
214
 
208
215
  .chart-stacked-bar .bar-row {
@@ -212,10 +219,13 @@
212
219
  .chart-stacked-bar .bar-label {
213
220
  font-size: 0.875rem;
214
221
  text-align: right;
222
+ display: flex;
223
+ align-items: center;
224
+ justify-content: flex-end;
215
225
  }
216
226
 
217
227
  .chart-stacked-bar .bar-track {
218
- height: var(--chart-bar-height);
228
+ min-height: var(--chart-bar-height);
219
229
  border-radius: 3px;
220
230
  overflow: hidden;
221
231
  background: var(--chart-bg);
@@ -234,6 +244,8 @@
234
244
  }
235
245
 
236
246
  .chart-stacked-bar .bar-value {
247
+ display: flex;
248
+ align-items: center;
237
249
  font-size: 0.875rem;
238
250
  font-variant-numeric: tabular-nums;
239
251
  min-width: 2rem;
@@ -244,19 +256,36 @@
244
256
  ========================================================================== */
245
257
 
246
258
  .chart-stacked-column .chart-body {
247
- align-items: stretch;
259
+ display: grid;
260
+ grid-template-columns: auto 1fr;
261
+ grid-template-rows: 1fr auto;
262
+ }
263
+
264
+ .chart-stacked-column .chart-y-axis {
265
+ grid-row: 1;
266
+ grid-column: 1;
267
+ }
268
+
269
+ .chart-stacked-column .chart-scroll {
270
+ grid-row: 1 / -1;
271
+ grid-column: 2;
272
+ display: grid;
273
+ grid-template-rows: subgrid;
274
+ overflow-x: auto;
275
+ overflow-y: visible;
248
276
  }
249
277
 
250
278
  .chart-stacked-column .chart-columns {
279
+ grid-row: 1;
280
+ position: relative;
251
281
  display: flex;
252
- gap: var(--chart-gap);
282
+ gap: clamp(0.375rem, 1.5cqi, 1rem);
253
283
  align-items: stretch;
254
- height: 12rem;
255
- flex: 1;
256
- background: var(--chart-bg);
257
- border-radius: 3px;
284
+ min-height: var(--chart-height);
258
285
  padding: 0.5rem 0.5rem 0 0.5rem;
259
286
  box-sizing: border-box;
287
+ background: var(--chart-bg);
288
+ border-radius: 3px;
260
289
  }
261
290
 
262
291
  .chart-stacked-column .column-track {
@@ -264,7 +293,6 @@
264
293
  flex-direction: column-reverse;
265
294
  flex: 1;
266
295
  min-width: var(--chart-column-width);
267
- max-width: calc(var(--chart-column-width) * 2);
268
296
  border-radius: 3px 3px 0 0;
269
297
  overflow: hidden;
270
298
  }
@@ -277,58 +305,101 @@
277
305
  }
278
306
 
279
307
  .chart-stacked-column .column-labels {
308
+ grid-row: 2;
280
309
  display: flex;
281
- gap: var(--chart-gap);
310
+ gap: clamp(0.375rem, 1.5cqi, 1rem);
282
311
  padding: 0.25rem 0.5rem 0;
283
- margin-left: 2.5rem;
284
312
  }
285
313
 
286
314
  .chart-stacked-column .column-label {
287
315
  flex: 1;
288
316
  min-width: var(--chart-column-width);
289
- max-width: calc(var(--chart-column-width) * 2);
290
- font-size: 0.75rem;
317
+ font-size: 0.7rem;
318
+ opacity: 0.6;
291
319
  text-align: center;
292
320
  white-space: nowrap;
293
321
  overflow: hidden;
294
322
  text-overflow: ellipsis;
295
323
  }
296
324
 
325
+ /* Rotated column labels (opt-in via rotateLabels config) */
326
+ .chart-stacked-column.rotate-labels .column-labels {
327
+ padding-top: 0.5rem;
328
+ align-items: flex-start;
329
+ }
330
+
331
+ .chart-stacked-column.rotate-labels .column-label {
332
+ writing-mode: vertical-rl;
333
+ transform: rotate(180deg);
334
+ display: flex;
335
+ align-items: center;
336
+ overflow: visible;
337
+ text-overflow: clip;
338
+ }
339
+
297
340
  /* ==========================================================================
298
341
  Donut Chart
299
342
  ========================================================================== */
300
343
 
301
344
  .chart-donut {
345
+ --donut-1: var(--chart-color-1);
346
+ --donut-2: var(--chart-color-2);
347
+ --donut-3: var(--chart-color-3);
348
+ --donut-4: var(--chart-color-4);
349
+ --donut-5: var(--chart-color-5);
350
+ --donut-6: var(--chart-color-6);
351
+ --donut-7: var(--chart-color-7);
352
+ --donut-8: var(--chart-color-8);
353
+ --donut-9: var(--chart-color-9);
354
+ --donut-10: var(--chart-color-10);
355
+ --donut-11: var(--chart-color-11);
356
+ --donut-12: var(--chart-color-12);
357
+ }
358
+
359
+ .chart-donut .donut-body {
360
+ container-type: inline-size;
361
+ container-name: donut;
302
362
  display: flex;
303
- flex-direction: column;
304
- align-items: flex-start;
363
+ flex-wrap: wrap;
364
+ align-items: center;
365
+ align-content: center;
366
+ justify-content: center;
367
+ gap: 1rem;
368
+ flex: 1;
305
369
  }
306
370
 
307
371
  .chart-donut .donut-container {
308
372
  position: relative;
309
373
  display: flex;
310
374
  justify-content: center;
311
- margin-bottom: 1rem;
375
+ /* Size based on container width, clamped */
376
+ width: clamp(8rem, 80cqi, var(--chart-donut-size));
377
+ aspect-ratio: 1;
378
+ }
379
+
380
+ /* In portrait, legend takes full width to wrap below */
381
+ .chart-donut .donut-body > .chart-legend {
382
+ flex: 0 0 100%;
383
+ justify-content: center;
312
384
  }
313
385
 
314
386
  .chart-donut .donut-ring {
315
- width: var(--chart-donut-size);
316
- height: var(--chart-donut-size);
387
+ width: 100%;
388
+ height: 100%;
317
389
  border-radius: 50%;
318
390
  display: flex;
319
391
  align-items: center;
320
392
  justify-content: center;
321
393
  /* Radial mask punches out the center hole */
322
- --_hole-size: calc((var(--chart-donut-size) - var(--chart-donut-thickness) * 2) / 2);
323
394
  mask-image: radial-gradient(
324
395
  circle at center,
325
- transparent var(--_hole-size),
326
- black var(--_hole-size)
396
+ transparent var(--chart-donut-hole, 30%),
397
+ black var(--chart-donut-hole, 30%)
327
398
  );
328
399
  -webkit-mask-image: radial-gradient(
329
400
  circle at center,
330
- transparent var(--_hole-size),
331
- black var(--_hole-size)
401
+ transparent var(--chart-donut-hole, 30%),
402
+ black var(--chart-donut-hole, 30%)
332
403
  );
333
404
  }
334
405
 
@@ -356,33 +427,78 @@
356
427
  }
357
428
 
358
429
  .chart-donut .chart-legend {
359
- flex-direction: column;
360
- gap: 0.5rem;
430
+ /* Default (portrait): horizontal legend below donut */
431
+ flex-direction: row;
432
+ flex-wrap: wrap;
433
+ justify-content: center;
434
+ gap: 0.5rem 1rem;
361
435
  }
362
436
 
363
437
  .chart-donut .chart-legend-item {
364
- min-width: 10rem;
438
+ min-width: 8rem;
365
439
  }
366
440
 
367
- .chart-donut .chart-legend-item .legend-label {
368
- flex: 1;
441
+ /* Wide container: row layout with legend beside donut */
442
+ @container donut (min-width: 24rem) {
443
+ .donut-container,
444
+ .chart-legend {
445
+ flex: 0 0 auto;
446
+ }
447
+
448
+ .chart-legend {
449
+ flex-direction: column;
450
+ flex-wrap: nowrap;
451
+ justify-content: center;
452
+ gap: 0.5rem;
453
+ }
369
454
  }
370
455
 
456
+ /* Donut legend uses same variables as gradient for consistent overrides */
457
+ .chart-donut .chart-color-1 { --color: var(--donut-1, var(--chart-color-1)); }
458
+ .chart-donut .chart-color-2 { --color: var(--donut-2, var(--chart-color-2)); }
459
+ .chart-donut .chart-color-3 { --color: var(--donut-3, var(--chart-color-3)); }
460
+ .chart-donut .chart-color-4 { --color: var(--donut-4, var(--chart-color-4)); }
461
+ .chart-donut .chart-color-5 { --color: var(--donut-5, var(--chart-color-5)); }
462
+ .chart-donut .chart-color-6 { --color: var(--donut-6, var(--chart-color-6)); }
463
+ .chart-donut .chart-color-7 { --color: var(--donut-7, var(--chart-color-7)); }
464
+ .chart-donut .chart-color-8 { --color: var(--donut-8, var(--chart-color-8)); }
465
+ .chart-donut .chart-color-9 { --color: var(--donut-9, var(--chart-color-9)); }
466
+ .chart-donut .chart-color-10 { --color: var(--donut-10, var(--chart-color-10)); }
467
+ .chart-donut .chart-color-11 { --color: var(--donut-11, var(--chart-color-11)); }
468
+ .chart-donut .chart-color-12 { --color: var(--donut-12, var(--chart-color-12)); }
469
+
470
+
371
471
  /* ==========================================================================
372
472
  Dot Chart (Categorical - columns with dots at Y positions)
373
473
  ========================================================================== */
374
474
 
375
475
  .chart-dot .chart-body {
376
- align-items: stretch;
476
+ display: grid;
477
+ grid-template-columns: auto 1fr;
478
+ grid-template-rows: 1fr auto;
479
+ }
480
+
481
+ .chart-dot .chart-y-axis {
482
+ grid-row: 1;
483
+ grid-column: 1;
484
+ }
485
+
486
+ .chart-dot .chart-scroll {
487
+ grid-row: 1 / -1;
488
+ grid-column: 2;
489
+ display: grid;
490
+ grid-template-rows: subgrid;
491
+ overflow-x: auto;
492
+ overflow-y: visible;
377
493
  }
378
494
 
379
495
  .chart-dot .dot-chart {
380
- height: 12rem;
496
+ grid-row: 1;
497
+ position: relative;
498
+ min-height: var(--chart-height);
499
+ box-sizing: border-box;
381
500
  background: var(--chart-bg);
382
501
  border-radius: 3px;
383
- flex: 1;
384
- box-sizing: border-box;
385
- position: relative;
386
502
  }
387
503
 
388
504
  /* Inner field sized to content area - dots position relative to this */
@@ -421,11 +537,11 @@
421
537
  }
422
538
 
423
539
  .chart-dot .dot-labels {
540
+ grid-row: 2;
424
541
  display: flex;
425
542
  gap: 6px;
426
543
  padding: 0 0.5rem;
427
544
  margin-top: 0.5rem;
428
- margin-left: calc(2rem + 0.5rem); /* y-axis width + gap */
429
545
  }
430
546
 
431
547
  .chart-dot .dot-label {
@@ -435,6 +551,21 @@
435
551
  min-width: 1.5rem;
436
552
  }
437
553
 
554
+ /* Rotated dot labels (opt-in via rotateLabels config) */
555
+ .chart-dot.rotate-labels .dot-labels {
556
+ padding-top: 0.5rem;
557
+ align-items: flex-start;
558
+ }
559
+
560
+ .chart-dot.rotate-labels .dot-label {
561
+ writing-mode: vertical-rl;
562
+ transform: rotate(180deg);
563
+ display: flex;
564
+ align-items: center;
565
+ overflow: visible;
566
+ text-overflow: clip;
567
+ }
568
+
438
569
  /* ==========================================================================
439
570
  Scatter Chart (Continuous X and Y axes)
440
571
  ========================================================================== */
@@ -451,7 +582,8 @@
451
582
 
452
583
  .chart-scatter .dot-area {
453
584
  position: relative;
454
- height: 12rem;
585
+ flex: 1;
586
+ min-height: var(--chart-height);
455
587
  background: var(--chart-bg);
456
588
  border-radius: 3px;
457
589
  box-sizing: border-box;
@@ -490,41 +622,13 @@
490
622
  ========================================================================== */
491
623
 
492
624
  /* 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; }
625
+ /* Row, column, and dot indices set via inline styles in renderers */
523
626
 
524
627
  /* Bar chart: clip-path on fills wrapper, reveals left-to-right */
525
628
  .chart-animate .bar-fills {
629
+ clip-path: inset(0 100% 0 0);
526
630
  animation: bar-reveal 1s cubic-bezier(0.25, 1, 0.5, 1) forwards;
527
- animation-delay: calc(var(--row-index, 0) * 0.08s);
631
+ animation-delay: calc(var(--row-index, 0) * var(--delay-step, 0.08s));
528
632
  }
529
633
 
530
634
  @keyframes bar-reveal {
@@ -534,8 +638,9 @@
534
638
 
535
639
  /* Column chart: clip-path on track, reveals bottom-to-top */
536
640
  .chart-animate .column-track {
641
+ clip-path: inset(100% 0 0 0);
537
642
  animation: column-reveal 0.6s cubic-bezier(0.25, 1, 0.5, 1) forwards;
538
- animation-delay: calc(var(--col-index, 0) * 0.05s);
643
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.05s));
539
644
  }
540
645
 
541
646
  @keyframes column-reveal {
@@ -547,7 +652,7 @@
547
652
  .chart-animate.has-negative-y .column-track {
548
653
  --zero-from-top: calc(100% - var(--zero-position, 0%));
549
654
  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);
655
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.05s));
551
656
  clip-path: polygon(
552
657
  0% var(--zero-from-top),
553
658
  100% var(--zero-from-top),
@@ -573,7 +678,7 @@
573
678
  /* Dot chart: dots rise from bottom with staggered delays */
574
679
  .chart-animate.chart-dot .dot {
575
680
  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);
681
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.08s));
577
682
  opacity: 0;
578
683
  }
579
684
 
@@ -593,7 +698,7 @@
593
698
  /* Dot chart with negatives: dots move from zero axis */
594
699
  .chart-animate.chart-dot.has-negative-y .dot {
595
700
  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);
701
+ animation-delay: calc(var(--col-index, 0) * var(--delay-step, 0.08s));
597
702
  opacity: 0;
598
703
  }
599
704
 
@@ -617,13 +722,12 @@
617
722
  }
618
723
 
619
724
  .chart-animate .donut-ring {
620
- --_hole-size: calc((var(--chart-donut-size) - var(--chart-donut-thickness) * 2) / 2);
621
725
  mask-image:
622
- radial-gradient(circle at center, transparent var(--_hole-size), black var(--_hole-size)),
726
+ radial-gradient(circle at center, transparent var(--chart-donut-hole, 30%), black var(--chart-donut-hole, 30%)),
623
727
  conic-gradient(from 0deg, black var(--donut-reveal), transparent var(--donut-reveal));
624
728
  mask-composite: intersect;
625
729
  -webkit-mask-image:
626
- radial-gradient(circle at center, transparent var(--_hole-size), black var(--_hole-size)),
730
+ radial-gradient(circle at center, transparent var(--chart-donut-hole, 30%), black var(--chart-donut-hole, 30%)),
627
731
  conic-gradient(from 0deg, black var(--donut-reveal), transparent var(--donut-reveal));
628
732
  -webkit-mask-composite: source-in;
629
733
  animation: donut-clockwise 0.8s ease-out 0.1s forwards;
@@ -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.3.0",
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)}`;
@@ -74,6 +77,9 @@ export function renderDonut(config) {
74
77
  html += `</figcaption>`;
75
78
  }
76
79
 
80
+ // Donut body wrapper (for container queries)
81
+ html += `<div class="donut-body">`;
82
+
77
83
  // Donut visual
78
84
  html += `<div class="donut-container">`;
79
85
  html += `<div class="donut-ring" style="background: ${gradient}"></div>`;
@@ -94,21 +100,28 @@ export function renderDonut(config) {
94
100
  html += `</div>`;
95
101
  html += `</div>`;
96
102
 
97
- // Legend with percentages
103
+ // Legend with values (or percentages if showPercentages is true)
98
104
  const legendLabels = legend ?? segments.map(s => s.label);
99
105
  html += `<ul class="chart-legend">`;
100
106
  segments.forEach((segment, i) => {
101
107
  const label = legendLabels[i] ?? segment.label;
102
- const percentage = ((segment.value / total) * 100).toFixed(1);
108
+ let displayValue;
109
+ if (showPercentages) {
110
+ displayValue = ((segment.value / total) * 100).toFixed(1) + '%';
111
+ } else {
112
+ displayValue = formatNumber(segment.value, format) || segment.value;
113
+ }
103
114
  const colorClass = `chart-color-${i + 1}`;
104
115
  const seriesClass = `chart-series-${slugify(segment.label)}`;
105
116
  html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">`;
106
117
  html += `<span class="legend-label">${escapeHtml(label)}</span>`;
107
- html += `<span class="legend-value">${percentage}%</span>`;
118
+ html += `<span class="legend-value">${escapeHtml(String(displayValue))}</span>`;
108
119
  html += `</li>`;
109
120
  });
110
121
  html += `</ul>`;
111
122
 
123
+ html += `</div>`; // Close donut-body
124
+
112
125
  html += `</figure>`;
113
126
 
114
127
  return html;
@@ -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;