kalfa 3.0.0.dev0__py3-none-any.whl

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.
Files changed (148) hide show
  1. kalfa/__init__.py +19 -0
  2. kalfa/api.py +284 -0
  3. kalfa/board/__init__.py +1 -0
  4. kalfa/board/answers.py +419 -0
  5. kalfa/board/server.py +536 -0
  6. kalfa/board/static/api.js +176 -0
  7. kalfa/board/static/app.js +122 -0
  8. kalfa/board/static/board.css +228 -0
  9. kalfa/board/static/charts.css +46 -0
  10. kalfa/board/static/components/badge.js +14 -0
  11. kalfa/board/static/components/chart.js +204 -0
  12. kalfa/board/static/components/diagram.js +857 -0
  13. kalfa/board/static/components/error.js +17 -0
  14. kalfa/board/static/components/loading.js +7 -0
  15. kalfa/board/static/components/progress.js +25 -0
  16. kalfa/board/static/components/settings.js +418 -0
  17. kalfa/board/static/components/spark.js +43 -0
  18. kalfa/board/static/components/table.js +156 -0
  19. kalfa/board/static/diagram.css +76 -0
  20. kalfa/board/static/format.js +138 -0
  21. kalfa/board/static/index.html +34 -0
  22. kalfa/board/static/plot.js +594 -0
  23. kalfa/board/static/predictions.css +20 -0
  24. kalfa/board/static/store.js +215 -0
  25. kalfa/board/static/views/compare.js +185 -0
  26. kalfa/board/static/views/home.js +90 -0
  27. kalfa/board/static/views/record/config.js +81 -0
  28. kalfa/board/static/views/record/curves.js +137 -0
  29. kalfa/board/static/views/record/data.js +378 -0
  30. kalfa/board/static/views/record/files.js +128 -0
  31. kalfa/board/static/views/record/log.js +164 -0
  32. kalfa/board/static/views/record/model.js +286 -0
  33. kalfa/board/static/views/record/overview.js +216 -0
  34. kalfa/board/static/views/record/plots.js +103 -0
  35. kalfa/board/static/views/record/predictions.js +834 -0
  36. kalfa/board/static/views/record.js +179 -0
  37. kalfa/board/static/views/runs.js +124 -0
  38. kalfa/board/static/views/sweep.js +282 -0
  39. kalfa/board/watch.py +87 -0
  40. kalfa/cli.py +371 -0
  41. kalfa/commands/__init__.py +0 -0
  42. kalfa/commands/common.py +8 -0
  43. kalfa/commands/export.py +117 -0
  44. kalfa/commands/generate.py +31 -0
  45. kalfa/commands/guide/__init__.py +101 -0
  46. kalfa/commands/guide/about.py +93 -0
  47. kalfa/commands/guide/calls.py +102 -0
  48. kalfa/commands/guide/sections.py +186 -0
  49. kalfa/commands/guide/seen.py +307 -0
  50. kalfa/commands/guide/show.py +106 -0
  51. kalfa/commands/guide/training.py +333 -0
  52. kalfa/commands/guide/words.py +97 -0
  53. kalfa/commands/legos.py +508 -0
  54. kalfa/commands/plots.py +27 -0
  55. kalfa/commands/report/__init__.py +64 -0
  56. kalfa/commands/report/page.py +112 -0
  57. kalfa/commands/report/predictions.py +216 -0
  58. kalfa/commands/report/sections.py +356 -0
  59. kalfa/commands/stop.py +60 -0
  60. kalfa/commands/sweep/__init__.py +202 -0
  61. kalfa/commands/sweep/checks.py +98 -0
  62. kalfa/commands/sweep/plan.py +183 -0
  63. kalfa/commands/sweep/points.py +123 -0
  64. kalfa/commands/sweep/runner.py +100 -0
  65. kalfa/commands/sweep/space.py +67 -0
  66. kalfa/commands/sweep/strategies.py +228 -0
  67. kalfa/core/__init__.py +0 -0
  68. kalfa/core/aliases.py +229 -0
  69. kalfa/core/build.py +727 -0
  70. kalfa/core/check.py +550 -0
  71. kalfa/core/coerce.py +36 -0
  72. kalfa/core/context.py +149 -0
  73. kalfa/core/device.py +63 -0
  74. kalfa/core/errors.py +45 -0
  75. kalfa/core/graph.py +304 -0
  76. kalfa/core/history.py +70 -0
  77. kalfa/core/loop.py +710 -0
  78. kalfa/core/make.py +121 -0
  79. kalfa/core/plan.py +156 -0
  80. kalfa/core/predict.py +290 -0
  81. kalfa/core/protocols.py +122 -0
  82. kalfa/core/record.py +347 -0
  83. kalfa/core/refs.py +88 -0
  84. kalfa/core/rng.py +100 -0
  85. kalfa/core/rules.py +513 -0
  86. kalfa/core/run.py +568 -0
  87. kalfa/core/spec.py +413 -0
  88. kalfa/core/state.py +100 -0
  89. kalfa/core/wrap.py +671 -0
  90. kalfa/records/__init__.py +24 -0
  91. kalfa/records/compare.py +86 -0
  92. kalfa/records/reader.py +561 -0
  93. kalfa/records/stats/__init__.py +36 -0
  94. kalfa/records/stats/choices.py +282 -0
  95. kalfa/records/stats/classes.py +236 -0
  96. kalfa/records/stats/measures.py +144 -0
  97. kalfa/records/stats/pairs.py +225 -0
  98. kalfa/records/stats/tables.py +153 -0
  99. kalfa/records/sweep.py +244 -0
  100. kalfa/std/__init__.py +17 -0
  101. kalfa/std/actions.py +35 -0
  102. kalfa/std/common.py +56 -0
  103. kalfa/std/data/__init__.py +1 -0
  104. kalfa/std/data/base.py +45 -0
  105. kalfa/std/data/columns.py +190 -0
  106. kalfa/std/data/flow.py +317 -0
  107. kalfa/std/data/images.py +443 -0
  108. kalfa/std/data/options.py +147 -0
  109. kalfa/std/data/pre.py +84 -0
  110. kalfa/std/data/splits.py +140 -0
  111. kalfa/std/data/stream.py +294 -0
  112. kalfa/std/data/table.py +642 -0
  113. kalfa/std/data/text.py +436 -0
  114. kalfa/std/data/transforms.py +327 -0
  115. kalfa/std/data/weights.py +113 -0
  116. kalfa/std/data/window.py +135 -0
  117. kalfa/std/figure.py +37 -0
  118. kalfa/std/init.py +72 -0
  119. kalfa/std/loggers.py +105 -0
  120. kalfa/std/losses.py +161 -0
  121. kalfa/std/metrics.py +463 -0
  122. kalfa/std/models.py +122 -0
  123. kalfa/std/notes.py +1553 -0
  124. kalfa/std/objectives.py +360 -0
  125. kalfa/std/plots/__init__.py +75 -0
  126. kalfa/std/plots/classification.py +207 -0
  127. kalfa/std/plots/common.py +44 -0
  128. kalfa/std/plots/curves.py +56 -0
  129. kalfa/std/plots/images.py +188 -0
  130. kalfa/std/plots/pairs.py +66 -0
  131. kalfa/std/plots/predictions.py +154 -0
  132. kalfa/std/samplers.py +261 -0
  133. kalfa/std/samples.py +72 -0
  134. kalfa/std/schedules.py +74 -0
  135. kalfa/std/steps.py +133 -0
  136. kalfa/std/triggers.py +135 -0
  137. kalfa/summary.py +296 -0
  138. kalfa/tui/__init__.py +4 -0
  139. kalfa/tui/app.py +107 -0
  140. kalfa/tui/config.py +61 -0
  141. kalfa/tui/guide.py +100 -0
  142. kalfa/tui/jobs.py +97 -0
  143. kalfa/tui/legos.py +62 -0
  144. kalfa/tui/records.py +439 -0
  145. kalfa-3.0.0.dev0.dist-info/METADATA +261 -0
  146. kalfa-3.0.0.dev0.dist-info/RECORD +148 -0
  147. kalfa-3.0.0.dev0.dist-info/WHEEL +4 -0
  148. kalfa-3.0.0.dev0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,594 @@
1
+ const FALLBACK = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"];
2
+ const AXES = ["turn", "update", "x", "step"];
3
+ const CONFIG = { responsive: true, displaylogo: false, modeBarButtonsToRemove: ["lasso2d", "select2d"] };
4
+ const MODES = ["lines", "markers", "lines+markers"];
5
+ const DASHES = ["solid", "dash", "dot"];
6
+ const SAMPLES = 120;
7
+ const MARK_LABELS = 12;
8
+ const UNSAFE = ["__proto__", "constructor", "prototype"];
9
+
10
+ export const PALETTES = {
11
+ theme: null,
12
+ colorblind: ["#0072b2", "#e69f00", "#009e73", "#cc79a7", "#56b4e9", "#d55e00", "#f0e442", "#999999"],
13
+ classic: ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"],
14
+ viridis: ["#440154", "#482878", "#3e4989", "#31688e", "#26828e", "#1f9e89", "#35b779", "#6ece58", "#b5de2b", "#fde725"],
15
+ };
16
+
17
+ function plotly() {
18
+ return typeof window !== "undefined" ? window.Plotly : undefined;
19
+ }
20
+
21
+ export function numberOf(value) {
22
+ if (value === "" || value === null || value === undefined || typeof value === "boolean") return null;
23
+ const number = Number(value);
24
+ return Number.isFinite(number) ? number : null;
25
+ }
26
+
27
+ function between(value, low, high, fallback) {
28
+ const number = numberOf(value);
29
+ return number === null ? fallback : Math.min(high, Math.max(low, number));
30
+ }
31
+
32
+ export function paletteOf(palette) {
33
+ if (Array.isArray(palette) && palette.length) return palette;
34
+ const found = typeof palette === "string" ? PALETTES[palette] : null;
35
+ return Array.isArray(found) ? found : null;
36
+ }
37
+
38
+ export function themeColors(palette) {
39
+ const style = getComputedStyle(document.documentElement);
40
+ const read = (name, fallback) => (style.getPropertyValue(name) || "").trim() || fallback;
41
+ return {
42
+ ink: read("--ink", "#17191c"),
43
+ muted: read("--muted", "#7a7974"),
44
+ line: read("--line", "#e4e3df"),
45
+ surface: read("--surface", "#fcfcfb"),
46
+ accent: read("--accent", "#2a78d6"),
47
+ font: read("--font", "system-ui, sans-serif"),
48
+ series: paletteOf(palette) || FALLBACK.map((color, index) => read("--series-" + (index + 1), color)),
49
+ };
50
+ }
51
+
52
+ export function colorAt(index) {
53
+ const series = themeColors().series;
54
+ return series[index % series.length];
55
+ }
56
+
57
+ function axis(colors, title, log, extra) {
58
+ return {
59
+ title: { text: title || "", font: { size: 11 } },
60
+ type: log ? "log" : "-",
61
+ gridcolor: colors.line,
62
+ linecolor: colors.line,
63
+ zerolinecolor: colors.line,
64
+ tickfont: { color: colors.muted },
65
+ automargin: true,
66
+ ...(extra || {}),
67
+ };
68
+ }
69
+
70
+ function veil(color) {
71
+ return /^#[0-9a-f]{6}$/i.test(color) ? color + "d9" : color;
72
+ }
73
+
74
+ function legendOf(place, colors, height) {
75
+ const font = { color: colors.muted };
76
+ if (place === "right") return { orientation: "v", x: 1.02, xanchor: "left", y: 1, yanchor: "top", font };
77
+ if (place === "bottom") {
78
+ const below = 56 / Math.max(120, (height || 300) - 80);
79
+ return { orientation: "h", x: 0, xanchor: "left", y: -Math.min(0.5, below), yanchor: "top", font };
80
+ }
81
+ if (place === "inside") {
82
+ return { orientation: "v", x: 0.99, xanchor: "right", y: 0.99, yanchor: "top", font,
83
+ bgcolor: veil(colors.surface), bordercolor: colors.line, borderwidth: 1 };
84
+ }
85
+ return { orientation: "h", x: 0, y: 1.02, yanchor: "bottom", font };
86
+ }
87
+
88
+ export function baseLayout(options = {}) {
89
+ const colors = themeColors(options.palette);
90
+ const layout = {
91
+ paper_bgcolor: "rgba(0,0,0,0)",
92
+ plot_bgcolor: "rgba(0,0,0,0)",
93
+ font: { family: colors.font, size: 11, color: colors.ink },
94
+ colorway: colors.series,
95
+ margin: { l: 48, r: 12, t: options.title ? 30 : 10, b: 36 },
96
+ title: options.title
97
+ ? { text: options.title, font: { size: 12 }, x: 0, xanchor: "left", xref: "paper", y: 1, yref: "container",
98
+ yanchor: "top", pad: { t: 8 } }
99
+ : undefined,
100
+ height: options.height || undefined,
101
+ autosize: true,
102
+ showlegend: options.legend !== undefined ? Boolean(options.legend) : undefined,
103
+ legend: legendOf(options.legendPlace, colors, options.height),
104
+ hoverlabel: { bgcolor: colors.surface, bordercolor: colors.line, font: { color: colors.ink } },
105
+ xaxis: axis(colors, options.xlabel, options.logx, options.xaxis),
106
+ yaxis: axis(colors, options.ylabel, options.logy, options.yaxis),
107
+ };
108
+ if (options.grid === false) {
109
+ layout.xaxis.showgrid = false;
110
+ layout.yaxis.showgrid = false;
111
+ }
112
+ return layout;
113
+ }
114
+
115
+ function observe(el) {
116
+ if (el.__kalfaSize || typeof ResizeObserver === "undefined") return;
117
+ let frame = 0;
118
+ const observer = new ResizeObserver(() => {
119
+ if (frame) cancelAnimationFrame(frame);
120
+ frame = requestAnimationFrame(() => {
121
+ frame = 0;
122
+ const Plotly = plotly();
123
+ if (!Plotly || !el.isConnected || !el.data) return;
124
+ const done = Plotly.Plots.resize(el);
125
+ if (done && done.catch) done.catch(() => null);
126
+ });
127
+ });
128
+ observer.observe(el);
129
+ el.__kalfaSize = observer;
130
+ }
131
+
132
+ export function render(el, traces, layout) {
133
+ if (!el) return;
134
+ const Plotly = plotly();
135
+ if (!Plotly) {
136
+ el.textContent = "Plotly did not load from the CDN, so the charts cannot be drawn";
137
+ return;
138
+ }
139
+ const done = Plotly.react(el, traces, layout, CONFIG);
140
+ if (done && done.catch) done.catch((error) => console.error(error));
141
+ observe(el);
142
+ }
143
+
144
+ export function purge(el) {
145
+ if (!el) return;
146
+ if (el.__kalfaSize) {
147
+ el.__kalfaSize.disconnect();
148
+ el.__kalfaSize = null;
149
+ }
150
+ const Plotly = plotly();
151
+ if (Plotly && el.data) Plotly.purge(el);
152
+ }
153
+
154
+ function copyOf(value) {
155
+ return JSON.parse(JSON.stringify(value));
156
+ }
157
+
158
+ export function merge(target, source) {
159
+ for (const [key, value] of Object.entries(source || {})) {
160
+ if (UNSAFE.includes(key)) continue;
161
+ const nested = value && typeof value === "object" && !Array.isArray(value);
162
+ const inside = target[key] && typeof target[key] === "object" && !Array.isArray(target[key]);
163
+ if (nested && inside) merge(target[key], value);
164
+ else target[key] = value;
165
+ }
166
+ return target;
167
+ }
168
+
169
+ function valuesOf(traces, key) {
170
+ const found = [];
171
+ for (const trace of traces) {
172
+ const values = trace[key];
173
+ if (!Array.isArray(values)) continue;
174
+ const across = trace.type === "bar" && (trace.orientation === "h" ? "y" : "x") === key;
175
+ const widths = across && Array.isArray(trace.width) ? trace.width : null;
176
+ if (trace.type === "bar" && !across) found.push(0);
177
+ values.forEach((value, index) => {
178
+ if (typeof value !== "number" || !Number.isFinite(value)) return;
179
+ const width = widths && typeof widths[index] === "number" ? widths[index] : 0;
180
+ if (width) found.push(value - width / 2, value + width / 2);
181
+ else found.push(value);
182
+ });
183
+ }
184
+ return found;
185
+ }
186
+
187
+ function spread(values, positive) {
188
+ let low = Infinity;
189
+ let high = -Infinity;
190
+ for (const value of values) {
191
+ if (positive && value <= 0) continue;
192
+ if (value < low) low = value;
193
+ if (value > high) high = value;
194
+ }
195
+ return Number.isFinite(low) ? [low, high] : null;
196
+ }
197
+
198
+ function limit(layoutAxis, low, high, values) {
199
+ if (!layoutAxis || layoutAxis.type === "category") return;
200
+ const log = layoutAxis.type === "log";
201
+ let from = numberOf(low);
202
+ let to = numberOf(high);
203
+ if (log && from !== null && from <= 0) from = null;
204
+ if (log && to !== null && to <= 0) to = null;
205
+ if (from === null && to === null) return;
206
+ const found = spread(values, log);
207
+ const start = from !== null ? from : found ? found[0] : null;
208
+ const end = to !== null ? to : found ? found[1] : null;
209
+ if (start === null || end === null || !(end > start)) return;
210
+ layoutAxis.range = log ? [Math.log10(start), Math.log10(end)] : [start, end];
211
+ layoutAxis.autorange = false;
212
+ }
213
+
214
+ function finish(el, traces, layout, options) {
215
+ limit(layout.xaxis, options.xmin, options.xmax, valuesOf(traces, "x"));
216
+ limit(layout.yaxis, options.ymin, options.ymax, valuesOf(traces, "y"));
217
+ const top = !options.legendPlace || options.legendPlace === "top";
218
+ if (layout.title && layout.showlegend && top) layout.margin.t = Math.max(layout.margin.t, 56);
219
+ layout.uirevision = JSON.stringify([options.revision || "", traces.map((trace) => trace.name || ""),
220
+ Boolean(options.logx), Boolean(options.logy)]);
221
+ const extra = options.layout && typeof options.layout === "object" && !Array.isArray(options.layout)
222
+ ? copyOf(options.layout)
223
+ : null;
224
+ render(el, traces, extra ? merge(layout, extra) : layout);
225
+ }
226
+
227
+ function axisOf(table, options) {
228
+ if (options.x && Array.isArray(table[options.x])) return options.x;
229
+ return AXES.find((key) => Array.isArray(table[key])) || null;
230
+ }
231
+
232
+ export function linesOf(series, options = {}) {
233
+ if (Array.isArray(series)) return series.filter(Boolean);
234
+ if (!series || typeof series !== "object") return [];
235
+ const xKey = axisOf(series, options);
236
+ const wanted = options.keys || Object.keys(series).filter((key) => !AXES.includes(key) && Array.isArray(series[key]));
237
+ const x = xKey ? series[xKey] : null;
238
+ return wanted.filter((key) => Array.isArray(series[key])).map((key) => ({
239
+ name: key,
240
+ x: x || series[key].map((value, index) => index + 1),
241
+ y: series[key],
242
+ }));
243
+ }
244
+
245
+ export function smooth(values, weight) {
246
+ let last = 0;
247
+ let count = 0;
248
+ return (values || []).map((value) => {
249
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
250
+ last = last * weight + (1 - weight) * value;
251
+ count += 1;
252
+ return last / (1 - Math.pow(weight, count));
253
+ });
254
+ }
255
+
256
+ function marksOf(marks, colors) {
257
+ const found = Array.isArray(marks) ? marks.filter((mark) => mark && numberOf(mark.x) !== null) : [];
258
+ const labelled = found.length <= MARK_LABELS;
259
+ return {
260
+ shapes: found.map((mark) => ({
261
+ type: "line",
262
+ xref: "x",
263
+ yref: "paper",
264
+ x0: mark.x,
265
+ x1: mark.x,
266
+ y0: 0,
267
+ y1: 1,
268
+ line: { width: 1, dash: "dot", color: colors.muted },
269
+ })),
270
+ annotations: !labelled ? [] : found.map((mark) => ({
271
+ xref: "x",
272
+ yref: "paper",
273
+ x: mark.x,
274
+ y: 1,
275
+ text: String(mark.text || ""),
276
+ showarrow: false,
277
+ textangle: -90,
278
+ xanchor: "left",
279
+ yanchor: "top",
280
+ font: { size: 9, color: colors.muted },
281
+ })),
282
+ };
283
+ }
284
+
285
+ function modeOf(mode, fallback, count) {
286
+ if (MODES.includes(mode)) return mode;
287
+ return fallback || (count <= 40 ? "lines+markers" : "lines");
288
+ }
289
+
290
+ function dashOf(dash, fallback) {
291
+ return DASHES.includes(dash) ? dash : fallback;
292
+ }
293
+
294
+ export function lines(el, series, options = {}) {
295
+ const colors = themeColors(options.palette);
296
+ const found = linesOf(series, options);
297
+ const weight = between(options.smoothing, 0, 0.99, 0);
298
+ const opacity = between(options.opacity, 0.05, 1, 1);
299
+ const traces = [];
300
+ found.forEach((line, index) => {
301
+ const count = (line.y || []).length;
302
+ const mode = modeOf(options.mode, line.mode, count);
303
+ const style = {
304
+ width: between(options.width, 0.5, 12, 2),
305
+ color: line.color || colors.series[index % colors.series.length],
306
+ dash: dashOf(options.dash, line.dash),
307
+ };
308
+ const marker = { size: between(options.size, 1, 30, 6) };
309
+ const trace = {
310
+ type: "scatter",
311
+ mode,
312
+ name: line.name,
313
+ x: line.x,
314
+ y: line.y,
315
+ connectgaps: true,
316
+ opacity,
317
+ line: style,
318
+ marker,
319
+ hovertemplate: "%{y:.4g}",
320
+ };
321
+ if (weight > 0) {
322
+ const group = "line " + index;
323
+ traces.push({
324
+ type: "scatter",
325
+ mode: mode === "markers" ? "markers" : "lines",
326
+ name: line.name,
327
+ x: line.x,
328
+ y: line.y,
329
+ connectgaps: true,
330
+ opacity: opacity * 0.25,
331
+ line: { ...style },
332
+ marker: { ...marker },
333
+ legendgroup: group,
334
+ showlegend: false,
335
+ hoverinfo: "skip",
336
+ });
337
+ trace.y = smooth(line.y, weight);
338
+ trace.customdata = line.y;
339
+ trace.legendgroup = group;
340
+ trace.hovertemplate = "%{y:.4g} (raw %{customdata:.4g})";
341
+ }
342
+ traces.push(trace);
343
+ });
344
+ const layout = baseLayout({ xlabel: options.xlabel || axisOf(series || {}, options) || "", ...options });
345
+ layout.hovermode = "x unified";
346
+ layout.showlegend = options.legend !== undefined ? Boolean(options.legend) : found.length > 1;
347
+ if (weight > 0) layout.legend.traceorder = "normal";
348
+ const marks = marksOf(options.marks, colors);
349
+ if (marks.shapes.length) {
350
+ layout.shapes = marks.shapes;
351
+ layout.annotations = marks.annotations;
352
+ }
353
+ finish(el, traces, layout, options);
354
+ }
355
+
356
+ function pointsOf(points) {
357
+ if (Array.isArray(points)) return points.filter(Boolean);
358
+ return points && typeof points === "object" ? [points] : [];
359
+ }
360
+
361
+ function extent(groups, positive) {
362
+ const values = [];
363
+ for (const group of groups) {
364
+ for (const value of [...(group.x || []), ...(group.y || [])]) {
365
+ if (typeof value === "number" && Number.isFinite(value)) values.push(value);
366
+ }
367
+ }
368
+ return spread(values, positive);
369
+ }
370
+
371
+ function diagonal(groups, logx, logy) {
372
+ const range = extent(groups, Boolean(logx || logy));
373
+ if (!range) return null;
374
+ const [low, high] = range;
375
+ if (Boolean(logx) === Boolean(logy) || !(high > low)) return [low, high];
376
+ const steps = [];
377
+ for (let index = 0; index <= SAMPLES; index += 1) {
378
+ const at = index / SAMPLES;
379
+ steps.push(low + at * (high - low), low * Math.pow(high / low, at));
380
+ }
381
+ return [...new Set(steps)].sort((first, second) => first - second);
382
+ }
383
+
384
+ function hoverOf(values, letter) {
385
+ const named = (values || []).some((value) => value !== null && value !== undefined && typeof value !== "number");
386
+ return named ? "%{" + letter + "}" : "%{" + letter + ":.4g}";
387
+ }
388
+
389
+ function sized(size, own, fallback) {
390
+ const wanted = numberOf(size);
391
+ if (wanted === null) return own || fallback;
392
+ const ratio = Math.min(30, Math.max(1, wanted)) / fallback;
393
+ if (Array.isArray(own)) return own.map((value) => value * ratio);
394
+ return typeof own === "number" ? own * ratio : ratio * fallback;
395
+ }
396
+
397
+ export function scatter(el, points, options = {}) {
398
+ const colors = themeColors(options.palette);
399
+ const groups = pointsOf(points);
400
+ const traces = groups.map((group, index) => ({
401
+ type: group.x && group.x.length > 5000 ? "scattergl" : "scatter",
402
+ mode: "markers",
403
+ name: group.name || "",
404
+ x: group.x,
405
+ y: group.y,
406
+ text: group.text,
407
+ marker: {
408
+ size: sized(options.size, group.size, 8),
409
+ color: group.color || colors.series[index % colors.series.length],
410
+ opacity: between(options.opacity, 0.05, 1, group.opacity || 0.85),
411
+ line: { width: 1, color: colors.surface },
412
+ },
413
+ hovertemplate: (group.text ? "%{text}<br>" : "") + hoverOf(group.x, "x") + ", " + hoverOf(group.y, "y")
414
+ + "<extra></extra>",
415
+ }));
416
+ const line = options.diagonal ? diagonal(groups, options.logx, options.logy) : null;
417
+ if (line) {
418
+ traces.push({
419
+ type: "scatter", mode: "lines", name: "y = x", x: line, y: line, hoverinfo: "skip",
420
+ line: { width: 1, dash: "dot", color: colors.muted },
421
+ });
422
+ }
423
+ const layout = baseLayout(options);
424
+ layout.hovermode = "closest";
425
+ layout.showlegend = options.legend !== undefined ? Boolean(options.legend) : groups.length > 1;
426
+ finish(el, traces, layout, options);
427
+ }
428
+
429
+ function barsOf(edges, counts) {
430
+ if (Array.isArray(edges) && (edges.length === 0 || typeof edges[0] === "number")) {
431
+ return [{ edges, counts: counts || [] }];
432
+ }
433
+ if (Array.isArray(edges)) return edges.filter(Boolean);
434
+ return edges && typeof edges === "object" ? [edges] : [];
435
+ }
436
+
437
+ export function histogram(el, edges, counts, options) {
438
+ const numeric = Array.isArray(edges) && (edges.length === 0 || typeof edges[0] === "number");
439
+ const settings = (numeric ? options : counts) || {};
440
+ const colors = themeColors(settings.palette);
441
+ const bars = barsOf(edges, numeric ? counts : null);
442
+ const traces = bars.map((bar, index) => {
443
+ const sides = bar.edges || [];
444
+ const centers = sides.slice(0, -1).map((edge, position) => (edge + sides[position + 1]) / 2);
445
+ const widths = sides.slice(0, -1).map((edge, position) => sides[position + 1] - edge);
446
+ return {
447
+ type: "bar",
448
+ name: bar.name || "",
449
+ x: centers,
450
+ y: bar.counts || [],
451
+ width: widths,
452
+ opacity: between(settings.opacity, 0.05, 1, bars.length > 1 ? 0.6 : 0.9),
453
+ marker: { color: bar.color || colors.series[index % colors.series.length], line: { width: 0 } },
454
+ hovertemplate: "%{x:.4g}: %{y}<extra></extra>",
455
+ };
456
+ });
457
+ const layout = baseLayout({ ylabel: "count", ...settings });
458
+ layout.barmode = "overlay";
459
+ layout.bargap = 0;
460
+ layout.showlegend = settings.legend !== undefined ? Boolean(settings.legend) : bars.length > 1;
461
+ finish(el, traces, layout, settings);
462
+ }
463
+
464
+ function cellsOf(matrix, labels) {
465
+ if (Array.isArray(matrix)) {
466
+ const names = Array.isArray(labels) ? labels : (labels && labels.x) || null;
467
+ const rows = Array.isArray(labels) ? labels : (labels && labels.y) || names;
468
+ return { z: matrix, x: names, y: rows };
469
+ }
470
+ const data = matrix || {};
471
+ const z = data.z || data.matrix || [];
472
+ const x = data.x || data.labels || null;
473
+ return { z, x, y: data.y || x };
474
+ }
475
+
476
+ export function heatmap(el, matrix, labels, options) {
477
+ const settings = (Array.isArray(matrix) ? options : labels) || {};
478
+ const cells = cellsOf(matrix, Array.isArray(matrix) ? labels : null);
479
+ const colors = themeColors(settings.palette);
480
+ const x = cells.x ? cells.x.map(String) : undefined;
481
+ const y = cells.y ? cells.y.map(String) : undefined;
482
+ const traces = [{
483
+ type: "heatmap",
484
+ z: cells.z,
485
+ x,
486
+ y,
487
+ colorscale: [[0, colors.surface], [1, settings.color || colors.series[0]]],
488
+ showscale: settings.scale !== undefined ? Boolean(settings.scale) : false,
489
+ texttemplate: settings.text === false ? undefined : "%{z}",
490
+ hovertemplate: "%{y} / %{x}: %{z}<extra></extra>",
491
+ opacity: between(settings.opacity, 0.05, 1, 1),
492
+ xgap: 2,
493
+ ygap: 2,
494
+ }];
495
+ const flat = { ...settings, logx: false, logy: false, xmin: null, xmax: null, ymin: null, ymax: null };
496
+ const layout = baseLayout({ xlabel: "predicted", ylabel: "truth", ...flat });
497
+ layout.xaxis.type = "category";
498
+ layout.yaxis.type = "category";
499
+ layout.yaxis.autorange = "reversed";
500
+ layout.xaxis.gridcolor = "rgba(0,0,0,0)";
501
+ layout.yaxis.gridcolor = "rgba(0,0,0,0)";
502
+ finish(el, traces, layout, flat);
503
+ }
504
+
505
+ export function bars(el, data, options = {}) {
506
+ const colors = themeColors(options.palette);
507
+ const labels = (data && (data.labels || data.x)) || [];
508
+ const values = (data && (data.values || data.y)) || [];
509
+ const horizontal = options.horizontal !== false;
510
+ const traces = [{
511
+ type: "bar",
512
+ orientation: horizontal ? "h" : "v",
513
+ x: horizontal ? values : labels.map(String),
514
+ y: horizontal ? labels.map(String) : values,
515
+ opacity: between(options.opacity, 0.05, 1, 1),
516
+ marker: { color: (data && data.color) || colors.series[0] },
517
+ hovertemplate: horizontal ? "%{y}: %{x:.4g}<extra></extra>" : "%{x}: %{y:.4g}<extra></extra>",
518
+ }];
519
+ const layout = baseLayout(options);
520
+ if (horizontal) {
521
+ layout.yaxis.type = "category";
522
+ layout.yaxis.autorange = "reversed";
523
+ } else {
524
+ layout.xaxis.type = "category";
525
+ }
526
+ layout.showlegend = false;
527
+ finish(el, traces, layout, options);
528
+ }
529
+
530
+ export function parallel(el, data, options = {}) {
531
+ const colors = themeColors(options.palette);
532
+ const axes = data && Array.isArray(data.axes) ? data.axes : [];
533
+ const last = axes.length ? axes[axes.length - 1] : null;
534
+ const dimensions = axes.map((item) => {
535
+ const dimension = { label: String(item.label || item.key || ""), values: item.values || [] };
536
+ if (Array.isArray(item.ticks) && Array.isArray(item.text)) {
537
+ dimension.tickvals = item.ticks;
538
+ dimension.ticktext = item.text;
539
+ }
540
+ return dimension;
541
+ });
542
+ const traces = [{
543
+ type: "parcoords",
544
+ dimensions,
545
+ line: {
546
+ color: last ? last.values : [],
547
+ colorscale: "Viridis",
548
+ reversescale: Boolean(data && data.mode === "min"),
549
+ showscale: true,
550
+ },
551
+ labelfont: { color: colors.ink, size: 11 },
552
+ tickfont: { color: colors.muted, size: 10 },
553
+ rangefont: { color: colors.muted, size: 9 },
554
+ }];
555
+ const flat = { ...options, logx: false, logy: false, xmin: null, xmax: null, ymin: null, ymax: null };
556
+ const layout = baseLayout(flat);
557
+ delete layout.xaxis;
558
+ delete layout.yaxis;
559
+ layout.margin = { l: 64, r: 64, t: 56, b: 28 };
560
+ layout.showlegend = false;
561
+ finish(el, traces, layout, flat);
562
+ }
563
+
564
+ function fileName(name) {
565
+ return String(name || "chart").trim().replace(/[^\w.-]+/g, "_").replace(/^_+|_+$/g, "") || "chart";
566
+ }
567
+
568
+ export function exportImage(el, options = {}) {
569
+ const Plotly = plotly();
570
+ if (!Plotly || !el || !el.data) return Promise.reject(new Error("the chart is not drawn yet"));
571
+ try {
572
+ const colors = themeColors();
573
+ const format = options.format === "svg" ? "svg" : "png";
574
+ const width = Math.round(between(options.width || null, 100, 8000, el.clientWidth || 900));
575
+ const height = Math.round(between(options.height || null, 100, 8000, el.clientHeight || 600));
576
+ const scale = between(options.scale || null, 0.25, 8, format === "svg" ? 1 : 2);
577
+ const layout = { ...copyOf(el.layout || {}), width, height, autosize: false,
578
+ paper_bgcolor: colors.surface, plot_bgcolor: colors.surface };
579
+ const figure = { data: copyOf(el.data), layout, config: { displaylogo: false } };
580
+ const done = Plotly.downloadImage(figure, { format, width, height, scale, filename: fileName(options.filename) });
581
+ return Promise.resolve(done);
582
+ } catch (error) {
583
+ return Promise.reject(error);
584
+ }
585
+ }
586
+
587
+ export function draw(kind, el, data, options = {}) {
588
+ if (kind === "scatter") return scatter(el, data, options);
589
+ if (kind === "histogram") return histogram(el, data, options);
590
+ if (kind === "heatmap") return heatmap(el, data, options);
591
+ if (kind === "bars") return bars(el, data, options);
592
+ if (kind === "parallel") return parallel(el, data, options);
593
+ return lines(el, data, options);
594
+ }
@@ -0,0 +1,20 @@
1
+ .pred-select { width: auto; max-width: 320px; }
2
+ .pred-overview table.data tbody tr { cursor: pointer; }
3
+ .pred-overview table.data tbody tr.selected td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
4
+ .pred-pairs { gap: 6px; }
5
+ .pred-task { display: inline-block; padding: 0 8px; border-radius: 10px; font-size: 11px; line-height: 18px; font-weight: 600; white-space: nowrap; vertical-align: 1px; --tone: var(--muted); color: var(--tone); background: color-mix(in srgb, var(--tone) 14%, transparent); border: 1px solid color-mix(in srgb, var(--tone) 35%, transparent); }
6
+ .pred-task.is-binary { --tone: var(--accent); }
7
+ .pred-task.is-multiclass { --tone: var(--series-2, var(--accent)); }
8
+ .pred-task.is-regression { --tone: var(--done); }
9
+ .pred-task.is-embedding { --tone: var(--series-7, var(--muted)); }
10
+ .pred-task.is-values { --tone: var(--stopped); }
11
+ .pred-controls { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 8px 12px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); }
12
+ .pred-control { display: inline-flex; align-items: center; gap: 6px; }
13
+ .pred-threshold input[type="range"] { width: min(260px, 50vw); accent-color: var(--accent); }
14
+ .pred-gap { margin-left: 12px; }
15
+ .pred-gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 10px; }
16
+ .pred-shot { margin: 0; display: flex; flex-direction: column; gap: 4px; padding: 6px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface); min-width: 0; }
17
+ .pred-shot img { width: 100%; height: 116px; object-fit: contain; image-rendering: pixelated; background: var(--surface-2); border-radius: 4px; }
18
+ .pred-shot figcaption { display: flex; flex-direction: column; font-size: 11px; line-height: 1.35; overflow-wrap: anywhere; }
19
+ .pred-shot.wrong { border-color: color-mix(in srgb, var(--failed) 60%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--failed) 35%, transparent); }
20
+ .pred-shot.wrong figcaption .mono { color: var(--failed); }