eleventy-plugin-uncharted 0.5.1 → 0.5.2
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.
Potentially problematic release.
This version of eleventy-plugin-uncharted might be problematic. Click here for more details.
- package/README.md +1 -1
- package/css/uncharted.css +124 -14
- package/package.json +1 -1
- package/src/renderers/donut.js +2 -2
- package/src/renderers/dot.js +17 -13
- package/src/renderers/sankey.js +17 -16
- package/src/renderers/scatter.js +92 -25
- package/src/renderers/stacked-bar.js +5 -2
- package/src/renderers/stacked-column.js +13 -12
package/README.md
CHANGED
package/css/uncharted.css
CHANGED
|
@@ -7,18 +7,33 @@
|
|
|
7
7
|
========================================================================== */
|
|
8
8
|
|
|
9
9
|
:root {
|
|
10
|
-
|
|
11
|
-
--chart-color-
|
|
12
|
-
--chart-color-
|
|
13
|
-
--chart-color-
|
|
14
|
-
--chart-color-
|
|
15
|
-
--chart-color-
|
|
16
|
-
--chart-color-
|
|
17
|
-
--chart-color-
|
|
18
|
-
--chart-color-
|
|
19
|
-
--chart-color-
|
|
20
|
-
--chart-color-
|
|
21
|
-
--chart-color-
|
|
10
|
+
/* Descriptive color names */
|
|
11
|
+
--chart-color-blue: #2196f3;
|
|
12
|
+
--chart-color-green: #4caf50;
|
|
13
|
+
--chart-color-orange: #ff7043;
|
|
14
|
+
--chart-color-yellow: #ffc107;
|
|
15
|
+
--chart-color-teal: #009688;
|
|
16
|
+
--chart-color-purple: #9c27b0;
|
|
17
|
+
--chart-color-pink: #e91e63;
|
|
18
|
+
--chart-color-indigo: #3f51b5;
|
|
19
|
+
--chart-color-red: #f44336;
|
|
20
|
+
--chart-color-cyan: #00bcd4;
|
|
21
|
+
--chart-color-lime: #cddc39;
|
|
22
|
+
--chart-color-gray: #78909c;
|
|
23
|
+
|
|
24
|
+
/* Numeric aliases (for chart data series) */
|
|
25
|
+
--chart-color-1: var(--chart-color-blue);
|
|
26
|
+
--chart-color-2: var(--chart-color-green);
|
|
27
|
+
--chart-color-3: var(--chart-color-orange);
|
|
28
|
+
--chart-color-4: var(--chart-color-yellow);
|
|
29
|
+
--chart-color-5: var(--chart-color-teal);
|
|
30
|
+
--chart-color-6: var(--chart-color-purple);
|
|
31
|
+
--chart-color-7: var(--chart-color-pink);
|
|
32
|
+
--chart-color-8: var(--chart-color-indigo);
|
|
33
|
+
--chart-color-9: var(--chart-color-red);
|
|
34
|
+
--chart-color-10: var(--chart-color-cyan);
|
|
35
|
+
--chart-color-11: var(--chart-color-lime);
|
|
36
|
+
--chart-color-12: var(--chart-color-gray);
|
|
22
37
|
|
|
23
38
|
/* Backgrounds - neutral with opacity for light/dark adaptability */
|
|
24
39
|
--chart-bg: rgba(128, 128, 128, 0.15);
|
|
@@ -30,6 +45,8 @@
|
|
|
30
45
|
--chart-donut-size: 20rem;
|
|
31
46
|
--chart-donut-hole: 30%;
|
|
32
47
|
--chart-dot-size: 0.75rem;
|
|
48
|
+
--chart-dot-size-min: 0.375rem;
|
|
49
|
+
--chart-dot-size-max: 1.5rem;
|
|
33
50
|
--chart-height: 12rem;
|
|
34
51
|
}
|
|
35
52
|
|
|
@@ -87,10 +104,28 @@
|
|
|
87
104
|
row-gap: 0.375rem;
|
|
88
105
|
list-style: none;
|
|
89
106
|
padding: 0;
|
|
90
|
-
margin: 0
|
|
107
|
+
margin: 0;
|
|
91
108
|
font-size: 0.875rem;
|
|
92
109
|
}
|
|
93
110
|
|
|
111
|
+
/* Legends below chart body */
|
|
112
|
+
.chart-body + .chart-legend,
|
|
113
|
+
.chart-body + .chart-legend-title,
|
|
114
|
+
.chart-body + .chart-size-legend,
|
|
115
|
+
.donut-body + .chart-legend {
|
|
116
|
+
margin-top: 1rem;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Spacing between series legend and size legend */
|
|
120
|
+
.chart-legend + .chart-size-legend {
|
|
121
|
+
margin-top: 0.75rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Bar charts keep legend above */
|
|
125
|
+
.chart-stacked-bar .chart-legend {
|
|
126
|
+
margin-bottom: 1rem;
|
|
127
|
+
}
|
|
128
|
+
|
|
94
129
|
.chart-legend-item {
|
|
95
130
|
display: flex;
|
|
96
131
|
align-items: center;
|
|
@@ -119,6 +154,13 @@
|
|
|
119
154
|
margin-left: 0.125rem;
|
|
120
155
|
}
|
|
121
156
|
|
|
157
|
+
.chart-legend-title {
|
|
158
|
+
display: block;
|
|
159
|
+
font-size: 0.75rem;
|
|
160
|
+
font-weight: 600;
|
|
161
|
+
margin-bottom: 0.375rem;
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
/* ==========================================================================
|
|
123
165
|
Axes
|
|
124
166
|
========================================================================== */
|
|
@@ -158,9 +200,13 @@
|
|
|
158
200
|
transform: translateY(50%);
|
|
159
201
|
}
|
|
160
202
|
|
|
203
|
+
.chart-y-axis:has(.axis-title) {
|
|
204
|
+
padding-left: 1.25rem;
|
|
205
|
+
}
|
|
206
|
+
|
|
161
207
|
.chart-y-axis .axis-title {
|
|
162
208
|
position: absolute;
|
|
163
|
-
left:
|
|
209
|
+
left: 0.25rem;
|
|
164
210
|
top: 50%;
|
|
165
211
|
transform: rotate(-90deg) translateX(-50%);
|
|
166
212
|
transform-origin: left center;
|
|
@@ -657,6 +703,70 @@
|
|
|
657
703
|
z-index: 1;
|
|
658
704
|
}
|
|
659
705
|
|
|
706
|
+
/* Variable-sized dots (size column) */
|
|
707
|
+
.chart-scatter .dot[style*="--size-scale"] {
|
|
708
|
+
--computed-size: calc(
|
|
709
|
+
var(--chart-dot-size-min) +
|
|
710
|
+
var(--size-scale) * (var(--chart-dot-size-max) - var(--chart-dot-size-min))
|
|
711
|
+
);
|
|
712
|
+
width: var(--computed-size);
|
|
713
|
+
height: var(--computed-size);
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/* Size legend */
|
|
717
|
+
.chart-size-legend {
|
|
718
|
+
display: flex;
|
|
719
|
+
flex-direction: column;
|
|
720
|
+
gap: 0.375rem;
|
|
721
|
+
font-size: 0.875rem;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.size-legend-items {
|
|
725
|
+
display: flex;
|
|
726
|
+
align-items: center;
|
|
727
|
+
gap: 1rem;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.size-legend-item {
|
|
731
|
+
display: flex;
|
|
732
|
+
align-items: center;
|
|
733
|
+
gap: 0.5rem;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
.size-dot {
|
|
737
|
+
border-radius: 50%;
|
|
738
|
+
background: currentColor;
|
|
739
|
+
opacity: 0.5;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
.size-dot-min {
|
|
743
|
+
width: var(--chart-dot-size-min);
|
|
744
|
+
height: var(--chart-dot-size-min);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.size-dot-max {
|
|
748
|
+
width: var(--chart-dot-size-max);
|
|
749
|
+
height: var(--chart-dot-size-max);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.size-value {
|
|
753
|
+
font-size: 0.75rem;
|
|
754
|
+
opacity: 0.7;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* Proportional scatter: maintain data aspect ratio */
|
|
758
|
+
.chart-scatter.chart-proportional .chart-body,
|
|
759
|
+
.chart-scatter.chart-proportional .chart-y-axis {
|
|
760
|
+
min-height: auto;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
.chart-scatter.chart-proportional .dot-area {
|
|
764
|
+
aspect-ratio: var(--data-aspect-ratio, 1);
|
|
765
|
+
min-height: auto;
|
|
766
|
+
width: 100%;
|
|
767
|
+
height: auto;
|
|
768
|
+
}
|
|
769
|
+
|
|
660
770
|
/* ==========================================================================
|
|
661
771
|
Sankey Chart
|
|
662
772
|
========================================================================== */
|
package/package.json
CHANGED
package/src/renderers/donut.js
CHANGED
|
@@ -100,6 +100,8 @@ export function renderDonut(config) {
|
|
|
100
100
|
html += `</div>`;
|
|
101
101
|
html += `</div>`;
|
|
102
102
|
|
|
103
|
+
html += `</div>`; // Close donut-body
|
|
104
|
+
|
|
103
105
|
// Legend with values (or percentages if showPercentages is true)
|
|
104
106
|
const legendLabels = legend ?? segments.map(s => s.label);
|
|
105
107
|
html += `<ul class="chart-legend">`;
|
|
@@ -120,8 +122,6 @@ export function renderDonut(config) {
|
|
|
120
122
|
});
|
|
121
123
|
html += `</ul>`;
|
|
122
124
|
|
|
123
|
-
html += `</div>`; // Close donut-body
|
|
124
|
-
|
|
125
125
|
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
126
126
|
html += `</figure>`;
|
|
127
127
|
|
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, id, rotateLabels, downloadData, downloadDataUrl, connectDots, dots: showDots = true, chartType = 'dot' } = config;
|
|
18
|
+
const { title, subtitle, data, max, min, legend, legendTitle, animate, format, id, rotateLabels, downloadData, downloadDataUrl, connectDots, dots: showDots = true, chartType = 'dot' } = config;
|
|
19
19
|
|
|
20
20
|
if (!data || data.length === 0) {
|
|
21
21
|
return `<!-- Dot chart: no data provided -->`;
|
|
@@ -58,18 +58,6 @@ export function renderDot(config) {
|
|
|
58
58
|
html += `</figcaption>`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
// Legend
|
|
62
|
-
if (seriesKeys.length > 0) {
|
|
63
|
-
html += `<ul class="chart-legend">`;
|
|
64
|
-
seriesKeys.forEach((key, i) => {
|
|
65
|
-
const label = legendLabels[i] ?? key;
|
|
66
|
-
const colorClass = `chart-color-${i + 1}`;
|
|
67
|
-
const seriesClass = `chart-series-${slugify(key)}`;
|
|
68
|
-
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</li>`;
|
|
69
|
-
});
|
|
70
|
-
html += `</ul>`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
61
|
html += `<div class="chart-body">`;
|
|
74
62
|
|
|
75
63
|
// Y-axis
|
|
@@ -154,6 +142,22 @@ export function renderDot(config) {
|
|
|
154
142
|
|
|
155
143
|
html += `</div>`; // close chart-scroll
|
|
156
144
|
html += `</div>`; // close chart-body
|
|
145
|
+
|
|
146
|
+
// Legend
|
|
147
|
+
if (seriesKeys.length > 0 || legendTitle) {
|
|
148
|
+
if (legendTitle) {
|
|
149
|
+
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
150
|
+
}
|
|
151
|
+
html += `<ul class="chart-legend">`;
|
|
152
|
+
seriesKeys.forEach((key, i) => {
|
|
153
|
+
const label = legendLabels[i] ?? key;
|
|
154
|
+
const colorClass = `chart-color-${i + 1}`;
|
|
155
|
+
const seriesClass = `chart-series-${slugify(key)}`;
|
|
156
|
+
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</li>`;
|
|
157
|
+
});
|
|
158
|
+
html += `</ul>`;
|
|
159
|
+
}
|
|
160
|
+
|
|
157
161
|
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
158
162
|
html += `</figure>`;
|
|
159
163
|
|
package/src/renderers/sankey.js
CHANGED
|
@@ -415,22 +415,6 @@ export function renderSankey(config) {
|
|
|
415
415
|
html += `</figcaption>`;
|
|
416
416
|
}
|
|
417
417
|
|
|
418
|
-
// Legend (optional)
|
|
419
|
-
if (legend) {
|
|
420
|
-
html += `<ul class="chart-legend">`;
|
|
421
|
-
nodes.forEach((node, i) => {
|
|
422
|
-
const colorClass = `chart-color-${nodeColors.get(node)}`;
|
|
423
|
-
const seriesClass = `chart-series-${slugify(node)}`;
|
|
424
|
-
const throughput = nodeThroughput.get(node);
|
|
425
|
-
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(node)}`;
|
|
426
|
-
if (format) {
|
|
427
|
-
html += ` <span class="legend-value">${formatNumber(throughput, format) || throughput}</span>`;
|
|
428
|
-
}
|
|
429
|
-
html += `</li>`;
|
|
430
|
-
});
|
|
431
|
-
html += `</ul>`;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
418
|
// Build neighbor map for node hover highlighting
|
|
435
419
|
const nodeNeighbors = new Map();
|
|
436
420
|
nodes.forEach(n => nodeNeighbors.set(n, new Set()));
|
|
@@ -521,6 +505,23 @@ export function renderSankey(config) {
|
|
|
521
505
|
});
|
|
522
506
|
|
|
523
507
|
html += `</div>`;
|
|
508
|
+
|
|
509
|
+
// Legend (optional)
|
|
510
|
+
if (legend) {
|
|
511
|
+
html += `<ul class="chart-legend">`;
|
|
512
|
+
nodes.forEach((node, i) => {
|
|
513
|
+
const colorClass = `chart-color-${nodeColors.get(node)}`;
|
|
514
|
+
const seriesClass = `chart-series-${slugify(node)}`;
|
|
515
|
+
const throughput = nodeThroughput.get(node);
|
|
516
|
+
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(node)}`;
|
|
517
|
+
if (format) {
|
|
518
|
+
html += ` <span class="legend-value">${formatNumber(throughput, format) || throughput}</span>`;
|
|
519
|
+
}
|
|
520
|
+
html += `</li>`;
|
|
521
|
+
});
|
|
522
|
+
html += `</ul>`;
|
|
523
|
+
}
|
|
524
|
+
|
|
524
525
|
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
525
526
|
html += `</figure>`;
|
|
526
527
|
|
package/src/renderers/scatter.js
CHANGED
|
@@ -6,19 +6,21 @@ import { formatNumber } from '../formatters.js';
|
|
|
6
6
|
* @param {Object} config - Chart configuration
|
|
7
7
|
* @param {string} config.title - Chart title
|
|
8
8
|
* @param {string} [config.subtitle] - Chart subtitle
|
|
9
|
-
* @param {Object[]} config.data - Chart data (
|
|
9
|
+
* @param {Object[]} config.data - Chart data (label + named columns: x, y, size, series)
|
|
10
10
|
* @param {number} [config.maxX] - Maximum X value (defaults to max in data)
|
|
11
11
|
* @param {number} [config.maxY] - Maximum Y value (defaults to max in data)
|
|
12
12
|
* @param {number} [config.minX] - Minimum X value (defaults to min in data or 0)
|
|
13
13
|
* @param {number} [config.minY] - Minimum Y value (defaults to min in data or 0)
|
|
14
14
|
* @param {string[]} [config.legend] - Legend labels for series
|
|
15
|
+
* @param {string} [config.legendTitle] - Title for series legend
|
|
16
|
+
* @param {string} [config.sizeTitle] - Title for size legend (enables size legend display)
|
|
15
17
|
* @param {boolean} [config.animate] - Enable animations
|
|
16
18
|
* @param {string} [config.titleX] - X-axis title (defaults to column name)
|
|
17
19
|
* @param {string} [config.titleY] - Y-axis title (defaults to column name)
|
|
18
20
|
* @returns {string} - HTML string
|
|
19
21
|
*/
|
|
20
22
|
export function renderScatter(config) {
|
|
21
|
-
const { title, subtitle, data, maxX, maxY, minX, minY, legend, animate, format, titleX, titleY, id, downloadData, downloadDataUrl } = config;
|
|
23
|
+
const { title, subtitle, data, maxX, maxY, minX, minY, legend, legendTitle, sizeTitle, animate, format, titleX, titleY, id, downloadData, downloadDataUrl, proportional } = config;
|
|
22
24
|
|
|
23
25
|
// Handle nested X/Y format for scatter charts
|
|
24
26
|
const fmtX = format?.x || format || {};
|
|
@@ -30,25 +32,52 @@ export function renderScatter(config) {
|
|
|
30
32
|
|
|
31
33
|
const animateClass = animate ? ' chart-animate' : '';
|
|
32
34
|
|
|
33
|
-
//
|
|
35
|
+
// Named column detection (case-insensitive), with positional fallback for x/y
|
|
34
36
|
const keys = Object.keys(data[0]);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
37
|
+
const findKey = name => keys.find(k => k.toLowerCase() === name) || null;
|
|
38
|
+
|
|
39
|
+
// First column is always label
|
|
40
|
+
const labelKey = keys[0];
|
|
41
|
+
|
|
42
|
+
// X and Y: named if both exist, otherwise positional (columns 2 and 3)
|
|
43
|
+
const namedX = findKey('x');
|
|
44
|
+
const namedY = findKey('y');
|
|
45
|
+
const xKey = (namedX && namedY) ? namedX : keys[1];
|
|
46
|
+
const yKey = (namedX && namedY) ? namedY : keys[2];
|
|
47
|
+
|
|
48
|
+
// Size and series: named only (no positional fallback)
|
|
49
|
+
const sizeKey = findKey('size');
|
|
50
|
+
const seriesKey = findKey('series');
|
|
39
51
|
|
|
40
52
|
// Axis titles: explicit config overrides column names
|
|
41
53
|
const xAxisTitle = titleX ?? xKey;
|
|
42
54
|
const yAxisTitle = titleY ?? yKey;
|
|
43
55
|
|
|
44
|
-
// Map data to dots
|
|
56
|
+
// Map data to dots
|
|
45
57
|
const dots = data.map(item => ({
|
|
46
58
|
label: item[labelKey] ?? '',
|
|
47
59
|
x: typeof item[xKey] === 'number' ? item[xKey] : parseFloat(item[xKey]) || 0,
|
|
48
60
|
y: typeof item[yKey] === 'number' ? item[yKey] : parseFloat(item[yKey]) || 0,
|
|
61
|
+
rawSize: sizeKey ? (typeof item[sizeKey] === 'number' ? item[sizeKey] : parseFloat(item[sizeKey]) || 0) : null,
|
|
49
62
|
series: seriesKey ? (item[seriesKey] ?? 'default') : 'default'
|
|
50
63
|
}));
|
|
51
64
|
|
|
65
|
+
// Size normalization: non-positive values get minimum size (scale 0)
|
|
66
|
+
if (sizeKey) {
|
|
67
|
+
const sizeValues = dots.map(d => d.rawSize).filter(v => v > 0);
|
|
68
|
+
const minSizeVal = sizeValues.length ? Math.min(...sizeValues) : 1;
|
|
69
|
+
const maxSizeVal = sizeValues.length ? Math.max(...sizeValues) : 1;
|
|
70
|
+
const sizeRange = maxSizeVal - minSizeVal;
|
|
71
|
+
|
|
72
|
+
dots.forEach(dot => {
|
|
73
|
+
if (dot.rawSize <= 0 || sizeRange === 0) {
|
|
74
|
+
dot.sizeScale = 0;
|
|
75
|
+
} else {
|
|
76
|
+
dot.sizeScale = (dot.rawSize - minSizeVal) / sizeRange;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
52
81
|
// Calculate bounds
|
|
53
82
|
const xValues = dots.map(d => d.x);
|
|
54
83
|
const yValues = dots.map(d => d.y);
|
|
@@ -63,6 +92,7 @@ export function renderScatter(config) {
|
|
|
63
92
|
const calcMinY = minY ?? (dataMinY < 0 ? dataMinY : 0);
|
|
64
93
|
const rangeX = calcMaxX - calcMinX;
|
|
65
94
|
const rangeY = calcMaxY - calcMinY;
|
|
95
|
+
const dataAspectRatio = rangeY > 0 ? rangeX / rangeY : 1;
|
|
66
96
|
|
|
67
97
|
const hasNegativeX = calcMinX < 0;
|
|
68
98
|
const hasNegativeY = calcMinY < 0;
|
|
@@ -77,8 +107,9 @@ export function renderScatter(config) {
|
|
|
77
107
|
const seriesIndex = new Map(seriesList.map((s, i) => [s, i]));
|
|
78
108
|
|
|
79
109
|
const negativeClasses = (hasNegativeX ? ' has-negative-x' : '') + (hasNegativeY ? ' has-negative-y' : '');
|
|
110
|
+
const proportionalClass = proportional ? ' chart-proportional' : '';
|
|
80
111
|
const idClass = id ? ` chart-${id}` : '';
|
|
81
|
-
let html = `<figure class="chart chart-scatter${animateClass}${negativeClasses}${idClass}">`;
|
|
112
|
+
let html = `<figure class="chart chart-scatter${animateClass}${negativeClasses}${proportionalClass}${idClass}">`;
|
|
82
113
|
|
|
83
114
|
if (title) {
|
|
84
115
|
html += `<figcaption class="chart-title">${escapeHtml(title)}`;
|
|
@@ -88,19 +119,6 @@ export function renderScatter(config) {
|
|
|
88
119
|
html += `</figcaption>`;
|
|
89
120
|
}
|
|
90
121
|
|
|
91
|
-
// Legend (if multiple series)
|
|
92
|
-
if (seriesList.length > 1 || legend) {
|
|
93
|
-
const legendLabels = legend ?? seriesList;
|
|
94
|
-
html += `<ul class="chart-legend">`;
|
|
95
|
-
seriesList.forEach((series, i) => {
|
|
96
|
-
const label = legendLabels[i] ?? series;
|
|
97
|
-
const colorClass = `chart-color-${i + 1}`;
|
|
98
|
-
const seriesClass = `chart-series-${slugify(series)}`;
|
|
99
|
-
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</li>`;
|
|
100
|
-
});
|
|
101
|
-
html += `</ul>`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
122
|
html += `<div class="chart-body">`;
|
|
105
123
|
|
|
106
124
|
// Y-axis
|
|
@@ -119,9 +137,11 @@ export function renderScatter(config) {
|
|
|
119
137
|
if (hasNegativeY) containerStyles.push(`--zero-position-y: ${zeroPctY.toFixed(2)}%`);
|
|
120
138
|
const containerStyle = containerStyles.length > 0 ? ` style="${containerStyles.join('; ')}"` : '';
|
|
121
139
|
html += `<div class="scatter-container"${containerStyle}>`;
|
|
122
|
-
|
|
140
|
+
const dotAreaStyle = proportional ? ` style="--data-aspect-ratio: ${dataAspectRatio.toFixed(4)}"` : '';
|
|
141
|
+
html += `<div class="dot-area"${dotAreaStyle}>`;
|
|
123
142
|
html += `<div class="dot-field">`;
|
|
124
143
|
|
|
144
|
+
const fmtSize = format?.size || {};
|
|
125
145
|
dots.forEach((dot, i) => {
|
|
126
146
|
const xPct = rangeX > 0 ? ((dot.x - calcMinX) / rangeX) * 100 : 0;
|
|
127
147
|
const yPct = rangeY > 0 ? ((dot.y - calcMinY) / rangeY) * 100 : 0;
|
|
@@ -130,10 +150,22 @@ export function renderScatter(config) {
|
|
|
130
150
|
const seriesClass = `chart-series-${slugify(dot.series)}`;
|
|
131
151
|
const fmtXVal = formatNumber(dot.x, fmtX) || dot.x;
|
|
132
152
|
const fmtYVal = formatNumber(dot.y, fmtY) || dot.y;
|
|
133
|
-
|
|
153
|
+
|
|
154
|
+
// Build tooltip with optional size value
|
|
155
|
+
let tooltipText = dot.label ? `${dot.label}: (${fmtXVal}, ${fmtYVal})` : `(${fmtXVal}, ${fmtYVal})`;
|
|
156
|
+
if (sizeKey && dot.rawSize !== null) {
|
|
157
|
+
const fmtSizeVal = formatNumber(dot.rawSize, fmtSize) || dot.rawSize;
|
|
158
|
+
tooltipText += ` [${fmtSizeVal}]`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Build style string with optional size scale
|
|
162
|
+
let styleStr = `--dot-index: ${i}; --x: ${xPct.toFixed(2)}%; --value: ${yPct.toFixed(2)}%`;
|
|
163
|
+
if (sizeKey) {
|
|
164
|
+
styleStr += `; --size-scale: ${dot.sizeScale.toFixed(4)}`;
|
|
165
|
+
}
|
|
134
166
|
|
|
135
167
|
html += `<div class="dot ${colorClass} ${seriesClass}" `;
|
|
136
|
-
html += `style="
|
|
168
|
+
html += `style="${styleStr}" `;
|
|
137
169
|
html += `title="${escapeHtml(tooltipText)}"`;
|
|
138
170
|
html += `></div>`;
|
|
139
171
|
});
|
|
@@ -153,6 +185,41 @@ export function renderScatter(config) {
|
|
|
153
185
|
|
|
154
186
|
html += `</div>`;
|
|
155
187
|
html += `</div>`;
|
|
188
|
+
|
|
189
|
+
// Legend (if multiple series or legendTitle specified)
|
|
190
|
+
if (seriesList.length > 1 || legend || legendTitle) {
|
|
191
|
+
const legendLabels = legend ?? seriesList;
|
|
192
|
+
if (legendTitle) {
|
|
193
|
+
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
194
|
+
}
|
|
195
|
+
html += `<ul class="chart-legend">`;
|
|
196
|
+
seriesList.forEach((series, i) => {
|
|
197
|
+
const label = legendLabels[i] ?? series;
|
|
198
|
+
const colorClass = `chart-color-${i + 1}`;
|
|
199
|
+
const seriesClass = `chart-series-${slugify(series)}`;
|
|
200
|
+
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</li>`;
|
|
201
|
+
});
|
|
202
|
+
html += `</ul>`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Size legend (when sizeTitle is specified and size column exists)
|
|
206
|
+
if (sizeTitle && sizeKey) {
|
|
207
|
+
const sizeValues = dots.map(d => d.rawSize).filter(v => v > 0);
|
|
208
|
+
const minSizeVal = sizeValues.length ? Math.min(...sizeValues) : 0;
|
|
209
|
+
const maxSizeVal = sizeValues.length ? Math.max(...sizeValues) : 0;
|
|
210
|
+
const fmtSizeLegend = format?.size || format || {};
|
|
211
|
+
const minFormatted = formatNumber(minSizeVal, fmtSizeLegend) || minSizeVal;
|
|
212
|
+
const maxFormatted = formatNumber(maxSizeVal, fmtSizeLegend) || maxSizeVal;
|
|
213
|
+
|
|
214
|
+
html += `<div class="chart-size-legend">`;
|
|
215
|
+
html += `<span class="chart-legend-title">${escapeHtml(sizeTitle)}</span>`;
|
|
216
|
+
html += `<div class="size-legend-items">`;
|
|
217
|
+
html += `<span class="size-legend-item"><span class="size-dot size-dot-min"></span><span class="size-value">${minFormatted}</span></span>`;
|
|
218
|
+
html += `<span class="size-legend-item"><span class="size-dot size-dot-max"></span><span class="size-value">${maxFormatted}</span></span>`;
|
|
219
|
+
html += `</div>`;
|
|
220
|
+
html += `</div>`;
|
|
221
|
+
}
|
|
222
|
+
|
|
156
223
|
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
157
224
|
html += `</figure>`;
|
|
158
225
|
|
|
@@ -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, id, downloadData, downloadDataUrl } = config;
|
|
16
|
+
const { title, subtitle, data, max, legend, legendTitle, animate, format, id, downloadData, downloadDataUrl } = config;
|
|
17
17
|
|
|
18
18
|
if (!data || data.length === 0) {
|
|
19
19
|
return `<!-- Stacked bar chart: no data provided -->`;
|
|
@@ -46,7 +46,10 @@ export function renderStackedBar(config) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Legend
|
|
49
|
-
if (seriesKeys.length > 0) {
|
|
49
|
+
if (seriesKeys.length > 0 || legendTitle) {
|
|
50
|
+
if (legendTitle) {
|
|
51
|
+
html += `<span class="chart-legend-title">${escapeHtml(legendTitle)}</span>`;
|
|
52
|
+
}
|
|
50
53
|
html += `<ul class="chart-legend">`;
|
|
51
54
|
seriesKeys.forEach((key, i) => {
|
|
52
55
|
const label = legendLabels[i] ?? key;
|
|
@@ -67,18 +67,6 @@ export function renderStackedColumn(config) {
|
|
|
67
67
|
html += `</figcaption>`;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Legend
|
|
71
|
-
if (seriesKeys.length > 0) {
|
|
72
|
-
html += `<ul class="chart-legend">`;
|
|
73
|
-
seriesKeys.forEach((key, i) => {
|
|
74
|
-
const label = legendLabels[i] ?? key;
|
|
75
|
-
const colorClass = `chart-color-${i + 1}`;
|
|
76
|
-
const seriesClass = `chart-series-${slugify(key)}`;
|
|
77
|
-
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</li>`;
|
|
78
|
-
});
|
|
79
|
-
html += `</ul>`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
70
|
html += `<div class="chart-body">`;
|
|
83
71
|
|
|
84
72
|
// Y-axis with --zero-position for label positioning
|
|
@@ -192,6 +180,19 @@ export function renderStackedColumn(config) {
|
|
|
192
180
|
|
|
193
181
|
html += `</div>`; // close chart-scroll
|
|
194
182
|
html += `</div>`; // close chart-body
|
|
183
|
+
|
|
184
|
+
// Legend
|
|
185
|
+
if (seriesKeys.length > 0) {
|
|
186
|
+
html += `<ul class="chart-legend">`;
|
|
187
|
+
seriesKeys.forEach((key, i) => {
|
|
188
|
+
const label = legendLabels[i] ?? key;
|
|
189
|
+
const colorClass = `chart-color-${i + 1}`;
|
|
190
|
+
const seriesClass = `chart-series-${slugify(key)}`;
|
|
191
|
+
html += `<li class="chart-legend-item ${colorClass} ${seriesClass}">${escapeHtml(label)}</li>`;
|
|
192
|
+
});
|
|
193
|
+
html += `</ul>`;
|
|
194
|
+
}
|
|
195
|
+
|
|
195
196
|
html += renderDownloadLink(downloadDataUrl, downloadData);
|
|
196
197
|
html += `</figure>`;
|
|
197
198
|
|