@expressivecss/expressive 0.12.0 → 0.13.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/CHANGELOG.md +231 -1
- package/README.md +37 -1
- package/dist/css/expressive.css +5111 -36
- package/dist/css/expressive.css.map +1 -1
- package/dist/css/expressive.min.css +1 -1
- package/dist/css/expressive.min.css.map +1 -1
- package/dist/fonts/material-symbols-outlined.woff2 +0 -0
- package/dist/fonts/material-symbols-rounded.woff2 +0 -0
- package/dist/fonts/material-symbols-sharp.woff2 +0 -0
- package/dist/js/chunks/{chunk-HLDXZ7XB.mjs → chunk-3CLXLPYS.mjs} +2 -2
- package/dist/js/chunks/chunk-3DHK5CZU.mjs +105 -0
- package/dist/js/chunks/chunk-3DHK5CZU.mjs.map +7 -0
- package/dist/js/chunks/chunk-4PWAPV6F.mjs +7 -0
- package/dist/js/chunks/chunk-4PWAPV6F.mjs.map +7 -0
- package/dist/js/chunks/{chunk-HB6IW7QQ.mjs → chunk-55IA2XUC.mjs} +2 -2
- package/dist/js/chunks/chunk-CCWDV5CR.mjs +292 -0
- package/dist/js/chunks/chunk-CCWDV5CR.mjs.map +7 -0
- package/dist/js/chunks/chunk-DTKR54I6.mjs +270 -0
- package/dist/js/chunks/chunk-DTKR54I6.mjs.map +7 -0
- package/dist/js/chunks/chunk-GVGT6MIU.mjs +47 -0
- package/dist/js/chunks/chunk-GVGT6MIU.mjs.map +7 -0
- package/dist/js/chunks/chunk-HKT2S7VM.mjs +48 -0
- package/dist/js/chunks/chunk-HKT2S7VM.mjs.map +7 -0
- package/dist/js/chunks/chunk-K4CJCRGE.mjs +262 -0
- package/dist/js/chunks/chunk-K4CJCRGE.mjs.map +7 -0
- package/dist/js/chunks/chunk-LNCCYGPC.mjs +163 -0
- package/dist/js/chunks/chunk-LNCCYGPC.mjs.map +7 -0
- package/dist/js/chunks/chunk-NQLC3YMW.mjs +1 -0
- package/dist/js/chunks/chunk-O5XPWXIQ.mjs +406 -0
- package/dist/js/chunks/chunk-O5XPWXIQ.mjs.map +7 -0
- package/dist/js/chunks/chunk-P3G3QHII.mjs +166 -0
- package/dist/js/chunks/chunk-P3G3QHII.mjs.map +7 -0
- package/dist/js/chunks/{chunk-H2WA4XHH.mjs → chunk-PLYXLSH3.mjs} +4 -4
- package/dist/js/chunks/chunk-QKWPQK5N.mjs +179 -0
- package/dist/js/chunks/chunk-QKWPQK5N.mjs.map +7 -0
- package/dist/js/chunks/chunk-RHRQKTTX.mjs +139 -0
- package/dist/js/chunks/chunk-RHRQKTTX.mjs.map +7 -0
- package/dist/js/chunks/chunk-SPETGK25.mjs +59 -0
- package/dist/js/chunks/chunk-SPETGK25.mjs.map +7 -0
- package/dist/js/chunks/{chunk-YSBCHYXP.mjs → chunk-U2OTHOTI.mjs} +116 -12
- package/dist/js/chunks/chunk-U2OTHOTI.mjs.map +7 -0
- package/dist/js/chunks/chunk-VTLBW7QS.mjs +124 -0
- package/dist/js/chunks/chunk-VTLBW7QS.mjs.map +7 -0
- package/dist/js/chunks/{chunk-RFZZCYVU.mjs → chunk-XGHORT7J.mjs} +2 -2
- package/dist/js/chunks/{chunk-HNFSMIBY.mjs → chunk-XH6BEEMY.mjs} +69 -16
- package/dist/js/chunks/chunk-XH6BEEMY.mjs.map +7 -0
- package/dist/js/expressive.cjs +2209 -32
- package/dist/js/expressive.cjs.map +4 -4
- package/dist/js/expressive.js +2209 -32
- package/dist/js/expressive.js.map +4 -4
- package/dist/js/expressive.min.js +10 -10
- package/dist/js/expressive.min.js.map +4 -4
- package/dist/js/expressive.mjs +68 -20
- package/dist/js/expressive.mjs.map +1 -1
- package/dist/js/internal/behaviors/forms.mjs +1 -1
- package/dist/js/internal/components/autocomplete.mjs +2 -2
- package/dist/js/internal/components/barChart.mjs +10 -0
- package/dist/js/internal/components/barChart.mjs.map +7 -0
- package/dist/js/internal/components/chips.mjs +3 -3
- package/dist/js/internal/components/columnChart.mjs +9 -0
- package/dist/js/internal/components/columnChart.mjs.map +7 -0
- package/dist/js/internal/components/commandPalette.mjs +9 -0
- package/dist/js/internal/components/commandPalette.mjs.map +7 -0
- package/dist/js/internal/components/datepicker.mjs +3 -3
- package/dist/js/internal/components/heatmapChart.mjs +9 -0
- package/dist/js/internal/components/heatmapChart.mjs.map +7 -0
- package/dist/js/internal/components/index.mjs +65 -17
- package/dist/js/internal/components/lineChart.mjs +12 -0
- package/dist/js/internal/components/lineChart.mjs.map +7 -0
- package/dist/js/internal/components/menu.mjs +1 -1
- package/dist/js/internal/components/messageScroller.mjs +9 -0
- package/dist/js/internal/components/messageScroller.mjs.map +7 -0
- package/dist/js/internal/components/mixedChart.mjs +10 -0
- package/dist/js/internal/components/mixedChart.mjs.map +7 -0
- package/dist/js/internal/components/pieChart.mjs +9 -0
- package/dist/js/internal/components/pieChart.mjs.map +7 -0
- package/dist/js/internal/components/questionnaire.mjs +9 -0
- package/dist/js/internal/components/questionnaire.mjs.map +7 -0
- package/dist/js/internal/components/radarChart.mjs +9 -0
- package/dist/js/internal/components/radarChart.mjs.map +7 -0
- package/dist/js/internal/components/radialChart.mjs +9 -0
- package/dist/js/internal/components/radialChart.mjs.map +7 -0
- package/dist/js/internal/components/registry.mjs +23 -11
- package/dist/js/internal/components/richTextEditor.mjs +9 -0
- package/dist/js/internal/components/richTextEditor.mjs.map +7 -0
- package/dist/js/internal/components/select.mjs +2 -2
- package/dist/js/modular.mjs +68 -20
- package/dist/types/behaviors/forms.d.ts +22 -0
- package/dist/types/components/barChart.d.ts +24 -0
- package/dist/types/components/columnChart.d.ts +21 -0
- package/dist/types/components/commandPalette.d.ts +96 -0
- package/dist/types/components/heatmapChart.d.ts +49 -0
- package/dist/types/components/index.d.ts +12 -0
- package/dist/types/components/lineChart.d.ts +134 -0
- package/dist/types/components/menu.d.ts +31 -0
- package/dist/types/components/messageScroller.d.ts +111 -0
- package/dist/types/components/mixedChart.d.ts +25 -0
- package/dist/types/components/pieChart.d.ts +40 -0
- package/dist/types/components/questionnaire.d.ts +96 -0
- package/dist/types/components/radarChart.d.ts +39 -0
- package/dist/types/components/radialChart.d.ts +45 -0
- package/dist/types/components/registry.d.ts +44 -0
- package/dist/types/components/richTextEditor.d.ts +71 -0
- package/dist/types/modular.d.ts +10 -1
- package/package.json +4 -1
- package/src/sass/README.md +3 -6
- package/src/sass/abstracts/_index.scss +1 -1
- package/src/sass/abstracts/_mixins.scss +226 -0
- package/src/sass/abstracts/_variables.scss +14 -0
- package/src/sass/base/_typography.scss +45 -0
- package/src/sass/components/_accordion.scss +161 -0
- package/src/sass/components/_aura.scss +119 -0
- package/src/sass/components/_avatar.scss +86 -0
- package/src/sass/components/_bar-chart.scss +285 -0
- package/src/sass/components/_bottom-sheet.scss +1 -0
- package/src/sass/components/_button-groups.scss +13 -3
- package/src/sass/components/_cards.scss +16 -13
- package/src/sass/components/_column-chart.scss +134 -0
- package/src/sass/components/_command-palette.scss +140 -0
- package/src/sass/components/_countdown.scss +89 -0
- package/src/sass/components/_data-table.scss +188 -0
- package/src/sass/components/_dialog.scss +1 -0
- package/src/sass/components/_empty-state.scss +78 -0
- package/src/sass/components/_expanding-card.scss +3 -0
- package/src/sass/components/_heatmap-chart.scss +156 -0
- package/src/sass/components/_index.scss +27 -0
- package/src/sass/components/_kpi.scss +162 -0
- package/src/sass/components/_line-chart.scss +102 -0
- package/src/sass/components/_marquee.scss +111 -0
- package/src/sass/components/_menu.scss +10 -0
- package/src/sass/components/_message-scroller.scss +66 -0
- package/src/sass/components/_message.scss +202 -0
- package/src/sass/components/_mixed-chart.scss +157 -0
- package/src/sass/components/_navigation-bar.scss +1 -0
- package/src/sass/components/_navigation-rail.scss +9 -0
- package/src/sass/components/_pie-chart.scss +203 -0
- package/src/sass/components/_popover.scss +112 -0
- package/src/sass/components/_questionnaire.scss +144 -0
- package/src/sass/components/_radar-chart.scss +205 -0
- package/src/sass/components/_radial-chart.scss +174 -0
- package/src/sass/components/_rich-text-editor.scss +99 -0
- package/src/sass/components/_search.scss +1 -0
- package/src/sass/components/_side-sheet.scss +1 -0
- package/src/sass/components/_skeleton.scss +81 -0
- package/src/sass/components/_snackbar.scss +1 -0
- package/src/sass/components/_stat.scss +86 -0
- package/src/sass/components/_stepper.scss +190 -0
- package/src/sass/components/_timeline.scss +114 -0
- package/src/sass/components/_toolbar.scss +1 -0
- package/src/sass/components/_tooltip.scss +2 -0
- package/src/sass/components/_tree.scss +139 -0
- package/src/sass/components/forms/_drop-zone.scss +128 -0
- package/src/sass/components/forms/_index.scss +2 -0
- package/src/sass/components/forms/_input-fields.scss +133 -2
- package/src/sass/components/forms/_rating.scss +165 -0
- package/src/sass/tokens/_reference.scss +3 -15
- package/src/sass/utilities/_index.scss +2 -0
- package/src/sass/utilities/_scroll-area.scss +35 -0
- package/src/sass/utilities/_shapes.scss +164 -0
- package/dist/js/chunks/chunk-G3I5H2U3.mjs +0 -7
- package/dist/js/chunks/chunk-G3I5H2U3.mjs.map +0 -7
- package/dist/js/chunks/chunk-HNFSMIBY.mjs.map +0 -7
- package/dist/js/chunks/chunk-PSSHQWCX.mjs +0 -100
- package/dist/js/chunks/chunk-PSSHQWCX.mjs.map +0 -7
- package/dist/js/chunks/chunk-WWPAPHMA.mjs +0 -1
- package/dist/js/chunks/chunk-YSBCHYXP.mjs.map +0 -7
- /package/dist/js/chunks/{chunk-HLDXZ7XB.mjs.map → chunk-3CLXLPYS.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-HB6IW7QQ.mjs.map → chunk-55IA2XUC.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-WWPAPHMA.mjs.map → chunk-NQLC3YMW.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-H2WA4XHH.mjs.map → chunk-PLYXLSH3.mjs.map} +0 -0
- /package/dist/js/chunks/{chunk-RFZZCYVU.mjs.map → chunk-XGHORT7J.mjs.map} +0 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LineChart
|
|
3
|
+
} from "./chunk-O5XPWXIQ.mjs";
|
|
4
|
+
|
|
5
|
+
// src/ts/components/heatmapChart.ts
|
|
6
|
+
var HeatmapChart = class extends LineChart {
|
|
7
|
+
get _type() {
|
|
8
|
+
return "heatmap-chart";
|
|
9
|
+
}
|
|
10
|
+
get _key() {
|
|
11
|
+
return "Expressive_HeatmapChart";
|
|
12
|
+
}
|
|
13
|
+
static init(els, options = {}) {
|
|
14
|
+
return super.init(els, options);
|
|
15
|
+
}
|
|
16
|
+
static getInstance(el) {
|
|
17
|
+
return el["Expressive_HeatmapChart"];
|
|
18
|
+
}
|
|
19
|
+
/** Highlights one cell, numbered row by row. Pass -1 to hide the tooltip. */
|
|
20
|
+
show(index) {
|
|
21
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
22
|
+
this._cells[this.activeIndex]?.classList.remove("active");
|
|
23
|
+
this.activeIndex = index;
|
|
24
|
+
const cell = this._cells[index];
|
|
25
|
+
this._tooltip.hidden = !cell;
|
|
26
|
+
if (!cell) return;
|
|
27
|
+
cell.classList.add("active");
|
|
28
|
+
const columns = this.series.length;
|
|
29
|
+
const [row, series] = [Math.floor(index / columns), this.series[index % columns]];
|
|
30
|
+
const title = document.createElement("strong");
|
|
31
|
+
title.textContent = this.labels[row];
|
|
32
|
+
const [name, text] = [document.createElement("span"), document.createElement("span")];
|
|
33
|
+
name.textContent = series.name;
|
|
34
|
+
if (this._levels[index] === null) name.className = "gap";
|
|
35
|
+
else name.style.setProperty("--level", String(this._levels[index]));
|
|
36
|
+
text.textContent = series.text[row];
|
|
37
|
+
this._tooltip.replaceChildren(title, name, text);
|
|
38
|
+
const end = cell.offsetLeft + cell.offsetWidth / 2 > this._plot.clientWidth / 2;
|
|
39
|
+
this._tooltip.classList.toggle("end", end);
|
|
40
|
+
this._tooltip.style.left = `${cell.offsetLeft + (end ? 0 : cell.offsetWidth)}px`;
|
|
41
|
+
this._tooltip.style.top = `${cell.offsetTop}px`;
|
|
42
|
+
}
|
|
43
|
+
/** Arrows move the way they point, so left and right swap in RTL. */
|
|
44
|
+
_keyTarget(key) {
|
|
45
|
+
if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
|
|
46
|
+
const [columns, last] = [this.series.length, this.labels.length - 1];
|
|
47
|
+
const index = Math.max(0, this.activeIndex);
|
|
48
|
+
const [row, column] = [Math.floor(index / columns), index % columns];
|
|
49
|
+
const [next, back] = this._reversed ? ["ArrowLeft", "ArrowRight"] : ["ArrowRight", "ArrowLeft"];
|
|
50
|
+
const to = {
|
|
51
|
+
[next]: [row, column + 1],
|
|
52
|
+
[back]: [row, column - 1],
|
|
53
|
+
ArrowDown: [row + 1, column],
|
|
54
|
+
ArrowUp: [row - 1, column],
|
|
55
|
+
Home: [row, 0],
|
|
56
|
+
End: [row, columns - 1]
|
|
57
|
+
}[key];
|
|
58
|
+
if (!to) return void 0;
|
|
59
|
+
if (this.activeIndex < 0) return 0;
|
|
60
|
+
const clamp = (value, max) => Math.min(max, Math.max(0, value));
|
|
61
|
+
return clamp(to[0], last) * columns + clamp(to[1], columns - 1);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The cell under the pointer by its position, or -1 off the grid. A touch
|
|
65
|
+
* keeps its first target while it drags, so the target cannot be used. The
|
|
66
|
+
* cells and gaps are even, so each band's edge falls in a gap and a gap
|
|
67
|
+
* counts as the nearer cell.
|
|
68
|
+
*/
|
|
69
|
+
_indexAt(e) {
|
|
70
|
+
const [columns, rows] = [this.series.length, this.labels.length];
|
|
71
|
+
const first = this._cells[0].getBoundingClientRect();
|
|
72
|
+
const last = this._cells[this._cells.length - 1].getBoundingClientRect();
|
|
73
|
+
const [left, right] = [Math.min(first.left, last.left), Math.max(first.right, last.right)];
|
|
74
|
+
const [x, y] = [e.clientX, e.clientY];
|
|
75
|
+
if (x < left || x > right || y < first.top || y > last.bottom) return -1;
|
|
76
|
+
const band = (offset, size, count) => Math.min(count - 1, Math.floor(offset / (size || 1) * count));
|
|
77
|
+
const column = band(x - left, right - left, columns);
|
|
78
|
+
return band(y - first.top, last.bottom - first.top, rows) * columns + (this._reversed ? columns - 1 - column : column);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The legend: a gradient from the lowest cell's shade to the highest's,
|
|
82
|
+
* labelled with their text. A fixed scale wider than the data shows only
|
|
83
|
+
* the part the data uses.
|
|
84
|
+
*/
|
|
85
|
+
_guides() {
|
|
86
|
+
const legend = document.createElement("div");
|
|
87
|
+
legend.className = "heatmap-chart-legend";
|
|
88
|
+
legend.setAttribute("aria-hidden", "true");
|
|
89
|
+
const [low, scale, high] = ["span", "span", "span"].map((tag) => document.createElement(tag));
|
|
90
|
+
[low.textContent, high.textContent] = this._ends;
|
|
91
|
+
scale.className = "heatmap-chart-scale";
|
|
92
|
+
if (this._reversed) scale.style.setProperty("--heatmap-chart-to", "left");
|
|
93
|
+
scale.style.setProperty("--low", String(this._endLevels[0]));
|
|
94
|
+
scale.style.setProperty("--high", String(this._endLevels[1]));
|
|
95
|
+
legend.append(low, scale, high);
|
|
96
|
+
return [legend];
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* A grid of the row headers and cells, row by row, with the column headers
|
|
100
|
+
* below. Each cell's --level shades it in CSS.
|
|
101
|
+
*/
|
|
102
|
+
_draw() {
|
|
103
|
+
const columns = this.series.length;
|
|
104
|
+
let [low, high] = [Infinity, -Infinity];
|
|
105
|
+
this._ends = ["", ""];
|
|
106
|
+
this.series.forEach((series) => series.values.forEach((value, row) => {
|
|
107
|
+
if (value === null) return;
|
|
108
|
+
if (value < low) [low, this._ends[0]] = [value, series.text[row]];
|
|
109
|
+
if (value > high) [high, this._ends[1]] = [value, series.text[row]];
|
|
110
|
+
}));
|
|
111
|
+
const option = (name) => {
|
|
112
|
+
const value = parseFloat(this.el.dataset[name] ?? "");
|
|
113
|
+
return Number.isFinite(value) ? value : this.options[name];
|
|
114
|
+
};
|
|
115
|
+
const [min, max] = [option("min") ?? low, option("max") ?? high];
|
|
116
|
+
const shade = (value) => Math.min(1, Math.max(0, (value - min) / (max - min || 1)));
|
|
117
|
+
this._endLevels = [shade(low), shade(high)];
|
|
118
|
+
const grid = document.createElement("div");
|
|
119
|
+
grid.className = "heatmap-chart-grid";
|
|
120
|
+
grid.setAttribute("aria-hidden", "true");
|
|
121
|
+
grid.style.setProperty("--columns", String(columns));
|
|
122
|
+
grid.style.setProperty("--rows", String(this.labels.length));
|
|
123
|
+
const span = (className, text = "") => {
|
|
124
|
+
const element = document.createElement("span");
|
|
125
|
+
element.className = className;
|
|
126
|
+
element.textContent = text;
|
|
127
|
+
return element;
|
|
128
|
+
};
|
|
129
|
+
const label = (cell) => cell?.dataset.label ?? cell?.textContent.trim() ?? "";
|
|
130
|
+
const values = this.el.classList.contains("values");
|
|
131
|
+
this._cells = [];
|
|
132
|
+
this._levels = [];
|
|
133
|
+
this.labels.forEach((_, row) => {
|
|
134
|
+
grid.append(span("heatmap-chart-row", label(this._rows[row].cells[0])));
|
|
135
|
+
this.series.forEach((series) => {
|
|
136
|
+
const value = series.values[row];
|
|
137
|
+
const level = value === null ? null : shade(value);
|
|
138
|
+
const cell = span("heatmap-chart-cell", values ? series.text[row] : "");
|
|
139
|
+
if (level === null) cell.classList.add("gap");
|
|
140
|
+
else cell.style.setProperty("--level", String(level));
|
|
141
|
+
this._cells.push(cell);
|
|
142
|
+
this._levels.push(level);
|
|
143
|
+
grid.append(cell);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
const labels = this._header.slice(1, columns + 1).map(label);
|
|
147
|
+
labels.forEach((text, column) => {
|
|
148
|
+
if (!text) return;
|
|
149
|
+
let end = column + 1;
|
|
150
|
+
while (end < columns && !labels[end]) end++;
|
|
151
|
+
const element = span("heatmap-chart-column", text);
|
|
152
|
+
element.classList.toggle("run", end > column + 1);
|
|
153
|
+
element.style.gridArea = `${this.labels.length + 1} / ${column + 2} / auto / ${end + 2}`;
|
|
154
|
+
grid.append(element);
|
|
155
|
+
});
|
|
156
|
+
return grid;
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
HeatmapChart
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=chunk-LNCCYGPC.mjs.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/ts/components/heatmapChart.ts"],
|
|
4
|
+
"sourcesContent": ["import { InitElements, InitElement } from '../core/component';\nimport { LineChart, LineChartOptions } from './lineChart';\n\nexport type HeatmapChartOptions = LineChartOptions;\n\n/**\n * Heatmap chart. It reads its <table> like LineChart, but draws every cell\n * as a square of the grid, shaded from the lowest value to the highest, with\n * the row headers beside it and the column headers below. A `data-label` on\n * a header cell replaces its text on the grid; an empty one hides it. The\n * tooltip shows one cell, and the arrow keys move round the grid.\n *\n * Re-run `HeatmapChart.init(el)` after changing the table.\n */\nexport class HeatmapChart extends LineChart {\n // No initializers: LineChart's constructor draws before they would run.\n /** Every cell, row by row. */\n private _cells: HTMLElement[];\n /** Each cell's shade from 0 to 1, or null for a gap. */\n private _levels: (number | null)[];\n /** The text of the lowest and highest cells, for the legend. */\n private _ends: [string, string];\n /** The shades of the lowest and highest cells, where the legend's gradient runs. */\n private _endLevels: [number, number];\n\n protected get _type() {\n return 'heatmap-chart';\n }\n\n protected get _key() {\n return 'Expressive_HeatmapChart';\n }\n\n static init(el: HTMLElement, options?: Partial<HeatmapChartOptions>): HeatmapChart;\n static init(els: InitElements<InitElement>, options?: Partial<HeatmapChartOptions>): HeatmapChart[];\n static init(\n els: HTMLElement | InitElements<InitElement>,\n options: Partial<HeatmapChartOptions> = {}\n ): HeatmapChart | HeatmapChart[] {\n return super.init(els as HTMLElement, options) as HeatmapChart | HeatmapChart[];\n }\n\n static getInstance(el: HTMLElement): HeatmapChart {\n return el['Expressive_HeatmapChart'];\n }\n\n /** Highlights one cell, numbered row by row. Pass -1 to hide the tooltip. */\n show(index: number) {\n if (!this._tooltip || index === this.activeIndex) return;\n this._cells[this.activeIndex]?.classList.remove('active');\n this.activeIndex = index;\n const cell = this._cells[index];\n this._tooltip.hidden = !cell;\n if (!cell) return;\n cell.classList.add('active');\n\n const columns = this.series.length;\n const [row, series] = [Math.floor(index / columns), this.series[index % columns]];\n const title = document.createElement('strong');\n title.textContent = this.labels[row];\n const [name, text] = [document.createElement('span'), document.createElement('span')];\n name.textContent = series.name;\n // A gap's swatch is outlined like its cell.\n if (this._levels[index] === null) name.className = 'gap';\n else name.style.setProperty('--level', String(this._levels[index]));\n text.textContent = series.text[row];\n this._tooltip.replaceChildren(title, name, text);\n\n // Beside the cell, on the side with more room.\n const end = cell.offsetLeft + cell.offsetWidth / 2 > this._plot.clientWidth / 2;\n this._tooltip.classList.toggle('end', end);\n this._tooltip.style.left = `${cell.offsetLeft + (end ? 0 : cell.offsetWidth)}px`;\n this._tooltip.style.top = `${cell.offsetTop}px`;\n }\n\n /** Arrows move the way they point, so left and right swap in RTL. */\n protected _keyTarget(key: string): number | undefined {\n // With nothing shown, Escape is left to a dialog or sheet around the chart.\n if (key === 'Escape') return this.activeIndex < 0 ? undefined : -1;\n const [columns, last] = [this.series.length, this.labels.length - 1];\n const index = Math.max(0, this.activeIndex);\n const [row, column] = [Math.floor(index / columns), index % columns];\n const [next, back] = this._reversed ? ['ArrowLeft', 'ArrowRight'] : ['ArrowRight', 'ArrowLeft'];\n const to = {\n [next]: [row, column + 1],\n [back]: [row, column - 1],\n ArrowDown: [row + 1, column],\n ArrowUp: [row - 1, column],\n Home: [row, 0],\n End: [row, columns - 1]\n }[key];\n if (!to) return undefined;\n // The first key after focus or Escape shows the first cell.\n if (this.activeIndex < 0) return 0;\n const clamp = (value: number, max: number) => Math.min(max, Math.max(0, value));\n return clamp(to[0], last) * columns + clamp(to[1], columns - 1);\n }\n\n /**\n * The cell under the pointer by its position, or -1 off the grid. A touch\n * keeps its first target while it drags, so the target cannot be used. The\n * cells and gaps are even, so each band's edge falls in a gap and a gap\n * counts as the nearer cell.\n */\n protected _indexAt(e: PointerEvent) {\n const [columns, rows] = [this.series.length, this.labels.length];\n const first = this._cells[0].getBoundingClientRect();\n const last = this._cells[this._cells.length - 1].getBoundingClientRect();\n const [left, right] = [Math.min(first.left, last.left), Math.max(first.right, last.right)];\n const [x, y] = [e.clientX, e.clientY];\n if (x < left || x > right || y < first.top || y > last.bottom) return -1;\n const band = (offset: number, size: number, count: number) =>\n Math.min(count - 1, Math.floor((offset / (size || 1)) * count));\n const column = band(x - left, right - left, columns);\n return band(y - first.top, last.bottom - first.top, rows) * columns + (this._reversed ? columns - 1 - column : column);\n }\n\n /**\n * The legend: a gradient from the lowest cell's shade to the highest's,\n * labelled with their text. A fixed scale wider than the data shows only\n * the part the data uses.\n */\n protected _guides(): Element[] {\n const legend = document.createElement('div');\n legend.className = 'heatmap-chart-legend';\n legend.setAttribute('aria-hidden', 'true');\n const [low, scale, high] = ['span', 'span', 'span'].map((tag) => document.createElement(tag));\n [low.textContent, high.textContent] = this._ends;\n scale.className = 'heatmap-chart-scale';\n if (this._reversed) scale.style.setProperty('--heatmap-chart-to', 'left');\n scale.style.setProperty('--low', String(this._endLevels[0]));\n scale.style.setProperty('--high', String(this._endLevels[1]));\n legend.append(low, scale, high);\n return [legend];\n }\n\n /**\n * A grid of the row headers and cells, row by row, with the column headers\n * below. Each cell's --level shades it in CSS.\n */\n protected _draw(): Element {\n const columns = this.series.length;\n let [low, high] = [Infinity, -Infinity];\n this._ends = ['', ''];\n this.series.forEach((series) => series.values.forEach((value, row) => {\n if (value === null) return;\n if (value < low) [low, this._ends[0]] = [value, series.text[row]];\n if (value > high) [high, this._ends[1]] = [value, series.text[row]];\n }));\n const option = (name: 'min' | 'max') => {\n const value = parseFloat(this.el.dataset[name] ?? '');\n return Number.isFinite(value) ? value : this.options[name];\n };\n const [min, max] = [option('min') ?? low, option('max') ?? high];\n const shade = (value: number) => Math.min(1, Math.max(0, (value - min) / (max - min || 1)));\n this._endLevels = [shade(low), shade(high)];\n\n const grid = document.createElement('div');\n grid.className = 'heatmap-chart-grid';\n grid.setAttribute('aria-hidden', 'true');\n grid.style.setProperty('--columns', String(columns));\n grid.style.setProperty('--rows', String(this.labels.length));\n const span = (className: string, text = '') => {\n const element = document.createElement('span');\n element.className = className;\n element.textContent = text;\n return element;\n };\n const label = (cell: HTMLTableCellElement | undefined) =>\n cell?.dataset.label ?? cell?.textContent.trim() ?? '';\n const values = this.el.classList.contains('values');\n\n this._cells = [];\n this._levels = [];\n this.labels.forEach((_, row) => {\n grid.append(span('heatmap-chart-row', label(this._rows[row].cells[0])));\n this.series.forEach((series) => {\n const value = series.values[row];\n const level = value === null ? null : shade(value);\n const cell = span('heatmap-chart-cell', values ? series.text[row] : '');\n if (level === null) cell.classList.add('gap');\n else cell.style.setProperty('--level', String(level));\n this._cells.push(cell);\n this._levels.push(level);\n grid.append(cell);\n });\n });\n\n // A column label runs on over the empty labels after it.\n const labels = this._header.slice(1, columns + 1).map(label);\n labels.forEach((text, column) => {\n if (!text) return;\n let end = column + 1;\n while (end < columns && !labels[end]) end++;\n const element = span('heatmap-chart-column', text);\n element.classList.toggle('run', end > column + 1);\n element.style.gridArea = `${this.labels.length + 1} / ${column + 2} / auto / ${end + 2}`;\n grid.append(element);\n });\n return grid;\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=chunk-NQLC3YMW.mjs.map
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component
|
|
3
|
+
} from "./chunk-LTWPGFBR.mjs";
|
|
4
|
+
|
|
5
|
+
// src/ts/components/lineChart.ts
|
|
6
|
+
var _defaults = {
|
|
7
|
+
min: null,
|
|
8
|
+
max: null
|
|
9
|
+
};
|
|
10
|
+
var SVG = "http://www.w3.org/2000/svg";
|
|
11
|
+
var COLORS = 4;
|
|
12
|
+
function svgNode(name, attributes = {}) {
|
|
13
|
+
const element = document.createElementNS(SVG, name);
|
|
14
|
+
for (const key in attributes) element.setAttribute(key, attributes[key]);
|
|
15
|
+
return element;
|
|
16
|
+
}
|
|
17
|
+
var _gradientId = 0;
|
|
18
|
+
function cellValue(cell) {
|
|
19
|
+
if (!cell) return null;
|
|
20
|
+
const raw = cell.dataset.value ?? cell.textContent;
|
|
21
|
+
const text = raw.replace(/\u2212/g, "-").replace(/[\s%\p{Sc}]/gu, "");
|
|
22
|
+
if (!/^[+-]?(\d{1,3}(,\d{3})+|\d+)?(\.\d+)?(e[+-]?\d+)?$/i.test(text) || !/\d/.test(text)) return null;
|
|
23
|
+
return parseFloat(text.replace(/,/g, ""));
|
|
24
|
+
}
|
|
25
|
+
function monotonePath(points) {
|
|
26
|
+
const n = points.length;
|
|
27
|
+
if (n === 0) return "";
|
|
28
|
+
if (n === 1) return `M${points[0][0]},${points[0][1]}h0`;
|
|
29
|
+
const slopes = [];
|
|
30
|
+
for (let i = 0; i < n - 1; i++) {
|
|
31
|
+
slopes.push((points[i + 1][1] - points[i][1]) / (points[i + 1][0] - points[i][0]));
|
|
32
|
+
}
|
|
33
|
+
const tangents = points.map((_, i) => {
|
|
34
|
+
if (i === 0) return slopes[0];
|
|
35
|
+
if (i === n - 1) return slopes[n - 2];
|
|
36
|
+
const [a, b] = [slopes[i - 1], slopes[i]];
|
|
37
|
+
return a * b <= 0 ? 0 : 2 / (1 / a + 1 / b);
|
|
38
|
+
});
|
|
39
|
+
let d = `M${points[0][0]},${points[0][1]}`;
|
|
40
|
+
for (let i = 0; i < n - 1; i++) {
|
|
41
|
+
const [[x0, y0], [x1, y1]] = [points[i], points[i + 1]];
|
|
42
|
+
const h = (x1 - x0) / 3;
|
|
43
|
+
d += `C${x0 + h},${y0 + tangents[i] * h} ${x1 - h},${y1 - tangents[i + 1] * h} ${x1},${y1}`;
|
|
44
|
+
}
|
|
45
|
+
return d;
|
|
46
|
+
}
|
|
47
|
+
var LineChart = class extends Component {
|
|
48
|
+
constructor(el, options) {
|
|
49
|
+
super(el, options, new.target);
|
|
50
|
+
/** The values' x positions and series, read from the table. */
|
|
51
|
+
this.labels = [];
|
|
52
|
+
this.series = [];
|
|
53
|
+
/** The highlighted row, or -1. */
|
|
54
|
+
this.activeIndex = -1;
|
|
55
|
+
this._generated = [];
|
|
56
|
+
/** The table's header cells and body rows, as read. */
|
|
57
|
+
this._header = [];
|
|
58
|
+
this._rows = [];
|
|
59
|
+
/**
|
|
60
|
+
* Whether the rows run right to left: a horizontal row axis in a
|
|
61
|
+
* right-to-left context, like the labels. Bar chart rows keep their order.
|
|
62
|
+
*/
|
|
63
|
+
this._reversed = false;
|
|
64
|
+
this._onPointerMove = (e) => {
|
|
65
|
+
this.show(this._indexAt(e));
|
|
66
|
+
};
|
|
67
|
+
this._onFocus = () => {
|
|
68
|
+
if (this.activeIndex < 0 && this._plot.matches(":focus-visible")) this.show(0);
|
|
69
|
+
};
|
|
70
|
+
this._onLeave = (e) => {
|
|
71
|
+
if (e.pointerType === "touch" || this._plot.matches(":focus-visible")) return;
|
|
72
|
+
this.show(-1);
|
|
73
|
+
};
|
|
74
|
+
/** The browser took the touch to scroll the page. */
|
|
75
|
+
this._onCancel = () => {
|
|
76
|
+
this.show(-1);
|
|
77
|
+
};
|
|
78
|
+
this._onKeyDown = (e) => {
|
|
79
|
+
const next = this._keyTarget(e.key);
|
|
80
|
+
if (next === void 0) return;
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
this.show(next);
|
|
83
|
+
};
|
|
84
|
+
this.el[this._key] = this;
|
|
85
|
+
this.options = {
|
|
86
|
+
...new.target.defaults,
|
|
87
|
+
...options
|
|
88
|
+
};
|
|
89
|
+
this._read();
|
|
90
|
+
if (!this.labels.length || !this.series.length) return;
|
|
91
|
+
this._reversed = !this._horizontal && getComputedStyle(this.el).direction === "rtl";
|
|
92
|
+
this._render();
|
|
93
|
+
if (!this.el.classList.contains("sparkline")) {
|
|
94
|
+
this._plot.tabIndex = 0;
|
|
95
|
+
this._plot.addEventListener("pointerdown", this._onPointerMove);
|
|
96
|
+
this._plot.addEventListener("pointermove", this._onPointerMove);
|
|
97
|
+
this._plot.addEventListener("pointerleave", this._onLeave);
|
|
98
|
+
this._plot.addEventListener("pointercancel", this._onCancel);
|
|
99
|
+
this._plot.addEventListener("keydown", this._onKeyDown);
|
|
100
|
+
this._plot.addEventListener("focus", this._onFocus);
|
|
101
|
+
this._plot.addEventListener("blur", this._onLeave);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** The class prefix of the generated parts; ColumnChart reuses this class. */
|
|
105
|
+
get _type() {
|
|
106
|
+
return "line-chart";
|
|
107
|
+
}
|
|
108
|
+
/** Whether the y axis always includes zero, as columns grow from it. */
|
|
109
|
+
get _fromZero() {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether the rows run down the plot and the values across it, as in a
|
|
114
|
+
* bar chart. The rows then keep their order in a right-to-left context.
|
|
115
|
+
*/
|
|
116
|
+
get _horizontal() {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Whether the rows go round a circle, as in a pie or radar chart. The
|
|
121
|
+
* arrows then step round it, wrapping from the last row to the first.
|
|
122
|
+
*/
|
|
123
|
+
get _circular() {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
/** Whether the cursor marks each series with a point. */
|
|
127
|
+
get _points() {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
get _key() {
|
|
131
|
+
return "Expressive_LineChart";
|
|
132
|
+
}
|
|
133
|
+
static get defaults() {
|
|
134
|
+
return _defaults;
|
|
135
|
+
}
|
|
136
|
+
static init(els, options = {}) {
|
|
137
|
+
return super.init(els, options, this);
|
|
138
|
+
}
|
|
139
|
+
static getInstance(el) {
|
|
140
|
+
return el["Expressive_LineChart"];
|
|
141
|
+
}
|
|
142
|
+
destroy() {
|
|
143
|
+
this._generated.forEach((node) => node.remove());
|
|
144
|
+
this._generated = [];
|
|
145
|
+
this.el[this._key] = void 0;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Highlights one row: moves the cursor and fills the tooltip.
|
|
149
|
+
* Pass -1 to hide them. Does nothing for a chart without data.
|
|
150
|
+
*/
|
|
151
|
+
show(index) {
|
|
152
|
+
if (!this._tooltip || index === this.activeIndex) return;
|
|
153
|
+
this.activeIndex = index;
|
|
154
|
+
const hidden = index < 0 || index >= this.labels.length;
|
|
155
|
+
this._cursor.hidden = this._tooltip.hidden = hidden;
|
|
156
|
+
if (hidden) return;
|
|
157
|
+
this._cursor.replaceChildren();
|
|
158
|
+
const title = document.createElement("strong");
|
|
159
|
+
title.textContent = this.labels[index];
|
|
160
|
+
this._tooltip.replaceChildren(title);
|
|
161
|
+
this.series.forEach((series, i) => {
|
|
162
|
+
const value = series.values[index];
|
|
163
|
+
if (value === null) return;
|
|
164
|
+
const color = String(i % COLORS + 1);
|
|
165
|
+
const top = series.tops[index];
|
|
166
|
+
if (top !== null && this._points) {
|
|
167
|
+
const dot = document.createElement("span");
|
|
168
|
+
dot.className = series.className;
|
|
169
|
+
dot.dataset.series = color;
|
|
170
|
+
dot.style.top = `${series.y(top)}%`;
|
|
171
|
+
this._cursor.append(dot);
|
|
172
|
+
}
|
|
173
|
+
const name = document.createElement("span");
|
|
174
|
+
name.dataset.series = color;
|
|
175
|
+
name.textContent = series.name;
|
|
176
|
+
const text = document.createElement("span");
|
|
177
|
+
text.textContent = series.text[index];
|
|
178
|
+
this._tooltip.append(name, text);
|
|
179
|
+
});
|
|
180
|
+
this._place(index);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Moves the filled cursor and tooltip to a row: beside its band, with the
|
|
184
|
+
* tooltip opening back over the plot past the middle.
|
|
185
|
+
*/
|
|
186
|
+
_place(index) {
|
|
187
|
+
const x = this._x(index);
|
|
188
|
+
const side = this._horizontal ? "top" : "left";
|
|
189
|
+
this._cursor.style[side] = this._tooltip.style[side] = `${x}%`;
|
|
190
|
+
this._tooltip.classList.toggle("end", x > 50);
|
|
191
|
+
}
|
|
192
|
+
_read() {
|
|
193
|
+
const table = this.el.querySelector(":scope > table");
|
|
194
|
+
if (!table) return;
|
|
195
|
+
const headerRow = table.tHead?.rows[0] ?? table.rows[0];
|
|
196
|
+
const header = Array.from(headerRow?.cells ?? []);
|
|
197
|
+
const rows = Array.from(table.tBodies).flatMap((body) => Array.from(body.rows)).filter((row) => row !== headerRow);
|
|
198
|
+
[this._header, this._rows] = [header, rows];
|
|
199
|
+
this.labels = rows.map((row) => row.cells[0]?.textContent.trim() ?? "");
|
|
200
|
+
this.series = header.slice(1).map((cell, i) => {
|
|
201
|
+
const values = rows.map((row) => cellValue(row.cells[i + 1]));
|
|
202
|
+
return {
|
|
203
|
+
name: cell.textContent.trim(),
|
|
204
|
+
className: cell.className,
|
|
205
|
+
text: rows.map((row) => row.cells[i + 1]?.textContent.trim() ?? ""),
|
|
206
|
+
values,
|
|
207
|
+
tops: values,
|
|
208
|
+
y: void 0
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
const stacked = this.el.classList.contains("stacked");
|
|
212
|
+
if (stacked) {
|
|
213
|
+
const totals = this.labels.map(() => 0);
|
|
214
|
+
for (const series of this.series) {
|
|
215
|
+
series.base = [...totals];
|
|
216
|
+
series.tops = series.values.map((value, i) => totals[i] = value === null || totals[i] === null ? null : totals[i] + value);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (this.series.every((series) => series.tops.every((value) => value === null))) this.series = [];
|
|
220
|
+
const option = (name) => this._number(name) ?? this.options[name];
|
|
221
|
+
this._y = this._scale(this.series, option("min"), option("max"));
|
|
222
|
+
}
|
|
223
|
+
/** A number from one of the chart's data attributes, or null. */
|
|
224
|
+
_number(name) {
|
|
225
|
+
const value = parseFloat(this.el.dataset[name] ?? "");
|
|
226
|
+
return Number.isFinite(value) ? value : null;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Fits a y scale to the series' tops, between `min` and `max` where they
|
|
230
|
+
* are set, and gives it to each series.
|
|
231
|
+
*/
|
|
232
|
+
_scale(series, min, max, fromZero = this._fromZero) {
|
|
233
|
+
const stacked = this.el.classList.contains("stacked");
|
|
234
|
+
let [low, high] = [Infinity, -Infinity];
|
|
235
|
+
for (const { tops } of series) {
|
|
236
|
+
for (const value of tops) {
|
|
237
|
+
if (value === null) continue;
|
|
238
|
+
low = Math.min(low, value);
|
|
239
|
+
high = Math.max(high, value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (low > high) low = high = 0;
|
|
243
|
+
if (fromZero) {
|
|
244
|
+
const [bottom, top] = [Math.min(0, low), Math.max(0, high)];
|
|
245
|
+
const room = (top - (min ?? bottom)) * 0.1 || 1;
|
|
246
|
+
min ?? (min = bottom < 0 ? bottom - room : 0);
|
|
247
|
+
max ?? (max = top + room);
|
|
248
|
+
} else {
|
|
249
|
+
const pad = (high - low) * 0.1 || 1;
|
|
250
|
+
min ?? (min = stacked ? Math.min(0, low) : low - pad);
|
|
251
|
+
max ?? (max = high + (stacked ? (high - min) * 0.1 || 1 : pad));
|
|
252
|
+
}
|
|
253
|
+
const y = (value) => 100 - (value - min) / (max - min || 1) * 100;
|
|
254
|
+
for (const each of series) each.y = y;
|
|
255
|
+
return y;
|
|
256
|
+
}
|
|
257
|
+
/** Each row sits in the middle of an equal band, beside its label. */
|
|
258
|
+
_x(index) {
|
|
259
|
+
const x = (index + 0.5) / this.labels.length * 100;
|
|
260
|
+
return this._reversed ? 100 - x : x;
|
|
261
|
+
}
|
|
262
|
+
_render() {
|
|
263
|
+
const type = this._type;
|
|
264
|
+
this._plot = document.createElement("div");
|
|
265
|
+
this._plot.className = `${type}-plot`;
|
|
266
|
+
const caption = this.el.querySelector(":scope > figcaption")?.textContent.trim();
|
|
267
|
+
this._plot.setAttribute("role", "group");
|
|
268
|
+
this._plot.setAttribute("aria-roledescription", type.replace(/-/g, " "));
|
|
269
|
+
if (caption) this._plot.setAttribute("aria-label", caption);
|
|
270
|
+
this._cursor = document.createElement("div");
|
|
271
|
+
this._cursor.className = `${type}-cursor`;
|
|
272
|
+
this._cursor.hidden = true;
|
|
273
|
+
this._tooltip = document.createElement("div");
|
|
274
|
+
this._tooltip.className = `${type}-tooltip`;
|
|
275
|
+
this._tooltip.hidden = true;
|
|
276
|
+
this._tooltip.setAttribute("aria-live", "polite");
|
|
277
|
+
this._plot.append(this._draw(), this._cursor, this._tooltip);
|
|
278
|
+
this._generated = [this._plot, ...this._guides()];
|
|
279
|
+
this.el.append(...this._generated);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The <tfoot> row as a total to show over the plot, its value over its
|
|
283
|
+
* label, or null without one.
|
|
284
|
+
*/
|
|
285
|
+
_total(className) {
|
|
286
|
+
const footer = this.el.querySelector(":scope > table")?.tFoot?.rows[0];
|
|
287
|
+
if (!footer) return null;
|
|
288
|
+
const total = document.createElement("div");
|
|
289
|
+
total.className = className;
|
|
290
|
+
const [label, value] = [document.createElement("span"), document.createElement("strong")];
|
|
291
|
+
label.textContent = footer.cells[0]?.textContent.trim() ?? "";
|
|
292
|
+
value.textContent = footer.cells[1]?.textContent.trim() ?? "";
|
|
293
|
+
total.append(value, label);
|
|
294
|
+
return total;
|
|
295
|
+
}
|
|
296
|
+
/** The parts below the plot: the row labels and the legend. */
|
|
297
|
+
_guides() {
|
|
298
|
+
const labels = document.createElement("ol");
|
|
299
|
+
labels.className = `${this._type}-labels`;
|
|
300
|
+
labels.setAttribute("aria-hidden", "true");
|
|
301
|
+
for (const text of this.labels) {
|
|
302
|
+
const li = document.createElement("li");
|
|
303
|
+
li.textContent = text;
|
|
304
|
+
labels.append(li);
|
|
305
|
+
}
|
|
306
|
+
return [labels, ...this._legend()];
|
|
307
|
+
}
|
|
308
|
+
/** The series legend, for more than one series. */
|
|
309
|
+
_legend() {
|
|
310
|
+
if (this.series.length < 2) return [];
|
|
311
|
+
const legend = document.createElement("ul");
|
|
312
|
+
legend.className = `${this._type}-legend`;
|
|
313
|
+
legend.setAttribute("aria-hidden", "true");
|
|
314
|
+
legend.append(...this.series.map((series, i) => {
|
|
315
|
+
const li = document.createElement("li");
|
|
316
|
+
li.dataset.series = String(i % COLORS + 1);
|
|
317
|
+
li.textContent = series.name;
|
|
318
|
+
return li;
|
|
319
|
+
}));
|
|
320
|
+
return [legend];
|
|
321
|
+
}
|
|
322
|
+
/** Draws the series, which fill the plot. */
|
|
323
|
+
_draw(drawn = this.series) {
|
|
324
|
+
const svg = document.createElementNS(SVG, "svg");
|
|
325
|
+
svg.setAttribute("viewBox", "0 0 100 100");
|
|
326
|
+
svg.setAttribute("preserveAspectRatio", "none");
|
|
327
|
+
svg.setAttribute("aria-hidden", "true");
|
|
328
|
+
svg.setAttribute("focusable", "false");
|
|
329
|
+
for (const series of drawn) {
|
|
330
|
+
const group = document.createElementNS(SVG, "g");
|
|
331
|
+
group.setAttribute("class", `${this._type}-series ${series.className}`.trim());
|
|
332
|
+
group.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
|
|
333
|
+
const runs = [[]];
|
|
334
|
+
series.tops.forEach((value, index) => {
|
|
335
|
+
if (value === null) runs.push([]);
|
|
336
|
+
else runs[runs.length - 1].push(index);
|
|
337
|
+
});
|
|
338
|
+
const filled = runs.filter((run) => run.length);
|
|
339
|
+
const points = (run, values) => run.map((index) => [this._x(index), series.y(values[index])]);
|
|
340
|
+
const tops = filled.map((run) => monotonePath(points(run, series.tops)));
|
|
341
|
+
const id = `line-chart-gradient-${++_gradientId}`;
|
|
342
|
+
const gradient = document.createElementNS(SVG, "linearGradient");
|
|
343
|
+
gradient.id = id;
|
|
344
|
+
gradient.setAttribute("x2", "0");
|
|
345
|
+
gradient.setAttribute("y2", "1");
|
|
346
|
+
gradient.append(document.createElementNS(SVG, "stop"), document.createElementNS(SVG, "stop"));
|
|
347
|
+
gradient.lastElementChild.setAttribute("offset", "1");
|
|
348
|
+
const area = document.createElementNS(SVG, "path");
|
|
349
|
+
area.setAttribute("class", `${this._type}-area`);
|
|
350
|
+
area.setAttribute("fill", `url(#${id})`);
|
|
351
|
+
area.setAttribute("d", filled.map((run, r) => {
|
|
352
|
+
if (!series.base) return `${tops[r]}V100H${this._x(run[0])}Z`;
|
|
353
|
+
const base = points(run, series.base).map(([x, y]) => [x, Math.min(100, y)]);
|
|
354
|
+
return `${tops[r]}L${monotonePath(base.reverse()).slice(1)}Z`;
|
|
355
|
+
}).join(""));
|
|
356
|
+
const line = document.createElementNS(SVG, "path");
|
|
357
|
+
line.setAttribute("class", `${this._type}-line`);
|
|
358
|
+
line.setAttribute("d", tops.join(""));
|
|
359
|
+
group.append(gradient, area, line);
|
|
360
|
+
svg.append(group);
|
|
361
|
+
}
|
|
362
|
+
return svg;
|
|
363
|
+
}
|
|
364
|
+
/** The row under the pointer, or -1. */
|
|
365
|
+
_indexAt(e) {
|
|
366
|
+
const box = this._plot.getBoundingClientRect();
|
|
367
|
+
const fraction = this._horizontal ? (e.clientY - box.top) / box.height : (e.clientX - box.left) / box.width;
|
|
368
|
+
const index = Math.floor((this._reversed ? 1 - fraction : fraction) * this.labels.length);
|
|
369
|
+
return Math.min(this.labels.length - 1, Math.max(0, index));
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* For a circular chart, the pointer's position from the plot's middle: a
|
|
373
|
+
* fraction of a turn clockwise from the top, and its distance as a share
|
|
374
|
+
* of the plot's width.
|
|
375
|
+
*/
|
|
376
|
+
_polarAt(e) {
|
|
377
|
+
const box = this._plot.getBoundingClientRect();
|
|
378
|
+
const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.height / 2];
|
|
379
|
+
return [(Math.atan2(x, -y) / (2 * Math.PI) + 1) % 1, Math.hypot(x, y) / box.width];
|
|
380
|
+
}
|
|
381
|
+
/** The keys that move to the next row and to the one before. */
|
|
382
|
+
get _arrows() {
|
|
383
|
+
if (this._circular) return [["ArrowRight", "ArrowDown"], ["ArrowLeft", "ArrowUp"]];
|
|
384
|
+
if (this._horizontal) return [["ArrowDown"], ["ArrowUp"]];
|
|
385
|
+
return this._reversed ? [["ArrowLeft"], ["ArrowRight"]] : [["ArrowRight"], ["ArrowLeft"]];
|
|
386
|
+
}
|
|
387
|
+
/** The index a key moves to, or undefined for a key the chart ignores. */
|
|
388
|
+
_keyTarget(key) {
|
|
389
|
+
const last = this.labels.length - 1;
|
|
390
|
+
const [forward, back] = this._arrows;
|
|
391
|
+
const step = (by) => {
|
|
392
|
+
if (!this._circular) return Math.min(last, Math.max(0, this.activeIndex + by));
|
|
393
|
+
if (this.activeIndex < 0) return by > 0 ? 0 : last;
|
|
394
|
+
return (this.activeIndex + by + last + 1) % (last + 1);
|
|
395
|
+
};
|
|
396
|
+
if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
|
|
397
|
+
return forward.includes(key) ? step(1) : back.includes(key) ? step(-1) : { Home: 0, End: last }[key];
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
export {
|
|
402
|
+
COLORS,
|
|
403
|
+
svgNode,
|
|
404
|
+
LineChart
|
|
405
|
+
};
|
|
406
|
+
//# sourceMappingURL=chunk-O5XPWXIQ.mjs.map
|