deepbox 0.1.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/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/CSRMatrix-CwGwQRea.d.cts +219 -0
- package/dist/CSRMatrix-KzNt6QpS.d.ts +219 -0
- package/dist/Tensor-BQLk1ltW.d.cts +147 -0
- package/dist/Tensor-g8mUClel.d.ts +147 -0
- package/dist/chunk-4S73VUBD.js +677 -0
- package/dist/chunk-4S73VUBD.js.map +1 -0
- package/dist/chunk-5R4S63PF.js +2925 -0
- package/dist/chunk-5R4S63PF.js.map +1 -0
- package/dist/chunk-6AE5FKKQ.cjs +9264 -0
- package/dist/chunk-6AE5FKKQ.cjs.map +1 -0
- package/dist/chunk-AD436M45.js +3854 -0
- package/dist/chunk-AD436M45.js.map +1 -0
- package/dist/chunk-ALS7ETWZ.cjs +4263 -0
- package/dist/chunk-ALS7ETWZ.cjs.map +1 -0
- package/dist/chunk-AU7XHGKJ.js +2092 -0
- package/dist/chunk-AU7XHGKJ.js.map +1 -0
- package/dist/chunk-B5TNKUEY.js +1481 -0
- package/dist/chunk-B5TNKUEY.js.map +1 -0
- package/dist/chunk-BCR7G3A6.js +9136 -0
- package/dist/chunk-BCR7G3A6.js.map +1 -0
- package/dist/chunk-C4PKXY74.cjs +1917 -0
- package/dist/chunk-C4PKXY74.cjs.map +1 -0
- package/dist/chunk-DWZY6PIP.cjs +6400 -0
- package/dist/chunk-DWZY6PIP.cjs.map +1 -0
- package/dist/chunk-E3EU5FZO.cjs +2113 -0
- package/dist/chunk-E3EU5FZO.cjs.map +1 -0
- package/dist/chunk-F3JWBINJ.js +1054 -0
- package/dist/chunk-F3JWBINJ.js.map +1 -0
- package/dist/chunk-FJYLIGJX.js +1940 -0
- package/dist/chunk-FJYLIGJX.js.map +1 -0
- package/dist/chunk-JSCDE774.cjs +729 -0
- package/dist/chunk-JSCDE774.cjs.map +1 -0
- package/dist/chunk-LWECRCW2.cjs +2412 -0
- package/dist/chunk-LWECRCW2.cjs.map +1 -0
- package/dist/chunk-MLBMYKCG.js +6379 -0
- package/dist/chunk-MLBMYKCG.js.map +1 -0
- package/dist/chunk-OX6QXFMV.cjs +3874 -0
- package/dist/chunk-OX6QXFMV.cjs.map +1 -0
- package/dist/chunk-PHV2DKRS.cjs +1072 -0
- package/dist/chunk-PHV2DKRS.cjs.map +1 -0
- package/dist/chunk-PL7TAYKI.js +4056 -0
- package/dist/chunk-PL7TAYKI.js.map +1 -0
- package/dist/chunk-PR647I7R.js +1898 -0
- package/dist/chunk-PR647I7R.js.map +1 -0
- package/dist/chunk-QERHVCHC.cjs +2960 -0
- package/dist/chunk-QERHVCHC.cjs.map +1 -0
- package/dist/chunk-XEG44RF6.cjs +1514 -0
- package/dist/chunk-XEG44RF6.cjs.map +1 -0
- package/dist/chunk-XMWVME2W.js +2377 -0
- package/dist/chunk-XMWVME2W.js.map +1 -0
- package/dist/chunk-ZB75FESB.cjs +1979 -0
- package/dist/chunk-ZB75FESB.cjs.map +1 -0
- package/dist/chunk-ZLW62TJG.cjs +4061 -0
- package/dist/chunk-ZLW62TJG.cjs.map +1 -0
- package/dist/chunk-ZXKBDFP3.js +4235 -0
- package/dist/chunk-ZXKBDFP3.js.map +1 -0
- package/dist/core/index.cjs +204 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +2 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/dataframe/index.cjs +22 -0
- package/dist/dataframe/index.cjs.map +1 -0
- package/dist/dataframe/index.d.cts +3 -0
- package/dist/dataframe/index.d.ts +3 -0
- package/dist/dataframe/index.js +5 -0
- package/dist/dataframe/index.js.map +1 -0
- package/dist/datasets/index.cjs +134 -0
- package/dist/datasets/index.cjs.map +1 -0
- package/dist/datasets/index.d.cts +3 -0
- package/dist/datasets/index.d.ts +3 -0
- package/dist/datasets/index.js +5 -0
- package/dist/datasets/index.js.map +1 -0
- package/dist/index-74AB8Cyh.d.cts +1126 -0
- package/dist/index-9oQx1HgV.d.cts +1180 -0
- package/dist/index-BJY2SI4i.d.ts +483 -0
- package/dist/index-BWGhrDlr.d.ts +733 -0
- package/dist/index-B_DK4FKY.d.cts +242 -0
- package/dist/index-BbA2Gxfl.d.ts +456 -0
- package/dist/index-BgHYAoSS.d.cts +837 -0
- package/dist/index-BndMbqsM.d.ts +1439 -0
- package/dist/index-C1mfVYoo.d.ts +2517 -0
- package/dist/index-CCvlwAmL.d.cts +809 -0
- package/dist/index-CDw5CnOU.d.ts +785 -0
- package/dist/index-Cn3SdB0O.d.ts +1126 -0
- package/dist/index-CrqLlS-a.d.ts +776 -0
- package/dist/index-D61yaSMY.d.cts +483 -0
- package/dist/index-D9Loo1_A.d.cts +2517 -0
- package/dist/index-DIT_OO9C.d.cts +785 -0
- package/dist/index-DIp_RrRt.d.ts +242 -0
- package/dist/index-DbultU6X.d.cts +1427 -0
- package/dist/index-DmEg_LCm.d.cts +776 -0
- package/dist/index-DoPWVxPo.d.cts +1439 -0
- package/dist/index-DuCxd-8d.d.ts +837 -0
- package/dist/index-Dx42TZaY.d.ts +809 -0
- package/dist/index-DyZ4QQf5.d.cts +456 -0
- package/dist/index-GFAVyOWO.d.ts +1427 -0
- package/dist/index-WHQLn0e8.d.cts +733 -0
- package/dist/index-ZtI1Iy4L.d.ts +1180 -0
- package/dist/index-eJgeni9c.d.cts +1911 -0
- package/dist/index-tk4lSYod.d.ts +1911 -0
- package/dist/index.cjs +72 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +17 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/linalg/index.cjs +86 -0
- package/dist/linalg/index.cjs.map +1 -0
- package/dist/linalg/index.d.cts +3 -0
- package/dist/linalg/index.d.ts +3 -0
- package/dist/linalg/index.js +5 -0
- package/dist/linalg/index.js.map +1 -0
- package/dist/metrics/index.cjs +158 -0
- package/dist/metrics/index.cjs.map +1 -0
- package/dist/metrics/index.d.cts +3 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.js +5 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/ml/index.cjs +87 -0
- package/dist/ml/index.cjs.map +1 -0
- package/dist/ml/index.d.cts +3 -0
- package/dist/ml/index.d.ts +3 -0
- package/dist/ml/index.js +6 -0
- package/dist/ml/index.js.map +1 -0
- package/dist/ndarray/index.cjs +501 -0
- package/dist/ndarray/index.cjs.map +1 -0
- package/dist/ndarray/index.d.cts +5 -0
- package/dist/ndarray/index.d.ts +5 -0
- package/dist/ndarray/index.js +4 -0
- package/dist/ndarray/index.js.map +1 -0
- package/dist/nn/index.cjs +142 -0
- package/dist/nn/index.cjs.map +1 -0
- package/dist/nn/index.d.cts +6 -0
- package/dist/nn/index.d.ts +6 -0
- package/dist/nn/index.js +5 -0
- package/dist/nn/index.js.map +1 -0
- package/dist/optim/index.cjs +77 -0
- package/dist/optim/index.cjs.map +1 -0
- package/dist/optim/index.d.cts +4 -0
- package/dist/optim/index.d.ts +4 -0
- package/dist/optim/index.js +4 -0
- package/dist/optim/index.js.map +1 -0
- package/dist/plot/index.cjs +114 -0
- package/dist/plot/index.cjs.map +1 -0
- package/dist/plot/index.d.cts +6 -0
- package/dist/plot/index.d.ts +6 -0
- package/dist/plot/index.js +5 -0
- package/dist/plot/index.js.map +1 -0
- package/dist/preprocess/index.cjs +82 -0
- package/dist/preprocess/index.cjs.map +1 -0
- package/dist/preprocess/index.d.cts +4 -0
- package/dist/preprocess/index.d.ts +4 -0
- package/dist/preprocess/index.js +5 -0
- package/dist/preprocess/index.js.map +1 -0
- package/dist/random/index.cjs +74 -0
- package/dist/random/index.cjs.map +1 -0
- package/dist/random/index.d.cts +3 -0
- package/dist/random/index.d.ts +3 -0
- package/dist/random/index.js +5 -0
- package/dist/random/index.js.map +1 -0
- package/dist/stats/index.cjs +142 -0
- package/dist/stats/index.cjs.map +1 -0
- package/dist/stats/index.d.cts +3 -0
- package/dist/stats/index.d.ts +3 -0
- package/dist/stats/index.js +5 -0
- package/dist/stats/index.js.map +1 -0
- package/dist/tensor-B96jjJLQ.d.cts +205 -0
- package/dist/tensor-B96jjJLQ.d.ts +205 -0
- package/package.json +226 -0
|
@@ -0,0 +1,4263 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunk6AE5FKKQ_cjs = require('./chunk-6AE5FKKQ.cjs');
|
|
4
|
+
var chunkJSCDE774_cjs = require('./chunk-JSCDE774.cjs');
|
|
5
|
+
|
|
6
|
+
// src/plot/index.ts
|
|
7
|
+
var plot_exports = {};
|
|
8
|
+
chunkJSCDE774_cjs.__export(plot_exports, {
|
|
9
|
+
Axes: () => Axes,
|
|
10
|
+
Figure: () => Figure,
|
|
11
|
+
bar: () => bar,
|
|
12
|
+
barh: () => barh,
|
|
13
|
+
boxplot: () => boxplot,
|
|
14
|
+
contour: () => contour,
|
|
15
|
+
contourf: () => contourf,
|
|
16
|
+
figure: () => figure,
|
|
17
|
+
gca: () => gca,
|
|
18
|
+
heatmap: () => heatmap,
|
|
19
|
+
hist: () => hist,
|
|
20
|
+
imshow: () => imshow,
|
|
21
|
+
legend: () => legend,
|
|
22
|
+
pie: () => pie,
|
|
23
|
+
plot: () => plot,
|
|
24
|
+
plotConfusionMatrix: () => plotConfusionMatrix,
|
|
25
|
+
plotDecisionBoundary: () => plotDecisionBoundary,
|
|
26
|
+
plotLearningCurve: () => plotLearningCurve,
|
|
27
|
+
plotPrecisionRecallCurve: () => plotPrecisionRecallCurve,
|
|
28
|
+
plotRocCurve: () => plotRocCurve,
|
|
29
|
+
plotValidationCurve: () => plotValidationCurve,
|
|
30
|
+
saveFig: () => saveFig,
|
|
31
|
+
scatter: () => scatter,
|
|
32
|
+
show: () => show,
|
|
33
|
+
subplot: () => subplot,
|
|
34
|
+
violinplot: () => violinplot
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// src/plot/utils/colors.ts
|
|
38
|
+
var colorCache = /* @__PURE__ */ new Map();
|
|
39
|
+
var namedColors = {
|
|
40
|
+
aliceblue: "#f0f8ff",
|
|
41
|
+
antiquewhite: "#faebd7",
|
|
42
|
+
aqua: "#00ffff",
|
|
43
|
+
aquamarine: "#7fffd4",
|
|
44
|
+
azure: "#f0ffff",
|
|
45
|
+
beige: "#f5f5dc",
|
|
46
|
+
bisque: "#ffe4c4",
|
|
47
|
+
black: "#000000",
|
|
48
|
+
blanchedalmond: "#ffebcd",
|
|
49
|
+
blue: "#0000ff",
|
|
50
|
+
blueviolet: "#8a2be2",
|
|
51
|
+
brown: "#a52a2a",
|
|
52
|
+
burlywood: "#deb887",
|
|
53
|
+
cadetblue: "#5f9ea0",
|
|
54
|
+
chartreuse: "#7fff00",
|
|
55
|
+
chocolate: "#d2691e",
|
|
56
|
+
coral: "#ff7f50",
|
|
57
|
+
cornflowerblue: "#6495ed",
|
|
58
|
+
cornsilk: "#fff8dc",
|
|
59
|
+
crimson: "#dc143c",
|
|
60
|
+
cyan: "#00ffff",
|
|
61
|
+
darkblue: "#00008b",
|
|
62
|
+
darkcyan: "#008b8b",
|
|
63
|
+
darkgoldenrod: "#b8860b",
|
|
64
|
+
darkgray: "#a9a9a9",
|
|
65
|
+
darkgrey: "#a9a9a9",
|
|
66
|
+
darkgreen: "#006400",
|
|
67
|
+
darkkhaki: "#bdb76b",
|
|
68
|
+
darkmagenta: "#8b008b",
|
|
69
|
+
darkolivegreen: "#556b2f",
|
|
70
|
+
darkorange: "#ff8c00",
|
|
71
|
+
darkorchid: "#9932cc",
|
|
72
|
+
darkred: "#8b0000",
|
|
73
|
+
darksalmon: "#e9967a",
|
|
74
|
+
darkseagreen: "#8fbc8f",
|
|
75
|
+
darkslateblue: "#483d8b",
|
|
76
|
+
darkslategray: "#2f4f4f",
|
|
77
|
+
darkslategrey: "#2f4f4f",
|
|
78
|
+
darkturquoise: "#00ced1",
|
|
79
|
+
darkviolet: "#9400d3",
|
|
80
|
+
deeppink: "#ff1493",
|
|
81
|
+
deepskyblue: "#00bfff",
|
|
82
|
+
dimgray: "#696969",
|
|
83
|
+
dimgrey: "#696969",
|
|
84
|
+
dodgerblue: "#1e90ff",
|
|
85
|
+
firebrick: "#b22222",
|
|
86
|
+
floralwhite: "#fffaf0",
|
|
87
|
+
forestgreen: "#228b22",
|
|
88
|
+
fuchsia: "#ff00ff",
|
|
89
|
+
gainsboro: "#dcdcdc",
|
|
90
|
+
ghostwhite: "#f8f8ff",
|
|
91
|
+
gold: "#ffd700",
|
|
92
|
+
goldenrod: "#daa520",
|
|
93
|
+
gray: "#808080",
|
|
94
|
+
grey: "#808080",
|
|
95
|
+
green: "#008000",
|
|
96
|
+
greenyellow: "#adff2f",
|
|
97
|
+
honeydew: "#f0fff0",
|
|
98
|
+
hotpink: "#ff69b4",
|
|
99
|
+
indianred: "#cd5c5c",
|
|
100
|
+
indigo: "#4b0082",
|
|
101
|
+
ivory: "#fffff0",
|
|
102
|
+
khaki: "#f0e68c",
|
|
103
|
+
lavender: "#e6e6fa",
|
|
104
|
+
lavenderblush: "#fff0f5",
|
|
105
|
+
lawngreen: "#7cfc00",
|
|
106
|
+
lemonchiffon: "#fffacd",
|
|
107
|
+
lightblue: "#add8e6",
|
|
108
|
+
lightcoral: "#f08080",
|
|
109
|
+
lightcyan: "#e0ffff",
|
|
110
|
+
lightgoldenrodyellow: "#fafad2",
|
|
111
|
+
lightgray: "#d3d3d3",
|
|
112
|
+
lightgrey: "#d3d3d3",
|
|
113
|
+
lightgreen: "#90ee90",
|
|
114
|
+
lightpink: "#ffb6c1",
|
|
115
|
+
lightsalmon: "#ffa07a",
|
|
116
|
+
lightseagreen: "#20b2aa",
|
|
117
|
+
lightskyblue: "#87cefa",
|
|
118
|
+
lightslategray: "#778899",
|
|
119
|
+
lightslategrey: "#778899",
|
|
120
|
+
lightsteelblue: "#b0c4de",
|
|
121
|
+
lightyellow: "#ffffe0",
|
|
122
|
+
lime: "#00ff00",
|
|
123
|
+
limegreen: "#32cd32",
|
|
124
|
+
linen: "#faf0e6",
|
|
125
|
+
magenta: "#ff00ff",
|
|
126
|
+
maroon: "#800000",
|
|
127
|
+
mediumaquamarine: "#66cdaa",
|
|
128
|
+
mediumblue: "#0000cd",
|
|
129
|
+
mediumorchid: "#ba55d3",
|
|
130
|
+
mediumpurple: "#9370db",
|
|
131
|
+
mediumseagreen: "#3cb371",
|
|
132
|
+
mediumslateblue: "#7b68ee",
|
|
133
|
+
mediumspringgreen: "#00fa9a",
|
|
134
|
+
mediumturquoise: "#48d1cc",
|
|
135
|
+
mediumvioletred: "#c71585",
|
|
136
|
+
midnightblue: "#191970",
|
|
137
|
+
mintcream: "#f5fffa",
|
|
138
|
+
mistyrose: "#ffe4e1",
|
|
139
|
+
moccasin: "#ffe4b5",
|
|
140
|
+
navajowhite: "#ffdead",
|
|
141
|
+
navy: "#000080",
|
|
142
|
+
oldlace: "#fdf5e6",
|
|
143
|
+
olive: "#808000",
|
|
144
|
+
olivedrab: "#6b8e23",
|
|
145
|
+
orange: "#ffa500",
|
|
146
|
+
orangered: "#ff4500",
|
|
147
|
+
orchid: "#da70d6",
|
|
148
|
+
palegoldenrod: "#eee8aa",
|
|
149
|
+
palegreen: "#98fb98",
|
|
150
|
+
paleturquoise: "#afeeee",
|
|
151
|
+
palevioletred: "#db7093",
|
|
152
|
+
papayawhip: "#ffefd5",
|
|
153
|
+
peachpuff: "#ffdab9",
|
|
154
|
+
peru: "#cd853f",
|
|
155
|
+
pink: "#ffc0cb",
|
|
156
|
+
plum: "#dda0dd",
|
|
157
|
+
powderblue: "#b0e0e6",
|
|
158
|
+
purple: "#800080",
|
|
159
|
+
rebeccapurple: "#663399",
|
|
160
|
+
red: "#ff0000",
|
|
161
|
+
rosybrown: "#bc8f8f",
|
|
162
|
+
royalblue: "#4169e1",
|
|
163
|
+
saddlebrown: "#8b4513",
|
|
164
|
+
salmon: "#fa8072",
|
|
165
|
+
sandybrown: "#f4a460",
|
|
166
|
+
seagreen: "#2e8b57",
|
|
167
|
+
seashell: "#fff5ee",
|
|
168
|
+
sienna: "#a0522d",
|
|
169
|
+
silver: "#c0c0c0",
|
|
170
|
+
skyblue: "#87ceeb",
|
|
171
|
+
slateblue: "#6a5acd",
|
|
172
|
+
slategray: "#708090",
|
|
173
|
+
slategrey: "#708090",
|
|
174
|
+
snow: "#fffafa",
|
|
175
|
+
springgreen: "#00ff7f",
|
|
176
|
+
steelblue: "#4682b4",
|
|
177
|
+
tan: "#d2b48c",
|
|
178
|
+
teal: "#008080",
|
|
179
|
+
thistle: "#d8bfd8",
|
|
180
|
+
tomato: "#ff6347",
|
|
181
|
+
turquoise: "#40e0d0",
|
|
182
|
+
violet: "#ee82ee",
|
|
183
|
+
wheat: "#f5deb3",
|
|
184
|
+
white: "#ffffff",
|
|
185
|
+
whitesmoke: "#f5f5f5",
|
|
186
|
+
yellow: "#ffff00",
|
|
187
|
+
yellowgreen: "#9acd32"
|
|
188
|
+
};
|
|
189
|
+
function normalizeColor(c, fallback) {
|
|
190
|
+
if (!c) return fallback;
|
|
191
|
+
const s = c.trim();
|
|
192
|
+
if (s.length === 0) return fallback;
|
|
193
|
+
const hex8 = s.match(/^#([0-9a-fA-F]{8})$/);
|
|
194
|
+
if (hex8 && hex8[1] !== void 0) {
|
|
195
|
+
return `#${hex8[1].toLowerCase()}`;
|
|
196
|
+
}
|
|
197
|
+
const rgba = parseHexColorToRGBA(s);
|
|
198
|
+
const r = rgba.r.toString(16).padStart(2, "0");
|
|
199
|
+
const g = rgba.g.toString(16).padStart(2, "0");
|
|
200
|
+
const b = rgba.b.toString(16).padStart(2, "0");
|
|
201
|
+
if (rgba.a === 255) {
|
|
202
|
+
return `#${r}${g}${b}`;
|
|
203
|
+
}
|
|
204
|
+
const a = rgba.a.toString(16).padStart(2, "0");
|
|
205
|
+
return `#${r}${g}${b}${a}`;
|
|
206
|
+
}
|
|
207
|
+
function parseHexColorToRGBA(c) {
|
|
208
|
+
const cached = colorCache.get(c);
|
|
209
|
+
if (cached) return cached;
|
|
210
|
+
const s = c.trim().toLowerCase();
|
|
211
|
+
const clampByte = (value) => {
|
|
212
|
+
if (!Number.isFinite(value)) return 0;
|
|
213
|
+
return Math.min(255, Math.max(0, Math.round(value)));
|
|
214
|
+
};
|
|
215
|
+
const clampAlpha = (value) => {
|
|
216
|
+
if (!Number.isFinite(value)) return 1;
|
|
217
|
+
return Math.min(1, Math.max(0, value));
|
|
218
|
+
};
|
|
219
|
+
if (namedColors[s]) {
|
|
220
|
+
const namedColor = namedColors[s];
|
|
221
|
+
if (typeof namedColor === "string") {
|
|
222
|
+
const result3 = parseHexColorToRGBA(namedColor);
|
|
223
|
+
colorCache.set(c, result3);
|
|
224
|
+
return result3;
|
|
225
|
+
}
|
|
226
|
+
const result2 = { r: 0, g: 0, b: 0, a: 255 };
|
|
227
|
+
colorCache.set(c, result2);
|
|
228
|
+
return result2;
|
|
229
|
+
}
|
|
230
|
+
if (s.startsWith("#")) {
|
|
231
|
+
const hex = s.slice(1);
|
|
232
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
233
|
+
const r = Number.parseInt(hex.slice(0, 2), 16);
|
|
234
|
+
const g = Number.parseInt(hex.slice(2, 4), 16);
|
|
235
|
+
const b = Number.parseInt(hex.slice(4, 6), 16);
|
|
236
|
+
const a = hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) : 255;
|
|
237
|
+
if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b) && Number.isFinite(a)) {
|
|
238
|
+
const result2 = { r, g, b, a };
|
|
239
|
+
colorCache.set(c, result2);
|
|
240
|
+
return result2;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const rgbMatch = s.match(/^rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)$/);
|
|
245
|
+
if (rgbMatch) {
|
|
246
|
+
const rStr = rgbMatch[1];
|
|
247
|
+
const gStr = rgbMatch[2];
|
|
248
|
+
const bStr = rgbMatch[3];
|
|
249
|
+
if (!rStr || !gStr || !bStr) {
|
|
250
|
+
const result3 = { r: 0, g: 0, b: 0, a: 255 };
|
|
251
|
+
colorCache.set(c, result3);
|
|
252
|
+
return result3;
|
|
253
|
+
}
|
|
254
|
+
const r = clampByte(Number.parseInt(rStr, 10));
|
|
255
|
+
const g = clampByte(Number.parseInt(gStr, 10));
|
|
256
|
+
const b = clampByte(Number.parseInt(bStr, 10));
|
|
257
|
+
const alpha = rgbMatch[4] ? clampAlpha(Number.parseFloat(rgbMatch[4])) : 1;
|
|
258
|
+
const result2 = { r, g, b, a: Math.round(alpha * 255) };
|
|
259
|
+
colorCache.set(c, result2);
|
|
260
|
+
return result2;
|
|
261
|
+
}
|
|
262
|
+
const hslMatch = s.match(
|
|
263
|
+
/^hsla?\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%\s*(?:,\s*([\d.]+)\s*)?\)$/
|
|
264
|
+
);
|
|
265
|
+
if (hslMatch) {
|
|
266
|
+
const hRaw = hslMatch[1];
|
|
267
|
+
const sRaw = hslMatch[2];
|
|
268
|
+
const lRaw = hslMatch[3];
|
|
269
|
+
if (!hRaw || !sRaw || !lRaw) {
|
|
270
|
+
const result3 = { r: 0, g: 0, b: 0, a: 255 };
|
|
271
|
+
colorCache.set(c, result3);
|
|
272
|
+
return result3;
|
|
273
|
+
}
|
|
274
|
+
const hueDegrees = Number.parseFloat(hRaw);
|
|
275
|
+
const sat = Number.parseFloat(sRaw);
|
|
276
|
+
const light = Number.parseFloat(lRaw);
|
|
277
|
+
if (!Number.isFinite(hueDegrees) || !Number.isFinite(sat) || !Number.isFinite(light)) {
|
|
278
|
+
const result3 = { r: 0, g: 0, b: 0, a: 255 };
|
|
279
|
+
colorCache.set(c, result3);
|
|
280
|
+
return result3;
|
|
281
|
+
}
|
|
282
|
+
const h = (hueDegrees % 360 + 360) % 360 / 360;
|
|
283
|
+
const sl = Math.min(1, Math.max(0, sat / 100));
|
|
284
|
+
const l = Math.min(1, Math.max(0, light / 100));
|
|
285
|
+
const alpha = hslMatch[4] ? clampAlpha(Number.parseFloat(hslMatch[4])) : 1;
|
|
286
|
+
const hslToRgb = (h2, s2, l2) => {
|
|
287
|
+
let r2, g2, b2;
|
|
288
|
+
if (s2 === 0) {
|
|
289
|
+
r2 = g2 = b2 = l2;
|
|
290
|
+
} else {
|
|
291
|
+
const hue2rgb = (p2, q2, t) => {
|
|
292
|
+
if (t < 0) t += 1;
|
|
293
|
+
if (t > 1) t -= 1;
|
|
294
|
+
if (t < 1 / 6) return p2 + (q2 - p2) * 6 * t;
|
|
295
|
+
if (t < 1 / 2) return q2;
|
|
296
|
+
if (t < 2 / 3) return p2 + (q2 - p2) * (2 / 3 - t) * 6;
|
|
297
|
+
return p2;
|
|
298
|
+
};
|
|
299
|
+
const q = l2 < 0.5 ? l2 * (1 + s2) : l2 + s2 - l2 * s2;
|
|
300
|
+
const p = 2 * l2 - q;
|
|
301
|
+
r2 = hue2rgb(p, q, h2 + 1 / 3);
|
|
302
|
+
g2 = hue2rgb(p, q, h2);
|
|
303
|
+
b2 = hue2rgb(p, q, h2 - 1 / 3);
|
|
304
|
+
}
|
|
305
|
+
return [Math.round(r2 * 255), Math.round(g2 * 255), Math.round(b2 * 255)];
|
|
306
|
+
};
|
|
307
|
+
const [r, g, b] = hslToRgb(h, sl, l);
|
|
308
|
+
const result2 = {
|
|
309
|
+
r: clampByte(r),
|
|
310
|
+
g: clampByte(g),
|
|
311
|
+
b: clampByte(b),
|
|
312
|
+
a: Math.round(alpha * 255)
|
|
313
|
+
};
|
|
314
|
+
colorCache.set(c, result2);
|
|
315
|
+
return result2;
|
|
316
|
+
}
|
|
317
|
+
const result = { r: 0, g: 0, b: 0, a: 255 };
|
|
318
|
+
colorCache.set(c, result);
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/plot/utils/legend.ts
|
|
323
|
+
function normalizeLegendLabel(label) {
|
|
324
|
+
if (typeof label !== "string") return null;
|
|
325
|
+
const trimmed = label.trim();
|
|
326
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
327
|
+
}
|
|
328
|
+
function buildLegendEntry(label, entry) {
|
|
329
|
+
if (!label) return null;
|
|
330
|
+
return { label, ...entry };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/plot/utils/validation.ts
|
|
334
|
+
function assertPositiveInt(name, v) {
|
|
335
|
+
if (!Number.isFinite(v)) {
|
|
336
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(`${name} must be a positive integer; received ${v}`, name, v);
|
|
337
|
+
}
|
|
338
|
+
if (v <= 0) {
|
|
339
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(`${name} must be a positive integer; received ${v}`, name, v);
|
|
340
|
+
}
|
|
341
|
+
if (Math.trunc(v) !== v) {
|
|
342
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(`${name} must be a positive integer; received ${v}`, name, v);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isFiniteNumber(x) {
|
|
346
|
+
return Number.isFinite(x);
|
|
347
|
+
}
|
|
348
|
+
function clampInt(x, lo, hi) {
|
|
349
|
+
if (!Number.isFinite(x)) return lo;
|
|
350
|
+
if (x < lo) return lo;
|
|
351
|
+
if (x > hi) return hi;
|
|
352
|
+
return Math.trunc(x);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/plot/utils/xml.ts
|
|
356
|
+
function escapeXml(s) {
|
|
357
|
+
return s.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// src/plot/plots/Bar2D.ts
|
|
361
|
+
var Bar2D = class {
|
|
362
|
+
kind = "bar";
|
|
363
|
+
x;
|
|
364
|
+
height;
|
|
365
|
+
color;
|
|
366
|
+
edgecolor;
|
|
367
|
+
barWidth;
|
|
368
|
+
label;
|
|
369
|
+
constructor(x, height, options) {
|
|
370
|
+
if (x.length !== height.length) throw new chunkJSCDE774_cjs.ShapeError("x and height must have the same length");
|
|
371
|
+
this.x = x;
|
|
372
|
+
this.height = height;
|
|
373
|
+
this.color = normalizeColor(options.color, "#2ca02c");
|
|
374
|
+
this.edgecolor = normalizeColor(options.edgecolor, "#000000");
|
|
375
|
+
this.barWidth = 0.8;
|
|
376
|
+
this.label = normalizeLegendLabel(options.label);
|
|
377
|
+
}
|
|
378
|
+
getDataRange() {
|
|
379
|
+
let xmin = Number.POSITIVE_INFINITY;
|
|
380
|
+
let xmax = Number.NEGATIVE_INFINITY;
|
|
381
|
+
let ymin = 0;
|
|
382
|
+
let ymax = 0;
|
|
383
|
+
let sawValue = false;
|
|
384
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
385
|
+
const xi = this.x[i] ?? 0;
|
|
386
|
+
const hi = this.height[i] ?? 0;
|
|
387
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(hi)) continue;
|
|
388
|
+
sawValue = true;
|
|
389
|
+
xmin = Math.min(xmin, xi - this.barWidth / 2);
|
|
390
|
+
xmax = Math.max(xmax, xi + this.barWidth / 2);
|
|
391
|
+
ymin = Math.min(ymin, hi);
|
|
392
|
+
ymax = Math.max(ymax, hi);
|
|
393
|
+
}
|
|
394
|
+
if (!sawValue || !Number.isFinite(xmin) || !Number.isFinite(xmax)) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
return { xmin, xmax, ymin, ymax };
|
|
398
|
+
}
|
|
399
|
+
drawSVG(ctx) {
|
|
400
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
401
|
+
const xi = this.x[i] ?? 0;
|
|
402
|
+
const hi = this.height[i] ?? 0;
|
|
403
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(hi)) continue;
|
|
404
|
+
const x0 = ctx.transform.xToPx(xi - this.barWidth / 2);
|
|
405
|
+
const x1 = ctx.transform.xToPx(xi + this.barWidth / 2);
|
|
406
|
+
const y0 = ctx.transform.yToPx(0);
|
|
407
|
+
const y1 = ctx.transform.yToPx(hi);
|
|
408
|
+
const w = Math.abs(x1 - x0);
|
|
409
|
+
const h = Math.abs(y1 - y0);
|
|
410
|
+
const rx = Math.min(x0, x1);
|
|
411
|
+
const ry = Math.min(y0, y1);
|
|
412
|
+
ctx.push(
|
|
413
|
+
`<rect x="${rx.toFixed(2)}" y="${ry.toFixed(2)}" width="${w.toFixed(2)}" height="${h.toFixed(2)}" fill="${escapeXml(this.color)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
drawRaster(ctx) {
|
|
418
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
419
|
+
const edge = parseHexColorToRGBA(this.edgecolor);
|
|
420
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
421
|
+
const xi = this.x[i] ?? 0;
|
|
422
|
+
const hi = this.height[i] ?? 0;
|
|
423
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(hi)) continue;
|
|
424
|
+
const x0 = Math.round(ctx.transform.xToPx(xi - this.barWidth / 2));
|
|
425
|
+
const x1 = Math.round(ctx.transform.xToPx(xi + this.barWidth / 2));
|
|
426
|
+
const y0 = Math.round(ctx.transform.yToPx(0));
|
|
427
|
+
const y1 = Math.round(ctx.transform.yToPx(hi));
|
|
428
|
+
const w = Math.abs(x1 - x0);
|
|
429
|
+
const h = Math.abs(y1 - y0);
|
|
430
|
+
const rx = Math.min(x0, x1);
|
|
431
|
+
const ry = Math.min(y0, y1);
|
|
432
|
+
ctx.canvas.fillRectRGBA(rx, ry, w, h, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
433
|
+
const r_e = edge.r;
|
|
434
|
+
const g_e = edge.g;
|
|
435
|
+
const b_e = edge.b;
|
|
436
|
+
const a_e = edge.a;
|
|
437
|
+
ctx.canvas.drawLineRGBA(rx, ry, rx + w, ry, r_e, g_e, b_e, a_e);
|
|
438
|
+
ctx.canvas.drawLineRGBA(rx + w, ry, rx + w, ry + h, r_e, g_e, b_e, a_e);
|
|
439
|
+
ctx.canvas.drawLineRGBA(rx + w, ry + h, rx, ry + h, r_e, g_e, b_e, a_e);
|
|
440
|
+
ctx.canvas.drawLineRGBA(rx, ry + h, rx, ry, r_e, g_e, b_e, a_e);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
getLegendEntries() {
|
|
444
|
+
const entry = buildLegendEntry(this.label, { color: this.color, shape: "box" });
|
|
445
|
+
return entry ? [entry] : null;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// src/plot/utils/statistics.ts
|
|
450
|
+
function calculateQuartiles(sortedData) {
|
|
451
|
+
const n = sortedData.length;
|
|
452
|
+
if (n === 0) {
|
|
453
|
+
return { q1: 0, median: 0, q3: 0 };
|
|
454
|
+
}
|
|
455
|
+
if (n === 1) {
|
|
456
|
+
const val = sortedData[0] ?? 0;
|
|
457
|
+
return { q1: val, median: val, q3: val };
|
|
458
|
+
}
|
|
459
|
+
const medianOf = (arr, start, end) => {
|
|
460
|
+
const length = end - start;
|
|
461
|
+
if (length === 0) return 0;
|
|
462
|
+
if (length === 1) return arr[start] ?? 0;
|
|
463
|
+
const mid2 = start + Math.floor(length / 2);
|
|
464
|
+
if (length % 2 === 1) {
|
|
465
|
+
return arr[mid2] ?? 0;
|
|
466
|
+
} else {
|
|
467
|
+
return ((arr[mid2 - 1] ?? 0) + (arr[mid2] ?? 0)) / 2;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const mid = Math.floor(n / 2);
|
|
471
|
+
const median = n % 2 === 1 ? sortedData[mid] ?? 0 : ((sortedData[mid - 1] ?? 0) + (sortedData[mid] ?? 0)) / 2;
|
|
472
|
+
const q1 = medianOf(sortedData, 0, mid);
|
|
473
|
+
const q3 = medianOf(sortedData, n % 2 === 1 ? mid + 1 : mid, n);
|
|
474
|
+
return { q1, median, q3 };
|
|
475
|
+
}
|
|
476
|
+
function calculateWhiskers(sortedData, q1, q3) {
|
|
477
|
+
if (sortedData.length === 0) {
|
|
478
|
+
return { lowerWhisker: 0, upperWhisker: 0, outliers: [] };
|
|
479
|
+
}
|
|
480
|
+
const iqr = q3 - q1;
|
|
481
|
+
const lowerBound = q1 - 1.5 * iqr;
|
|
482
|
+
const upperBound = q3 + 1.5 * iqr;
|
|
483
|
+
const outliers = [];
|
|
484
|
+
let lowerWhisker = null;
|
|
485
|
+
let upperWhisker = null;
|
|
486
|
+
for (const value of sortedData) {
|
|
487
|
+
if (value < lowerBound || value > upperBound) {
|
|
488
|
+
outliers.push(value);
|
|
489
|
+
} else {
|
|
490
|
+
if (lowerWhisker === null) {
|
|
491
|
+
lowerWhisker = value;
|
|
492
|
+
}
|
|
493
|
+
upperWhisker = value;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (lowerWhisker === null) {
|
|
497
|
+
lowerWhisker = q1;
|
|
498
|
+
}
|
|
499
|
+
if (upperWhisker === null) {
|
|
500
|
+
upperWhisker = q3;
|
|
501
|
+
}
|
|
502
|
+
return { lowerWhisker, upperWhisker, outliers };
|
|
503
|
+
}
|
|
504
|
+
function kernelDensityEstimation(data, points, bandwidth) {
|
|
505
|
+
const n = data.length;
|
|
506
|
+
const m = points.length;
|
|
507
|
+
const result = new Float64Array(m);
|
|
508
|
+
if (n === 0) {
|
|
509
|
+
return Array.from(result);
|
|
510
|
+
}
|
|
511
|
+
if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
|
|
512
|
+
let sum = 0;
|
|
513
|
+
let sumSq = 0;
|
|
514
|
+
let min = Number.POSITIVE_INFINITY;
|
|
515
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
516
|
+
for (let i = 0; i < n; i++) {
|
|
517
|
+
const v = data[i] ?? 0;
|
|
518
|
+
sum += v;
|
|
519
|
+
sumSq += v * v;
|
|
520
|
+
if (v < min) min = v;
|
|
521
|
+
if (v > max) max = v;
|
|
522
|
+
}
|
|
523
|
+
const mean = sum / n;
|
|
524
|
+
const variance = Math.max(0, sumSq / n - mean * mean);
|
|
525
|
+
const stdDev = Math.sqrt(variance);
|
|
526
|
+
const silverman = 1.06 * stdDev * n ** -0.2;
|
|
527
|
+
if (Number.isFinite(silverman) && silverman > 0) {
|
|
528
|
+
bandwidth = silverman;
|
|
529
|
+
} else {
|
|
530
|
+
const range = max - min;
|
|
531
|
+
bandwidth = Number.isFinite(range) && range > 0 ? range * 0.1 : 1;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (!Number.isFinite(bandwidth) || bandwidth <= 0) {
|
|
535
|
+
bandwidth = 1;
|
|
536
|
+
}
|
|
537
|
+
const invSqrt2PiBandwidth = 1 / (bandwidth * Math.sqrt(2 * Math.PI));
|
|
538
|
+
for (let i = 0; i < m; i++) {
|
|
539
|
+
const x = points[i] ?? 0;
|
|
540
|
+
let sum = 0;
|
|
541
|
+
for (let j = 0; j < n; j++) {
|
|
542
|
+
const u = (x - (data[j] ?? 0)) / bandwidth;
|
|
543
|
+
sum += Math.exp(-0.5 * u * u);
|
|
544
|
+
}
|
|
545
|
+
result[i] = sum * invSqrt2PiBandwidth / n;
|
|
546
|
+
}
|
|
547
|
+
return Array.from(result);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/plot/plots/Boxplot.ts
|
|
551
|
+
var Boxplot = class {
|
|
552
|
+
kind = "boxplot";
|
|
553
|
+
position;
|
|
554
|
+
q1;
|
|
555
|
+
median;
|
|
556
|
+
q3;
|
|
557
|
+
whiskerLow;
|
|
558
|
+
whiskerHigh;
|
|
559
|
+
outliers;
|
|
560
|
+
color;
|
|
561
|
+
edgecolor;
|
|
562
|
+
boxWidth;
|
|
563
|
+
label;
|
|
564
|
+
hasData;
|
|
565
|
+
constructor(position, data, options) {
|
|
566
|
+
this.position = position;
|
|
567
|
+
this.color = normalizeColor(options.color, "#8c564b");
|
|
568
|
+
this.edgecolor = normalizeColor(options.edgecolor, "#000000");
|
|
569
|
+
this.boxWidth = 0.5;
|
|
570
|
+
this.label = normalizeLegendLabel(options.label);
|
|
571
|
+
const sorted = Array.from(data).filter(isFiniteNumber).sort((a, b) => a - b);
|
|
572
|
+
const n = sorted.length;
|
|
573
|
+
if (n === 0) {
|
|
574
|
+
if (data.length > 0) {
|
|
575
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
576
|
+
"boxplot data must contain at least one finite value",
|
|
577
|
+
"data",
|
|
578
|
+
data
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
this.hasData = false;
|
|
582
|
+
this.q1 = 0;
|
|
583
|
+
this.median = 0;
|
|
584
|
+
this.q3 = 0;
|
|
585
|
+
this.whiskerLow = 0;
|
|
586
|
+
this.whiskerHigh = 0;
|
|
587
|
+
this.outliers = [];
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
this.hasData = true;
|
|
591
|
+
const { q1, median, q3 } = calculateQuartiles(sorted);
|
|
592
|
+
this.q1 = q1;
|
|
593
|
+
this.median = median;
|
|
594
|
+
this.q3 = q3;
|
|
595
|
+
const { lowerWhisker, upperWhisker, outliers } = calculateWhiskers(sorted, q1, q3);
|
|
596
|
+
this.whiskerLow = lowerWhisker;
|
|
597
|
+
this.whiskerHigh = upperWhisker;
|
|
598
|
+
this.outliers = outliers;
|
|
599
|
+
}
|
|
600
|
+
getDataRange() {
|
|
601
|
+
if (!this.hasData) return null;
|
|
602
|
+
let ymin = this.whiskerLow;
|
|
603
|
+
let ymax = this.whiskerHigh;
|
|
604
|
+
for (const o of this.outliers) {
|
|
605
|
+
if (o < ymin) ymin = o;
|
|
606
|
+
if (o > ymax) ymax = o;
|
|
607
|
+
}
|
|
608
|
+
return {
|
|
609
|
+
xmin: this.position - this.boxWidth,
|
|
610
|
+
xmax: this.position + this.boxWidth,
|
|
611
|
+
ymin,
|
|
612
|
+
ymax
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
drawSVG(ctx) {
|
|
616
|
+
if (!this.hasData) return;
|
|
617
|
+
const x = this.position;
|
|
618
|
+
const x0 = ctx.transform.xToPx(x - this.boxWidth / 2);
|
|
619
|
+
const x1 = ctx.transform.xToPx(x + this.boxWidth / 2);
|
|
620
|
+
const xc = ctx.transform.xToPx(x);
|
|
621
|
+
const yq1 = ctx.transform.yToPx(this.q1);
|
|
622
|
+
const ymed = ctx.transform.yToPx(this.median);
|
|
623
|
+
const yq3 = ctx.transform.yToPx(this.q3);
|
|
624
|
+
const ywl = ctx.transform.yToPx(this.whiskerLow);
|
|
625
|
+
const ywh = ctx.transform.yToPx(this.whiskerHigh);
|
|
626
|
+
ctx.push(
|
|
627
|
+
`<rect x="${Math.min(x0, x1).toFixed(2)}" y="${Math.min(yq1, yq3).toFixed(2)}" width="${Math.abs(x1 - x0).toFixed(2)}" height="${Math.abs(yq3 - yq1).toFixed(2)}" fill="${escapeXml(this.color)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
628
|
+
);
|
|
629
|
+
ctx.push(
|
|
630
|
+
`<line x1="${x0.toFixed(2)}" y1="${ymed.toFixed(2)}" x2="${x1.toFixed(2)}" y2="${ymed.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" stroke-width="2" />`
|
|
631
|
+
);
|
|
632
|
+
ctx.push(
|
|
633
|
+
`<line x1="${xc.toFixed(2)}" y1="${yq1.toFixed(2)}" x2="${xc.toFixed(2)}" y2="${ywl.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
634
|
+
);
|
|
635
|
+
ctx.push(
|
|
636
|
+
`<line x1="${xc.toFixed(2)}" y1="${yq3.toFixed(2)}" x2="${xc.toFixed(2)}" y2="${ywh.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
637
|
+
);
|
|
638
|
+
const capHalf = Math.abs(x1 - x0) / 4;
|
|
639
|
+
ctx.push(
|
|
640
|
+
`<line x1="${(xc - capHalf).toFixed(2)}" y1="${ywl.toFixed(2)}" x2="${(xc + capHalf).toFixed(2)}" y2="${ywl.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
641
|
+
);
|
|
642
|
+
ctx.push(
|
|
643
|
+
`<line x1="${(xc - capHalf).toFixed(2)}" y1="${ywh.toFixed(2)}" x2="${(xc + capHalf).toFixed(2)}" y2="${ywh.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
644
|
+
);
|
|
645
|
+
for (const outlier of this.outliers) {
|
|
646
|
+
const yo = ctx.transform.yToPx(outlier);
|
|
647
|
+
ctx.push(
|
|
648
|
+
`<circle cx="${xc.toFixed(2)}" cy="${yo.toFixed(2)}" r="3" fill="${escapeXml(this.edgecolor)}" stroke="none" />`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
drawRaster(ctx) {
|
|
653
|
+
if (!this.hasData) return;
|
|
654
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
655
|
+
const edge = parseHexColorToRGBA(this.edgecolor);
|
|
656
|
+
const x = this.position;
|
|
657
|
+
const x0 = Math.round(ctx.transform.xToPx(x - this.boxWidth / 2));
|
|
658
|
+
const x1 = Math.round(ctx.transform.xToPx(x + this.boxWidth / 2));
|
|
659
|
+
const xc = Math.round(ctx.transform.xToPx(x));
|
|
660
|
+
const yq1 = Math.round(ctx.transform.yToPx(this.q1));
|
|
661
|
+
const ymed = Math.round(ctx.transform.yToPx(this.median));
|
|
662
|
+
const yq3 = Math.round(ctx.transform.yToPx(this.q3));
|
|
663
|
+
const ywl = Math.round(ctx.transform.yToPx(this.whiskerLow));
|
|
664
|
+
const ywh = Math.round(ctx.transform.yToPx(this.whiskerHigh));
|
|
665
|
+
const rx = Math.min(x0, x1);
|
|
666
|
+
const ry = Math.min(yq1, yq3);
|
|
667
|
+
const w = Math.abs(x1 - x0);
|
|
668
|
+
const h = Math.abs(yq3 - yq1);
|
|
669
|
+
ctx.canvas.fillRectRGBA(rx, ry, w, h, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
670
|
+
ctx.canvas.drawLineRGBA(rx, ry, rx + w, ry, edge.r, edge.g, edge.b, edge.a);
|
|
671
|
+
ctx.canvas.drawLineRGBA(rx + w, ry, rx + w, ry + h, edge.r, edge.g, edge.b, edge.a);
|
|
672
|
+
ctx.canvas.drawLineRGBA(rx + w, ry + h, rx, ry + h, edge.r, edge.g, edge.b, edge.a);
|
|
673
|
+
ctx.canvas.drawLineRGBA(rx, ry + h, rx, ry, edge.r, edge.g, edge.b, edge.a);
|
|
674
|
+
ctx.canvas.drawLineRGBA(x0, ymed, x1, ymed, edge.r, edge.g, edge.b, edge.a);
|
|
675
|
+
ctx.canvas.drawLineRGBA(xc, yq1, xc, ywl, edge.r, edge.g, edge.b, edge.a);
|
|
676
|
+
ctx.canvas.drawLineRGBA(xc, yq3, xc, ywh, edge.r, edge.g, edge.b, edge.a);
|
|
677
|
+
const capHalf = Math.round(Math.abs(x1 - x0) / 4);
|
|
678
|
+
ctx.canvas.drawLineRGBA(xc - capHalf, ywl, xc + capHalf, ywl, edge.r, edge.g, edge.b, edge.a);
|
|
679
|
+
ctx.canvas.drawLineRGBA(xc - capHalf, ywh, xc + capHalf, ywh, edge.r, edge.g, edge.b, edge.a);
|
|
680
|
+
for (const outlier of this.outliers) {
|
|
681
|
+
const yo = Math.round(ctx.transform.yToPx(outlier));
|
|
682
|
+
ctx.canvas.drawCircleRGBA(xc, yo, 3, edge.r, edge.g, edge.b, edge.a);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
getLegendEntries() {
|
|
686
|
+
const entry = buildLegendEntry(this.label, {
|
|
687
|
+
color: this.color,
|
|
688
|
+
shape: "box"
|
|
689
|
+
});
|
|
690
|
+
return entry ? [entry] : null;
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// src/plot/utils/colormaps.ts
|
|
695
|
+
var colormaps = {
|
|
696
|
+
viridis: [
|
|
697
|
+
[68, 1, 84],
|
|
698
|
+
[72, 40, 120],
|
|
699
|
+
[62, 73, 137],
|
|
700
|
+
[49, 104, 142],
|
|
701
|
+
[38, 130, 142],
|
|
702
|
+
[31, 158, 137],
|
|
703
|
+
[53, 183, 121],
|
|
704
|
+
[110, 206, 88],
|
|
705
|
+
[181, 222, 43],
|
|
706
|
+
[253, 231, 37]
|
|
707
|
+
],
|
|
708
|
+
plasma: [
|
|
709
|
+
[13, 8, 135],
|
|
710
|
+
[75, 3, 161],
|
|
711
|
+
[125, 3, 168],
|
|
712
|
+
[168, 34, 150],
|
|
713
|
+
[203, 70, 121],
|
|
714
|
+
[229, 107, 93],
|
|
715
|
+
[248, 148, 65],
|
|
716
|
+
[253, 195, 40],
|
|
717
|
+
[240, 249, 33],
|
|
718
|
+
[240, 249, 33]
|
|
719
|
+
],
|
|
720
|
+
inferno: [
|
|
721
|
+
[0, 0, 4],
|
|
722
|
+
[40, 11, 84],
|
|
723
|
+
[101, 21, 110],
|
|
724
|
+
[159, 42, 99],
|
|
725
|
+
[212, 72, 66],
|
|
726
|
+
[245, 125, 21],
|
|
727
|
+
[250, 193, 39],
|
|
728
|
+
[245, 251, 161],
|
|
729
|
+
[252, 255, 164],
|
|
730
|
+
[252, 255, 164]
|
|
731
|
+
],
|
|
732
|
+
magma: [
|
|
733
|
+
[0, 0, 4],
|
|
734
|
+
[28, 16, 68],
|
|
735
|
+
[79, 18, 123],
|
|
736
|
+
[129, 37, 129],
|
|
737
|
+
[181, 54, 122],
|
|
738
|
+
[229, 80, 100],
|
|
739
|
+
[251, 135, 97],
|
|
740
|
+
[254, 194, 135],
|
|
741
|
+
[252, 253, 191],
|
|
742
|
+
[252, 253, 191]
|
|
743
|
+
],
|
|
744
|
+
grayscale: [
|
|
745
|
+
[0, 0, 0],
|
|
746
|
+
[28, 28, 28],
|
|
747
|
+
[57, 57, 57],
|
|
748
|
+
[85, 85, 85],
|
|
749
|
+
[113, 113, 113],
|
|
750
|
+
[142, 142, 142],
|
|
751
|
+
[170, 170, 170],
|
|
752
|
+
[198, 198, 198],
|
|
753
|
+
[227, 227, 227],
|
|
754
|
+
[255, 255, 255]
|
|
755
|
+
]
|
|
756
|
+
};
|
|
757
|
+
function applyColormap(value, colormap) {
|
|
758
|
+
const cmap = colormaps[colormap];
|
|
759
|
+
const n = cmap.length;
|
|
760
|
+
const clamped = Math.max(0, Math.min(1, value));
|
|
761
|
+
const idx = Math.max(0, Math.min(n - 2, Math.floor(clamped * (n - 1))));
|
|
762
|
+
const nextIdx = idx + 1;
|
|
763
|
+
const t = Math.max(0, Math.min(1, clamped * (n - 1) - idx));
|
|
764
|
+
const c1 = cmap[idx];
|
|
765
|
+
const c2 = cmap[nextIdx];
|
|
766
|
+
if (!c1 || !c2) return [0, 0, 0];
|
|
767
|
+
return [
|
|
768
|
+
Math.round(c1[0] + (c2[0] - c1[0]) * t),
|
|
769
|
+
Math.round(c1[1] + (c2[1] - c1[1]) * t),
|
|
770
|
+
Math.round(c1[2] + (c2[2] - c1[2]) * t)
|
|
771
|
+
];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/plot/plots/Contour2D.ts
|
|
775
|
+
var DEFAULT_COLORS = [
|
|
776
|
+
"#1f77b4",
|
|
777
|
+
"#ff7f0e",
|
|
778
|
+
"#2ca02c",
|
|
779
|
+
"#d62728",
|
|
780
|
+
"#9467bd",
|
|
781
|
+
"#8c564b",
|
|
782
|
+
"#e377c2",
|
|
783
|
+
"#7f7f7f",
|
|
784
|
+
"#bcbd22",
|
|
785
|
+
"#17becf"
|
|
786
|
+
];
|
|
787
|
+
function resolveLevels(min, max, levels) {
|
|
788
|
+
if (Array.isArray(levels)) {
|
|
789
|
+
const filtered = levels.filter((v) => Number.isFinite(v));
|
|
790
|
+
if (filtered.length === 0) {
|
|
791
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
792
|
+
"levels must contain at least one finite value",
|
|
793
|
+
"levels",
|
|
794
|
+
levels
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
const unique = Array.from(new Set(filtered));
|
|
798
|
+
unique.sort((a, b) => a - b);
|
|
799
|
+
return unique;
|
|
800
|
+
}
|
|
801
|
+
const numLevels = typeof levels === "number" ? levels : 10;
|
|
802
|
+
if (!Number.isFinite(numLevels) || Math.trunc(numLevels) !== numLevels || numLevels <= 0) {
|
|
803
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
804
|
+
`levels must be a positive integer; received ${numLevels}`,
|
|
805
|
+
"levels",
|
|
806
|
+
numLevels
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (numLevels === 1) {
|
|
810
|
+
return [min === max ? min : (min + max) / 2];
|
|
811
|
+
}
|
|
812
|
+
const step = (max - min) / (numLevels - 1);
|
|
813
|
+
const values = new Array(numLevels);
|
|
814
|
+
for (let i = 0; i < numLevels; i++) {
|
|
815
|
+
values[i] = min + i * step;
|
|
816
|
+
}
|
|
817
|
+
return values;
|
|
818
|
+
}
|
|
819
|
+
function resolveLevelColors(levels, options) {
|
|
820
|
+
const count = levels.length;
|
|
821
|
+
const colors = [];
|
|
822
|
+
if (options.colors && options.colors.length > 0) {
|
|
823
|
+
for (let i = 0; i < count; i++) {
|
|
824
|
+
const color = options.colors[i % options.colors.length];
|
|
825
|
+
colors.push(normalizeColor(color, DEFAULT_COLORS[i % DEFAULT_COLORS.length] ?? "#1f77b4"));
|
|
826
|
+
}
|
|
827
|
+
} else if (options.color) {
|
|
828
|
+
const normalized = normalizeColor(options.color, "#1f77b4");
|
|
829
|
+
for (let i = 0; i < count; i++) colors.push(normalized);
|
|
830
|
+
} else if (options.colormap) {
|
|
831
|
+
if (!["viridis", "plasma", "inferno", "magma", "grayscale"].includes(options.colormap)) {
|
|
832
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
833
|
+
`colormap must be one of viridis, plasma, inferno, magma, grayscale; received ${options.colormap}`,
|
|
834
|
+
"colormap",
|
|
835
|
+
options.colormap
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const denom = Math.max(1, count - 1);
|
|
839
|
+
for (let i = 0; i < count; i++) {
|
|
840
|
+
const t = i / denom;
|
|
841
|
+
const [r, g, b] = applyColormap(t, options.colormap);
|
|
842
|
+
colors.push(normalizeColor(`rgb(${r},${g},${b})`, "#1f77b4"));
|
|
843
|
+
}
|
|
844
|
+
} else {
|
|
845
|
+
for (let i = 0; i < count; i++) {
|
|
846
|
+
const color = DEFAULT_COLORS[i % DEFAULT_COLORS.length] ?? "#1f77b4";
|
|
847
|
+
colors.push(i === 0 ? color : normalizeColor(color, "#1f77b4"));
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
const rgba = colors.map((c) => parseHexColorToRGBA(c));
|
|
851
|
+
return { colors, rgba };
|
|
852
|
+
}
|
|
853
|
+
function interpolate(level, v0, v1, c0, c1) {
|
|
854
|
+
if (v0 === v1) return (c0 + c1) / 2;
|
|
855
|
+
const t = Math.max(0, Math.min(1, (level - v0) / (v1 - v0)));
|
|
856
|
+
return c0 + t * (c1 - c0);
|
|
857
|
+
}
|
|
858
|
+
var Contour2D = class {
|
|
859
|
+
kind = "contour";
|
|
860
|
+
segments;
|
|
861
|
+
levelColors;
|
|
862
|
+
levelRGBA;
|
|
863
|
+
linewidth;
|
|
864
|
+
xmin;
|
|
865
|
+
xmax;
|
|
866
|
+
ymin;
|
|
867
|
+
ymax;
|
|
868
|
+
label;
|
|
869
|
+
constructor(grid, options = {}) {
|
|
870
|
+
if (options.vmin !== void 0 && !Number.isFinite(options.vmin)) {
|
|
871
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
872
|
+
`vmin must be finite; received ${options.vmin}`,
|
|
873
|
+
"vmin",
|
|
874
|
+
options.vmin
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
if (options.vmax !== void 0 && !Number.isFinite(options.vmax)) {
|
|
878
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
879
|
+
`vmax must be finite; received ${options.vmax}`,
|
|
880
|
+
"vmax",
|
|
881
|
+
options.vmax
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
const min = options.vmin ?? grid.dataMin;
|
|
885
|
+
const max = options.vmax ?? grid.dataMax;
|
|
886
|
+
if (Number.isFinite(min) && Number.isFinite(max) && min > max) {
|
|
887
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
888
|
+
`vmin must be <= vmax; received vmin=${min} vmax=${max}`,
|
|
889
|
+
"vmin/vmax",
|
|
890
|
+
{ vmin: min, vmax: max }
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
const levels = resolveLevels(min, max, options.levels);
|
|
894
|
+
const { colors, rgba } = resolveLevelColors(levels, options);
|
|
895
|
+
let levelColors = colors;
|
|
896
|
+
let levelRGBA = rgba;
|
|
897
|
+
this.label = normalizeLegendLabel(options.label);
|
|
898
|
+
const lw = options.linewidth ?? 1;
|
|
899
|
+
if (!Number.isFinite(lw) || lw <= 0) {
|
|
900
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
901
|
+
`linewidth must be a positive number; received ${lw}`,
|
|
902
|
+
"linewidth",
|
|
903
|
+
lw
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
this.linewidth = lw;
|
|
907
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
908
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
909
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
910
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
911
|
+
for (let i = 0; i < grid.xCoords.length; i++) {
|
|
912
|
+
const v = grid.xCoords[i] ?? 0;
|
|
913
|
+
if (v < xMin) xMin = v;
|
|
914
|
+
if (v > xMax) xMax = v;
|
|
915
|
+
}
|
|
916
|
+
for (let i = 0; i < grid.yCoords.length; i++) {
|
|
917
|
+
const v = grid.yCoords[i] ?? 0;
|
|
918
|
+
if (v < yMin) yMin = v;
|
|
919
|
+
if (v > yMax) yMax = v;
|
|
920
|
+
}
|
|
921
|
+
this.xmin = xMin;
|
|
922
|
+
this.xmax = xMax;
|
|
923
|
+
this.ymin = yMin;
|
|
924
|
+
this.ymax = yMax;
|
|
925
|
+
let segments = [];
|
|
926
|
+
const rows = grid.rows;
|
|
927
|
+
const cols = grid.cols;
|
|
928
|
+
const data = grid.data;
|
|
929
|
+
const xCoords = grid.xCoords;
|
|
930
|
+
const yCoords = grid.yCoords;
|
|
931
|
+
if (rows >= 2 && cols >= 2) {
|
|
932
|
+
for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
|
|
933
|
+
const level = levels[levelIndex] ?? 0;
|
|
934
|
+
for (let i = 0; i < rows - 1; i++) {
|
|
935
|
+
const y0 = yCoords[i] ?? 0;
|
|
936
|
+
const y1 = yCoords[i + 1] ?? 0;
|
|
937
|
+
const rowOffset = i * cols;
|
|
938
|
+
const rowOffsetNext = (i + 1) * cols;
|
|
939
|
+
for (let j = 0; j < cols - 1; j++) {
|
|
940
|
+
const x0 = xCoords[j] ?? 0;
|
|
941
|
+
const x1 = xCoords[j + 1] ?? 0;
|
|
942
|
+
const v00 = data[rowOffset + j] ?? 0;
|
|
943
|
+
const v10 = data[rowOffset + j + 1] ?? 0;
|
|
944
|
+
const v11 = data[rowOffsetNext + j + 1] ?? 0;
|
|
945
|
+
const v01 = data[rowOffsetNext + j] ?? 0;
|
|
946
|
+
if (!isFiniteNumber(v00) || !isFiniteNumber(v10) || !isFiniteNumber(v11) || !isFiniteNumber(v01)) {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
const idx = (v00 >= level ? 1 : 0) | (v10 >= level ? 2 : 0) | (v11 >= level ? 4 : 0) | (v01 >= level ? 8 : 0);
|
|
950
|
+
if (idx === 0 || idx === 15) continue;
|
|
951
|
+
const edgePoint = (edge) => {
|
|
952
|
+
switch (edge) {
|
|
953
|
+
case 0:
|
|
954
|
+
return { x: interpolate(level, v00, v10, x0, x1), y: y0 };
|
|
955
|
+
case 1:
|
|
956
|
+
return { x: x1, y: interpolate(level, v10, v11, y0, y1) };
|
|
957
|
+
case 2:
|
|
958
|
+
return { x: interpolate(level, v11, v01, x1, x0), y: y1 };
|
|
959
|
+
case 3:
|
|
960
|
+
return { x: x0, y: interpolate(level, v01, v00, y1, y0) };
|
|
961
|
+
default:
|
|
962
|
+
return { x: x0, y: y0 };
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
const pushSegment = (e1, e2) => {
|
|
966
|
+
const p1 = edgePoint(e1);
|
|
967
|
+
const p2 = edgePoint(e2);
|
|
968
|
+
segments.push({
|
|
969
|
+
x1: p1.x,
|
|
970
|
+
y1: p1.y,
|
|
971
|
+
x2: p2.x,
|
|
972
|
+
y2: p2.y,
|
|
973
|
+
levelIndex
|
|
974
|
+
});
|
|
975
|
+
};
|
|
976
|
+
switch (idx) {
|
|
977
|
+
case 1:
|
|
978
|
+
pushSegment(3, 0);
|
|
979
|
+
break;
|
|
980
|
+
case 2:
|
|
981
|
+
pushSegment(0, 1);
|
|
982
|
+
break;
|
|
983
|
+
case 3:
|
|
984
|
+
pushSegment(3, 1);
|
|
985
|
+
break;
|
|
986
|
+
case 4:
|
|
987
|
+
pushSegment(1, 2);
|
|
988
|
+
break;
|
|
989
|
+
case 5: {
|
|
990
|
+
const center = (v00 + v10 + v11 + v01) / 4;
|
|
991
|
+
if (center >= level) {
|
|
992
|
+
pushSegment(3, 2);
|
|
993
|
+
pushSegment(0, 1);
|
|
994
|
+
} else {
|
|
995
|
+
pushSegment(3, 0);
|
|
996
|
+
pushSegment(1, 2);
|
|
997
|
+
}
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
case 6:
|
|
1001
|
+
pushSegment(0, 2);
|
|
1002
|
+
break;
|
|
1003
|
+
case 7:
|
|
1004
|
+
pushSegment(3, 2);
|
|
1005
|
+
break;
|
|
1006
|
+
case 8:
|
|
1007
|
+
pushSegment(2, 3);
|
|
1008
|
+
break;
|
|
1009
|
+
case 9:
|
|
1010
|
+
pushSegment(0, 2);
|
|
1011
|
+
break;
|
|
1012
|
+
case 10: {
|
|
1013
|
+
const center = (v00 + v10 + v11 + v01) / 4;
|
|
1014
|
+
if (center >= level) {
|
|
1015
|
+
pushSegment(0, 3);
|
|
1016
|
+
pushSegment(2, 1);
|
|
1017
|
+
} else {
|
|
1018
|
+
pushSegment(0, 1);
|
|
1019
|
+
pushSegment(2, 3);
|
|
1020
|
+
}
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
case 11:
|
|
1024
|
+
pushSegment(1, 2);
|
|
1025
|
+
break;
|
|
1026
|
+
case 12:
|
|
1027
|
+
pushSegment(1, 3);
|
|
1028
|
+
break;
|
|
1029
|
+
case 13:
|
|
1030
|
+
pushSegment(0, 1);
|
|
1031
|
+
break;
|
|
1032
|
+
case 14:
|
|
1033
|
+
pushSegment(3, 0);
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (segments.length > 0) {
|
|
1041
|
+
const used = Array.from(new Set(segments.map((seg) => seg.levelIndex))).sort((a, b) => a - b);
|
|
1042
|
+
if (used.length > 0 && used.length < levels.length) {
|
|
1043
|
+
const usedLevels = used.map((idx) => levels[idx] ?? 0);
|
|
1044
|
+
const resolved = resolveLevelColors(usedLevels, options);
|
|
1045
|
+
levelColors = resolved.colors;
|
|
1046
|
+
levelRGBA = resolved.rgba;
|
|
1047
|
+
const map = /* @__PURE__ */ new Map();
|
|
1048
|
+
for (let i = 0; i < used.length; i++) {
|
|
1049
|
+
const idx = used[i];
|
|
1050
|
+
if (idx !== void 0) {
|
|
1051
|
+
map.set(idx, i);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
segments = segments.map((seg) => ({
|
|
1055
|
+
x1: seg.x1,
|
|
1056
|
+
y1: seg.y1,
|
|
1057
|
+
x2: seg.x2,
|
|
1058
|
+
y2: seg.y2,
|
|
1059
|
+
levelIndex: map.get(seg.levelIndex) ?? 0
|
|
1060
|
+
}));
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
this.levelColors = levelColors;
|
|
1064
|
+
this.levelRGBA = levelRGBA;
|
|
1065
|
+
this.segments = segments;
|
|
1066
|
+
}
|
|
1067
|
+
getDataRange() {
|
|
1068
|
+
if (!Number.isFinite(this.xmin) || !Number.isFinite(this.xmax)) return null;
|
|
1069
|
+
if (!Number.isFinite(this.ymin) || !Number.isFinite(this.ymax)) return null;
|
|
1070
|
+
return {
|
|
1071
|
+
xmin: this.xmin,
|
|
1072
|
+
xmax: this.xmax,
|
|
1073
|
+
ymin: this.ymin,
|
|
1074
|
+
ymax: this.ymax
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
drawSVG(ctx) {
|
|
1078
|
+
for (const seg of this.segments) {
|
|
1079
|
+
const color = this.levelColors[seg.levelIndex] ?? "#1f77b4";
|
|
1080
|
+
ctx.push(
|
|
1081
|
+
`<line x1="${ctx.transform.xToPx(seg.x1).toFixed(2)}" y1="${ctx.transform.yToPx(seg.y1).toFixed(2)}" x2="${ctx.transform.xToPx(seg.x2).toFixed(2)}" y2="${ctx.transform.yToPx(seg.y2).toFixed(
|
|
1082
|
+
2
|
|
1083
|
+
)}" stroke="${escapeXml(color)}" stroke-width="${this.linewidth}" fill="none" />`
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
drawRaster(ctx) {
|
|
1088
|
+
for (const seg of this.segments) {
|
|
1089
|
+
const rgba = this.levelRGBA[seg.levelIndex] ?? {
|
|
1090
|
+
r: 0,
|
|
1091
|
+
g: 0,
|
|
1092
|
+
b: 0,
|
|
1093
|
+
a: 255
|
|
1094
|
+
};
|
|
1095
|
+
ctx.canvas.drawLineRGBA(
|
|
1096
|
+
Math.round(ctx.transform.xToPx(seg.x1)),
|
|
1097
|
+
Math.round(ctx.transform.yToPx(seg.y1)),
|
|
1098
|
+
Math.round(ctx.transform.xToPx(seg.x2)),
|
|
1099
|
+
Math.round(ctx.transform.yToPx(seg.y2)),
|
|
1100
|
+
rgba.r,
|
|
1101
|
+
rgba.g,
|
|
1102
|
+
rgba.b,
|
|
1103
|
+
rgba.a
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
getLegendEntries() {
|
|
1108
|
+
const color = this.levelColors[0] ?? "#1f77b4";
|
|
1109
|
+
const entry = buildLegendEntry(this.label, {
|
|
1110
|
+
color,
|
|
1111
|
+
shape: "line",
|
|
1112
|
+
lineWidth: this.linewidth
|
|
1113
|
+
});
|
|
1114
|
+
return entry ? [entry] : null;
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
// src/plot/plots/ContourF2D.ts
|
|
1119
|
+
var DEFAULT_COLORS2 = [
|
|
1120
|
+
"#1f77b4",
|
|
1121
|
+
"#ff7f0e",
|
|
1122
|
+
"#2ca02c",
|
|
1123
|
+
"#d62728",
|
|
1124
|
+
"#9467bd",
|
|
1125
|
+
"#8c564b",
|
|
1126
|
+
"#e377c2",
|
|
1127
|
+
"#7f7f7f",
|
|
1128
|
+
"#bcbd22",
|
|
1129
|
+
"#17becf"
|
|
1130
|
+
];
|
|
1131
|
+
function interpolateVertex(a, b, threshold) {
|
|
1132
|
+
const dv = b.v - a.v;
|
|
1133
|
+
const t = dv === 0 ? 0.5 : Math.max(0, Math.min(1, (threshold - a.v) / dv));
|
|
1134
|
+
return {
|
|
1135
|
+
x: a.x + t * (b.x - a.x),
|
|
1136
|
+
y: a.y + t * (b.y - a.y),
|
|
1137
|
+
v: threshold
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function clipPolygonByThreshold(points, threshold, keepAbove) {
|
|
1141
|
+
if (points.length === 0) return [];
|
|
1142
|
+
const output = [];
|
|
1143
|
+
for (let i = 0; i < points.length; i++) {
|
|
1144
|
+
const current = points[i];
|
|
1145
|
+
const next = points[(i + 1) % points.length];
|
|
1146
|
+
if (!current || !next) continue;
|
|
1147
|
+
const currentInside = keepAbove ? current.v >= threshold : current.v <= threshold;
|
|
1148
|
+
const nextInside = keepAbove ? next.v >= threshold : next.v <= threshold;
|
|
1149
|
+
if (currentInside && nextInside) {
|
|
1150
|
+
output.push(next);
|
|
1151
|
+
} else if (currentInside && !nextInside) {
|
|
1152
|
+
output.push(interpolateVertex(current, next, threshold));
|
|
1153
|
+
} else if (!currentInside && nextInside) {
|
|
1154
|
+
output.push(interpolateVertex(current, next, threshold));
|
|
1155
|
+
output.push(next);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
return output;
|
|
1159
|
+
}
|
|
1160
|
+
function triangulatePolygon(polygon, band, out) {
|
|
1161
|
+
if (polygon.length < 3) return;
|
|
1162
|
+
const origin = polygon[0];
|
|
1163
|
+
if (!origin) return;
|
|
1164
|
+
for (let i = 1; i < polygon.length - 1; i++) {
|
|
1165
|
+
const v1 = polygon[i];
|
|
1166
|
+
const v2 = polygon[i + 1];
|
|
1167
|
+
if (!v1 || !v2) continue;
|
|
1168
|
+
out.push({
|
|
1169
|
+
x1: origin.x,
|
|
1170
|
+
y1: origin.y,
|
|
1171
|
+
x2: v1.x,
|
|
1172
|
+
y2: v1.y,
|
|
1173
|
+
x3: v2.x,
|
|
1174
|
+
y3: v2.y,
|
|
1175
|
+
band
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function addBandTriangles(tri, levels, out) {
|
|
1180
|
+
if (tri.length !== 3 || levels.length < 2) return;
|
|
1181
|
+
const v0 = tri[0]?.v ?? 0;
|
|
1182
|
+
const v1 = tri[1]?.v ?? 0;
|
|
1183
|
+
const v2 = tri[2]?.v ?? 0;
|
|
1184
|
+
const triMin = Math.min(v0, v1, v2);
|
|
1185
|
+
const triMax = Math.max(v0, v1, v2);
|
|
1186
|
+
const lastLevel = levels[levels.length - 1] ?? 0;
|
|
1187
|
+
if (triMax < (levels[0] ?? 0) || triMin > lastLevel) return;
|
|
1188
|
+
const bandCount = levels.length - 1;
|
|
1189
|
+
for (let band = 0; band < bandCount; band++) {
|
|
1190
|
+
const lo = levels[band] ?? 0;
|
|
1191
|
+
const hi = levels[band + 1] ?? lo;
|
|
1192
|
+
if (triMax < lo || triMin > hi) continue;
|
|
1193
|
+
let poly = clipPolygonByThreshold(tri, lo, true);
|
|
1194
|
+
if (poly.length === 0) continue;
|
|
1195
|
+
poly = clipPolygonByThreshold(poly, hi, false);
|
|
1196
|
+
if (poly.length < 3) continue;
|
|
1197
|
+
triangulatePolygon(poly, band, out);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
function resolveFillLevels(min, max, levels) {
|
|
1201
|
+
if (Array.isArray(levels)) {
|
|
1202
|
+
const filtered = levels.filter((v) => Number.isFinite(v));
|
|
1203
|
+
const unique = Array.from(new Set(filtered));
|
|
1204
|
+
unique.sort((a, b) => a - b);
|
|
1205
|
+
if (unique.length < 2) {
|
|
1206
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1207
|
+
"levels must contain at least two finite values",
|
|
1208
|
+
"levels",
|
|
1209
|
+
levels
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
return unique;
|
|
1213
|
+
}
|
|
1214
|
+
const numLevels = typeof levels === "number" ? levels : 10;
|
|
1215
|
+
if (!Number.isFinite(numLevels) || Math.trunc(numLevels) !== numLevels || numLevels <= 0) {
|
|
1216
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1217
|
+
`levels must be a positive integer; received ${numLevels}`,
|
|
1218
|
+
"levels",
|
|
1219
|
+
numLevels
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
if (min === max) {
|
|
1223
|
+
return [min, max];
|
|
1224
|
+
}
|
|
1225
|
+
const step = (max - min) / numLevels;
|
|
1226
|
+
const boundaries = new Array(numLevels + 1);
|
|
1227
|
+
for (let i = 0; i <= numLevels; i++) {
|
|
1228
|
+
boundaries[i] = min + i * step;
|
|
1229
|
+
}
|
|
1230
|
+
if (boundaries.length < 2) {
|
|
1231
|
+
return [min, max];
|
|
1232
|
+
}
|
|
1233
|
+
return boundaries;
|
|
1234
|
+
}
|
|
1235
|
+
function resolveBandColors(bandCount, options) {
|
|
1236
|
+
const colors = [];
|
|
1237
|
+
if (options.colors && options.colors.length > 0) {
|
|
1238
|
+
for (let i = 0; i < bandCount; i++) {
|
|
1239
|
+
const color = options.colors[i % options.colors.length];
|
|
1240
|
+
colors.push(normalizeColor(color, DEFAULT_COLORS2[i % DEFAULT_COLORS2.length] ?? "#1f77b4"));
|
|
1241
|
+
}
|
|
1242
|
+
} else if (options.color) {
|
|
1243
|
+
const normalized = normalizeColor(options.color, "#1f77b4");
|
|
1244
|
+
for (let i = 0; i < bandCount; i++) colors.push(normalized);
|
|
1245
|
+
} else if (options.colormap) {
|
|
1246
|
+
if (!["viridis", "plasma", "inferno", "magma", "grayscale"].includes(options.colormap)) {
|
|
1247
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1248
|
+
`colormap must be one of viridis, plasma, inferno, magma, grayscale; received ${options.colormap}`,
|
|
1249
|
+
"colormap",
|
|
1250
|
+
options.colormap
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1253
|
+
const denom = Math.max(1, bandCount - 1);
|
|
1254
|
+
for (let i = 0; i < bandCount; i++) {
|
|
1255
|
+
const t = i / denom;
|
|
1256
|
+
const [r, g, b] = applyColormap(t, options.colormap);
|
|
1257
|
+
colors.push(`rgb(${r},${g},${b})`);
|
|
1258
|
+
}
|
|
1259
|
+
} else {
|
|
1260
|
+
const colormap = "viridis";
|
|
1261
|
+
const denom = Math.max(1, bandCount - 1);
|
|
1262
|
+
for (let i = 0; i < bandCount; i++) {
|
|
1263
|
+
const t = i / denom;
|
|
1264
|
+
const [r, g, b] = applyColormap(t, colormap);
|
|
1265
|
+
colors.push(`rgb(${r},${g},${b})`);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const rgba = colors.map((c) => parseHexColorToRGBA(c));
|
|
1269
|
+
return { colors, rgba };
|
|
1270
|
+
}
|
|
1271
|
+
var ContourF2D = class {
|
|
1272
|
+
kind = "contourf";
|
|
1273
|
+
rows;
|
|
1274
|
+
cols;
|
|
1275
|
+
xCoords;
|
|
1276
|
+
yCoords;
|
|
1277
|
+
levels;
|
|
1278
|
+
bandColors;
|
|
1279
|
+
bandRGBA;
|
|
1280
|
+
triangles;
|
|
1281
|
+
xmin;
|
|
1282
|
+
xmax;
|
|
1283
|
+
ymin;
|
|
1284
|
+
ymax;
|
|
1285
|
+
label;
|
|
1286
|
+
constructor(grid, options = {}) {
|
|
1287
|
+
this.rows = grid.rows;
|
|
1288
|
+
this.cols = grid.cols;
|
|
1289
|
+
this.xCoords = grid.xCoords;
|
|
1290
|
+
this.yCoords = grid.yCoords;
|
|
1291
|
+
if (options.vmin !== void 0 && !Number.isFinite(options.vmin)) {
|
|
1292
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1293
|
+
`vmin must be finite; received ${options.vmin}`,
|
|
1294
|
+
"vmin",
|
|
1295
|
+
options.vmin
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (options.vmax !== void 0 && !Number.isFinite(options.vmax)) {
|
|
1299
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1300
|
+
`vmax must be finite; received ${options.vmax}`,
|
|
1301
|
+
"vmax",
|
|
1302
|
+
options.vmax
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
const min = options.vmin ?? grid.dataMin;
|
|
1306
|
+
const max = options.vmax ?? grid.dataMax;
|
|
1307
|
+
if (Number.isFinite(min) && Number.isFinite(max) && min > max) {
|
|
1308
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1309
|
+
`vmin must be <= vmax; received vmin=${min} vmax=${max}`,
|
|
1310
|
+
"vmin/vmax",
|
|
1311
|
+
{ vmin: min, vmax: max }
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
this.levels = resolveFillLevels(min, max, options.levels);
|
|
1315
|
+
const bandCount = this.levels.length - 1;
|
|
1316
|
+
if (bandCount <= 0) {
|
|
1317
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1318
|
+
"levels must define at least one fill band",
|
|
1319
|
+
"levels",
|
|
1320
|
+
this.levels
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
const { colors, rgba } = resolveBandColors(bandCount, options);
|
|
1324
|
+
this.bandColors = colors;
|
|
1325
|
+
this.bandRGBA = rgba;
|
|
1326
|
+
this.label = normalizeLegendLabel(options.label);
|
|
1327
|
+
let xMin = Number.POSITIVE_INFINITY;
|
|
1328
|
+
let xMax = Number.NEGATIVE_INFINITY;
|
|
1329
|
+
let yMin = Number.POSITIVE_INFINITY;
|
|
1330
|
+
let yMax = Number.NEGATIVE_INFINITY;
|
|
1331
|
+
for (let i = 0; i < this.xCoords.length; i++) {
|
|
1332
|
+
const v = this.xCoords[i] ?? 0;
|
|
1333
|
+
if (v < xMin) xMin = v;
|
|
1334
|
+
if (v > xMax) xMax = v;
|
|
1335
|
+
}
|
|
1336
|
+
for (let i = 0; i < this.yCoords.length; i++) {
|
|
1337
|
+
const v = this.yCoords[i] ?? 0;
|
|
1338
|
+
if (v < yMin) yMin = v;
|
|
1339
|
+
if (v > yMax) yMax = v;
|
|
1340
|
+
}
|
|
1341
|
+
this.xmin = xMin;
|
|
1342
|
+
this.xmax = xMax;
|
|
1343
|
+
this.ymin = yMin;
|
|
1344
|
+
this.ymax = yMax;
|
|
1345
|
+
const cellRows = Math.max(0, this.rows - 1);
|
|
1346
|
+
const cellCols = Math.max(0, this.cols - 1);
|
|
1347
|
+
const triangles = [];
|
|
1348
|
+
if (cellRows > 0 && cellCols > 0) {
|
|
1349
|
+
const data = grid.data;
|
|
1350
|
+
for (let i = 0; i < cellRows; i++) {
|
|
1351
|
+
const rowOffset = i * this.cols;
|
|
1352
|
+
const rowOffsetNext = (i + 1) * this.cols;
|
|
1353
|
+
const y0 = this.yCoords[i] ?? 0;
|
|
1354
|
+
const y1 = this.yCoords[i + 1] ?? 0;
|
|
1355
|
+
for (let j = 0; j < cellCols; j++) {
|
|
1356
|
+
const x0 = this.xCoords[j] ?? 0;
|
|
1357
|
+
const x1 = this.xCoords[j + 1] ?? 0;
|
|
1358
|
+
const v00 = data[rowOffset + j] ?? 0;
|
|
1359
|
+
const v10 = data[rowOffset + j + 1] ?? 0;
|
|
1360
|
+
const v11 = data[rowOffsetNext + j + 1] ?? 0;
|
|
1361
|
+
const v01 = data[rowOffsetNext + j] ?? 0;
|
|
1362
|
+
if (!isFiniteNumber(v00) || !isFiniteNumber(v10) || !isFiniteNumber(v11) || !isFiniteNumber(v01)) {
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
const t0 = { x: x0, y: y0, v: v00 };
|
|
1366
|
+
const t1 = { x: x1, y: y0, v: v10 };
|
|
1367
|
+
const t2 = { x: x1, y: y1, v: v11 };
|
|
1368
|
+
const t3 = { x: x0, y: y1, v: v01 };
|
|
1369
|
+
addBandTriangles([t0, t1, t2], this.levels, triangles);
|
|
1370
|
+
addBandTriangles([t0, t2, t3], this.levels, triangles);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
this.triangles = triangles;
|
|
1375
|
+
}
|
|
1376
|
+
getDataRange() {
|
|
1377
|
+
if (!Number.isFinite(this.xmin) || !Number.isFinite(this.xmax)) return null;
|
|
1378
|
+
if (!Number.isFinite(this.ymin) || !Number.isFinite(this.ymax)) return null;
|
|
1379
|
+
return {
|
|
1380
|
+
xmin: this.xmin,
|
|
1381
|
+
xmax: this.xmax,
|
|
1382
|
+
ymin: this.ymin,
|
|
1383
|
+
ymax: this.ymax
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
drawSVG(ctx) {
|
|
1387
|
+
if (this.triangles.length === 0) return;
|
|
1388
|
+
for (const tri of this.triangles) {
|
|
1389
|
+
const color = this.bandColors[tri.band] ?? "#1f77b4";
|
|
1390
|
+
const x1 = ctx.transform.xToPx(tri.x1);
|
|
1391
|
+
const y1 = ctx.transform.yToPx(tri.y1);
|
|
1392
|
+
const x2 = ctx.transform.xToPx(tri.x2);
|
|
1393
|
+
const y2 = ctx.transform.yToPx(tri.y2);
|
|
1394
|
+
const x3 = ctx.transform.xToPx(tri.x3);
|
|
1395
|
+
const y3 = ctx.transform.yToPx(tri.y3);
|
|
1396
|
+
ctx.push(
|
|
1397
|
+
`<path d="M ${x1.toFixed(2)} ${y1.toFixed(2)} L ${x2.toFixed(2)} ${y2.toFixed(
|
|
1398
|
+
2
|
|
1399
|
+
)} L ${x3.toFixed(2)} ${y3.toFixed(2)} Z" fill="${escapeXml(color)}" />`
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
drawRaster(ctx) {
|
|
1404
|
+
if (this.triangles.length === 0) return;
|
|
1405
|
+
for (const tri of this.triangles) {
|
|
1406
|
+
const rgba = this.bandRGBA[tri.band] ?? { r: 0, g: 0, b: 0, a: 255 };
|
|
1407
|
+
ctx.canvas.fillTriangleRGBA(
|
|
1408
|
+
Math.round(ctx.transform.xToPx(tri.x1)),
|
|
1409
|
+
Math.round(ctx.transform.yToPx(tri.y1)),
|
|
1410
|
+
Math.round(ctx.transform.xToPx(tri.x2)),
|
|
1411
|
+
Math.round(ctx.transform.yToPx(tri.y2)),
|
|
1412
|
+
Math.round(ctx.transform.xToPx(tri.x3)),
|
|
1413
|
+
Math.round(ctx.transform.yToPx(tri.y3)),
|
|
1414
|
+
rgba.r,
|
|
1415
|
+
rgba.g,
|
|
1416
|
+
rgba.b,
|
|
1417
|
+
rgba.a
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
getLegendEntries() {
|
|
1422
|
+
const color = this.bandColors[Math.floor(this.bandColors.length / 2)] ?? "#1f77b4";
|
|
1423
|
+
const entry = buildLegendEntry(this.label, { color, shape: "box" });
|
|
1424
|
+
return entry ? [entry] : null;
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
|
|
1428
|
+
// src/plot/plots/Heatmap2D.ts
|
|
1429
|
+
var Heatmap2D = class {
|
|
1430
|
+
kind = "heatmap";
|
|
1431
|
+
data;
|
|
1432
|
+
rows;
|
|
1433
|
+
cols;
|
|
1434
|
+
vmin;
|
|
1435
|
+
vmax;
|
|
1436
|
+
colormap;
|
|
1437
|
+
xMin;
|
|
1438
|
+
xMax;
|
|
1439
|
+
yMin;
|
|
1440
|
+
yMax;
|
|
1441
|
+
label;
|
|
1442
|
+
constructor(data, rows, cols, options) {
|
|
1443
|
+
this.data = data;
|
|
1444
|
+
this.rows = rows;
|
|
1445
|
+
this.cols = cols;
|
|
1446
|
+
const colormap = options.colormap ?? "viridis";
|
|
1447
|
+
if (!["viridis", "plasma", "inferno", "magma", "grayscale"].includes(colormap)) {
|
|
1448
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1449
|
+
`colormap must be one of viridis, plasma, inferno, magma, grayscale; received ${colormap}`,
|
|
1450
|
+
"colormap",
|
|
1451
|
+
colormap
|
|
1452
|
+
);
|
|
1453
|
+
}
|
|
1454
|
+
this.colormap = colormap;
|
|
1455
|
+
this.label = normalizeLegendLabel(options.label);
|
|
1456
|
+
const extent = options.extent;
|
|
1457
|
+
if (extent) {
|
|
1458
|
+
const { xmin, xmax, ymin, ymax } = extent;
|
|
1459
|
+
if (!Number.isFinite(xmin) || !Number.isFinite(xmax) || !Number.isFinite(ymin) || !Number.isFinite(ymax)) {
|
|
1460
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("extent values must be finite", "extent", extent);
|
|
1461
|
+
}
|
|
1462
|
+
if (xmax <= xmin || ymax <= ymin) {
|
|
1463
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("extent ranges must be positive", "extent", extent);
|
|
1464
|
+
}
|
|
1465
|
+
this.xMin = xmin;
|
|
1466
|
+
this.xMax = xmax;
|
|
1467
|
+
this.yMin = ymin;
|
|
1468
|
+
this.yMax = ymax;
|
|
1469
|
+
} else {
|
|
1470
|
+
this.xMin = 0;
|
|
1471
|
+
this.xMax = cols;
|
|
1472
|
+
this.yMin = 0;
|
|
1473
|
+
this.yMax = rows;
|
|
1474
|
+
}
|
|
1475
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1476
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
1477
|
+
for (let i = 0; i < data.length; i++) {
|
|
1478
|
+
const v = data[i] ?? 0;
|
|
1479
|
+
if (isFiniteNumber(v)) {
|
|
1480
|
+
min = Math.min(min, v);
|
|
1481
|
+
max = Math.max(max, v);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
1485
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1486
|
+
"heatmap data must contain at least one finite value",
|
|
1487
|
+
"data",
|
|
1488
|
+
data
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
if (options.vmin !== void 0 && !Number.isFinite(options.vmin)) {
|
|
1492
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1493
|
+
`vmin must be finite; received ${options.vmin}`,
|
|
1494
|
+
"vmin",
|
|
1495
|
+
options.vmin
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
if (options.vmax !== void 0 && !Number.isFinite(options.vmax)) {
|
|
1499
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1500
|
+
`vmax must be finite; received ${options.vmax}`,
|
|
1501
|
+
"vmax",
|
|
1502
|
+
options.vmax
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
const vmin = options.vmin ?? min;
|
|
1506
|
+
const vmax = options.vmax ?? max;
|
|
1507
|
+
if (Number.isFinite(vmin) && Number.isFinite(vmax) && vmin > vmax) {
|
|
1508
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1509
|
+
`vmin must be <= vmax; received vmin=${vmin} vmax=${vmax}`,
|
|
1510
|
+
"vmin/vmax",
|
|
1511
|
+
{ vmin, vmax }
|
|
1512
|
+
);
|
|
1513
|
+
}
|
|
1514
|
+
this.vmin = Number.isFinite(vmin) ? vmin : 0;
|
|
1515
|
+
this.vmax = Number.isFinite(vmax) ? vmax : 1;
|
|
1516
|
+
}
|
|
1517
|
+
getDataRange() {
|
|
1518
|
+
return { xmin: this.xMin, xmax: this.xMax, ymin: this.yMin, ymax: this.yMax };
|
|
1519
|
+
}
|
|
1520
|
+
drawSVG(ctx) {
|
|
1521
|
+
if (this.rows <= 0 || this.cols <= 0) return;
|
|
1522
|
+
const range = this.vmax - this.vmin;
|
|
1523
|
+
const xSpan = this.xMax - this.xMin;
|
|
1524
|
+
const ySpan = this.yMax - this.yMin;
|
|
1525
|
+
for (let i = 0; i < this.rows; i++) {
|
|
1526
|
+
for (let j = 0; j < this.cols; j++) {
|
|
1527
|
+
const v = this.data[i * this.cols + j] ?? 0;
|
|
1528
|
+
if (!isFiniteNumber(v)) continue;
|
|
1529
|
+
const normalized = range !== 0 ? (v - this.vmin) / range : 0;
|
|
1530
|
+
const intensity = Math.max(0, Math.min(1, normalized));
|
|
1531
|
+
const [r, g, b] = applyColormap(intensity, this.colormap);
|
|
1532
|
+
const color = `rgb(${r},${g},${b})`;
|
|
1533
|
+
const x0 = ctx.transform.xToPx(this.xMin + j / this.cols * xSpan);
|
|
1534
|
+
const x1 = ctx.transform.xToPx(this.xMin + (j + 1) / this.cols * xSpan);
|
|
1535
|
+
const y0 = ctx.transform.yToPx(this.yMin + i / this.rows * ySpan);
|
|
1536
|
+
const y1 = ctx.transform.yToPx(this.yMin + (i + 1) / this.rows * ySpan);
|
|
1537
|
+
const w = Math.abs(x1 - x0);
|
|
1538
|
+
const h = Math.abs(y1 - y0);
|
|
1539
|
+
const rx = Math.min(x0, x1);
|
|
1540
|
+
const ry = Math.min(y0, y1);
|
|
1541
|
+
ctx.push(
|
|
1542
|
+
`<rect x="${rx.toFixed(2)}" y="${ry.toFixed(2)}" width="${w.toFixed(2)}" height="${h.toFixed(2)}" fill="${color}" />`
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
drawRaster(ctx) {
|
|
1548
|
+
if (this.rows <= 0 || this.cols <= 0) return;
|
|
1549
|
+
const range = this.vmax - this.vmin;
|
|
1550
|
+
const xSpan = this.xMax - this.xMin;
|
|
1551
|
+
const ySpan = this.yMax - this.yMin;
|
|
1552
|
+
for (let i = 0; i < this.rows; i++) {
|
|
1553
|
+
for (let j = 0; j < this.cols; j++) {
|
|
1554
|
+
const v = this.data[i * this.cols + j] ?? 0;
|
|
1555
|
+
if (!isFiniteNumber(v)) continue;
|
|
1556
|
+
const normalized = range !== 0 ? (v - this.vmin) / range : 0;
|
|
1557
|
+
const intensity = Math.max(0, Math.min(1, normalized));
|
|
1558
|
+
const [r, g, b] = applyColormap(intensity, this.colormap);
|
|
1559
|
+
const x0 = Math.round(ctx.transform.xToPx(this.xMin + j / this.cols * xSpan));
|
|
1560
|
+
const x1 = Math.round(ctx.transform.xToPx(this.xMin + (j + 1) / this.cols * xSpan));
|
|
1561
|
+
const y0 = Math.round(ctx.transform.yToPx(this.yMin + i / this.rows * ySpan));
|
|
1562
|
+
const y1 = Math.round(ctx.transform.yToPx(this.yMin + (i + 1) / this.rows * ySpan));
|
|
1563
|
+
const w = Math.abs(x1 - x0);
|
|
1564
|
+
const h = Math.abs(y1 - y0);
|
|
1565
|
+
const rx = Math.min(x0, x1);
|
|
1566
|
+
const ry = Math.min(y0, y1);
|
|
1567
|
+
ctx.canvas.fillRectRGBA(rx, ry, w, h, r, g, b, 255);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
getLegendEntries() {
|
|
1572
|
+
const entry = buildLegendEntry(this.label, {
|
|
1573
|
+
color: normalizeColor(`rgb(${applyColormap(0.5, this.colormap).join(",")})`, "#1f77b4"),
|
|
1574
|
+
shape: "box"
|
|
1575
|
+
});
|
|
1576
|
+
return entry ? [entry] : null;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
|
|
1580
|
+
// src/plot/plots/Histogram.ts
|
|
1581
|
+
var Histogram = class {
|
|
1582
|
+
kind = "histogram";
|
|
1583
|
+
bins;
|
|
1584
|
+
counts;
|
|
1585
|
+
binWidth;
|
|
1586
|
+
color;
|
|
1587
|
+
edgecolor;
|
|
1588
|
+
label;
|
|
1589
|
+
constructor(data, numBins, options) {
|
|
1590
|
+
if (!Number.isFinite(numBins) || Math.trunc(numBins) !== numBins || numBins <= 0) {
|
|
1591
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1592
|
+
`bins must be a positive integer; received ${numBins}`,
|
|
1593
|
+
"bins",
|
|
1594
|
+
numBins
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
this.color = normalizeColor(options.color, "#d62728");
|
|
1598
|
+
this.edgecolor = normalizeColor(options.edgecolor, "#000000");
|
|
1599
|
+
this.label = normalizeLegendLabel(options.label);
|
|
1600
|
+
let min = Number.POSITIVE_INFINITY;
|
|
1601
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
1602
|
+
let hasFinite = false;
|
|
1603
|
+
let hasInfinity = false;
|
|
1604
|
+
for (let i = 0; i < data.length; i++) {
|
|
1605
|
+
const v = data[i] ?? 0;
|
|
1606
|
+
if (isFiniteNumber(v)) {
|
|
1607
|
+
hasFinite = true;
|
|
1608
|
+
min = Math.min(min, v);
|
|
1609
|
+
max = Math.max(max, v);
|
|
1610
|
+
} else if (v === Infinity || v === -Infinity) {
|
|
1611
|
+
hasInfinity = true;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (!hasFinite) {
|
|
1615
|
+
if (hasInfinity) {
|
|
1616
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1617
|
+
"histogram data must contain at least one finite value",
|
|
1618
|
+
"data",
|
|
1619
|
+
data
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
this.bins = new Float64Array(0);
|
|
1623
|
+
this.counts = new Float64Array(0);
|
|
1624
|
+
this.binWidth = 1;
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const span = max - min;
|
|
1628
|
+
const binWidth = span > 0 ? span / numBins : 1;
|
|
1629
|
+
const binStart = span > 0 ? min : min - numBins * binWidth / 2;
|
|
1630
|
+
this.bins = new Float64Array(numBins);
|
|
1631
|
+
this.counts = new Float64Array(numBins);
|
|
1632
|
+
this.binWidth = binWidth;
|
|
1633
|
+
for (let i = 0; i < numBins; i++) {
|
|
1634
|
+
this.bins[i] = binStart + i * binWidth;
|
|
1635
|
+
this.counts[i] = 0;
|
|
1636
|
+
}
|
|
1637
|
+
if (span === 0) {
|
|
1638
|
+
let finiteCount = 0;
|
|
1639
|
+
for (let i = 0; i < data.length; i++) {
|
|
1640
|
+
const v = data[i] ?? 0;
|
|
1641
|
+
if (isFiniteNumber(v)) finiteCount++;
|
|
1642
|
+
}
|
|
1643
|
+
const mid = Math.floor(numBins / 2);
|
|
1644
|
+
this.counts[mid] = finiteCount;
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
for (let i = 0; i < data.length; i++) {
|
|
1648
|
+
const v = data[i] ?? 0;
|
|
1649
|
+
if (!isFiniteNumber(v)) continue;
|
|
1650
|
+
const rawIdx = Math.floor((v - min) / binWidth);
|
|
1651
|
+
const binIdx = Math.min(Math.max(0, rawIdx), numBins - 1);
|
|
1652
|
+
this.counts[binIdx] = (this.counts[binIdx] ?? 0) + 1;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
getDataRange() {
|
|
1656
|
+
if (this.bins.length === 0) return null;
|
|
1657
|
+
const xmin = this.bins[0] ?? 0;
|
|
1658
|
+
const xmax = (this.bins[this.bins.length - 1] ?? 0) + this.binWidth;
|
|
1659
|
+
let ymax = 0;
|
|
1660
|
+
for (let i = 0; i < this.counts.length; i++) {
|
|
1661
|
+
ymax = Math.max(ymax, this.counts[i] ?? 0);
|
|
1662
|
+
}
|
|
1663
|
+
return { xmin, xmax, ymin: 0, ymax };
|
|
1664
|
+
}
|
|
1665
|
+
drawSVG(ctx) {
|
|
1666
|
+
if (this.bins.length === 0) return;
|
|
1667
|
+
const binWidth = this.binWidth;
|
|
1668
|
+
for (let i = 0; i < this.bins.length; i++) {
|
|
1669
|
+
const x0 = this.bins[i] ?? 0;
|
|
1670
|
+
const count = this.counts[i] ?? 0;
|
|
1671
|
+
const px0 = ctx.transform.xToPx(x0);
|
|
1672
|
+
const px1 = ctx.transform.xToPx(x0 + binWidth);
|
|
1673
|
+
const py0 = ctx.transform.yToPx(0);
|
|
1674
|
+
const py1 = ctx.transform.yToPx(count);
|
|
1675
|
+
const w = Math.abs(px1 - px0);
|
|
1676
|
+
const h = Math.abs(py1 - py0);
|
|
1677
|
+
const rx = Math.min(px0, px1);
|
|
1678
|
+
const ry = Math.min(py0, py1);
|
|
1679
|
+
ctx.push(
|
|
1680
|
+
`<rect x="${rx.toFixed(2)}" y="${ry.toFixed(2)}" width="${w.toFixed(2)}" height="${h.toFixed(2)}" fill="${escapeXml(this.color)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
drawRaster(ctx) {
|
|
1685
|
+
if (this.bins.length === 0) return;
|
|
1686
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
1687
|
+
const edge = parseHexColorToRGBA(this.edgecolor);
|
|
1688
|
+
const binWidth = this.binWidth;
|
|
1689
|
+
for (let i = 0; i < this.bins.length; i++) {
|
|
1690
|
+
const x0 = this.bins[i] ?? 0;
|
|
1691
|
+
const count = this.counts[i] ?? 0;
|
|
1692
|
+
const px0 = Math.round(ctx.transform.xToPx(x0));
|
|
1693
|
+
const px1 = Math.round(ctx.transform.xToPx(x0 + binWidth));
|
|
1694
|
+
const py0 = Math.round(ctx.transform.yToPx(0));
|
|
1695
|
+
const py1 = Math.round(ctx.transform.yToPx(count));
|
|
1696
|
+
const w = Math.abs(px1 - px0);
|
|
1697
|
+
const h = Math.abs(py1 - py0);
|
|
1698
|
+
const rx = Math.min(px0, px1);
|
|
1699
|
+
const ry = Math.min(py0, py1);
|
|
1700
|
+
ctx.canvas.fillRectRGBA(rx, ry, w, h, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
1701
|
+
const r_e = edge.r;
|
|
1702
|
+
const g_e = edge.g;
|
|
1703
|
+
const b_e = edge.b;
|
|
1704
|
+
const a_e = edge.a;
|
|
1705
|
+
ctx.canvas.drawLineRGBA(rx, ry, rx + w, ry, r_e, g_e, b_e, a_e);
|
|
1706
|
+
ctx.canvas.drawLineRGBA(rx + w, ry, rx + w, ry + h, r_e, g_e, b_e, a_e);
|
|
1707
|
+
ctx.canvas.drawLineRGBA(rx + w, ry + h, rx, ry + h, r_e, g_e, b_e, a_e);
|
|
1708
|
+
ctx.canvas.drawLineRGBA(rx, ry + h, rx, ry, r_e, g_e, b_e, a_e);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
getLegendEntries() {
|
|
1712
|
+
const entry = buildLegendEntry(this.label, { color: this.color, shape: "box" });
|
|
1713
|
+
return entry ? [entry] : null;
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
// src/plot/plots/HorizontalBar2D.ts
|
|
1718
|
+
var HorizontalBar2D = class {
|
|
1719
|
+
kind = "barh";
|
|
1720
|
+
y;
|
|
1721
|
+
width;
|
|
1722
|
+
color;
|
|
1723
|
+
edgecolor;
|
|
1724
|
+
barHeight;
|
|
1725
|
+
label;
|
|
1726
|
+
constructor(y, width, options) {
|
|
1727
|
+
if (y.length !== width.length) throw new chunkJSCDE774_cjs.ShapeError("y and width must have the same length");
|
|
1728
|
+
this.y = y;
|
|
1729
|
+
this.width = width;
|
|
1730
|
+
this.color = normalizeColor(options.color, "#9467bd");
|
|
1731
|
+
this.edgecolor = normalizeColor(options.edgecolor, "#000000");
|
|
1732
|
+
this.barHeight = 0.8;
|
|
1733
|
+
this.label = normalizeLegendLabel(options.label);
|
|
1734
|
+
}
|
|
1735
|
+
getDataRange() {
|
|
1736
|
+
let ymin = Number.POSITIVE_INFINITY;
|
|
1737
|
+
let ymax = Number.NEGATIVE_INFINITY;
|
|
1738
|
+
let xmin = 0;
|
|
1739
|
+
let xmax = 0;
|
|
1740
|
+
let sawValue = false;
|
|
1741
|
+
for (let i = 0; i < this.y.length; i++) {
|
|
1742
|
+
const yi = this.y[i] ?? 0;
|
|
1743
|
+
const wi = this.width[i] ?? 0;
|
|
1744
|
+
if (!isFiniteNumber(yi) || !isFiniteNumber(wi)) continue;
|
|
1745
|
+
sawValue = true;
|
|
1746
|
+
ymin = Math.min(ymin, yi - this.barHeight / 2);
|
|
1747
|
+
ymax = Math.max(ymax, yi + this.barHeight / 2);
|
|
1748
|
+
xmin = Math.min(xmin, wi);
|
|
1749
|
+
xmax = Math.max(xmax, wi);
|
|
1750
|
+
}
|
|
1751
|
+
if (!sawValue || !Number.isFinite(ymin) || !Number.isFinite(ymax)) {
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
return { xmin, xmax, ymin, ymax };
|
|
1755
|
+
}
|
|
1756
|
+
drawSVG(ctx) {
|
|
1757
|
+
for (let i = 0; i < this.y.length; i++) {
|
|
1758
|
+
const yi = this.y[i] ?? 0;
|
|
1759
|
+
const wi = this.width[i] ?? 0;
|
|
1760
|
+
if (!isFiniteNumber(yi) || !isFiniteNumber(wi)) continue;
|
|
1761
|
+
const x0 = ctx.transform.xToPx(0);
|
|
1762
|
+
const x1 = ctx.transform.xToPx(wi);
|
|
1763
|
+
const y0 = ctx.transform.yToPx(yi - this.barHeight / 2);
|
|
1764
|
+
const y1 = ctx.transform.yToPx(yi + this.barHeight / 2);
|
|
1765
|
+
const w = Math.abs(x1 - x0);
|
|
1766
|
+
const h = Math.abs(y1 - y0);
|
|
1767
|
+
const rx = Math.min(x0, x1);
|
|
1768
|
+
const ry = Math.min(y0, y1);
|
|
1769
|
+
ctx.push(
|
|
1770
|
+
`<rect x="${rx.toFixed(2)}" y="${ry.toFixed(2)}" width="${w.toFixed(2)}" height="${h.toFixed(2)}" fill="${escapeXml(this.color)}" stroke="${escapeXml(this.edgecolor)}" />`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
drawRaster(ctx) {
|
|
1775
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
1776
|
+
const edge = parseHexColorToRGBA(this.edgecolor);
|
|
1777
|
+
for (let i = 0; i < this.y.length; i++) {
|
|
1778
|
+
const yi = this.y[i] ?? 0;
|
|
1779
|
+
const wi = this.width[i] ?? 0;
|
|
1780
|
+
if (!isFiniteNumber(yi) || !isFiniteNumber(wi)) continue;
|
|
1781
|
+
const x0 = Math.round(ctx.transform.xToPx(0));
|
|
1782
|
+
const x1 = Math.round(ctx.transform.xToPx(wi));
|
|
1783
|
+
const y0 = Math.round(ctx.transform.yToPx(yi - this.barHeight / 2));
|
|
1784
|
+
const y1 = Math.round(ctx.transform.yToPx(yi + this.barHeight / 2));
|
|
1785
|
+
const w = Math.abs(x1 - x0);
|
|
1786
|
+
const h = Math.abs(y1 - y0);
|
|
1787
|
+
const rx = Math.min(x0, x1);
|
|
1788
|
+
const ry = Math.min(y0, y1);
|
|
1789
|
+
ctx.canvas.fillRectRGBA(rx, ry, w, h, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
1790
|
+
const r_e = edge.r;
|
|
1791
|
+
const g_e = edge.g;
|
|
1792
|
+
const b_e = edge.b;
|
|
1793
|
+
const a_e = edge.a;
|
|
1794
|
+
ctx.canvas.drawLineRGBA(rx, ry, rx + w, ry, r_e, g_e, b_e, a_e);
|
|
1795
|
+
ctx.canvas.drawLineRGBA(rx + w, ry, rx + w, ry + h, r_e, g_e, b_e, a_e);
|
|
1796
|
+
ctx.canvas.drawLineRGBA(rx + w, ry + h, rx, ry + h, r_e, g_e, b_e, a_e);
|
|
1797
|
+
ctx.canvas.drawLineRGBA(rx, ry + h, rx, ry, r_e, g_e, b_e, a_e);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
getLegendEntries() {
|
|
1801
|
+
const entry = buildLegendEntry(this.label, { color: this.color, shape: "box" });
|
|
1802
|
+
return entry ? [entry] : null;
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
|
|
1806
|
+
// src/plot/plots/Line2D.ts
|
|
1807
|
+
var Line2D = class {
|
|
1808
|
+
kind = "line";
|
|
1809
|
+
x;
|
|
1810
|
+
y;
|
|
1811
|
+
color;
|
|
1812
|
+
linewidth;
|
|
1813
|
+
label;
|
|
1814
|
+
constructor(x, y, options) {
|
|
1815
|
+
if (x.length !== y.length) throw new chunkJSCDE774_cjs.ShapeError("x and y must have the same length");
|
|
1816
|
+
this.x = x;
|
|
1817
|
+
this.y = y;
|
|
1818
|
+
this.color = normalizeColor(options.color, "#1f77b4");
|
|
1819
|
+
const lw = options.linewidth ?? 2;
|
|
1820
|
+
if (!Number.isFinite(lw) || lw <= 0) {
|
|
1821
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1822
|
+
`linewidth must be a positive number; received ${lw}`,
|
|
1823
|
+
"linewidth",
|
|
1824
|
+
lw
|
|
1825
|
+
);
|
|
1826
|
+
}
|
|
1827
|
+
this.linewidth = lw;
|
|
1828
|
+
this.label = normalizeLegendLabel(options.label);
|
|
1829
|
+
}
|
|
1830
|
+
getDataRange() {
|
|
1831
|
+
let xmin = Number.POSITIVE_INFINITY;
|
|
1832
|
+
let xmax = Number.NEGATIVE_INFINITY;
|
|
1833
|
+
let ymin = Number.POSITIVE_INFINITY;
|
|
1834
|
+
let ymax = Number.NEGATIVE_INFINITY;
|
|
1835
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
1836
|
+
const xi = this.x[i] ?? 0;
|
|
1837
|
+
const yi = this.y[i] ?? 0;
|
|
1838
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(yi)) continue;
|
|
1839
|
+
xmin = Math.min(xmin, xi);
|
|
1840
|
+
xmax = Math.max(xmax, xi);
|
|
1841
|
+
ymin = Math.min(ymin, yi);
|
|
1842
|
+
ymax = Math.max(ymax, yi);
|
|
1843
|
+
}
|
|
1844
|
+
if (!Number.isFinite(xmin) || !Number.isFinite(xmax) || !Number.isFinite(ymin) || !Number.isFinite(ymax)) {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
return { xmin, xmax, ymin, ymax };
|
|
1848
|
+
}
|
|
1849
|
+
drawSVG(ctx) {
|
|
1850
|
+
const pts = [];
|
|
1851
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
1852
|
+
const xi = this.x[i] ?? 0;
|
|
1853
|
+
const yi = this.y[i] ?? 0;
|
|
1854
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(yi)) continue;
|
|
1855
|
+
pts.push(`${ctx.transform.xToPx(xi).toFixed(2)},${ctx.transform.yToPx(yi).toFixed(2)}`);
|
|
1856
|
+
}
|
|
1857
|
+
ctx.push(
|
|
1858
|
+
`<polyline fill="none" stroke="${escapeXml(this.color)}" stroke-width="${this.linewidth}" points="${pts.join(" ")}" />`
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
drawRaster(ctx) {
|
|
1862
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
1863
|
+
for (let i = 1; i < this.x.length; i++) {
|
|
1864
|
+
const x0 = this.x[i - 1] ?? 0;
|
|
1865
|
+
const y0 = this.y[i - 1] ?? 0;
|
|
1866
|
+
const x1 = this.x[i] ?? 0;
|
|
1867
|
+
const y1 = this.y[i] ?? 0;
|
|
1868
|
+
if (!isFiniteNumber(x0) || !isFiniteNumber(y0) || !isFiniteNumber(x1) || !isFiniteNumber(y1))
|
|
1869
|
+
continue;
|
|
1870
|
+
ctx.canvas.drawLineRGBA(
|
|
1871
|
+
Math.round(ctx.transform.xToPx(x0)),
|
|
1872
|
+
Math.round(ctx.transform.yToPx(y0)),
|
|
1873
|
+
Math.round(ctx.transform.xToPx(x1)),
|
|
1874
|
+
Math.round(ctx.transform.yToPx(y1)),
|
|
1875
|
+
rgba.r,
|
|
1876
|
+
rgba.g,
|
|
1877
|
+
rgba.b,
|
|
1878
|
+
rgba.a
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
getLegendEntries() {
|
|
1883
|
+
const entry = buildLegendEntry(this.label, {
|
|
1884
|
+
color: this.color,
|
|
1885
|
+
shape: "line",
|
|
1886
|
+
lineWidth: this.linewidth
|
|
1887
|
+
});
|
|
1888
|
+
return entry ? [entry] : null;
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
// src/plot/plots/Pie.ts
|
|
1893
|
+
var Pie = class {
|
|
1894
|
+
kind = "pie";
|
|
1895
|
+
centerX;
|
|
1896
|
+
centerY;
|
|
1897
|
+
radius;
|
|
1898
|
+
values;
|
|
1899
|
+
angles;
|
|
1900
|
+
colors;
|
|
1901
|
+
labels;
|
|
1902
|
+
rangeOverride;
|
|
1903
|
+
label;
|
|
1904
|
+
constructor(centerX, centerY, radius, values, labels, options = {}, rangeOverride) {
|
|
1905
|
+
if (!Number.isFinite(centerX) || !Number.isFinite(centerY)) {
|
|
1906
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("pie center must be finite", "center", { centerX, centerY });
|
|
1907
|
+
}
|
|
1908
|
+
if (!Number.isFinite(radius) || radius <= 0) {
|
|
1909
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1910
|
+
`pie radius must be positive; received ${radius}`,
|
|
1911
|
+
"radius",
|
|
1912
|
+
radius
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
this.centerX = centerX;
|
|
1916
|
+
this.centerY = centerY;
|
|
1917
|
+
this.radius = radius;
|
|
1918
|
+
if (values.length === 0) {
|
|
1919
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("pie requires at least one value", "values", values.length);
|
|
1920
|
+
}
|
|
1921
|
+
if (labels && labels.length !== values.length) {
|
|
1922
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1923
|
+
`labels length (${labels.length}) must match values length (${values.length})`,
|
|
1924
|
+
"labels",
|
|
1925
|
+
labels
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
this.labels = labels ?? [];
|
|
1929
|
+
this.label = normalizeLegendLabel(options.label);
|
|
1930
|
+
this.rangeOverride = rangeOverride;
|
|
1931
|
+
const validValues = [];
|
|
1932
|
+
for (let i = 0; i < values.length; i++) {
|
|
1933
|
+
const v = values[i] ?? 0;
|
|
1934
|
+
if (!isFiniteNumber(v)) {
|
|
1935
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("pie values must be finite", "values", v);
|
|
1936
|
+
}
|
|
1937
|
+
if (v < 0) {
|
|
1938
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("pie values must be non-negative", "values", v);
|
|
1939
|
+
}
|
|
1940
|
+
validValues.push(v);
|
|
1941
|
+
}
|
|
1942
|
+
const total = validValues.reduce((sum, val) => sum + val, 0);
|
|
1943
|
+
if (!Number.isFinite(total) || total <= 0) {
|
|
1944
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
1945
|
+
"pie values must sum to a positive number",
|
|
1946
|
+
"values",
|
|
1947
|
+
validValues
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
this.values = validValues;
|
|
1951
|
+
const angles = [0];
|
|
1952
|
+
let cumulative = 0;
|
|
1953
|
+
for (const value of validValues) {
|
|
1954
|
+
cumulative += value / total * 2 * Math.PI;
|
|
1955
|
+
angles.push(cumulative);
|
|
1956
|
+
}
|
|
1957
|
+
this.angles = angles;
|
|
1958
|
+
const defaultColors = [
|
|
1959
|
+
"#1f77b4",
|
|
1960
|
+
"#ff7f0e",
|
|
1961
|
+
"#2ca02c",
|
|
1962
|
+
"#d62728",
|
|
1963
|
+
"#9467bd",
|
|
1964
|
+
"#8c564b",
|
|
1965
|
+
"#e377c2",
|
|
1966
|
+
"#7f7f7f",
|
|
1967
|
+
"#bcbd22",
|
|
1968
|
+
"#17becf"
|
|
1969
|
+
];
|
|
1970
|
+
const colors = [];
|
|
1971
|
+
if (options.colors && options.colors.length > 0) {
|
|
1972
|
+
for (let i = 0; i < validValues.length; i++) {
|
|
1973
|
+
const colorOption = options.colors[i % options.colors.length];
|
|
1974
|
+
const colorToUse = colorOption;
|
|
1975
|
+
const defaultColor = defaultColors[i % defaultColors.length] ?? "#1f77b4";
|
|
1976
|
+
colors.push(normalizeColor(colorToUse, defaultColor));
|
|
1977
|
+
}
|
|
1978
|
+
} else {
|
|
1979
|
+
for (let i = 0; i < validValues.length; i++) {
|
|
1980
|
+
const defaultColor = defaultColors[i % defaultColors.length] ?? "#1f77b4";
|
|
1981
|
+
colors.push(normalizeColor(void 0, defaultColor));
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
this.colors = colors;
|
|
1985
|
+
}
|
|
1986
|
+
getDataRange() {
|
|
1987
|
+
if (this.rangeOverride) return this.rangeOverride;
|
|
1988
|
+
return {
|
|
1989
|
+
xmin: this.centerX - this.radius,
|
|
1990
|
+
xmax: this.centerX + this.radius,
|
|
1991
|
+
ymin: this.centerY - this.radius,
|
|
1992
|
+
ymax: this.centerY + this.radius
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
drawSVG(ctx) {
|
|
1996
|
+
const cx = ctx.transform.xToPx(this.centerX);
|
|
1997
|
+
const cy = ctx.transform.yToPx(this.centerY);
|
|
1998
|
+
const rx = Math.abs(ctx.transform.xToPx(this.centerX + this.radius) - cx);
|
|
1999
|
+
const ry = Math.abs(ctx.transform.yToPx(this.centerY + this.radius) - cy);
|
|
2000
|
+
const r = Math.min(rx, ry);
|
|
2001
|
+
for (let i = 0; i < this.values.length; i++) {
|
|
2002
|
+
const startAngle = this.angles[i] ?? 0;
|
|
2003
|
+
const endAngle = this.angles[i + 1] ?? 2 * Math.PI;
|
|
2004
|
+
const x1 = cx + r * Math.cos(startAngle);
|
|
2005
|
+
const y1 = cy - r * Math.sin(startAngle);
|
|
2006
|
+
const x2 = cx + r * Math.cos(endAngle);
|
|
2007
|
+
const y2 = cy - r * Math.sin(endAngle);
|
|
2008
|
+
const largeArcFlag = endAngle - startAngle > Math.PI ? 1 : 0;
|
|
2009
|
+
const pathData = [
|
|
2010
|
+
`M ${cx.toFixed(2)} ${cy.toFixed(2)}`,
|
|
2011
|
+
`L ${x1.toFixed(2)} ${y1.toFixed(2)}`,
|
|
2012
|
+
`A ${r.toFixed(2)} ${r.toFixed(2)} 0 ${largeArcFlag} 0 ${x2.toFixed(2)} ${y2.toFixed(2)}`,
|
|
2013
|
+
"Z"
|
|
2014
|
+
].join(" ");
|
|
2015
|
+
const fillColor = this.colors[i] ?? "#1f77b4";
|
|
2016
|
+
ctx.push(
|
|
2017
|
+
`<path d="${pathData}" fill="${escapeXml(fillColor)}" stroke="#ffffff" stroke-width="1" />`
|
|
2018
|
+
);
|
|
2019
|
+
const label = this.labels[i];
|
|
2020
|
+
if (i < this.labels.length && label) {
|
|
2021
|
+
const midAngle = (startAngle + endAngle) / 2;
|
|
2022
|
+
const labelRadius = r * 0.7;
|
|
2023
|
+
const labelX = cx + labelRadius * Math.cos(midAngle);
|
|
2024
|
+
const labelY = cy - labelRadius * Math.sin(midAngle);
|
|
2025
|
+
ctx.push(
|
|
2026
|
+
`<text x="${labelX.toFixed(2)}" y="${labelY.toFixed(2)}" text-anchor="middle" font-size="12" fill="#ffffff">${escapeXml(label)}</text>`
|
|
2027
|
+
);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
drawRaster(ctx) {
|
|
2032
|
+
const cx = Math.round(ctx.transform.xToPx(this.centerX));
|
|
2033
|
+
const cy = Math.round(ctx.transform.yToPx(this.centerY));
|
|
2034
|
+
const rx = Math.abs(ctx.transform.xToPx(this.centerX + this.radius) - cx);
|
|
2035
|
+
const ry = Math.abs(ctx.transform.yToPx(this.centerY + this.radius) - cy);
|
|
2036
|
+
const r = Math.round(Math.min(rx, ry));
|
|
2037
|
+
const edge = parseHexColorToRGBA("#ffffff");
|
|
2038
|
+
for (let i = 0; i < this.values.length; i++) {
|
|
2039
|
+
const startAngle = this.angles[i] ?? 0;
|
|
2040
|
+
const endAngle = this.angles[i + 1] ?? 2 * Math.PI;
|
|
2041
|
+
const fillColor = this.colors[i] ?? "#1f77b4";
|
|
2042
|
+
const rgba = parseHexColorToRGBA(fillColor);
|
|
2043
|
+
const arcLength = (endAngle - startAngle) * r;
|
|
2044
|
+
const numSegments = Math.max(5, Math.ceil(arcLength / 5));
|
|
2045
|
+
for (let j = 0; j < numSegments; j++) {
|
|
2046
|
+
const angle1 = startAngle + j / numSegments * (endAngle - startAngle);
|
|
2047
|
+
const angle2 = startAngle + (j + 1) / numSegments * (endAngle - startAngle);
|
|
2048
|
+
const x1 = cx + r * Math.cos(angle1);
|
|
2049
|
+
const y1 = cy - r * Math.sin(angle1);
|
|
2050
|
+
const x2 = cx + r * Math.cos(angle2);
|
|
2051
|
+
const y2 = cy - r * Math.sin(angle2);
|
|
2052
|
+
ctx.canvas.fillTriangleRGBA(cx, cy, x1, y1, x2, y2, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2053
|
+
}
|
|
2054
|
+
const edgeX = Math.round(cx + r * Math.cos(startAngle));
|
|
2055
|
+
const edgeY = Math.round(cy - r * Math.sin(startAngle));
|
|
2056
|
+
ctx.canvas.drawLineRGBA(cx, cy, edgeX, edgeY, edge.r, edge.g, edge.b, edge.a);
|
|
2057
|
+
const label = this.labels[i];
|
|
2058
|
+
if (i < this.labels.length && label) {
|
|
2059
|
+
const midAngle = (startAngle + endAngle) / 2;
|
|
2060
|
+
const labelRadius = r * 0.7;
|
|
2061
|
+
const labelX = cx + labelRadius * Math.cos(midAngle);
|
|
2062
|
+
const labelY = cy - labelRadius * Math.sin(midAngle);
|
|
2063
|
+
const textColor = parseHexColorToRGBA("#ffffff");
|
|
2064
|
+
ctx.canvas.drawTextRGBA(
|
|
2065
|
+
label,
|
|
2066
|
+
Math.round(labelX),
|
|
2067
|
+
Math.round(labelY),
|
|
2068
|
+
textColor.r,
|
|
2069
|
+
textColor.g,
|
|
2070
|
+
textColor.b,
|
|
2071
|
+
textColor.a,
|
|
2072
|
+
{ fontSize: 12, align: "middle", baseline: "middle" }
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
getLegendEntries() {
|
|
2078
|
+
const entries = [];
|
|
2079
|
+
if (this.labels.length > 0) {
|
|
2080
|
+
for (let i = 0; i < this.labels.length; i++) {
|
|
2081
|
+
const label = normalizeLegendLabel(this.labels[i]);
|
|
2082
|
+
if (!label) continue;
|
|
2083
|
+
const color = this.colors[i] ?? "#1f77b4";
|
|
2084
|
+
entries.push({ label, color, shape: "box" });
|
|
2085
|
+
}
|
|
2086
|
+
return entries.length > 0 ? entries : null;
|
|
2087
|
+
}
|
|
2088
|
+
if (!this.label) {
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
const entry = {
|
|
2092
|
+
label: this.label,
|
|
2093
|
+
color: this.colors[0] ?? "#1f77b4",
|
|
2094
|
+
shape: "box"
|
|
2095
|
+
};
|
|
2096
|
+
return [entry];
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
|
|
2100
|
+
// src/plot/plots/Scatter2D.ts
|
|
2101
|
+
var Scatter2D = class {
|
|
2102
|
+
kind = "scatter";
|
|
2103
|
+
x;
|
|
2104
|
+
y;
|
|
2105
|
+
color;
|
|
2106
|
+
size;
|
|
2107
|
+
label;
|
|
2108
|
+
constructor(x, y, options) {
|
|
2109
|
+
if (x.length !== y.length) throw new chunkJSCDE774_cjs.ShapeError("x and y must have the same length");
|
|
2110
|
+
this.x = x;
|
|
2111
|
+
this.y = y;
|
|
2112
|
+
this.color = normalizeColor(options.color, "#ff7f0e");
|
|
2113
|
+
const size = options.size ?? 5;
|
|
2114
|
+
if (!Number.isFinite(size) || size <= 0) {
|
|
2115
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2116
|
+
`size must be a positive number; received ${size}`,
|
|
2117
|
+
"size",
|
|
2118
|
+
size
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
this.size = size;
|
|
2122
|
+
this.label = normalizeLegendLabel(options.label);
|
|
2123
|
+
}
|
|
2124
|
+
getDataRange() {
|
|
2125
|
+
let xmin = Number.POSITIVE_INFINITY;
|
|
2126
|
+
let xmax = Number.NEGATIVE_INFINITY;
|
|
2127
|
+
let ymin = Number.POSITIVE_INFINITY;
|
|
2128
|
+
let ymax = Number.NEGATIVE_INFINITY;
|
|
2129
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
2130
|
+
const xi = this.x[i] ?? 0;
|
|
2131
|
+
const yi = this.y[i] ?? 0;
|
|
2132
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(yi)) continue;
|
|
2133
|
+
xmin = Math.min(xmin, xi);
|
|
2134
|
+
xmax = Math.max(xmax, xi);
|
|
2135
|
+
ymin = Math.min(ymin, yi);
|
|
2136
|
+
ymax = Math.max(ymax, yi);
|
|
2137
|
+
}
|
|
2138
|
+
if (!Number.isFinite(xmin) || !Number.isFinite(xmax) || !Number.isFinite(ymin) || !Number.isFinite(ymax)) {
|
|
2139
|
+
return null;
|
|
2140
|
+
}
|
|
2141
|
+
return { xmin, xmax, ymin, ymax };
|
|
2142
|
+
}
|
|
2143
|
+
drawSVG(ctx) {
|
|
2144
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
2145
|
+
const xi = this.x[i] ?? 0;
|
|
2146
|
+
const yi = this.y[i] ?? 0;
|
|
2147
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(yi)) continue;
|
|
2148
|
+
const px = ctx.transform.xToPx(xi);
|
|
2149
|
+
const py = ctx.transform.yToPx(yi);
|
|
2150
|
+
ctx.push(
|
|
2151
|
+
`<circle cx="${px.toFixed(2)}" cy="${py.toFixed(2)}" r="${this.size}" fill="${escapeXml(this.color)}" />`
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
drawRaster(ctx) {
|
|
2156
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
2157
|
+
for (let i = 0; i < this.x.length; i++) {
|
|
2158
|
+
const xi = this.x[i] ?? 0;
|
|
2159
|
+
const yi = this.y[i] ?? 0;
|
|
2160
|
+
if (!isFiniteNumber(xi) || !isFiniteNumber(yi)) continue;
|
|
2161
|
+
const px = Math.round(ctx.transform.xToPx(xi));
|
|
2162
|
+
const py = Math.round(ctx.transform.yToPx(yi));
|
|
2163
|
+
ctx.canvas.drawCircleRGBA(px, py, this.size, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
getLegendEntries() {
|
|
2167
|
+
const entry = buildLegendEntry(this.label, {
|
|
2168
|
+
color: this.color,
|
|
2169
|
+
shape: "marker",
|
|
2170
|
+
markerSize: this.size
|
|
2171
|
+
});
|
|
2172
|
+
return entry ? [entry] : null;
|
|
2173
|
+
}
|
|
2174
|
+
};
|
|
2175
|
+
|
|
2176
|
+
// src/plot/plots/Violinplot.ts
|
|
2177
|
+
var Violinplot = class {
|
|
2178
|
+
kind = "violinplot";
|
|
2179
|
+
position;
|
|
2180
|
+
q1;
|
|
2181
|
+
median;
|
|
2182
|
+
q3;
|
|
2183
|
+
kdePoints;
|
|
2184
|
+
kdeValues;
|
|
2185
|
+
color;
|
|
2186
|
+
edgecolor;
|
|
2187
|
+
violinWidth;
|
|
2188
|
+
label;
|
|
2189
|
+
hasData;
|
|
2190
|
+
constructor(position, data, options) {
|
|
2191
|
+
this.position = position;
|
|
2192
|
+
this.color = normalizeColor(options.color, "#8c564b");
|
|
2193
|
+
this.edgecolor = normalizeColor(options.edgecolor, "#000000");
|
|
2194
|
+
this.violinWidth = 0.8;
|
|
2195
|
+
this.label = normalizeLegendLabel(options.label);
|
|
2196
|
+
const sorted = Array.from(data).filter(isFiniteNumber).sort((a, b) => a - b);
|
|
2197
|
+
const n = sorted.length;
|
|
2198
|
+
if (n === 0) {
|
|
2199
|
+
if (data.length > 0) {
|
|
2200
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2201
|
+
"violinplot data must contain at least one finite value",
|
|
2202
|
+
"data",
|
|
2203
|
+
data
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
this.hasData = false;
|
|
2207
|
+
this.q1 = 0;
|
|
2208
|
+
this.median = 0;
|
|
2209
|
+
this.q3 = 0;
|
|
2210
|
+
this.kdeValues = [];
|
|
2211
|
+
this.kdePoints = [];
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
this.hasData = true;
|
|
2215
|
+
const { q1, median, q3 } = calculateQuartiles(sorted);
|
|
2216
|
+
this.q1 = q1;
|
|
2217
|
+
this.median = median;
|
|
2218
|
+
this.q3 = q3;
|
|
2219
|
+
const firstVal = sorted[0] ?? 0;
|
|
2220
|
+
const lastVal = sorted[sorted.length - 1] ?? 0;
|
|
2221
|
+
const dataRange = lastVal - firstVal;
|
|
2222
|
+
const padding = dataRange * 0.1;
|
|
2223
|
+
const min = firstVal - padding;
|
|
2224
|
+
const max = lastVal + padding;
|
|
2225
|
+
const numPoints = 100;
|
|
2226
|
+
const kdePoints = [];
|
|
2227
|
+
for (let i = 0; i < numPoints; i++) {
|
|
2228
|
+
kdePoints.push(min + i / (numPoints - 1) * (max - min));
|
|
2229
|
+
}
|
|
2230
|
+
this.kdeValues = kernelDensityEstimation(sorted, kdePoints, 0);
|
|
2231
|
+
this.kdePoints = kdePoints;
|
|
2232
|
+
}
|
|
2233
|
+
getDataRange() {
|
|
2234
|
+
if (!this.hasData) return null;
|
|
2235
|
+
if (this.kdePoints.length === 0) return null;
|
|
2236
|
+
const minY = this.kdePoints[0] ?? 0;
|
|
2237
|
+
const maxY = this.kdePoints[this.kdePoints.length - 1] ?? 0;
|
|
2238
|
+
return {
|
|
2239
|
+
xmin: this.position - this.violinWidth / 2,
|
|
2240
|
+
xmax: this.position + this.violinWidth / 2,
|
|
2241
|
+
ymin: minY,
|
|
2242
|
+
ymax: maxY
|
|
2243
|
+
};
|
|
2244
|
+
}
|
|
2245
|
+
drawSVG(ctx) {
|
|
2246
|
+
if (!this.hasData) return;
|
|
2247
|
+
const x = this.position;
|
|
2248
|
+
let maxKDE = 0;
|
|
2249
|
+
for (let i = 0; i < this.kdeValues.length; i++) {
|
|
2250
|
+
const v = this.kdeValues[i] ?? 0;
|
|
2251
|
+
if (v > maxKDE) maxKDE = v;
|
|
2252
|
+
}
|
|
2253
|
+
if (maxKDE === 0) return;
|
|
2254
|
+
const pathPoints = [];
|
|
2255
|
+
for (let i = 0; i < this.kdePoints.length; i++) {
|
|
2256
|
+
const y = this.kdePoints[i] ?? 0;
|
|
2257
|
+
const kde = this.kdeValues[i] ?? 0;
|
|
2258
|
+
const width = kde / maxKDE * this.violinWidth;
|
|
2259
|
+
const xLeft2 = x - width / 2;
|
|
2260
|
+
const px = ctx.transform.xToPx(xLeft2);
|
|
2261
|
+
const py = ctx.transform.yToPx(y);
|
|
2262
|
+
pathPoints.push(`${px.toFixed(2)},${py.toFixed(2)}`);
|
|
2263
|
+
}
|
|
2264
|
+
for (let i = this.kdePoints.length - 1; i >= 0; i--) {
|
|
2265
|
+
const y = this.kdePoints[i] ?? 0;
|
|
2266
|
+
const kde = this.kdeValues[i] ?? 0;
|
|
2267
|
+
const width = kde / maxKDE * this.violinWidth;
|
|
2268
|
+
const xRight2 = x + width / 2;
|
|
2269
|
+
const px = ctx.transform.xToPx(xRight2);
|
|
2270
|
+
const py = ctx.transform.yToPx(y);
|
|
2271
|
+
pathPoints.push(`${px.toFixed(2)},${py.toFixed(2)}`);
|
|
2272
|
+
}
|
|
2273
|
+
if (pathPoints.length > 0) {
|
|
2274
|
+
ctx.push(
|
|
2275
|
+
`<path d="M ${pathPoints.join(" L ")} Z" fill="${escapeXml(this.color)}" stroke="${escapeXml(this.edgecolor)}" stroke-width="1" />`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
const yq1 = ctx.transform.yToPx(this.q1);
|
|
2279
|
+
const ymed = ctx.transform.yToPx(this.median);
|
|
2280
|
+
const yq3 = ctx.transform.yToPx(this.q3);
|
|
2281
|
+
const indicatorWidth = this.violinWidth * 0.8;
|
|
2282
|
+
const xLeft = ctx.transform.xToPx(x - indicatorWidth / 2);
|
|
2283
|
+
const xRight = ctx.transform.xToPx(x + indicatorWidth / 2);
|
|
2284
|
+
ctx.push(
|
|
2285
|
+
`<line x1="${xLeft.toFixed(2)}" y1="${yq1.toFixed(2)}" x2="${xRight.toFixed(2)}" y2="${yq1.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" stroke-width="2" />`
|
|
2286
|
+
);
|
|
2287
|
+
ctx.push(
|
|
2288
|
+
`<line x1="${xLeft.toFixed(2)}" y1="${ymed.toFixed(2)}" x2="${xRight.toFixed(2)}" y2="${ymed.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" stroke-width="3" />`
|
|
2289
|
+
);
|
|
2290
|
+
ctx.push(
|
|
2291
|
+
`<line x1="${xLeft.toFixed(2)}" y1="${yq3.toFixed(2)}" x2="${xRight.toFixed(2)}" y2="${yq3.toFixed(2)}" stroke="${escapeXml(this.edgecolor)}" stroke-width="2" />`
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
drawRaster(ctx) {
|
|
2295
|
+
if (!this.hasData) return;
|
|
2296
|
+
const rgba = parseHexColorToRGBA(this.color);
|
|
2297
|
+
const edge = parseHexColorToRGBA(this.edgecolor);
|
|
2298
|
+
const x = this.position;
|
|
2299
|
+
let maxKDE = 0;
|
|
2300
|
+
for (let i = 0; i < this.kdeValues.length; i++) {
|
|
2301
|
+
const v = this.kdeValues[i] ?? 0;
|
|
2302
|
+
if (v > maxKDE) maxKDE = v;
|
|
2303
|
+
}
|
|
2304
|
+
if (maxKDE === 0) return;
|
|
2305
|
+
for (let i = 0; i < this.kdePoints.length - 1; i++) {
|
|
2306
|
+
const y1 = this.kdePoints[i] ?? 0;
|
|
2307
|
+
const y2 = this.kdePoints[i + 1] ?? 0;
|
|
2308
|
+
const kde1 = this.kdeValues[i] ?? 0;
|
|
2309
|
+
const kde2 = this.kdeValues[i + 1] ?? 0;
|
|
2310
|
+
const width1 = kde1 / maxKDE * this.violinWidth;
|
|
2311
|
+
const width2 = kde2 / maxKDE * this.violinWidth;
|
|
2312
|
+
const xLeft1 = x - width1 / 2;
|
|
2313
|
+
const xRight1 = x + width1 / 2;
|
|
2314
|
+
const xLeft2 = x - width2 / 2;
|
|
2315
|
+
const xRight2 = x + width2 / 2;
|
|
2316
|
+
const pxLeft1 = Math.round(ctx.transform.xToPx(xLeft1));
|
|
2317
|
+
const pxRight1 = Math.round(ctx.transform.xToPx(xRight1));
|
|
2318
|
+
const pxLeft2 = Math.round(ctx.transform.xToPx(xLeft2));
|
|
2319
|
+
const pxRight2 = Math.round(ctx.transform.xToPx(xRight2));
|
|
2320
|
+
const py1 = Math.round(ctx.transform.yToPx(y1));
|
|
2321
|
+
const py2 = Math.round(ctx.transform.yToPx(y2));
|
|
2322
|
+
ctx.canvas.drawLineRGBA(pxLeft1, py1, pxRight1, py1, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2323
|
+
ctx.canvas.drawLineRGBA(pxRight1, py1, pxRight2, py2, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2324
|
+
ctx.canvas.drawLineRGBA(pxRight2, py2, pxLeft2, py2, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2325
|
+
ctx.canvas.drawLineRGBA(pxLeft2, py2, pxLeft1, py1, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2326
|
+
const minY = Math.min(py1, py2);
|
|
2327
|
+
const maxY = Math.max(py1, py2);
|
|
2328
|
+
if (py1 === py2) {
|
|
2329
|
+
const leftX = Math.min(pxLeft1, pxLeft2);
|
|
2330
|
+
const rightX = Math.max(pxRight1, pxRight2);
|
|
2331
|
+
ctx.canvas.drawLineRGBA(leftX, py1, rightX, py1, rgba.r, rgba.g, rgba.b, rgba.a);
|
|
2332
|
+
} else {
|
|
2333
|
+
for (let y = minY; y <= maxY; y++) {
|
|
2334
|
+
const t = (y - py1) / (py2 - py1);
|
|
2335
|
+
const leftX = pxLeft1 + t * (pxLeft2 - pxLeft1);
|
|
2336
|
+
const rightX = pxRight1 + t * (pxRight2 - pxRight1);
|
|
2337
|
+
ctx.canvas.drawLineRGBA(
|
|
2338
|
+
Math.round(leftX),
|
|
2339
|
+
y,
|
|
2340
|
+
Math.round(rightX),
|
|
2341
|
+
y,
|
|
2342
|
+
rgba.r,
|
|
2343
|
+
rgba.g,
|
|
2344
|
+
rgba.b,
|
|
2345
|
+
rgba.a
|
|
2346
|
+
);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const yq1 = Math.round(ctx.transform.yToPx(this.q1));
|
|
2351
|
+
const ymed = Math.round(ctx.transform.yToPx(this.median));
|
|
2352
|
+
const yq3 = Math.round(ctx.transform.yToPx(this.q3));
|
|
2353
|
+
const indicatorWidth = this.violinWidth * 0.8;
|
|
2354
|
+
const xLeft = Math.round(ctx.transform.xToPx(x - indicatorWidth / 2));
|
|
2355
|
+
const xRight = Math.round(ctx.transform.xToPx(x + indicatorWidth / 2));
|
|
2356
|
+
ctx.canvas.drawLineRGBA(xLeft, yq1, xRight, yq1, edge.r, edge.g, edge.b, edge.a);
|
|
2357
|
+
ctx.canvas.drawLineRGBA(xLeft, ymed, xRight, ymed, edge.r, edge.g, edge.b, edge.a);
|
|
2358
|
+
ctx.canvas.drawLineRGBA(xLeft, yq3, xRight, yq3, edge.r, edge.g, edge.b, edge.a);
|
|
2359
|
+
}
|
|
2360
|
+
getLegendEntries() {
|
|
2361
|
+
const entry = buildLegendEntry(this.label, {
|
|
2362
|
+
color: this.color,
|
|
2363
|
+
shape: "box"
|
|
2364
|
+
});
|
|
2365
|
+
return entry ? [entry] : null;
|
|
2366
|
+
}
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
// src/plot/utils/tensor.ts
|
|
2370
|
+
function tensorToFloat64Vector1D(t) {
|
|
2371
|
+
const tensor2 = "tensor" in t ? t.tensor : t;
|
|
2372
|
+
if (tensor2.ndim !== 1) throw new chunkJSCDE774_cjs.ShapeError("Expected a 1D tensor");
|
|
2373
|
+
if (tensor2.dtype === "string") throw new chunkJSCDE774_cjs.DTypeError("Plotting does not support string tensors");
|
|
2374
|
+
const n = tensor2.shape[0] ?? 0;
|
|
2375
|
+
const stride = tensor2.strides[0] ?? 0;
|
|
2376
|
+
const out = new Float64Array(n);
|
|
2377
|
+
const base = tensor2.offset;
|
|
2378
|
+
for (let i = 0; i < n; i++) {
|
|
2379
|
+
const v = tensor2.data[base + i * stride];
|
|
2380
|
+
out[i] = safeConvertToNumber(v);
|
|
2381
|
+
}
|
|
2382
|
+
return out;
|
|
2383
|
+
}
|
|
2384
|
+
function tensorToFloat64Matrix2D(t) {
|
|
2385
|
+
const tensor2 = "tensor" in t ? t.tensor : t;
|
|
2386
|
+
if (tensor2.ndim !== 2) throw new chunkJSCDE774_cjs.ShapeError("Expected a 2D tensor");
|
|
2387
|
+
if (tensor2.dtype === "string") throw new chunkJSCDE774_cjs.DTypeError("Plotting does not support string tensors");
|
|
2388
|
+
const rows = tensor2.shape[0] ?? 0;
|
|
2389
|
+
const cols = tensor2.shape[1] ?? 0;
|
|
2390
|
+
const strideRow = tensor2.strides[0] ?? 0;
|
|
2391
|
+
const strideCol = tensor2.strides[1] ?? 0;
|
|
2392
|
+
const out = new Float64Array(rows * cols);
|
|
2393
|
+
if (strideCol === 1 && strideRow === cols) {
|
|
2394
|
+
const start = tensor2.offset;
|
|
2395
|
+
const end = start + rows * cols;
|
|
2396
|
+
for (let i = 0, j = start; j < end; i++, j++) {
|
|
2397
|
+
const v = tensor2.data[j];
|
|
2398
|
+
out[i] = safeConvertToNumber(v);
|
|
2399
|
+
}
|
|
2400
|
+
return { rows, cols, data: out };
|
|
2401
|
+
}
|
|
2402
|
+
const base = tensor2.offset;
|
|
2403
|
+
for (let i = 0; i < rows; i++) {
|
|
2404
|
+
const rowBase = base + i * strideRow;
|
|
2405
|
+
for (let j = 0; j < cols; j++) {
|
|
2406
|
+
const v = tensor2.data[rowBase + j * strideCol];
|
|
2407
|
+
out[i * cols + j] = safeConvertToNumber(v);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
return { rows, cols, data: out };
|
|
2411
|
+
}
|
|
2412
|
+
function safeConvertToNumber(value) {
|
|
2413
|
+
if (typeof value === "number") {
|
|
2414
|
+
return value;
|
|
2415
|
+
}
|
|
2416
|
+
if (typeof value === "bigint") {
|
|
2417
|
+
return Number(value);
|
|
2418
|
+
}
|
|
2419
|
+
throw new chunkJSCDE774_cjs.DTypeError(`Cannot convert ${typeof value} to number for plotting`);
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
// src/plot/utils/contours.ts
|
|
2423
|
+
function tensorSize(t) {
|
|
2424
|
+
const tensor2 = "tensor" in t ? t.tensor : t;
|
|
2425
|
+
if (tensor2.ndim === 0) return 1;
|
|
2426
|
+
let size = 1;
|
|
2427
|
+
for (const dim of tensor2.shape) {
|
|
2428
|
+
const d = dim ?? 0;
|
|
2429
|
+
if (d === 0) return 0;
|
|
2430
|
+
size *= d;
|
|
2431
|
+
}
|
|
2432
|
+
return size;
|
|
2433
|
+
}
|
|
2434
|
+
function assertFiniteCoords(values, name) {
|
|
2435
|
+
for (let i = 0; i < values.length; i++) {
|
|
2436
|
+
const v = values[i] ?? 0;
|
|
2437
|
+
if (!isFiniteNumber(v)) {
|
|
2438
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(`${name} coordinates must be finite`, name, v);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
function computeDataRange(data) {
|
|
2443
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2444
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2445
|
+
let hasNaN = false;
|
|
2446
|
+
for (let i = 0; i < data.length; i++) {
|
|
2447
|
+
const v = data[i] ?? 0;
|
|
2448
|
+
if (!isFiniteNumber(v)) {
|
|
2449
|
+
if (Number.isNaN(v)) {
|
|
2450
|
+
hasNaN = true;
|
|
2451
|
+
}
|
|
2452
|
+
continue;
|
|
2453
|
+
}
|
|
2454
|
+
if (v < min) min = v;
|
|
2455
|
+
if (v > max) max = v;
|
|
2456
|
+
}
|
|
2457
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) {
|
|
2458
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("Contour requires at least one finite Z value", "Z", data);
|
|
2459
|
+
}
|
|
2460
|
+
return { min, max, hasNaN };
|
|
2461
|
+
}
|
|
2462
|
+
function meshgridToCoords(X, Y, rows, cols) {
|
|
2463
|
+
const xMat = tensorToFloat64Matrix2D(X);
|
|
2464
|
+
const yMat = tensorToFloat64Matrix2D(Y);
|
|
2465
|
+
if (xMat.rows !== rows || xMat.cols !== cols) {
|
|
2466
|
+
throw new chunkJSCDE774_cjs.ShapeError("X must match Z shape for meshgrid input");
|
|
2467
|
+
}
|
|
2468
|
+
if (yMat.rows !== rows || yMat.cols !== cols) {
|
|
2469
|
+
throw new chunkJSCDE774_cjs.ShapeError("Y must match Z shape for meshgrid input");
|
|
2470
|
+
}
|
|
2471
|
+
const xCoords = new Float64Array(cols);
|
|
2472
|
+
const yCoords = new Float64Array(rows);
|
|
2473
|
+
for (let j = 0; j < cols; j++) {
|
|
2474
|
+
xCoords[j] = xMat.data[j] ?? 0;
|
|
2475
|
+
}
|
|
2476
|
+
for (let i = 0; i < rows; i++) {
|
|
2477
|
+
yCoords[i] = xMat.cols > 0 ? yMat.data[i * cols] ?? 0 : 0;
|
|
2478
|
+
}
|
|
2479
|
+
const tol = 1e-12;
|
|
2480
|
+
for (let i = 0; i < rows; i++) {
|
|
2481
|
+
const rowOffset = i * cols;
|
|
2482
|
+
for (let j = 0; j < cols; j++) {
|
|
2483
|
+
const expectedX = xCoords[j] ?? 0;
|
|
2484
|
+
const expectedY = yCoords[i] ?? 0;
|
|
2485
|
+
const xv = xMat.data[rowOffset + j] ?? 0;
|
|
2486
|
+
const yv = yMat.data[rowOffset + j] ?? 0;
|
|
2487
|
+
if (!isFiniteNumber(xv) || !isFiniteNumber(yv)) {
|
|
2488
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("X/Y meshgrid values must be finite", "X/Y", {
|
|
2489
|
+
x: xv,
|
|
2490
|
+
y: yv
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
if (Math.abs(xv - expectedX) > tol || Math.abs(yv - expectedY) > tol) {
|
|
2494
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2495
|
+
"Contour supports only rectilinear grids (use 1D X/Y or meshgrid)",
|
|
2496
|
+
"X/Y",
|
|
2497
|
+
{ x: xv, y: yv }
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
assertFiniteCoords(xCoords, "X");
|
|
2503
|
+
assertFiniteCoords(yCoords, "Y");
|
|
2504
|
+
return { xCoords, yCoords };
|
|
2505
|
+
}
|
|
2506
|
+
function buildContourGrid(X, Y, Z, extent) {
|
|
2507
|
+
const zMat = tensorToFloat64Matrix2D(Z);
|
|
2508
|
+
const { rows, cols, data } = zMat;
|
|
2509
|
+
const { min, max, hasNaN } = computeDataRange(data);
|
|
2510
|
+
const xSize = tensorSize(X);
|
|
2511
|
+
const ySize = tensorSize(Y);
|
|
2512
|
+
let xCoords;
|
|
2513
|
+
let yCoords;
|
|
2514
|
+
if (xSize === 0 && ySize === 0) {
|
|
2515
|
+
xCoords = new Float64Array(cols);
|
|
2516
|
+
yCoords = new Float64Array(rows);
|
|
2517
|
+
if (extent) {
|
|
2518
|
+
const { xmin, xmax, ymin, ymax } = extent;
|
|
2519
|
+
if (!Number.isFinite(xmin) || !Number.isFinite(xmax) || !Number.isFinite(ymin) || !Number.isFinite(ymax)) {
|
|
2520
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("extent values must be finite", "extent", extent);
|
|
2521
|
+
}
|
|
2522
|
+
if (xmax <= xmin || ymax <= ymin) {
|
|
2523
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("extent ranges must be positive", "extent", extent);
|
|
2524
|
+
}
|
|
2525
|
+
const xDenom = Math.max(1, cols - 1);
|
|
2526
|
+
const yDenom = Math.max(1, rows - 1);
|
|
2527
|
+
for (let j = 0; j < cols; j++) {
|
|
2528
|
+
xCoords[j] = xmin + (xmax - xmin) * j / xDenom;
|
|
2529
|
+
}
|
|
2530
|
+
for (let i = 0; i < rows; i++) {
|
|
2531
|
+
yCoords[i] = ymin + (ymax - ymin) * i / yDenom;
|
|
2532
|
+
}
|
|
2533
|
+
} else {
|
|
2534
|
+
for (let j = 0; j < cols; j++) xCoords[j] = j;
|
|
2535
|
+
for (let i = 0; i < rows; i++) yCoords[i] = i;
|
|
2536
|
+
}
|
|
2537
|
+
} else if (xSize > 0 && ySize > 0) {
|
|
2538
|
+
if (extent) {
|
|
2539
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2540
|
+
"extent cannot be combined with explicit X/Y coordinates",
|
|
2541
|
+
"extent",
|
|
2542
|
+
extent
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
const rawX = "tensor" in X ? X.tensor : X;
|
|
2546
|
+
const rawY = "tensor" in Y ? Y.tensor : Y;
|
|
2547
|
+
if (rawX.ndim === 1 && rawY.ndim === 1) {
|
|
2548
|
+
xCoords = tensorToFloat64Vector1D(X);
|
|
2549
|
+
yCoords = tensorToFloat64Vector1D(Y);
|
|
2550
|
+
if (xCoords.length !== cols) {
|
|
2551
|
+
throw new chunkJSCDE774_cjs.ShapeError(`X must have length ${cols}; received ${xCoords.length}`);
|
|
2552
|
+
}
|
|
2553
|
+
if (yCoords.length !== rows) {
|
|
2554
|
+
throw new chunkJSCDE774_cjs.ShapeError(`Y must have length ${rows}; received ${yCoords.length}`);
|
|
2555
|
+
}
|
|
2556
|
+
assertFiniteCoords(xCoords, "X");
|
|
2557
|
+
assertFiniteCoords(yCoords, "Y");
|
|
2558
|
+
} else if (rawX.ndim === 2 && rawY.ndim === 2) {
|
|
2559
|
+
({ xCoords, yCoords } = meshgridToCoords(X, Y, rows, cols));
|
|
2560
|
+
} else {
|
|
2561
|
+
throw new chunkJSCDE774_cjs.ShapeError("X and Y must be 1D vectors or 2D meshgrids matching Z");
|
|
2562
|
+
}
|
|
2563
|
+
} else {
|
|
2564
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("Both X and Y must be provided or both empty", "X/Y", {
|
|
2565
|
+
xSize,
|
|
2566
|
+
ySize
|
|
2567
|
+
});
|
|
2568
|
+
}
|
|
2569
|
+
return {
|
|
2570
|
+
rows,
|
|
2571
|
+
cols,
|
|
2572
|
+
data,
|
|
2573
|
+
xCoords,
|
|
2574
|
+
yCoords,
|
|
2575
|
+
dataMin: min,
|
|
2576
|
+
dataMax: max + (hasNaN ? Number.EPSILON : 0)
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// src/plot/utils/text.ts
|
|
2581
|
+
function estimateTextWidth(text, fontSize) {
|
|
2582
|
+
if (text.length === 0) return 0;
|
|
2583
|
+
return text.length * fontSize * 0.6;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// src/plot/utils/ticks.ts
|
|
2587
|
+
function niceNumber(range, round) {
|
|
2588
|
+
if (!Number.isFinite(range) || range <= 0) return 1;
|
|
2589
|
+
const exponent = Math.floor(Math.log10(range));
|
|
2590
|
+
const fraction = range / 10 ** exponent;
|
|
2591
|
+
let niceFraction;
|
|
2592
|
+
if (round) {
|
|
2593
|
+
if (fraction < 1.5) niceFraction = 1;
|
|
2594
|
+
else if (fraction < 3) niceFraction = 2;
|
|
2595
|
+
else if (fraction < 4.5) niceFraction = 2.5;
|
|
2596
|
+
else if (fraction < 7) niceFraction = 5;
|
|
2597
|
+
else niceFraction = 10;
|
|
2598
|
+
} else {
|
|
2599
|
+
if (fraction <= 1) niceFraction = 1;
|
|
2600
|
+
else if (fraction <= 2) niceFraction = 2;
|
|
2601
|
+
else if (fraction <= 2.5) niceFraction = 2.5;
|
|
2602
|
+
else if (fraction <= 5) niceFraction = 5;
|
|
2603
|
+
else niceFraction = 10;
|
|
2604
|
+
}
|
|
2605
|
+
return niceFraction * 10 ** exponent;
|
|
2606
|
+
}
|
|
2607
|
+
function formatTick(value, step) {
|
|
2608
|
+
if (!Number.isFinite(value)) return "";
|
|
2609
|
+
const abs = Math.abs(value);
|
|
2610
|
+
if (abs < 1e-12) return "0";
|
|
2611
|
+
if (abs > 0 && abs < 1e-4 || abs >= 1e6) {
|
|
2612
|
+
return value.toExponential(2);
|
|
2613
|
+
}
|
|
2614
|
+
const absStep = Math.abs(step);
|
|
2615
|
+
let decimals = 0;
|
|
2616
|
+
if (absStep > 0 && absStep < 1) {
|
|
2617
|
+
decimals = Math.min(6, Math.ceil(-Math.log10(absStep)));
|
|
2618
|
+
}
|
|
2619
|
+
let text = value.toFixed(decimals);
|
|
2620
|
+
if (decimals > 0) {
|
|
2621
|
+
text = text.replace(/\.?0+$/, "");
|
|
2622
|
+
}
|
|
2623
|
+
return text;
|
|
2624
|
+
}
|
|
2625
|
+
function generateTicks(min, max, maxTicks = 5) {
|
|
2626
|
+
if (!Number.isFinite(min) || !Number.isFinite(max)) return [];
|
|
2627
|
+
if (maxTicks <= 0 || !Number.isFinite(maxTicks)) return [];
|
|
2628
|
+
const m = Math.min(min, max);
|
|
2629
|
+
const M = Math.max(min, max);
|
|
2630
|
+
if (m === M) {
|
|
2631
|
+
const span = Math.max(1, Math.abs(m) * 0.05);
|
|
2632
|
+
return generateTicks(m - span, M + span, maxTicks);
|
|
2633
|
+
}
|
|
2634
|
+
const range = niceNumber(M - m, false);
|
|
2635
|
+
const step = niceNumber(range / Math.max(1, maxTicks - 1), true);
|
|
2636
|
+
if (!Number.isFinite(step) || step <= 0) return [];
|
|
2637
|
+
const niceMin = Math.floor(m / step) * step;
|
|
2638
|
+
const niceMax = Math.ceil(M / step) * step;
|
|
2639
|
+
const ticks = [];
|
|
2640
|
+
const epsilon = step * 1e-9;
|
|
2641
|
+
for (let v = niceMin; v <= niceMax + epsilon; v += step) {
|
|
2642
|
+
if (v + epsilon < m || v - epsilon > M) continue;
|
|
2643
|
+
ticks.push({ value: v, label: formatTick(v, step) });
|
|
2644
|
+
}
|
|
2645
|
+
return ticks;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// src/plot/utils/transforms.ts
|
|
2649
|
+
function computeAutoRange(drawables) {
|
|
2650
|
+
let xmin = Number.POSITIVE_INFINITY;
|
|
2651
|
+
let xmax = Number.NEGATIVE_INFINITY;
|
|
2652
|
+
let ymin = Number.POSITIVE_INFINITY;
|
|
2653
|
+
let ymax = Number.NEGATIVE_INFINITY;
|
|
2654
|
+
for (const d of drawables) {
|
|
2655
|
+
const r = d.getDataRange();
|
|
2656
|
+
if (!r) continue;
|
|
2657
|
+
xmin = Math.min(xmin, r.xmin);
|
|
2658
|
+
xmax = Math.max(xmax, r.xmax);
|
|
2659
|
+
ymin = Math.min(ymin, r.ymin);
|
|
2660
|
+
ymax = Math.max(ymax, r.ymax);
|
|
2661
|
+
}
|
|
2662
|
+
if (!Number.isFinite(xmin) || !Number.isFinite(xmax)) {
|
|
2663
|
+
xmin = 0;
|
|
2664
|
+
xmax = 1;
|
|
2665
|
+
} else if (xmin === xmax) {
|
|
2666
|
+
const span = Math.max(1, Math.abs(xmin) * 0.05);
|
|
2667
|
+
xmin -= span;
|
|
2668
|
+
xmax += span;
|
|
2669
|
+
}
|
|
2670
|
+
if (!Number.isFinite(ymin) || !Number.isFinite(ymax)) {
|
|
2671
|
+
ymin = 0;
|
|
2672
|
+
ymax = 1;
|
|
2673
|
+
} else if (ymin === ymax) {
|
|
2674
|
+
const span = Math.max(1, Math.abs(ymin) * 0.05);
|
|
2675
|
+
ymin -= span;
|
|
2676
|
+
ymax += span;
|
|
2677
|
+
}
|
|
2678
|
+
const xPad = (xmax - xmin) * 0.05;
|
|
2679
|
+
const yPad = (ymax - ymin) * 0.05;
|
|
2680
|
+
return {
|
|
2681
|
+
xmin: xmin - xPad,
|
|
2682
|
+
xmax: xmax + xPad,
|
|
2683
|
+
ymin: ymin - yPad,
|
|
2684
|
+
ymax: ymax + yPad
|
|
2685
|
+
};
|
|
2686
|
+
}
|
|
2687
|
+
function makeTransform(range, viewport) {
|
|
2688
|
+
const dx = range.xmax - range.xmin;
|
|
2689
|
+
const dy = range.ymax - range.ymin;
|
|
2690
|
+
const sx = dx !== 0 ? viewport.width / dx : 0;
|
|
2691
|
+
const sy = dy !== 0 ? viewport.height / dy : 0;
|
|
2692
|
+
return {
|
|
2693
|
+
xToPx: (x) => viewport.x + (x - range.xmin) * sx,
|
|
2694
|
+
yToPx: (y) => viewport.y + viewport.height - (y - range.ymin) * sy
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// src/plot/figure/Axes.ts
|
|
2699
|
+
var Axes = class {
|
|
2700
|
+
fig;
|
|
2701
|
+
padding;
|
|
2702
|
+
paddingProvided;
|
|
2703
|
+
facecolor;
|
|
2704
|
+
drawables;
|
|
2705
|
+
title;
|
|
2706
|
+
xlabel;
|
|
2707
|
+
ylabel;
|
|
2708
|
+
legendOptions;
|
|
2709
|
+
xTicksOverride;
|
|
2710
|
+
yTicksOverride;
|
|
2711
|
+
baseViewport;
|
|
2712
|
+
constructor(fig, options) {
|
|
2713
|
+
this.fig = fig;
|
|
2714
|
+
this.paddingProvided = options.padding !== void 0;
|
|
2715
|
+
const base = options.viewport ?? {
|
|
2716
|
+
x: 0,
|
|
2717
|
+
y: 0,
|
|
2718
|
+
width: this.fig.width,
|
|
2719
|
+
height: this.fig.height
|
|
2720
|
+
};
|
|
2721
|
+
if (!Number.isFinite(base.x) || !Number.isFinite(base.y) || !Number.isFinite(base.width) || !Number.isFinite(base.height) || base.width <= 0 || base.height <= 0) {
|
|
2722
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2723
|
+
"viewport must have positive finite width/height",
|
|
2724
|
+
"viewport",
|
|
2725
|
+
base
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
const p = options.padding ?? Math.min(50, Math.max(0, Math.floor(Math.min(base.width, base.height) / 4)));
|
|
2729
|
+
if (!Number.isFinite(p) || p < 0) {
|
|
2730
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(`padding must be non-negative; received ${p}`, "padding", p);
|
|
2731
|
+
}
|
|
2732
|
+
if (2 * p >= base.width || 2 * p >= base.height) {
|
|
2733
|
+
if (this.paddingProvided) {
|
|
2734
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("padding is too large", "padding", p);
|
|
2735
|
+
}
|
|
2736
|
+
const maxSafe = Math.max(0, Math.floor((Math.min(base.width, base.height) - 1) / 2));
|
|
2737
|
+
this.padding = maxSafe;
|
|
2738
|
+
} else {
|
|
2739
|
+
this.padding = p;
|
|
2740
|
+
}
|
|
2741
|
+
this.facecolor = normalizeColor(options.facecolor, "#ffffff");
|
|
2742
|
+
this.baseViewport = options.viewport;
|
|
2743
|
+
this.drawables = [];
|
|
2744
|
+
this.title = "";
|
|
2745
|
+
this.xlabel = "";
|
|
2746
|
+
this.ylabel = "";
|
|
2747
|
+
this.legendOptions = null;
|
|
2748
|
+
this.xTicksOverride = null;
|
|
2749
|
+
this.yTicksOverride = null;
|
|
2750
|
+
}
|
|
2751
|
+
/** Set the axes title text. */
|
|
2752
|
+
setTitle(title) {
|
|
2753
|
+
this.title = title;
|
|
2754
|
+
}
|
|
2755
|
+
/** Set the x-axis label text. */
|
|
2756
|
+
setXLabel(label) {
|
|
2757
|
+
this.xlabel = label;
|
|
2758
|
+
}
|
|
2759
|
+
/** Set the y-axis label text. */
|
|
2760
|
+
setYLabel(label) {
|
|
2761
|
+
this.ylabel = label;
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Set custom x-axis tick positions and labels.
|
|
2765
|
+
* @param values - Tick positions in data coordinates
|
|
2766
|
+
* @param labels - Optional tick labels
|
|
2767
|
+
*/
|
|
2768
|
+
setXTicks(values, labels) {
|
|
2769
|
+
this.xTicksOverride = this.buildTickOverride(values, labels, "xTicks");
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Set custom y-axis tick positions and labels.
|
|
2773
|
+
* @param values - Tick positions in data coordinates
|
|
2774
|
+
* @param labels - Optional tick labels
|
|
2775
|
+
*/
|
|
2776
|
+
setYTicks(values, labels) {
|
|
2777
|
+
this.yTicksOverride = this.buildTickOverride(values, labels, "yTicks");
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Show or configure the legend for this axes.
|
|
2781
|
+
* @param options - Legend display options
|
|
2782
|
+
*/
|
|
2783
|
+
legend(options = {}) {
|
|
2784
|
+
if (options.location !== void 0 && !["upper-right", "upper-left", "lower-right", "lower-left"].includes(options.location)) {
|
|
2785
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2786
|
+
`legend location must be one of upper-right, upper-left, lower-right, lower-left; received ${options.location}`,
|
|
2787
|
+
"location",
|
|
2788
|
+
options.location
|
|
2789
|
+
);
|
|
2790
|
+
}
|
|
2791
|
+
if (options.fontSize !== void 0) {
|
|
2792
|
+
if (!Number.isFinite(options.fontSize) || options.fontSize <= 0) {
|
|
2793
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2794
|
+
`legend fontSize must be positive; received ${options.fontSize}`,
|
|
2795
|
+
"fontSize",
|
|
2796
|
+
options.fontSize
|
|
2797
|
+
);
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
if (options.padding !== void 0) {
|
|
2801
|
+
if (!Number.isFinite(options.padding) || options.padding < 0) {
|
|
2802
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
2803
|
+
`legend padding must be non-negative; received ${options.padding}`,
|
|
2804
|
+
"padding",
|
|
2805
|
+
options.padding
|
|
2806
|
+
);
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
this.legendOptions = options;
|
|
2810
|
+
}
|
|
2811
|
+
/**
|
|
2812
|
+
* Plot a connected line series.
|
|
2813
|
+
* @param x - 1D tensor of x coordinates
|
|
2814
|
+
* @param y - 1D tensor of y coordinates
|
|
2815
|
+
* @param options - Styling options
|
|
2816
|
+
*/
|
|
2817
|
+
plot(x, y, options = {}) {
|
|
2818
|
+
const xv = tensorToFloat64Vector1D(x);
|
|
2819
|
+
const yv = tensorToFloat64Vector1D(y);
|
|
2820
|
+
const d = new Line2D(xv, yv, options);
|
|
2821
|
+
this.drawables.push(d);
|
|
2822
|
+
return d;
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Plot unconnected points.
|
|
2826
|
+
* @param x - 1D tensor of x coordinates
|
|
2827
|
+
* @param y - 1D tensor of y coordinates
|
|
2828
|
+
* @param options - Styling options
|
|
2829
|
+
*/
|
|
2830
|
+
scatter(x, y, options = {}) {
|
|
2831
|
+
const xv = tensorToFloat64Vector1D(x);
|
|
2832
|
+
const yv = tensorToFloat64Vector1D(y);
|
|
2833
|
+
const d = new Scatter2D(xv, yv, options);
|
|
2834
|
+
this.drawables.push(d);
|
|
2835
|
+
return d;
|
|
2836
|
+
}
|
|
2837
|
+
/**
|
|
2838
|
+
* Plot vertical bars.
|
|
2839
|
+
* @param x - 1D tensor of bar centers
|
|
2840
|
+
* @param height - 1D tensor of bar heights
|
|
2841
|
+
* @param options - Styling options
|
|
2842
|
+
*/
|
|
2843
|
+
bar(x, height, options = {}) {
|
|
2844
|
+
const xv = tensorToFloat64Vector1D(x);
|
|
2845
|
+
const hv = tensorToFloat64Vector1D(height);
|
|
2846
|
+
const d = new Bar2D(xv, hv, options);
|
|
2847
|
+
this.drawables.push(d);
|
|
2848
|
+
return d;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* Plot a histogram.
|
|
2852
|
+
* @param x - 1D tensor of sample values
|
|
2853
|
+
* @param bins - Number of bins
|
|
2854
|
+
* @param options - Styling options
|
|
2855
|
+
*/
|
|
2856
|
+
hist(x, bins = 10, options = {}) {
|
|
2857
|
+
const xv = tensorToFloat64Vector1D(x);
|
|
2858
|
+
const resolvedBins = options.bins ?? bins;
|
|
2859
|
+
const d = new Histogram(xv, resolvedBins, options);
|
|
2860
|
+
this.drawables.push(d);
|
|
2861
|
+
return d;
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Plot horizontal bars.
|
|
2865
|
+
* @param y - 1D tensor of bar centers
|
|
2866
|
+
* @param width - 1D tensor of bar widths
|
|
2867
|
+
* @param options - Styling options
|
|
2868
|
+
*/
|
|
2869
|
+
barh(y, width, options = {}) {
|
|
2870
|
+
const yv = tensorToFloat64Vector1D(y);
|
|
2871
|
+
const wv = tensorToFloat64Vector1D(width);
|
|
2872
|
+
const d = new HorizontalBar2D(yv, wv, options);
|
|
2873
|
+
this.drawables.push(d);
|
|
2874
|
+
return d;
|
|
2875
|
+
}
|
|
2876
|
+
/**
|
|
2877
|
+
* Plot a heatmap for 2D data.
|
|
2878
|
+
* @param data - 2D tensor of values
|
|
2879
|
+
* @param options - Styling and scale options
|
|
2880
|
+
*/
|
|
2881
|
+
heatmap(data, options = {}) {
|
|
2882
|
+
const mat = tensorToFloat64Matrix2D(data);
|
|
2883
|
+
const d = new Heatmap2D(mat.data, mat.rows, mat.cols, options);
|
|
2884
|
+
this.drawables.push(d);
|
|
2885
|
+
return d;
|
|
2886
|
+
}
|
|
2887
|
+
/**
|
|
2888
|
+
* Display a matrix as an image (alias of heatmap).
|
|
2889
|
+
* @param data - 2D tensor of values
|
|
2890
|
+
* @param options - Styling and scale options
|
|
2891
|
+
*/
|
|
2892
|
+
imshow(data, options = {}) {
|
|
2893
|
+
const mat = tensorToFloat64Matrix2D(data);
|
|
2894
|
+
const d = new Heatmap2D(mat.data, mat.rows, mat.cols, options);
|
|
2895
|
+
this.drawables.push(d);
|
|
2896
|
+
return d;
|
|
2897
|
+
}
|
|
2898
|
+
/**
|
|
2899
|
+
* Plot contour lines for a 2D grid.
|
|
2900
|
+
* @param X - 1D/2D tensor of x coordinates (or empty)
|
|
2901
|
+
* @param Y - 1D/2D tensor of y coordinates (or empty)
|
|
2902
|
+
* @param Z - 2D tensor of values
|
|
2903
|
+
* @param options - Styling and level options
|
|
2904
|
+
*/
|
|
2905
|
+
contour(X, Y, Z, options = {}) {
|
|
2906
|
+
const grid = buildContourGrid(X, Y, Z, options.extent);
|
|
2907
|
+
const d = new Contour2D(grid, options);
|
|
2908
|
+
this.drawables.push(d);
|
|
2909
|
+
return d;
|
|
2910
|
+
}
|
|
2911
|
+
/**
|
|
2912
|
+
* Plot filled contours for a 2D grid.
|
|
2913
|
+
* @param X - 1D/2D tensor of x coordinates (or empty)
|
|
2914
|
+
* @param Y - 1D/2D tensor of y coordinates (or empty)
|
|
2915
|
+
* @param Z - 2D tensor of values
|
|
2916
|
+
* @param options - Styling and level options
|
|
2917
|
+
*/
|
|
2918
|
+
contourf(X, Y, Z, options = {}) {
|
|
2919
|
+
const grid = buildContourGrid(X, Y, Z, options.extent);
|
|
2920
|
+
const d = new ContourF2D(grid, options);
|
|
2921
|
+
this.drawables.push(d);
|
|
2922
|
+
return d;
|
|
2923
|
+
}
|
|
2924
|
+
/**
|
|
2925
|
+
* Plot a box-and-whisker summary for a 1D dataset.
|
|
2926
|
+
* @param data - 1D tensor of values
|
|
2927
|
+
* @param options - Styling options
|
|
2928
|
+
*/
|
|
2929
|
+
boxplot(data, options = {}) {
|
|
2930
|
+
const values = tensorToFloat64Vector1D(data);
|
|
2931
|
+
const d = new Boxplot(1, values, options);
|
|
2932
|
+
this.drawables.push(d);
|
|
2933
|
+
return d;
|
|
2934
|
+
}
|
|
2935
|
+
/**
|
|
2936
|
+
* Plot a violin distribution summary for a 1D dataset.
|
|
2937
|
+
* @param data - 1D tensor of values
|
|
2938
|
+
* @param options - Styling options
|
|
2939
|
+
*/
|
|
2940
|
+
violinplot(data, options = {}) {
|
|
2941
|
+
const values = tensorToFloat64Vector1D(data);
|
|
2942
|
+
const d = new Violinplot(1, values, options);
|
|
2943
|
+
this.drawables.push(d);
|
|
2944
|
+
return d;
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Plot a pie chart.
|
|
2948
|
+
* @param values - 1D tensor of non-negative values
|
|
2949
|
+
* @param labels - Optional labels (must match values length)
|
|
2950
|
+
* @param options - Styling options
|
|
2951
|
+
*/
|
|
2952
|
+
pie(values, labels, options = {}) {
|
|
2953
|
+
const data = tensorToFloat64Vector1D(values);
|
|
2954
|
+
const range = { xmin: 0, xmax: 1, ymin: 0, ymax: 1 };
|
|
2955
|
+
const d = new Pie(0.5, 0.5, 0.35, data, labels, options, range);
|
|
2956
|
+
this.drawables.push(d);
|
|
2957
|
+
return d;
|
|
2958
|
+
}
|
|
2959
|
+
viewport() {
|
|
2960
|
+
const p = this.padding;
|
|
2961
|
+
const base = this.baseViewport ?? {
|
|
2962
|
+
x: 0,
|
|
2963
|
+
y: 0,
|
|
2964
|
+
width: this.fig.width,
|
|
2965
|
+
height: this.fig.height
|
|
2966
|
+
};
|
|
2967
|
+
return {
|
|
2968
|
+
x: base.x + p,
|
|
2969
|
+
y: base.y + p,
|
|
2970
|
+
width: base.width - 2 * p,
|
|
2971
|
+
height: base.height - 2 * p
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
collectLegendEntries() {
|
|
2975
|
+
const entries = [];
|
|
2976
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2977
|
+
for (const drawable of this.drawables) {
|
|
2978
|
+
const list = drawable.getLegendEntries?.();
|
|
2979
|
+
if (!list) continue;
|
|
2980
|
+
for (const entry of list) {
|
|
2981
|
+
const trimmed = entry.label.trim();
|
|
2982
|
+
if (!trimmed) continue;
|
|
2983
|
+
if (seen.has(trimmed)) continue;
|
|
2984
|
+
seen.add(trimmed);
|
|
2985
|
+
entries.push({ ...entry, label: trimmed });
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return entries;
|
|
2989
|
+
}
|
|
2990
|
+
resolveLegendOptions() {
|
|
2991
|
+
const options = this.legendOptions ?? {};
|
|
2992
|
+
return {
|
|
2993
|
+
visible: options.visible ?? true,
|
|
2994
|
+
location: options.location ?? "upper-right",
|
|
2995
|
+
fontSize: options.fontSize ?? 12,
|
|
2996
|
+
padding: options.padding ?? 6,
|
|
2997
|
+
background: normalizeColor(options.background, "#ffffff"),
|
|
2998
|
+
borderColor: normalizeColor(options.borderColor, "#000000")
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
buildTickOverride(values, labels, name) {
|
|
3002
|
+
if (values.length === 0) return null;
|
|
3003
|
+
if (labels && labels.length !== values.length) {
|
|
3004
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3005
|
+
`${name} labels length must match values length (${values.length}); received ${labels.length}`,
|
|
3006
|
+
name,
|
|
3007
|
+
labels
|
|
3008
|
+
);
|
|
3009
|
+
}
|
|
3010
|
+
const ticks = [];
|
|
3011
|
+
for (const [i, value] of values.entries()) {
|
|
3012
|
+
if (!Number.isFinite(value)) {
|
|
3013
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(`${name} values must be finite`, name, value);
|
|
3014
|
+
}
|
|
3015
|
+
const label = labels ? labels[i] ?? "" : String(value);
|
|
3016
|
+
ticks.push({ value, label: label.trim() });
|
|
3017
|
+
}
|
|
3018
|
+
return ticks;
|
|
3019
|
+
}
|
|
3020
|
+
renderTicksSVG(elements, vp, range, transform) {
|
|
3021
|
+
const maxXTicks = Math.max(2, Math.floor(vp.width / 80));
|
|
3022
|
+
const maxYTicks = Math.max(2, Math.floor(vp.height / 60));
|
|
3023
|
+
const xTicks = (this.xTicksOverride ?? generateTicks(range.xmin, range.xmax, maxXTicks)).filter(
|
|
3024
|
+
(tick) => tick.value >= range.xmin && tick.value <= range.xmax
|
|
3025
|
+
);
|
|
3026
|
+
const yTicks = (this.yTicksOverride ?? generateTicks(range.ymin, range.ymax, maxYTicks)).filter(
|
|
3027
|
+
(tick) => tick.value >= range.ymin && tick.value <= range.ymax
|
|
3028
|
+
);
|
|
3029
|
+
const tickLength = 5;
|
|
3030
|
+
const labelOffset = 2;
|
|
3031
|
+
for (const tick of xTicks) {
|
|
3032
|
+
const px = transform.xToPx(tick.value);
|
|
3033
|
+
elements.push(
|
|
3034
|
+
`<line class="x-tick" x1="${px.toFixed(2)}" y1="${(vp.y + vp.height).toFixed(
|
|
3035
|
+
2
|
|
3036
|
+
)}" x2="${px.toFixed(2)}" y2="${(vp.y + vp.height + tickLength).toFixed(
|
|
3037
|
+
2
|
|
3038
|
+
)}" stroke="#000" />`
|
|
3039
|
+
);
|
|
3040
|
+
elements.push(
|
|
3041
|
+
`<text class="tick-label tick-label-x" x="${px.toFixed(2)}" y="${(vp.y + vp.height + tickLength + labelOffset).toFixed(
|
|
3042
|
+
2
|
|
3043
|
+
)}" text-anchor="middle" dominant-baseline="hanging" font-size="10" fill="#000">${escapeXml(
|
|
3044
|
+
tick.label
|
|
3045
|
+
)}</text>`
|
|
3046
|
+
);
|
|
3047
|
+
}
|
|
3048
|
+
for (const tick of yTicks) {
|
|
3049
|
+
const py = transform.yToPx(tick.value);
|
|
3050
|
+
elements.push(
|
|
3051
|
+
`<line class="y-tick" x1="${vp.x.toFixed(2)}" y1="${py.toFixed(2)}" x2="${(vp.x - tickLength).toFixed(2)}" y2="${py.toFixed(2)}" stroke="#000" />`
|
|
3052
|
+
);
|
|
3053
|
+
elements.push(
|
|
3054
|
+
`<text class="tick-label tick-label-y" x="${(vp.x - tickLength - labelOffset).toFixed(
|
|
3055
|
+
2
|
|
3056
|
+
)}" y="${py.toFixed(2)}" text-anchor="end" dominant-baseline="middle" font-size="10" fill="#000">${escapeXml(
|
|
3057
|
+
tick.label
|
|
3058
|
+
)}</text>`
|
|
3059
|
+
);
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
renderLegendSVG(elements, vp) {
|
|
3063
|
+
if (!this.legendOptions) return;
|
|
3064
|
+
const entries = this.collectLegendEntries();
|
|
3065
|
+
if (entries.length === 0) return;
|
|
3066
|
+
const options = this.resolveLegendOptions();
|
|
3067
|
+
if (!options.visible) return;
|
|
3068
|
+
const fontSize = options.fontSize;
|
|
3069
|
+
const padding = options.padding;
|
|
3070
|
+
const symbolSize = Math.max(8, Math.round(fontSize * 0.9));
|
|
3071
|
+
const gap = 6;
|
|
3072
|
+
let maxLabelWidth = 0;
|
|
3073
|
+
for (const entry of entries) {
|
|
3074
|
+
const width = estimateTextWidth(entry.label, fontSize);
|
|
3075
|
+
maxLabelWidth = Math.max(maxLabelWidth, width);
|
|
3076
|
+
}
|
|
3077
|
+
const lineHeight = fontSize + 4;
|
|
3078
|
+
const boxWidth = padding * 2 + symbolSize + gap + maxLabelWidth;
|
|
3079
|
+
const boxHeight = padding * 2 + entries.length * lineHeight;
|
|
3080
|
+
const margin = 6;
|
|
3081
|
+
const isRight = options.location.includes("right");
|
|
3082
|
+
const isUpper = options.location.includes("upper");
|
|
3083
|
+
const boxX = isRight ? vp.x + vp.width - boxWidth - margin : vp.x + margin;
|
|
3084
|
+
const boxY = isUpper ? vp.y + margin : vp.y + vp.height - boxHeight - margin;
|
|
3085
|
+
elements.push(`<g class="legend">`);
|
|
3086
|
+
elements.push(
|
|
3087
|
+
`<rect class="legend-box" x="${boxX.toFixed(2)}" y="${boxY.toFixed(
|
|
3088
|
+
2
|
|
3089
|
+
)}" width="${boxWidth.toFixed(2)}" height="${boxHeight.toFixed(
|
|
3090
|
+
2
|
|
3091
|
+
)}" fill="${escapeXml(options.background)}" stroke="${escapeXml(options.borderColor)}" />`
|
|
3092
|
+
);
|
|
3093
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3094
|
+
const entry = entries[i];
|
|
3095
|
+
if (!entry) continue;
|
|
3096
|
+
const rowY = boxY + padding + i * lineHeight + lineHeight / 2;
|
|
3097
|
+
const symbolX = boxX + padding;
|
|
3098
|
+
const symbolY = rowY - symbolSize / 2;
|
|
3099
|
+
if (entry.shape === "line") {
|
|
3100
|
+
const lineY = rowY;
|
|
3101
|
+
const lineWidth = entry.lineWidth ?? 2;
|
|
3102
|
+
elements.push(
|
|
3103
|
+
`<line class="legend-line" x1="${symbolX.toFixed(2)}" y1="${lineY.toFixed(
|
|
3104
|
+
2
|
|
3105
|
+
)}" x2="${(symbolX + symbolSize).toFixed(2)}" y2="${lineY.toFixed(
|
|
3106
|
+
2
|
|
3107
|
+
)}" stroke="${escapeXml(entry.color)}" stroke-width="${lineWidth}" />`
|
|
3108
|
+
);
|
|
3109
|
+
} else if (entry.shape === "marker") {
|
|
3110
|
+
const radius = Math.max(2, Math.min(symbolSize / 2, entry.markerSize ?? symbolSize / 2));
|
|
3111
|
+
const cx = symbolX + symbolSize / 2;
|
|
3112
|
+
const cy = rowY;
|
|
3113
|
+
elements.push(
|
|
3114
|
+
`<circle class="legend-marker" cx="${cx.toFixed(2)}" cy="${cy.toFixed(
|
|
3115
|
+
2
|
|
3116
|
+
)}" r="${radius.toFixed(2)}" fill="${escapeXml(entry.color)}" />`
|
|
3117
|
+
);
|
|
3118
|
+
} else {
|
|
3119
|
+
elements.push(
|
|
3120
|
+
`<rect class="legend-swatch" x="${symbolX.toFixed(2)}" y="${symbolY.toFixed(
|
|
3121
|
+
2
|
|
3122
|
+
)}" width="${symbolSize.toFixed(2)}" height="${symbolSize.toFixed(
|
|
3123
|
+
2
|
|
3124
|
+
)}" fill="${escapeXml(entry.color)}" stroke="#000" />`
|
|
3125
|
+
);
|
|
3126
|
+
}
|
|
3127
|
+
const textX = symbolX + symbolSize + gap;
|
|
3128
|
+
elements.push(
|
|
3129
|
+
`<text class="legend-label" x="${textX.toFixed(2)}" y="${rowY.toFixed(
|
|
3130
|
+
2
|
|
3131
|
+
)}" text-anchor="start" dominant-baseline="middle" font-size="${fontSize}" fill="#000">${escapeXml(
|
|
3132
|
+
entry.label
|
|
3133
|
+
)}</text>`
|
|
3134
|
+
);
|
|
3135
|
+
}
|
|
3136
|
+
elements.push(`</g>`);
|
|
3137
|
+
}
|
|
3138
|
+
renderTicksRaster(canvas, vp, range, transform) {
|
|
3139
|
+
const maxXTicks = Math.max(2, Math.floor(vp.width / 80));
|
|
3140
|
+
const maxYTicks = Math.max(2, Math.floor(vp.height / 60));
|
|
3141
|
+
const xTicks = (this.xTicksOverride ?? generateTicks(range.xmin, range.xmax, maxXTicks)).filter(
|
|
3142
|
+
(tick) => tick.value >= range.xmin && tick.value <= range.xmax
|
|
3143
|
+
);
|
|
3144
|
+
const yTicks = (this.yTicksOverride ?? generateTicks(range.ymin, range.ymax, maxYTicks)).filter(
|
|
3145
|
+
(tick) => tick.value >= range.ymin && tick.value <= range.ymax
|
|
3146
|
+
);
|
|
3147
|
+
const tickLength = 5;
|
|
3148
|
+
const labelOffset = 2;
|
|
3149
|
+
const text = parseHexColorToRGBA("#000000");
|
|
3150
|
+
for (const tick of xTicks) {
|
|
3151
|
+
const px = Math.round(transform.xToPx(tick.value));
|
|
3152
|
+
const y0 = Math.round(vp.y + vp.height);
|
|
3153
|
+
const y1 = y0 + tickLength;
|
|
3154
|
+
canvas.drawLineRGBA(px, y0, px, y1, text.r, text.g, text.b, text.a);
|
|
3155
|
+
canvas.drawTextRGBA(tick.label, px, y1 + labelOffset, text.r, text.g, text.b, text.a, {
|
|
3156
|
+
fontSize: 10,
|
|
3157
|
+
align: "middle",
|
|
3158
|
+
baseline: "top"
|
|
3159
|
+
});
|
|
3160
|
+
}
|
|
3161
|
+
for (const tick of yTicks) {
|
|
3162
|
+
const py = Math.round(transform.yToPx(tick.value));
|
|
3163
|
+
const x0 = Math.round(vp.x);
|
|
3164
|
+
const x1 = x0 - tickLength;
|
|
3165
|
+
canvas.drawLineRGBA(x0, py, x1, py, text.r, text.g, text.b, text.a);
|
|
3166
|
+
canvas.drawTextRGBA(tick.label, x1 - labelOffset, py, text.r, text.g, text.b, text.a, {
|
|
3167
|
+
fontSize: 10,
|
|
3168
|
+
align: "end",
|
|
3169
|
+
baseline: "middle"
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
renderLegendRaster(canvas, vp) {
|
|
3174
|
+
if (!this.legendOptions) return;
|
|
3175
|
+
const entries = this.collectLegendEntries();
|
|
3176
|
+
if (entries.length === 0) return;
|
|
3177
|
+
const options = this.resolveLegendOptions();
|
|
3178
|
+
if (!options.visible) return;
|
|
3179
|
+
const fontSize = options.fontSize;
|
|
3180
|
+
const padding = options.padding;
|
|
3181
|
+
const symbolSize = Math.max(8, Math.round(fontSize * 0.9));
|
|
3182
|
+
const gap = 6;
|
|
3183
|
+
let maxLabelWidth = 0;
|
|
3184
|
+
for (const entry of entries) {
|
|
3185
|
+
maxLabelWidth = Math.max(maxLabelWidth, estimateTextWidth(entry.label, fontSize));
|
|
3186
|
+
}
|
|
3187
|
+
const lineHeight = fontSize + 4;
|
|
3188
|
+
const boxWidth = padding * 2 + symbolSize + gap + maxLabelWidth;
|
|
3189
|
+
const boxHeight = padding * 2 + entries.length * lineHeight;
|
|
3190
|
+
const margin = 6;
|
|
3191
|
+
const isRight = options.location.includes("right");
|
|
3192
|
+
const isUpper = options.location.includes("upper");
|
|
3193
|
+
const boxX = isRight ? vp.x + vp.width - boxWidth - margin : vp.x + margin;
|
|
3194
|
+
const boxY = isUpper ? vp.y + margin : vp.y + vp.height - boxHeight - margin;
|
|
3195
|
+
const bg = parseHexColorToRGBA(options.background);
|
|
3196
|
+
const border = parseHexColorToRGBA(options.borderColor);
|
|
3197
|
+
const bx = Math.round(boxX);
|
|
3198
|
+
const by = Math.round(boxY);
|
|
3199
|
+
const bw = Math.round(boxWidth);
|
|
3200
|
+
const bh = Math.round(boxHeight);
|
|
3201
|
+
canvas.fillRectRGBA(bx, by, bw, bh, bg.r, bg.g, bg.b, bg.a);
|
|
3202
|
+
canvas.drawLineRGBA(bx, by, bx + bw, by, border.r, border.g, border.b, border.a);
|
|
3203
|
+
canvas.drawLineRGBA(bx, by + bh, bx + bw, by + bh, border.r, border.g, border.b, border.a);
|
|
3204
|
+
canvas.drawLineRGBA(bx, by, bx, by + bh, border.r, border.g, border.b, border.a);
|
|
3205
|
+
canvas.drawLineRGBA(bx + bw, by, bx + bw, by + bh, border.r, border.g, border.b, border.a);
|
|
3206
|
+
const text = parseHexColorToRGBA("#000000");
|
|
3207
|
+
for (let i = 0; i < entries.length; i++) {
|
|
3208
|
+
const entry = entries[i];
|
|
3209
|
+
if (!entry) continue;
|
|
3210
|
+
const rowY = boxY + padding + i * lineHeight + lineHeight / 2;
|
|
3211
|
+
const symbolX = boxX + padding;
|
|
3212
|
+
const symbolY = rowY - symbolSize / 2;
|
|
3213
|
+
const color = parseHexColorToRGBA(entry.color);
|
|
3214
|
+
if (entry.shape === "line") {
|
|
3215
|
+
const y = Math.round(rowY);
|
|
3216
|
+
const x0 = Math.round(symbolX);
|
|
3217
|
+
const x1 = Math.round(symbolX + symbolSize);
|
|
3218
|
+
canvas.drawLineRGBA(x0, y, x1, y, color.r, color.g, color.b, color.a);
|
|
3219
|
+
} else if (entry.shape === "marker") {
|
|
3220
|
+
const radius = Math.max(2, Math.min(symbolSize / 2, entry.markerSize ?? symbolSize / 2));
|
|
3221
|
+
const cx = Math.round(symbolX + symbolSize / 2);
|
|
3222
|
+
const cy = Math.round(rowY);
|
|
3223
|
+
canvas.drawCircleRGBA(cx, cy, Math.round(radius), color.r, color.g, color.b, color.a);
|
|
3224
|
+
} else {
|
|
3225
|
+
canvas.fillRectRGBA(
|
|
3226
|
+
Math.round(symbolX),
|
|
3227
|
+
Math.round(symbolY),
|
|
3228
|
+
Math.round(symbolSize),
|
|
3229
|
+
Math.round(symbolSize),
|
|
3230
|
+
color.r,
|
|
3231
|
+
color.g,
|
|
3232
|
+
color.b,
|
|
3233
|
+
color.a
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
const textX = symbolX + symbolSize + gap;
|
|
3237
|
+
canvas.drawTextRGBA(
|
|
3238
|
+
entry.label,
|
|
3239
|
+
Math.round(textX),
|
|
3240
|
+
Math.round(rowY),
|
|
3241
|
+
text.r,
|
|
3242
|
+
text.g,
|
|
3243
|
+
text.b,
|
|
3244
|
+
text.a,
|
|
3245
|
+
{ fontSize, align: "start", baseline: "middle" }
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
/**
|
|
3250
|
+
* Render axes to SVG elements.
|
|
3251
|
+
* @internal
|
|
3252
|
+
*/
|
|
3253
|
+
renderSVGInto(elements) {
|
|
3254
|
+
const vp = this.viewport();
|
|
3255
|
+
const range = computeAutoRange(this.drawables);
|
|
3256
|
+
const transform = makeTransform(range, vp);
|
|
3257
|
+
elements.push(
|
|
3258
|
+
`<rect x="${vp.x}" y="${vp.y}" width="${vp.width}" height="${vp.height}" fill="${escapeXml(this.facecolor)}" stroke="#000" />`
|
|
3259
|
+
);
|
|
3260
|
+
const ctx = {
|
|
3261
|
+
transform,
|
|
3262
|
+
push: (el) => {
|
|
3263
|
+
elements.push(el);
|
|
3264
|
+
}
|
|
3265
|
+
};
|
|
3266
|
+
for (const d of this.drawables) d.drawSVG(ctx);
|
|
3267
|
+
const shouldRenderTicks = this.drawables.length > 0;
|
|
3268
|
+
if (shouldRenderTicks) {
|
|
3269
|
+
this.renderTicksSVG(elements, vp, range, transform);
|
|
3270
|
+
}
|
|
3271
|
+
if (this.title) {
|
|
3272
|
+
const titleX = vp.x + vp.width / 2;
|
|
3273
|
+
const titleY = vp.y - 10;
|
|
3274
|
+
elements.push(
|
|
3275
|
+
`<text x="${titleX}" y="${titleY}" text-anchor="middle" font-size="14" font-weight="bold" fill="#000">${escapeXml(this.title)}</text>`
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
if (this.xlabel) {
|
|
3279
|
+
const xlabelX = vp.x + vp.width / 2;
|
|
3280
|
+
const xlabelY = vp.y + vp.height + 35;
|
|
3281
|
+
elements.push(
|
|
3282
|
+
`<text x="${xlabelX}" y="${xlabelY}" text-anchor="middle" font-size="12" fill="#000">${escapeXml(this.xlabel)}</text>`
|
|
3283
|
+
);
|
|
3284
|
+
}
|
|
3285
|
+
if (this.ylabel) {
|
|
3286
|
+
const ylabelX = vp.x - 35;
|
|
3287
|
+
const ylabelY = vp.y + vp.height / 2;
|
|
3288
|
+
elements.push(
|
|
3289
|
+
`<text x="${ylabelX}" y="${ylabelY}" text-anchor="middle" font-size="12" fill="#000" transform="rotate(-90 ${ylabelX} ${ylabelY})">${escapeXml(this.ylabel)}</text>`
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
this.renderLegendSVG(elements, vp);
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* Render axes to raster canvas.
|
|
3296
|
+
* @internal
|
|
3297
|
+
*/
|
|
3298
|
+
renderRasterInto(canvas) {
|
|
3299
|
+
const vp = this.viewport();
|
|
3300
|
+
const range = computeAutoRange(this.drawables);
|
|
3301
|
+
const transform = makeTransform(range, vp);
|
|
3302
|
+
const ctx = { transform, canvas };
|
|
3303
|
+
const bg = parseHexColorToRGBA(this.facecolor);
|
|
3304
|
+
const x0 = Math.round(vp.x);
|
|
3305
|
+
const y0 = Math.round(vp.y);
|
|
3306
|
+
const w = Math.max(0, Math.round(vp.width));
|
|
3307
|
+
const h = Math.max(0, Math.round(vp.height));
|
|
3308
|
+
if (w > 0 && h > 0) {
|
|
3309
|
+
canvas.fillRectRGBA(x0, y0, w, h, bg.r, bg.g, bg.b, bg.a);
|
|
3310
|
+
const edge = parseHexColorToRGBA("#000000");
|
|
3311
|
+
const x1 = x0 + w;
|
|
3312
|
+
const y1 = y0 + h;
|
|
3313
|
+
canvas.drawLineRGBA(x0, y0, x1, y0, edge.r, edge.g, edge.b, edge.a);
|
|
3314
|
+
canvas.drawLineRGBA(x0, y1, x1, y1, edge.r, edge.g, edge.b, edge.a);
|
|
3315
|
+
canvas.drawLineRGBA(x0, y0, x0, y1, edge.r, edge.g, edge.b, edge.a);
|
|
3316
|
+
canvas.drawLineRGBA(x1, y0, x1, y1, edge.r, edge.g, edge.b, edge.a);
|
|
3317
|
+
}
|
|
3318
|
+
for (const d of this.drawables) d.drawRaster(ctx);
|
|
3319
|
+
const shouldRenderTicks = this.drawables.length > 0;
|
|
3320
|
+
if (shouldRenderTicks) {
|
|
3321
|
+
this.renderTicksRaster(canvas, vp, range, transform);
|
|
3322
|
+
}
|
|
3323
|
+
const text = parseHexColorToRGBA("#000000");
|
|
3324
|
+
if (this.title) {
|
|
3325
|
+
const titleX = vp.x + vp.width / 2;
|
|
3326
|
+
const titleY = vp.y - 10;
|
|
3327
|
+
canvas.drawTextRGBA(this.title, titleX, titleY, text.r, text.g, text.b, text.a, {
|
|
3328
|
+
fontSize: 14,
|
|
3329
|
+
align: "middle",
|
|
3330
|
+
baseline: "bottom"
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
if (this.xlabel) {
|
|
3334
|
+
const xlabelX = vp.x + vp.width / 2;
|
|
3335
|
+
const xlabelY = vp.y + vp.height + 35;
|
|
3336
|
+
canvas.drawTextRGBA(this.xlabel, xlabelX, xlabelY, text.r, text.g, text.b, text.a, {
|
|
3337
|
+
fontSize: 12,
|
|
3338
|
+
align: "middle",
|
|
3339
|
+
baseline: "top"
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3342
|
+
if (this.ylabel) {
|
|
3343
|
+
const ylabelX = vp.x - 35;
|
|
3344
|
+
const ylabelY = vp.y + vp.height / 2;
|
|
3345
|
+
canvas.drawTextRGBA(this.ylabel, ylabelX, ylabelY, text.r, text.g, text.b, text.a, {
|
|
3346
|
+
fontSize: 12,
|
|
3347
|
+
align: "middle",
|
|
3348
|
+
baseline: "middle",
|
|
3349
|
+
rotation: -90
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
this.renderLegendRaster(canvas, vp);
|
|
3353
|
+
}
|
|
3354
|
+
};
|
|
3355
|
+
|
|
3356
|
+
// src/plot/canvas/RasterCanvas.ts
|
|
3357
|
+
var RasterCanvas = class {
|
|
3358
|
+
width;
|
|
3359
|
+
height;
|
|
3360
|
+
data;
|
|
3361
|
+
constructor(width, height) {
|
|
3362
|
+
assertPositiveInt("width", width);
|
|
3363
|
+
assertPositiveInt("height", height);
|
|
3364
|
+
this.width = width;
|
|
3365
|
+
this.height = height;
|
|
3366
|
+
this.data = new Uint8ClampedArray(width * height * 4);
|
|
3367
|
+
}
|
|
3368
|
+
clearRGBA(r, g, b, a) {
|
|
3369
|
+
const n = this.width * this.height;
|
|
3370
|
+
for (let i = 0; i < n; i++) {
|
|
3371
|
+
const k = i * 4;
|
|
3372
|
+
this.data[k] = r;
|
|
3373
|
+
this.data[k + 1] = g;
|
|
3374
|
+
this.data[k + 2] = b;
|
|
3375
|
+
this.data[k + 3] = a;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
setPixelRGBA(x, y, r, g, b, a) {
|
|
3379
|
+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return;
|
|
3380
|
+
const idx = (y * this.width + x) * 4;
|
|
3381
|
+
this.data[idx] = r;
|
|
3382
|
+
this.data[idx + 1] = g;
|
|
3383
|
+
this.data[idx + 2] = b;
|
|
3384
|
+
this.data[idx + 3] = a;
|
|
3385
|
+
}
|
|
3386
|
+
fillRectRGBA(x0, y0, w, h, r, g, b, a) {
|
|
3387
|
+
const x1 = x0 + w;
|
|
3388
|
+
const y1 = y0 + h;
|
|
3389
|
+
const cx0 = clampInt(x0, 0, this.width);
|
|
3390
|
+
const cy0 = clampInt(y0, 0, this.height);
|
|
3391
|
+
const cx1 = clampInt(x1, 0, this.width);
|
|
3392
|
+
const cy1 = clampInt(y1, 0, this.height);
|
|
3393
|
+
for (let y = cy0; y < cy1; y++) {
|
|
3394
|
+
for (let x = cx0; x < cx1; x++) this.setPixelRGBA(x, y, r, g, b, a);
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
drawLineRGBA(x0, y0, x1, y1, r, g, b, a) {
|
|
3398
|
+
let x = clampInt(x0, -1e6, 1e6);
|
|
3399
|
+
let y = clampInt(y0, -1e6, 1e6);
|
|
3400
|
+
const xEnd = clampInt(x1, -1e6, 1e6);
|
|
3401
|
+
const yEnd = clampInt(y1, -1e6, 1e6);
|
|
3402
|
+
const dx = Math.abs(xEnd - x);
|
|
3403
|
+
const sx = x < xEnd ? 1 : -1;
|
|
3404
|
+
const dy = -Math.abs(yEnd - y);
|
|
3405
|
+
const sy = y < yEnd ? 1 : -1;
|
|
3406
|
+
let err = dx + dy;
|
|
3407
|
+
for (; ; ) {
|
|
3408
|
+
this.setPixelRGBA(x, y, r, g, b, a);
|
|
3409
|
+
if (x === xEnd && y === yEnd) break;
|
|
3410
|
+
const e2 = 2 * err;
|
|
3411
|
+
if (e2 >= dy) {
|
|
3412
|
+
err += dy;
|
|
3413
|
+
x += sx;
|
|
3414
|
+
}
|
|
3415
|
+
if (e2 <= dx) {
|
|
3416
|
+
err += dx;
|
|
3417
|
+
y += sy;
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
fillTriangleRGBA(x0, y0, x1, y1, x2, y2, r, g, b, a) {
|
|
3422
|
+
if (y0 > y1) {
|
|
3423
|
+
[x0, x1] = [x1, x0];
|
|
3424
|
+
[y0, y1] = [y1, y0];
|
|
3425
|
+
}
|
|
3426
|
+
if (y0 > y2) {
|
|
3427
|
+
[x0, x2] = [x2, x0];
|
|
3428
|
+
[y0, y2] = [y2, y0];
|
|
3429
|
+
}
|
|
3430
|
+
if (y1 > y2) {
|
|
3431
|
+
[x1, x2] = [x2, x1];
|
|
3432
|
+
[y1, y2] = [y2, y1];
|
|
3433
|
+
}
|
|
3434
|
+
const totalHeight = y2 - y0;
|
|
3435
|
+
if (totalHeight === 0) return;
|
|
3436
|
+
for (let i = 0; i < totalHeight; i++) {
|
|
3437
|
+
const secondHalf = i > y1 - y0 || y1 === y0;
|
|
3438
|
+
const segmentHeight = secondHalf ? y2 - y1 : y1 - y0;
|
|
3439
|
+
const alpha = i / totalHeight;
|
|
3440
|
+
const beta = (i - (secondHalf ? y1 - y0 : 0)) / segmentHeight;
|
|
3441
|
+
let ax = x0 + (x2 - x0) * alpha;
|
|
3442
|
+
let bx = secondHalf ? x1 + (x2 - x1) * beta : x0 + (x1 - x0) * beta;
|
|
3443
|
+
if (ax > bx) {
|
|
3444
|
+
[ax, bx] = [bx, ax];
|
|
3445
|
+
}
|
|
3446
|
+
const y = Math.floor(y0 + i);
|
|
3447
|
+
const startX = Math.floor(ax);
|
|
3448
|
+
const endX = Math.ceil(bx);
|
|
3449
|
+
for (let x = startX; x < endX; x++) {
|
|
3450
|
+
this.setPixelRGBA(x, y, r, g, b, a);
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
drawCircleRGBA(cx, cy, radius, r, g, b, a) {
|
|
3455
|
+
const rr = Math.max(0, radius);
|
|
3456
|
+
const r2 = rr * rr;
|
|
3457
|
+
const x0 = clampInt(cx - rr, 0, this.width);
|
|
3458
|
+
const x1 = clampInt(cx + rr + 1, 0, this.width);
|
|
3459
|
+
const y0 = clampInt(cy - rr, 0, this.height);
|
|
3460
|
+
const y1 = clampInt(cy + rr + 1, 0, this.height);
|
|
3461
|
+
for (let y = y0; y < y1; y++) {
|
|
3462
|
+
const dy = y - cy;
|
|
3463
|
+
for (let x = x0; x < x1; x++) {
|
|
3464
|
+
const dx = x - cx;
|
|
3465
|
+
if (dx * dx + dy * dy <= r2) this.setPixelRGBA(x, y, r, g, b, a);
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
measureText(text, fontSize = 12) {
|
|
3470
|
+
const scale = Math.max(1, Math.round(fontSize / FONT_HEIGHT));
|
|
3471
|
+
const charWidth = FONT_WIDTH * scale;
|
|
3472
|
+
const charHeight = FONT_HEIGHT * scale;
|
|
3473
|
+
const spacing = scale;
|
|
3474
|
+
if (text.length === 0) return { width: 0, height: charHeight };
|
|
3475
|
+
return { width: text.length * charWidth + (text.length - 1) * spacing, height: charHeight };
|
|
3476
|
+
}
|
|
3477
|
+
drawTextRGBA(text, x, y, r, g, b, a, options = {}) {
|
|
3478
|
+
if (text.length === 0) return;
|
|
3479
|
+
const fontSize = options.fontSize ?? 12;
|
|
3480
|
+
const align = options.align ?? "start";
|
|
3481
|
+
const baseline = options.baseline ?? "top";
|
|
3482
|
+
const rotation = options.rotation ?? 0;
|
|
3483
|
+
const scale = Math.max(1, Math.round(fontSize / FONT_HEIGHT));
|
|
3484
|
+
const charWidth = FONT_WIDTH * scale;
|
|
3485
|
+
const charHeight = FONT_HEIGHT * scale;
|
|
3486
|
+
const spacing = scale;
|
|
3487
|
+
const textWidth = text.length * charWidth + (text.length - 1) * spacing;
|
|
3488
|
+
const textHeight = charHeight;
|
|
3489
|
+
const boxWidth = rotation === 0 ? textWidth : textHeight;
|
|
3490
|
+
const boxHeight = rotation === 0 ? textHeight : textWidth;
|
|
3491
|
+
let originX = Math.round(x);
|
|
3492
|
+
let originY = Math.round(y);
|
|
3493
|
+
if (align === "middle") originX -= Math.round(boxWidth / 2);
|
|
3494
|
+
else if (align === "end") originX -= boxWidth;
|
|
3495
|
+
if (baseline === "middle") originY -= Math.round(boxHeight / 2);
|
|
3496
|
+
else if (baseline === "bottom") originY -= boxHeight;
|
|
3497
|
+
for (let i = 0; i < text.length; i++) {
|
|
3498
|
+
const ch = text[i] ?? "";
|
|
3499
|
+
const rows = glyphRows(ch);
|
|
3500
|
+
const xOffset = i * (charWidth + spacing);
|
|
3501
|
+
for (let row = 0; row < rows.length; row++) {
|
|
3502
|
+
const mask = rows[row] ?? 0;
|
|
3503
|
+
for (let col = 0; col < FONT_WIDTH; col++) {
|
|
3504
|
+
if ((mask & 1 << FONT_WIDTH - 1 - col) === 0) continue;
|
|
3505
|
+
const baseX = xOffset + col * scale;
|
|
3506
|
+
const baseY = row * scale;
|
|
3507
|
+
for (let sy = 0; sy < scale; sy++) {
|
|
3508
|
+
for (let sx = 0; sx < scale; sx++) {
|
|
3509
|
+
const px = baseX + sx;
|
|
3510
|
+
const py = baseY + sy;
|
|
3511
|
+
let rx = px;
|
|
3512
|
+
let ry = py;
|
|
3513
|
+
if (rotation === -90) {
|
|
3514
|
+
rx = py;
|
|
3515
|
+
ry = textWidth - 1 - px;
|
|
3516
|
+
} else if (rotation === 90) {
|
|
3517
|
+
rx = textHeight - 1 - py;
|
|
3518
|
+
ry = px;
|
|
3519
|
+
}
|
|
3520
|
+
this.setPixelRGBA(originX + rx, originY + ry, r, g, b, a);
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
};
|
|
3528
|
+
var FONT_WIDTH = 5;
|
|
3529
|
+
var FONT_HEIGHT = 7;
|
|
3530
|
+
var FONT = {
|
|
3531
|
+
" ": [0, 0, 0, 0, 0, 0, 0],
|
|
3532
|
+
"!": [4, 4, 4, 4, 4, 0, 4],
|
|
3533
|
+
'"': [10, 10, 0, 0, 0, 0, 0],
|
|
3534
|
+
"'": [4, 4, 0, 0, 0, 0, 0],
|
|
3535
|
+
"(": [2, 4, 8, 8, 8, 4, 2],
|
|
3536
|
+
")": [8, 4, 2, 2, 2, 4, 8],
|
|
3537
|
+
"*": [0, 10, 4, 31, 4, 10, 0],
|
|
3538
|
+
"+": [0, 4, 4, 31, 4, 4, 0],
|
|
3539
|
+
",": [0, 0, 0, 0, 0, 4, 8],
|
|
3540
|
+
"-": [0, 0, 0, 31, 0, 0, 0],
|
|
3541
|
+
".": [0, 0, 0, 0, 0, 0, 4],
|
|
3542
|
+
"/": [1, 2, 4, 8, 16, 0, 0],
|
|
3543
|
+
"0": [14, 17, 19, 21, 25, 17, 14],
|
|
3544
|
+
"1": [4, 12, 4, 4, 4, 4, 14],
|
|
3545
|
+
"2": [14, 17, 1, 2, 4, 8, 31],
|
|
3546
|
+
"3": [31, 2, 4, 2, 1, 17, 14],
|
|
3547
|
+
"4": [2, 6, 10, 18, 31, 2, 2],
|
|
3548
|
+
"5": [31, 16, 30, 1, 1, 17, 14],
|
|
3549
|
+
"6": [6, 8, 16, 30, 17, 17, 14],
|
|
3550
|
+
"7": [31, 1, 2, 4, 8, 8, 8],
|
|
3551
|
+
"8": [14, 17, 17, 14, 17, 17, 14],
|
|
3552
|
+
"9": [14, 17, 17, 15, 1, 2, 12],
|
|
3553
|
+
":": [0, 4, 0, 0, 0, 4, 0],
|
|
3554
|
+
";": [0, 4, 0, 0, 0, 4, 8],
|
|
3555
|
+
"<": [2, 4, 8, 16, 8, 4, 2],
|
|
3556
|
+
"=": [0, 31, 0, 31, 0, 0, 0],
|
|
3557
|
+
">": [16, 8, 4, 2, 4, 8, 16],
|
|
3558
|
+
"?": [14, 17, 1, 2, 4, 0, 4],
|
|
3559
|
+
"%": [25, 25, 2, 4, 8, 19, 19],
|
|
3560
|
+
_: [0, 0, 0, 0, 0, 0, 31],
|
|
3561
|
+
A: [14, 17, 17, 31, 17, 17, 17],
|
|
3562
|
+
B: [30, 17, 17, 30, 17, 17, 30],
|
|
3563
|
+
C: [14, 17, 16, 16, 16, 17, 14],
|
|
3564
|
+
D: [30, 17, 17, 17, 17, 17, 30],
|
|
3565
|
+
E: [31, 16, 16, 30, 16, 16, 31],
|
|
3566
|
+
F: [31, 16, 16, 30, 16, 16, 16],
|
|
3567
|
+
G: [14, 17, 16, 23, 17, 17, 14],
|
|
3568
|
+
H: [17, 17, 17, 31, 17, 17, 17],
|
|
3569
|
+
I: [14, 4, 4, 4, 4, 4, 14],
|
|
3570
|
+
J: [7, 2, 2, 2, 18, 18, 12],
|
|
3571
|
+
K: [17, 18, 20, 24, 20, 18, 17],
|
|
3572
|
+
L: [16, 16, 16, 16, 16, 16, 31],
|
|
3573
|
+
M: [17, 27, 21, 17, 17, 17, 17],
|
|
3574
|
+
N: [17, 25, 21, 19, 17, 17, 17],
|
|
3575
|
+
O: [14, 17, 17, 17, 17, 17, 14],
|
|
3576
|
+
P: [30, 17, 17, 30, 16, 16, 16],
|
|
3577
|
+
Q: [14, 17, 17, 17, 21, 18, 13],
|
|
3578
|
+
R: [30, 17, 17, 30, 20, 18, 17],
|
|
3579
|
+
S: [15, 16, 16, 14, 1, 1, 30],
|
|
3580
|
+
T: [31, 4, 4, 4, 4, 4, 4],
|
|
3581
|
+
U: [17, 17, 17, 17, 17, 17, 14],
|
|
3582
|
+
V: [17, 17, 17, 17, 17, 10, 4],
|
|
3583
|
+
W: [17, 17, 17, 17, 21, 27, 17],
|
|
3584
|
+
X: [17, 17, 10, 4, 10, 17, 17],
|
|
3585
|
+
Y: [17, 17, 10, 4, 4, 4, 4],
|
|
3586
|
+
Z: [31, 1, 2, 4, 8, 16, 31],
|
|
3587
|
+
"[": [14, 8, 8, 8, 8, 8, 14],
|
|
3588
|
+
"]": [14, 2, 2, 2, 2, 2, 14]
|
|
3589
|
+
};
|
|
3590
|
+
function glyphRows(ch) {
|
|
3591
|
+
const direct = FONT[ch];
|
|
3592
|
+
if (direct) return direct;
|
|
3593
|
+
const upper = ch.toUpperCase();
|
|
3594
|
+
return FONT[upper] ?? FONT["?"] ?? [0, 0, 0, 0, 0, 0, 0];
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
// src/plot/renderers/png.ts
|
|
3598
|
+
function isNodeEnvironment() {
|
|
3599
|
+
return typeof process !== "undefined" && typeof process.versions !== "undefined" && typeof process.versions.node !== "undefined";
|
|
3600
|
+
}
|
|
3601
|
+
function u32be(n) {
|
|
3602
|
+
return Uint8Array.of(n >>> 24 & 255, n >>> 16 & 255, n >>> 8 & 255, n & 255);
|
|
3603
|
+
}
|
|
3604
|
+
function ascii4(s) {
|
|
3605
|
+
return Uint8Array.of(
|
|
3606
|
+
s.charCodeAt(0) & 255,
|
|
3607
|
+
s.charCodeAt(1) & 255,
|
|
3608
|
+
s.charCodeAt(2) & 255,
|
|
3609
|
+
s.charCodeAt(3) & 255
|
|
3610
|
+
);
|
|
3611
|
+
}
|
|
3612
|
+
function concatBytes(chunks) {
|
|
3613
|
+
let total = 0;
|
|
3614
|
+
for (const c of chunks) total += c.length;
|
|
3615
|
+
const out = new Uint8Array(total);
|
|
3616
|
+
let o = 0;
|
|
3617
|
+
for (const c of chunks) {
|
|
3618
|
+
out.set(c, o);
|
|
3619
|
+
o += c.length;
|
|
3620
|
+
}
|
|
3621
|
+
return out;
|
|
3622
|
+
}
|
|
3623
|
+
function crc32(buf) {
|
|
3624
|
+
let crc = 4294967295;
|
|
3625
|
+
for (let i = 0; i < buf.length; i++) {
|
|
3626
|
+
let x = (crc ^ (buf[i] ?? 0)) & 255;
|
|
3627
|
+
for (let k = 0; k < 8; k++) {
|
|
3628
|
+
x = (x & 1) !== 0 ? 3988292384 ^ x >>> 1 : x >>> 1;
|
|
3629
|
+
}
|
|
3630
|
+
crc = crc >>> 8 ^ x;
|
|
3631
|
+
}
|
|
3632
|
+
return (crc ^ 4294967295) >>> 0;
|
|
3633
|
+
}
|
|
3634
|
+
function pngChunk(type, data) {
|
|
3635
|
+
const t = ascii4(type);
|
|
3636
|
+
const len = u32be(data.length);
|
|
3637
|
+
const crc = u32be(crc32(concatBytes([t, data])));
|
|
3638
|
+
return concatBytes([len, t, data, crc]);
|
|
3639
|
+
}
|
|
3640
|
+
function deflateUncompressed(data) {
|
|
3641
|
+
const maxBlockSize = 65535;
|
|
3642
|
+
const numBlocks = Math.ceil(data.length / maxBlockSize);
|
|
3643
|
+
let totalSize = 0;
|
|
3644
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
3645
|
+
const blockStart = i * maxBlockSize;
|
|
3646
|
+
const blockSize = Math.min(maxBlockSize, data.length - blockStart);
|
|
3647
|
+
totalSize += 5 + blockSize;
|
|
3648
|
+
}
|
|
3649
|
+
const output = new Uint8Array(2 + totalSize + 4);
|
|
3650
|
+
let outPos = 0;
|
|
3651
|
+
output[outPos++] = 120;
|
|
3652
|
+
output[outPos++] = 1;
|
|
3653
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
3654
|
+
const blockStart = i * maxBlockSize;
|
|
3655
|
+
const blockSize = Math.min(maxBlockSize, data.length - blockStart);
|
|
3656
|
+
const isLast = i === numBlocks - 1;
|
|
3657
|
+
output[outPos++] = isLast ? 1 : 0;
|
|
3658
|
+
output[outPos++] = blockSize & 255;
|
|
3659
|
+
output[outPos++] = blockSize >> 8 & 255;
|
|
3660
|
+
const nlen = ~blockSize & 65535;
|
|
3661
|
+
output[outPos++] = nlen & 255;
|
|
3662
|
+
output[outPos++] = nlen >> 8 & 255;
|
|
3663
|
+
output.set(data.subarray(blockStart, blockStart + blockSize), outPos);
|
|
3664
|
+
outPos += blockSize;
|
|
3665
|
+
}
|
|
3666
|
+
let a = 1;
|
|
3667
|
+
let b = 0;
|
|
3668
|
+
for (let i = 0; i < data.length; i++) {
|
|
3669
|
+
a = (a + (data[i] ?? 0)) % 65521;
|
|
3670
|
+
b = (b + a) % 65521;
|
|
3671
|
+
}
|
|
3672
|
+
const adler32 = (b << 16 | a) >>> 0;
|
|
3673
|
+
output[outPos++] = adler32 >>> 24 & 255;
|
|
3674
|
+
output[outPos++] = adler32 >>> 16 & 255;
|
|
3675
|
+
output[outPos++] = adler32 >>> 8 & 255;
|
|
3676
|
+
output[outPos++] = adler32 & 255;
|
|
3677
|
+
return output;
|
|
3678
|
+
}
|
|
3679
|
+
async function pngEncodeRGBA(width, height, rgba) {
|
|
3680
|
+
assertPositiveInt("width", width);
|
|
3681
|
+
assertPositiveInt("height", height);
|
|
3682
|
+
if (rgba.length !== width * height * 4)
|
|
3683
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("RGBA buffer has incorrect length", "rgba", rgba.length);
|
|
3684
|
+
const signature = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
|
|
3685
|
+
const ihdrData = concatBytes([u32be(width), u32be(height), Uint8Array.of(8, 6, 0, 0, 0)]);
|
|
3686
|
+
const ihdr = pngChunk("IHDR", ihdrData);
|
|
3687
|
+
const stride = width * 4;
|
|
3688
|
+
const raw = new Uint8Array(height * (1 + stride));
|
|
3689
|
+
for (let y = 0; y < height; y++) {
|
|
3690
|
+
const rowOff = y * (1 + stride);
|
|
3691
|
+
raw[rowOff] = 0;
|
|
3692
|
+
raw.set(rgba.subarray(y * stride, y * stride + stride), rowOff + 1);
|
|
3693
|
+
}
|
|
3694
|
+
let compressed;
|
|
3695
|
+
if (isNodeEnvironment()) {
|
|
3696
|
+
try {
|
|
3697
|
+
const zlib = await import('zlib');
|
|
3698
|
+
compressed = typeof zlib.deflateSync === "function" ? zlib.deflateSync(raw, { level: 9 }) : deflateUncompressed(raw);
|
|
3699
|
+
} catch {
|
|
3700
|
+
compressed = deflateUncompressed(raw);
|
|
3701
|
+
}
|
|
3702
|
+
} else {
|
|
3703
|
+
compressed = deflateUncompressed(raw);
|
|
3704
|
+
}
|
|
3705
|
+
const idat = pngChunk("IDAT", compressed);
|
|
3706
|
+
const iend = pngChunk("IEND", new Uint8Array(0));
|
|
3707
|
+
return concatBytes([signature, ihdr, idat, iend]);
|
|
3708
|
+
}
|
|
3709
|
+
function isNodeEnvironment_export() {
|
|
3710
|
+
return isNodeEnvironment();
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// src/plot/figure/Figure.ts
|
|
3714
|
+
var Figure = class {
|
|
3715
|
+
width;
|
|
3716
|
+
height;
|
|
3717
|
+
background;
|
|
3718
|
+
axesList;
|
|
3719
|
+
constructor(options = {}) {
|
|
3720
|
+
this.width = options.width ?? 640;
|
|
3721
|
+
this.height = options.height ?? 480;
|
|
3722
|
+
assertPositiveInt("Figure.width", this.width);
|
|
3723
|
+
assertPositiveInt("Figure.height", this.height);
|
|
3724
|
+
if (this.width > 32768 || this.height > 32768) {
|
|
3725
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3726
|
+
"Figure dimensions too large. Maximum is 32,768 pixels.",
|
|
3727
|
+
"width/height",
|
|
3728
|
+
{ width: this.width, height: this.height }
|
|
3729
|
+
);
|
|
3730
|
+
}
|
|
3731
|
+
this.background = normalizeColor(options.background, "#ffffff");
|
|
3732
|
+
this.axesList = [];
|
|
3733
|
+
}
|
|
3734
|
+
/**
|
|
3735
|
+
* Add a new axes to the figure.
|
|
3736
|
+
* @param options - Axes layout options
|
|
3737
|
+
*/
|
|
3738
|
+
addAxes(options = {}) {
|
|
3739
|
+
const ax = new Axes(this, options);
|
|
3740
|
+
this.axesList.push(ax);
|
|
3741
|
+
return ax;
|
|
3742
|
+
}
|
|
3743
|
+
/**
|
|
3744
|
+
* Render this figure to SVG.
|
|
3745
|
+
*/
|
|
3746
|
+
renderSVG() {
|
|
3747
|
+
const elements = [];
|
|
3748
|
+
elements.push(
|
|
3749
|
+
`<rect x="0" y="0" width="${this.width}" height="${this.height}" fill="${escapeXml(this.background)}" />`
|
|
3750
|
+
);
|
|
3751
|
+
for (const ax of this.axesList) ax.renderSVGInto(elements);
|
|
3752
|
+
const svg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
3753
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${this.width}" height="${this.height}" viewBox="0 0 ${this.width} ${this.height}">
|
|
3754
|
+
${elements.join("\n")}
|
|
3755
|
+
</svg>`;
|
|
3756
|
+
return { kind: "svg", svg };
|
|
3757
|
+
}
|
|
3758
|
+
/**
|
|
3759
|
+
* Render this figure to PNG (Node.js only).
|
|
3760
|
+
*
|
|
3761
|
+
* Note: PNG text rendering uses a built-in bitmap font for basic ASCII.
|
|
3762
|
+
* Unsupported characters are rendered as "?".
|
|
3763
|
+
*/
|
|
3764
|
+
async renderPNG() {
|
|
3765
|
+
if (!isNodeEnvironment_export()) {
|
|
3766
|
+
throw new chunkJSCDE774_cjs.NotImplementedError(
|
|
3767
|
+
"PNG rendering is only available in Node.js environments. Use renderSVG() for browser compatibility, or run in Node.js to generate PNG files."
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
const canvas = new RasterCanvas(this.width, this.height);
|
|
3771
|
+
const bg = parseHexColorToRGBA(this.background);
|
|
3772
|
+
canvas.clearRGBA(bg.r, bg.g, bg.b, bg.a);
|
|
3773
|
+
for (const ax of this.axesList) ax.renderRasterInto(canvas);
|
|
3774
|
+
const bytes = await pngEncodeRGBA(this.width, this.height, canvas.data);
|
|
3775
|
+
return { kind: "png", width: this.width, height: this.height, bytes };
|
|
3776
|
+
}
|
|
3777
|
+
};
|
|
3778
|
+
|
|
3779
|
+
// src/plot/figure/state.ts
|
|
3780
|
+
var _currentFigure = null;
|
|
3781
|
+
var _currentAxes = null;
|
|
3782
|
+
function gcf() {
|
|
3783
|
+
if (!_currentFigure) {
|
|
3784
|
+
_currentFigure = new Figure({ width: 320, height: 240 });
|
|
3785
|
+
}
|
|
3786
|
+
return _currentFigure;
|
|
3787
|
+
}
|
|
3788
|
+
function gca() {
|
|
3789
|
+
const fig = gcf();
|
|
3790
|
+
if (_currentAxes && fig.axesList.includes(_currentAxes)) return _currentAxes;
|
|
3791
|
+
if (fig.axesList.length === 0) {
|
|
3792
|
+
_currentAxes = fig.addAxes();
|
|
3793
|
+
return _currentAxes;
|
|
3794
|
+
}
|
|
3795
|
+
const firstAxes = fig.axesList[0];
|
|
3796
|
+
if (!firstAxes) {
|
|
3797
|
+
_currentAxes = fig.addAxes();
|
|
3798
|
+
return _currentAxes;
|
|
3799
|
+
}
|
|
3800
|
+
_currentAxes = firstAxes;
|
|
3801
|
+
return _currentAxes;
|
|
3802
|
+
}
|
|
3803
|
+
function figure(options = {}) {
|
|
3804
|
+
_currentFigure = new Figure({
|
|
3805
|
+
width: options.width ?? 320,
|
|
3806
|
+
height: options.height ?? 240,
|
|
3807
|
+
...options.background !== void 0 && { background: options.background }
|
|
3808
|
+
});
|
|
3809
|
+
_currentAxes = _currentFigure.addAxes();
|
|
3810
|
+
return _currentFigure;
|
|
3811
|
+
}
|
|
3812
|
+
function subplot(rows, cols, index, options = {}) {
|
|
3813
|
+
if (!Number.isFinite(rows) || Math.trunc(rows) !== rows || rows <= 0) {
|
|
3814
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3815
|
+
`rows must be a positive integer; received ${rows}`,
|
|
3816
|
+
"rows",
|
|
3817
|
+
rows
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
if (!Number.isFinite(cols) || Math.trunc(cols) !== cols || cols <= 0) {
|
|
3821
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3822
|
+
`cols must be a positive integer; received ${cols}`,
|
|
3823
|
+
"cols",
|
|
3824
|
+
cols
|
|
3825
|
+
);
|
|
3826
|
+
}
|
|
3827
|
+
const total = rows * cols;
|
|
3828
|
+
if (total > 1e4) {
|
|
3829
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3830
|
+
`Subplot grid too large (${rows}\xD7${cols}=${total}). Maximum is 10,000 subplots.`,
|
|
3831
|
+
"rows*cols",
|
|
3832
|
+
total
|
|
3833
|
+
);
|
|
3834
|
+
}
|
|
3835
|
+
if (!Number.isFinite(index) || Math.trunc(index) !== index || index < 1 || index > total) {
|
|
3836
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3837
|
+
`index must be in [1, ${total}]; received ${index}`,
|
|
3838
|
+
"index",
|
|
3839
|
+
index
|
|
3840
|
+
);
|
|
3841
|
+
}
|
|
3842
|
+
const fig = gcf();
|
|
3843
|
+
const idx0 = index - 1;
|
|
3844
|
+
const row = Math.floor(idx0 / cols);
|
|
3845
|
+
const col = idx0 % cols;
|
|
3846
|
+
const cellW = fig.width / cols;
|
|
3847
|
+
const cellH = fig.height / rows;
|
|
3848
|
+
const viewport = {
|
|
3849
|
+
x: col * cellW,
|
|
3850
|
+
y: row * cellH,
|
|
3851
|
+
width: cellW,
|
|
3852
|
+
height: cellH
|
|
3853
|
+
};
|
|
3854
|
+
const ax = fig.addAxes({ ...options, viewport });
|
|
3855
|
+
_currentAxes = ax;
|
|
3856
|
+
return ax;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// src/plot/index.ts
|
|
3860
|
+
function show(options = {}) {
|
|
3861
|
+
const fig = options.figure ?? gca().fig;
|
|
3862
|
+
if (options.format === "png") return fig.renderPNG();
|
|
3863
|
+
return fig.renderSVG();
|
|
3864
|
+
}
|
|
3865
|
+
async function saveFig(path, options = {}) {
|
|
3866
|
+
if (!path || path.trim().length === 0) {
|
|
3867
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("path must be a non-empty string", "path", path);
|
|
3868
|
+
}
|
|
3869
|
+
const dotIndex = path.lastIndexOf(".");
|
|
3870
|
+
const ext = dotIndex > 0 ? path.slice(dotIndex + 1).toLowerCase() : void 0;
|
|
3871
|
+
const fmt = options.format ?? (ext === "png" ? "png" : "svg");
|
|
3872
|
+
if (ext && ext !== fmt) {
|
|
3873
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3874
|
+
`File extension .${ext} does not match format ${fmt}`,
|
|
3875
|
+
"path",
|
|
3876
|
+
path
|
|
3877
|
+
);
|
|
3878
|
+
}
|
|
3879
|
+
const fig = options.figure ?? gca().fig;
|
|
3880
|
+
if (fmt === "png") {
|
|
3881
|
+
const { writeFile } = await import('fs/promises');
|
|
3882
|
+
const png = await fig.renderPNG();
|
|
3883
|
+
await writeFile(path, png.bytes);
|
|
3884
|
+
} else {
|
|
3885
|
+
const { writeFile } = await import('fs/promises');
|
|
3886
|
+
const svg = fig.renderSVG();
|
|
3887
|
+
await writeFile(path, svg.svg, "utf-8");
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
function plot(x, y, options = {}) {
|
|
3891
|
+
gca().plot(x, y, options);
|
|
3892
|
+
}
|
|
3893
|
+
function scatter(x, y, options = {}) {
|
|
3894
|
+
gca().scatter(x, y, options);
|
|
3895
|
+
}
|
|
3896
|
+
function bar(x, height, options = {}) {
|
|
3897
|
+
gca().bar(x, height, options);
|
|
3898
|
+
}
|
|
3899
|
+
function barh(y, width, options = {}) {
|
|
3900
|
+
gca().barh(y, width, options);
|
|
3901
|
+
}
|
|
3902
|
+
function hist(x, bins = 10, options = {}) {
|
|
3903
|
+
gca().hist(x, bins, options);
|
|
3904
|
+
}
|
|
3905
|
+
function boxplot(data, options = {}) {
|
|
3906
|
+
gca().boxplot(data, options);
|
|
3907
|
+
}
|
|
3908
|
+
function violinplot(data, options = {}) {
|
|
3909
|
+
gca().violinplot(data, options);
|
|
3910
|
+
}
|
|
3911
|
+
function pie(values, labels, options = {}) {
|
|
3912
|
+
gca().pie(values, labels, options);
|
|
3913
|
+
}
|
|
3914
|
+
function legend(options = {}) {
|
|
3915
|
+
gca().legend(options);
|
|
3916
|
+
}
|
|
3917
|
+
function heatmap(data, options = {}) {
|
|
3918
|
+
gca().heatmap(data, options);
|
|
3919
|
+
}
|
|
3920
|
+
function imshow(data, options = {}) {
|
|
3921
|
+
gca().imshow(data, options);
|
|
3922
|
+
}
|
|
3923
|
+
function contour(X, Y, Z, options = {}) {
|
|
3924
|
+
gca().contour(X, Y, Z, options);
|
|
3925
|
+
}
|
|
3926
|
+
function contourf(X, Y, Z, options = {}) {
|
|
3927
|
+
gca().contourf(X, Y, Z, options);
|
|
3928
|
+
}
|
|
3929
|
+
function plotConfusionMatrix(cm, labels, options = {}) {
|
|
3930
|
+
const ax = gca();
|
|
3931
|
+
ax.heatmap(cm, options);
|
|
3932
|
+
if (cm.ndim === 2) {
|
|
3933
|
+
const rows = cm.shape[0] ?? 0;
|
|
3934
|
+
const cols = cm.shape[1] ?? 0;
|
|
3935
|
+
ax.setTitle("Confusion Matrix");
|
|
3936
|
+
ax.setXLabel("Predicted");
|
|
3937
|
+
ax.setYLabel("Actual");
|
|
3938
|
+
if (labels && labels.length < Math.max(rows, cols)) {
|
|
3939
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
3940
|
+
`labels length must be >= ${Math.max(rows, cols)}; received ${labels.length}`,
|
|
3941
|
+
"labels",
|
|
3942
|
+
labels
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
if (labels && rows > 0 && cols > 0) {
|
|
3946
|
+
const xLabels = labels.slice(0, cols);
|
|
3947
|
+
const yLabels = labels.slice(0, rows);
|
|
3948
|
+
const xValues = xLabels.map((_, i) => i + 0.5);
|
|
3949
|
+
const yValues = yLabels.map((_, i) => i + 0.5);
|
|
3950
|
+
ax.setXTicks(xValues, xLabels);
|
|
3951
|
+
ax.setYTicks(yValues, yLabels);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
function plotRocCurve(fpr, tpr, auc, options = {}) {
|
|
3956
|
+
const ax = gca();
|
|
3957
|
+
ax.plot(fpr, tpr, {
|
|
3958
|
+
...options,
|
|
3959
|
+
color: options.color ?? "#1f77b4",
|
|
3960
|
+
label: options.label ?? "ROC"
|
|
3961
|
+
});
|
|
3962
|
+
ax.plot(chunk6AE5FKKQ_cjs.tensor([0, 1]), chunk6AE5FKKQ_cjs.tensor([0, 1]), {
|
|
3963
|
+
color: "#999999",
|
|
3964
|
+
linewidth: 1,
|
|
3965
|
+
label: "Chance"
|
|
3966
|
+
});
|
|
3967
|
+
if (auc !== void 0) {
|
|
3968
|
+
ax.setTitle(`ROC Curve (AUC = ${auc.toFixed(3)})`);
|
|
3969
|
+
} else {
|
|
3970
|
+
ax.setTitle("ROC Curve");
|
|
3971
|
+
}
|
|
3972
|
+
ax.setXLabel("False Positive Rate");
|
|
3973
|
+
ax.setYLabel("True Positive Rate");
|
|
3974
|
+
}
|
|
3975
|
+
function plotPrecisionRecallCurve(precision, recall, averagePrecision, options = {}) {
|
|
3976
|
+
const ax = gca();
|
|
3977
|
+
ax.plot(recall, precision, {
|
|
3978
|
+
...options,
|
|
3979
|
+
color: options.color ?? "#1f77b4",
|
|
3980
|
+
label: options.label ?? "Precision-Recall"
|
|
3981
|
+
});
|
|
3982
|
+
if (averagePrecision !== void 0) {
|
|
3983
|
+
ax.setTitle(`Precision-Recall Curve (AP = ${averagePrecision.toFixed(3)})`);
|
|
3984
|
+
} else {
|
|
3985
|
+
ax.setTitle("Precision-Recall Curve");
|
|
3986
|
+
}
|
|
3987
|
+
ax.setXLabel("Recall");
|
|
3988
|
+
ax.setYLabel("Precision");
|
|
3989
|
+
}
|
|
3990
|
+
function plotLearningCurve(trainSizes, trainScores, valScores, options = {}) {
|
|
3991
|
+
const ax = gca();
|
|
3992
|
+
const trainColor = options.colors?.[0] ?? options.color ?? "#1f77b4";
|
|
3993
|
+
const valColor = options.colors?.[1] ?? options.color ?? "#ff7f0e";
|
|
3994
|
+
ax.plot(trainSizes, trainScores, {
|
|
3995
|
+
color: trainColor,
|
|
3996
|
+
label: "Training Score"
|
|
3997
|
+
});
|
|
3998
|
+
ax.plot(trainSizes, valScores, {
|
|
3999
|
+
color: valColor,
|
|
4000
|
+
label: "Validation Score"
|
|
4001
|
+
});
|
|
4002
|
+
ax.setTitle("Learning Curve");
|
|
4003
|
+
ax.setXLabel("Training Set Size");
|
|
4004
|
+
ax.setYLabel("Score");
|
|
4005
|
+
}
|
|
4006
|
+
function plotValidationCurve(paramRange, trainScores, valScores, options = {}) {
|
|
4007
|
+
const ax = gca();
|
|
4008
|
+
const trainColor = options.colors?.[0] ?? options.color ?? "#1f77b4";
|
|
4009
|
+
const valColor = options.colors?.[1] ?? options.color ?? "#ff7f0e";
|
|
4010
|
+
ax.plot(paramRange, trainScores, {
|
|
4011
|
+
color: trainColor,
|
|
4012
|
+
label: "Training Score"
|
|
4013
|
+
});
|
|
4014
|
+
ax.plot(paramRange, valScores, {
|
|
4015
|
+
color: valColor,
|
|
4016
|
+
label: "Validation Score"
|
|
4017
|
+
});
|
|
4018
|
+
ax.setTitle("Validation Curve");
|
|
4019
|
+
ax.setXLabel("Parameter Value");
|
|
4020
|
+
ax.setYLabel("Score");
|
|
4021
|
+
}
|
|
4022
|
+
function plotDecisionBoundary(X, y, model, options = {}) {
|
|
4023
|
+
if (X.dtype === "string") {
|
|
4024
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("plotDecisionBoundary: X must be numeric", "X", X.dtype);
|
|
4025
|
+
}
|
|
4026
|
+
if (X.ndim !== 2 || (X.shape[1] ?? 0) !== 2) {
|
|
4027
|
+
throw new chunkJSCDE774_cjs.ShapeError("plotDecisionBoundary: X must be shape [n, 2]");
|
|
4028
|
+
}
|
|
4029
|
+
const n = X.shape[0] ?? 0;
|
|
4030
|
+
if (n === 0) {
|
|
4031
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("plotDecisionBoundary: X must have at least one row", "X", n);
|
|
4032
|
+
}
|
|
4033
|
+
if (y.ndim !== 1 || (y.shape[0] ?? -1) !== n) {
|
|
4034
|
+
throw new chunkJSCDE774_cjs.ShapeError("plotDecisionBoundary: y must be shape [n]");
|
|
4035
|
+
}
|
|
4036
|
+
const { data: xData } = tensorToFloat64Matrix2D(X);
|
|
4037
|
+
const x0 = new Float64Array(n);
|
|
4038
|
+
const x1 = new Float64Array(n);
|
|
4039
|
+
for (let i = 0; i < n; i++) {
|
|
4040
|
+
const vx = xData[i * 2] ?? NaN;
|
|
4041
|
+
const vy = xData[i * 2 + 1] ?? NaN;
|
|
4042
|
+
if (!Number.isFinite(vx) || !Number.isFinite(vy)) {
|
|
4043
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("plotDecisionBoundary: X must be finite", "X", {
|
|
4044
|
+
index: i,
|
|
4045
|
+
x: vx,
|
|
4046
|
+
y: vy
|
|
4047
|
+
});
|
|
4048
|
+
}
|
|
4049
|
+
x0[i] = vx;
|
|
4050
|
+
x1[i] = vy;
|
|
4051
|
+
}
|
|
4052
|
+
const readLabel = (labelTensor, index) => {
|
|
4053
|
+
const value = labelTensor.at(index);
|
|
4054
|
+
if (typeof value === "string") return value;
|
|
4055
|
+
if (typeof value === "number") {
|
|
4056
|
+
if (!Number.isFinite(value)) {
|
|
4057
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("plotDecisionBoundary: invalid label value", "y", value);
|
|
4058
|
+
}
|
|
4059
|
+
return value;
|
|
4060
|
+
}
|
|
4061
|
+
if (typeof value === "bigint") return value;
|
|
4062
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("plotDecisionBoundary: invalid label value", "y", value);
|
|
4063
|
+
};
|
|
4064
|
+
const toFiniteNumber = (value) => {
|
|
4065
|
+
if (typeof value === "number") {
|
|
4066
|
+
if (!Number.isFinite(value)) {
|
|
4067
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
4068
|
+
"plotDecisionBoundary: model.predict must return finite numeric scores",
|
|
4069
|
+
"predict",
|
|
4070
|
+
value
|
|
4071
|
+
);
|
|
4072
|
+
}
|
|
4073
|
+
return value;
|
|
4074
|
+
}
|
|
4075
|
+
if (typeof value === "bigint") {
|
|
4076
|
+
const n2 = Number(value);
|
|
4077
|
+
if (!Number.isFinite(n2)) {
|
|
4078
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
4079
|
+
"plotDecisionBoundary: model.predict must return finite numeric scores",
|
|
4080
|
+
"predict",
|
|
4081
|
+
value
|
|
4082
|
+
);
|
|
4083
|
+
}
|
|
4084
|
+
return n2;
|
|
4085
|
+
}
|
|
4086
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError(
|
|
4087
|
+
"plotDecisionBoundary: model.predict must return numeric scores",
|
|
4088
|
+
"predict",
|
|
4089
|
+
value
|
|
4090
|
+
);
|
|
4091
|
+
};
|
|
4092
|
+
const labelKey = (value) => `${typeof value}:${String(value)}`;
|
|
4093
|
+
let xMin = x0[0] ?? 0;
|
|
4094
|
+
let xMax = x0[0] ?? 0;
|
|
4095
|
+
let yMin = x1[0] ?? 0;
|
|
4096
|
+
let yMax = x1[0] ?? 0;
|
|
4097
|
+
for (let i = 1; i < n; i++) {
|
|
4098
|
+
const vx = x0[i] ?? 0;
|
|
4099
|
+
const vy = x1[i] ?? 0;
|
|
4100
|
+
if (vx < xMin) xMin = vx;
|
|
4101
|
+
if (vx > xMax) xMax = vx;
|
|
4102
|
+
if (vy < yMin) yMin = vy;
|
|
4103
|
+
if (vy > yMax) yMax = vy;
|
|
4104
|
+
}
|
|
4105
|
+
const marginX = xMax > xMin ? (xMax - xMin) * 0.05 : 1;
|
|
4106
|
+
const marginY = yMax > yMin ? (yMax - yMin) * 0.05 : 1;
|
|
4107
|
+
xMin -= marginX;
|
|
4108
|
+
xMax += marginX;
|
|
4109
|
+
yMin -= marginY;
|
|
4110
|
+
yMax += marginY;
|
|
4111
|
+
const gridResolution = 100;
|
|
4112
|
+
const gridCount = gridResolution * gridResolution;
|
|
4113
|
+
const gridFlat = new Float64Array(gridCount * 2);
|
|
4114
|
+
for (let gy = 0; gy < gridResolution; gy++) {
|
|
4115
|
+
const fy = yMin + (yMax - yMin) * gy / Math.max(1, gridResolution - 1);
|
|
4116
|
+
for (let gx = 0; gx < gridResolution; gx++) {
|
|
4117
|
+
const fx = xMin + (xMax - xMin) * gx / Math.max(1, gridResolution - 1);
|
|
4118
|
+
const idx = (gy * gridResolution + gx) * 2;
|
|
4119
|
+
gridFlat[idx] = fx;
|
|
4120
|
+
gridFlat[idx + 1] = fy;
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
const gridTensor = chunk6AE5FKKQ_cjs.tensor(gridFlat).reshape([gridCount, 2]);
|
|
4124
|
+
const predictions = model.predict(gridTensor);
|
|
4125
|
+
const predictedLabels = new Array(gridCount);
|
|
4126
|
+
if (predictions.ndim === 1) {
|
|
4127
|
+
if ((predictions.shape[0] ?? -1) !== gridCount) {
|
|
4128
|
+
throw new chunkJSCDE774_cjs.ShapeError("plotDecisionBoundary: model.predict must return [gridSize] labels");
|
|
4129
|
+
}
|
|
4130
|
+
for (let i = 0; i < gridCount; i++) {
|
|
4131
|
+
predictedLabels[i] = readLabel(predictions, i);
|
|
4132
|
+
}
|
|
4133
|
+
} else if (predictions.ndim === 2) {
|
|
4134
|
+
const rows = predictions.shape[0] ?? -1;
|
|
4135
|
+
const cols = predictions.shape[1] ?? -1;
|
|
4136
|
+
if (rows !== gridCount || cols <= 0) {
|
|
4137
|
+
throw new chunkJSCDE774_cjs.ShapeError("plotDecisionBoundary: model.predict must return [gridSize, k]");
|
|
4138
|
+
}
|
|
4139
|
+
for (let i = 0; i < rows; i++) {
|
|
4140
|
+
let bestCol = 0;
|
|
4141
|
+
let bestValue = toFiniteNumber(predictions.at(i, 0));
|
|
4142
|
+
for (let c = 1; c < cols; c++) {
|
|
4143
|
+
const value = toFiniteNumber(predictions.at(i, c));
|
|
4144
|
+
if (value > bestValue) {
|
|
4145
|
+
bestValue = value;
|
|
4146
|
+
bestCol = c;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
predictedLabels[i] = bestCol;
|
|
4150
|
+
}
|
|
4151
|
+
} else {
|
|
4152
|
+
throw new chunkJSCDE774_cjs.ShapeError("plotDecisionBoundary: model.predict output must be 1D or 2D");
|
|
4153
|
+
}
|
|
4154
|
+
const classIndex = /* @__PURE__ */ new Map();
|
|
4155
|
+
const classValues = [];
|
|
4156
|
+
for (let i = 0; i < n; i++) {
|
|
4157
|
+
const label = readLabel(y, i);
|
|
4158
|
+
const key = labelKey(label);
|
|
4159
|
+
if (!classIndex.has(key)) {
|
|
4160
|
+
classIndex.set(key, classValues.length);
|
|
4161
|
+
classValues.push(label);
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
for (const label of predictedLabels) {
|
|
4165
|
+
const key = labelKey(label);
|
|
4166
|
+
if (!classIndex.has(key)) {
|
|
4167
|
+
classIndex.set(key, classValues.length);
|
|
4168
|
+
classValues.push(label);
|
|
4169
|
+
}
|
|
4170
|
+
}
|
|
4171
|
+
const boundaryMatrix = Array.from(
|
|
4172
|
+
{ length: gridResolution },
|
|
4173
|
+
() => Array(gridResolution).fill(0)
|
|
4174
|
+
);
|
|
4175
|
+
for (let gy = 0; gy < gridResolution; gy++) {
|
|
4176
|
+
const row = boundaryMatrix[gy];
|
|
4177
|
+
if (!row) {
|
|
4178
|
+
throw new chunkJSCDE774_cjs.InvalidParameterError("plotDecisionBoundary: grid row access failed", "gy", gy);
|
|
4179
|
+
}
|
|
4180
|
+
for (let gx = 0; gx < gridResolution; gx++) {
|
|
4181
|
+
const label = predictedLabels[gy * gridResolution + gx];
|
|
4182
|
+
const mapped = label === void 0 ? 0 : classIndex.get(labelKey(label)) ?? 0;
|
|
4183
|
+
row[gx] = mapped;
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
const ax = gca();
|
|
4187
|
+
ax.imshow(chunk6AE5FKKQ_cjs.tensor(boundaryMatrix), {
|
|
4188
|
+
colormap: options.colormap ?? "grayscale",
|
|
4189
|
+
vmin: options.vmin ?? 0,
|
|
4190
|
+
vmax: options.vmax ?? Math.max(1, classValues.length - 1),
|
|
4191
|
+
extent: { xmin: xMin, xmax: xMax, ymin: yMin, ymax: yMax }
|
|
4192
|
+
});
|
|
4193
|
+
const defaultClassColors = [
|
|
4194
|
+
"#1f77b4",
|
|
4195
|
+
"#ff7f0e",
|
|
4196
|
+
"#2ca02c",
|
|
4197
|
+
"#d62728",
|
|
4198
|
+
"#9467bd",
|
|
4199
|
+
"#8c564b",
|
|
4200
|
+
"#e377c2",
|
|
4201
|
+
"#7f7f7f",
|
|
4202
|
+
"#bcbd22",
|
|
4203
|
+
"#17becf"
|
|
4204
|
+
];
|
|
4205
|
+
const classColors = options.colors !== void 0 && options.colors.length > 0 ? options.colors : defaultClassColors;
|
|
4206
|
+
const byClass = /* @__PURE__ */ new Map();
|
|
4207
|
+
for (let i = 0; i < n; i++) {
|
|
4208
|
+
const label = readLabel(y, i);
|
|
4209
|
+
const key = labelKey(label);
|
|
4210
|
+
const labelText = String(label);
|
|
4211
|
+
const mapped = classIndex.get(key) ?? 0;
|
|
4212
|
+
const current = byClass.get(key) ?? {
|
|
4213
|
+
x: [],
|
|
4214
|
+
y: [],
|
|
4215
|
+
index: mapped,
|
|
4216
|
+
label: labelText
|
|
4217
|
+
};
|
|
4218
|
+
current.x.push(x0[i] ?? 0);
|
|
4219
|
+
current.y.push(x1[i] ?? 0);
|
|
4220
|
+
byClass.set(key, current);
|
|
4221
|
+
}
|
|
4222
|
+
for (const group of byClass.values()) {
|
|
4223
|
+
const color = classColors[group.index % classColors.length] ?? "#000000";
|
|
4224
|
+
ax.scatter(chunk6AE5FKKQ_cjs.tensor(group.x), chunk6AE5FKKQ_cjs.tensor(group.y), {
|
|
4225
|
+
color,
|
|
4226
|
+
size: options.size ?? 4,
|
|
4227
|
+
label: group.label
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
ax.setTitle("Decision Boundary");
|
|
4231
|
+
ax.setXLabel("Feature 1");
|
|
4232
|
+
ax.setYLabel("Feature 2");
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
exports.Axes = Axes;
|
|
4236
|
+
exports.Figure = Figure;
|
|
4237
|
+
exports.bar = bar;
|
|
4238
|
+
exports.barh = barh;
|
|
4239
|
+
exports.boxplot = boxplot;
|
|
4240
|
+
exports.contour = contour;
|
|
4241
|
+
exports.contourf = contourf;
|
|
4242
|
+
exports.figure = figure;
|
|
4243
|
+
exports.gca = gca;
|
|
4244
|
+
exports.heatmap = heatmap;
|
|
4245
|
+
exports.hist = hist;
|
|
4246
|
+
exports.imshow = imshow;
|
|
4247
|
+
exports.legend = legend;
|
|
4248
|
+
exports.pie = pie;
|
|
4249
|
+
exports.plot = plot;
|
|
4250
|
+
exports.plotConfusionMatrix = plotConfusionMatrix;
|
|
4251
|
+
exports.plotDecisionBoundary = plotDecisionBoundary;
|
|
4252
|
+
exports.plotLearningCurve = plotLearningCurve;
|
|
4253
|
+
exports.plotPrecisionRecallCurve = plotPrecisionRecallCurve;
|
|
4254
|
+
exports.plotRocCurve = plotRocCurve;
|
|
4255
|
+
exports.plotValidationCurve = plotValidationCurve;
|
|
4256
|
+
exports.plot_exports = plot_exports;
|
|
4257
|
+
exports.saveFig = saveFig;
|
|
4258
|
+
exports.scatter = scatter;
|
|
4259
|
+
exports.show = show;
|
|
4260
|
+
exports.subplot = subplot;
|
|
4261
|
+
exports.violinplot = violinplot;
|
|
4262
|
+
//# sourceMappingURL=chunk-ALS7ETWZ.cjs.map
|
|
4263
|
+
//# sourceMappingURL=chunk-ALS7ETWZ.cjs.map
|