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 +35 -1
- package/css/uncharted.css +184 -80
- package/eleventy.config.js +1 -0
- package/package.json +1 -1
- package/src/renderers/donut.js +19 -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,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:
|
|
30
|
-
--chart-donut-size:
|
|
31
|
-
--chart-donut-
|
|
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.
|
|
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:
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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:
|
|
282
|
+
gap: clamp(0.375rem, 1.5cqi, 1rem);
|
|
253
283
|
align-items: stretch;
|
|
254
|
-
height:
|
|
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:
|
|
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
|
-
|
|
290
|
-
|
|
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-
|
|
304
|
-
align-items:
|
|
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
|
-
|
|
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:
|
|
316
|
-
height:
|
|
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(--
|
|
326
|
-
black var(--
|
|
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(--
|
|
331
|
-
black var(--
|
|
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
|
-
|
|
360
|
-
|
|
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:
|
|
438
|
+
min-width: 8rem;
|
|
365
439
|
}
|
|
366
440
|
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(--
|
|
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(--
|
|
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;
|
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)}`;
|
|
@@ -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
|
-
|
|
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">${
|
|
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;
|
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;
|