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 +26 -1
- package/css/uncharted.css +120 -53
- package/eleventy.config.js +1 -0
- package/package.json +1 -1
- package/src/renderers/donut.js +14 -6
- package/src/renderers/dot.js +19 -8
- package/src/renderers/scatter.js +3 -2
- package/src/renderers/stacked-bar.js +10 -5
- package/src/renderers/stacked-column.js +19 -7
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:
|
|
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:
|
|
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-
|
|
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:
|
|
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.
|
|
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:
|
|
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 .
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
package/eleventy.config.js
CHANGED
package/package.json
CHANGED
package/src/renderers/donut.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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">${
|
|
115
|
+
html += `<span class="legend-value">${escapeHtml(String(displayValue))}</span>`;
|
|
108
116
|
html += `</li>`;
|
|
109
117
|
});
|
|
110
118
|
html += `</ul>`;
|
package/src/renderers/dot.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
82
|
-
html += `<div class="
|
|
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(
|
|
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;
|
package/src/renderers/scatter.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
html += `<div class="chart-
|
|
94
|
+
// Scroll wrapper for columns + labels
|
|
95
|
+
html += `<div class="chart-scroll">`;
|
|
94
96
|
|
|
95
|
-
|
|
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;
|