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,17 @@
1
+ export function messageOf(error) {
2
+ if (!error) return "";
3
+ if (typeof error === "string") return error;
4
+ return error.message || String(error);
5
+ }
6
+
7
+ export const ErrorBox = {
8
+ name: "KError",
9
+ props: { error: { default: null } },
10
+ setup(props) {
11
+ const message = () => messageOf(props.error);
12
+ return { message };
13
+ },
14
+ template: '<div v-if="message()" class="error-box" role="alert">{{ message() }}</div>',
15
+ };
16
+
17
+ export default ErrorBox;
@@ -0,0 +1,7 @@
1
+ export const Loading = {
2
+ name: "KLoading",
3
+ props: { label: { type: String, default: "loading" } },
4
+ template: '<div class="loading">{{ label }}</div>',
5
+ };
6
+
7
+ export default Loading;
@@ -0,0 +1,25 @@
1
+ import { fraction, isNumber, share } from "../format.js";
2
+
3
+
4
+ export const Progress = {
5
+ name: "KProgress",
6
+ props: {
7
+ value: { type: Number, default: 0 },
8
+ total: { type: Number, default: null },
9
+ label: { type: String, default: "" },
10
+ part: { type: Number, default: null },
11
+ },
12
+ setup(props) {
13
+ const part = () => (isNumber(props.part) ? Math.max(0, Math.min(1, props.part)) : share(props.value, props.total));
14
+ const width = () => (part() === null ? "0%" : (100 * part()).toFixed(1) + "%");
15
+ const caption = () => props.label || fraction(props.value, props.total);
16
+ return { part, width, caption };
17
+ },
18
+ template: `
19
+ <span class="progress" :class="{ unknown: part() === null }">
20
+ <span class="progress-track"><span class="progress-fill" :style="{ width: width() }"></span></span>
21
+ <span class="progress-text">{{ caption() }}</span>
22
+ </span>`,
23
+ };
24
+
25
+ export default Progress;
@@ -0,0 +1,418 @@
1
+ import { computed, reactive, ref, watch }
2
+ from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
3
+
4
+ import { PALETTES, numberOf, themeColors } from "../plot.js";
5
+ import { store } from "../store.js";
6
+
7
+
8
+ const PREFIX = "kalfa.chart.";
9
+ const shared = new Map();
10
+
11
+ export const PALETTE_NAMES = Object.keys(PALETTES);
12
+ export const PLACES = ["top", "bottom", "right", "inside"];
13
+
14
+ const CHOICES = {
15
+ xscale: ["", "linear", "log"],
16
+ yscale: ["", "linear", "log"],
17
+ dash: ["", "solid", "dash", "dot"],
18
+ mode: ["auto", "lines", "markers", "lines+markers"],
19
+ legend: ["auto", "on", "off"],
20
+ place: PLACES,
21
+ palette: PALETTE_NAMES,
22
+ };
23
+
24
+ export function blankSettings() {
25
+ return {
26
+ xscale: "",
27
+ yscale: "",
28
+ xmin: "",
29
+ xmax: "",
30
+ ymin: "",
31
+ ymax: "",
32
+ xtitle: "",
33
+ ytitle: "",
34
+ grid: true,
35
+ width: "",
36
+ dash: "",
37
+ mode: "auto",
38
+ size: "",
39
+ opacity: "",
40
+ smoothing: 0,
41
+ bins: "",
42
+ title: "",
43
+ legend: "auto",
44
+ place: "top",
45
+ palette: "theme",
46
+ boxWidth: "",
47
+ boxHeight: "",
48
+ imageWidth: "",
49
+ imageHeight: "",
50
+ imageScale: "",
51
+ layout: "",
52
+ };
53
+ }
54
+
55
+ function cleaned(found) {
56
+ const settings = blankSettings();
57
+ if (!found || typeof found !== "object" || Array.isArray(found)) return settings;
58
+ for (const key of Object.keys(settings)) {
59
+ const value = found[key];
60
+ if (typeof settings[key] === "boolean") {
61
+ if (typeof value === "boolean") settings[key] = value;
62
+ } else if (typeof value === "string" || (typeof value === "number" && Number.isFinite(value))) {
63
+ settings[key] = value;
64
+ }
65
+ }
66
+ for (const [key, allowed] of Object.entries(CHOICES)) {
67
+ if (!allowed.includes(settings[key])) settings[key] = blankSettings()[key];
68
+ }
69
+ settings.smoothing = Math.min(0.99, Math.max(0, numberOf(settings.smoothing) || 0));
70
+ return settings;
71
+ }
72
+
73
+ export function changedSettings(settings) {
74
+ const blank = blankSettings();
75
+ const found = {};
76
+ for (const key of Object.keys(blank)) {
77
+ if (settings && settings[key] !== undefined && settings[key] !== blank[key]) found[key] = settings[key];
78
+ }
79
+ return found;
80
+ }
81
+
82
+ export function isTuned(settings) {
83
+ return Object.keys(changedSettings(settings)).length > 0;
84
+ }
85
+
86
+ export function loadSettings(key) {
87
+ try {
88
+ const text = window.localStorage.getItem(PREFIX + key);
89
+ return cleaned(text ? JSON.parse(text) : null);
90
+ } catch (error) {
91
+ return blankSettings();
92
+ }
93
+ }
94
+
95
+ export function saveSettings(key, settings) {
96
+ const changed = changedSettings(settings);
97
+ try {
98
+ if (Object.keys(changed).length) window.localStorage.setItem(PREFIX + key, JSON.stringify(changed));
99
+ else window.localStorage.removeItem(PREFIX + key);
100
+ } catch (error) {
101
+ return;
102
+ }
103
+ }
104
+
105
+ export function chartSettings(key) {
106
+ if (!shared.has(key)) shared.set(key, reactive(loadSettings(key)));
107
+ return shared.get(key);
108
+ }
109
+
110
+ export function forgetSettings(key) {
111
+ Object.assign(chartSettings(key), blankSettings());
112
+ try {
113
+ window.localStorage.removeItem(PREFIX + key);
114
+ } catch (error) {
115
+ return;
116
+ }
117
+ }
118
+
119
+ export function parseLayout(text) {
120
+ const trimmed = String(text || "").trim();
121
+ if (!trimmed) return { layout: null, error: "" };
122
+ try {
123
+ const parsed = JSON.parse(trimmed);
124
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return { layout: parsed, error: "" };
125
+ return { layout: null, error: "the layout must be a JSON object, like {\"margin\": {\"l\": 80}}" };
126
+ } catch (error) {
127
+ return { layout: null, error: "not valid JSON yet: " + error.message };
128
+ }
129
+ }
130
+
131
+ export function drawOptions(settings) {
132
+ const found = {};
133
+ if (settings.xscale) found.logx = settings.xscale === "log";
134
+ if (settings.yscale) found.logy = settings.yscale === "log";
135
+ for (const key of ["xmin", "xmax", "ymin", "ymax", "width", "size", "opacity"]) {
136
+ const value = numberOf(settings[key]);
137
+ if (value !== null) found[key] = value;
138
+ }
139
+ const smoothing = numberOf(settings.smoothing);
140
+ if (smoothing) found.smoothing = smoothing;
141
+ if (settings.xtitle) found.xlabel = settings.xtitle;
142
+ if (settings.ytitle) found.ylabel = settings.ytitle;
143
+ if (settings.grid === false) found.grid = false;
144
+ if (settings.dash) found.dash = settings.dash;
145
+ if (settings.mode && settings.mode !== "auto") found.mode = settings.mode;
146
+ if (settings.title) found.title = settings.title;
147
+ if (settings.legend === "on" || settings.legend === "off") found.legend = settings.legend === "on";
148
+ if (settings.place && settings.place !== "top") found.legendPlace = settings.place;
149
+ if (settings.palette && settings.palette !== "theme") found.palette = settings.palette;
150
+ const layout = parseLayout(settings.layout).layout;
151
+ if (layout) found.layout = layout;
152
+ return found;
153
+ }
154
+
155
+ export function payloadOf(key, settings, options) {
156
+ const found = drawOptions(settings);
157
+ const base = options || {};
158
+ const bins = numberOf(settings.bins);
159
+ return {
160
+ key,
161
+ logx: found.logx !== undefined ? found.logx : Boolean(base.logx),
162
+ logy: found.logy !== undefined ? found.logy : Boolean(base.logy),
163
+ xmin: numberOf(settings.xmin),
164
+ xmax: numberOf(settings.xmax),
165
+ ymin: numberOf(settings.ymin),
166
+ ymax: numberOf(settings.ymax),
167
+ bins: bins !== null && bins >= 1 ? Math.round(bins) : null,
168
+ smoothing: numberOf(settings.smoothing) || 0,
169
+ settings: changedSettings(settings),
170
+ };
171
+ }
172
+
173
+ function featuresOf(kind, horizontal, binsBox) {
174
+ const grid = kind !== "heatmap" && kind !== "parallel";
175
+ return {
176
+ x: grid && !(kind === "bars" && !horizontal),
177
+ y: grid && !(kind === "bars" && horizontal),
178
+ grid,
179
+ line: kind === "lines",
180
+ marker: kind === "lines" || kind === "scatter",
181
+ bins: kind === "histogram" || Boolean(binsBox),
182
+ };
183
+ }
184
+
185
+ function defaultsOf(kind) {
186
+ return {
187
+ width: 2,
188
+ size: kind === "scatter" ? 8 : 6,
189
+ opacity: kind === "scatter" ? 0.85 : kind === "histogram" ? 0.9 : 1,
190
+ };
191
+ }
192
+
193
+ export const ChartSettings = {
194
+ name: "KChartSettings",
195
+ props: {
196
+ settings: { type: Object, required: true },
197
+ kind: { type: String, default: "lines" },
198
+ auto: { type: Object, default: () => ({}) },
199
+ size: { type: Object, default: () => ({}) },
200
+ binsBox: { type: Boolean, default: false },
201
+ },
202
+ emits: ["reset", "download"],
203
+ setup(props, { emit }) {
204
+ const draft = ref(props.settings.layout || "");
205
+ const advanced = ref(Boolean(props.settings.layout));
206
+ const palettes = PALETTE_NAMES;
207
+ const places = PLACES;
208
+ const has = computed(() => featuresOf(props.kind, props.auto.horizontal !== false, props.binsBox));
209
+ const defaults = computed(() => defaultsOf(props.kind));
210
+ const swatches = computed(() => (store.paint >= 0 ? themeColors(props.settings.palette).series : []));
211
+ const layoutError = computed(() => parseLayout(draft.value).error);
212
+
213
+ watch(() => props.settings.layout, (value) => {
214
+ if (value !== draft.value) draft.value = value || "";
215
+ });
216
+
217
+ const scaleText = (log) => "auto (" + (log ? "log" : "linear") + ")";
218
+ const shown = (name) => (props.settings[name] === "" ? defaults.value[name] : props.settings[name]);
219
+ const readout = (name) => (props.settings[name] === "" ? "auto" : String(props.settings[name]));
220
+ const put = (name, event) => {
221
+ props.settings[name] = Number(event.target.value);
222
+ };
223
+ const clear = (name) => {
224
+ props.settings[name] = "";
225
+ };
226
+ const fit = (value) => (value ? "fit, " + value : "fit");
227
+ const typed = (event) => {
228
+ draft.value = event.target.value;
229
+ if (!parseLayout(draft.value).error) props.settings.layout = draft.value.trim() ? draft.value : "";
230
+ };
231
+ const opened = (event) => {
232
+ advanced.value = event.target.open;
233
+ };
234
+ const resetAll = () => {
235
+ draft.value = "";
236
+ emit("reset");
237
+ };
238
+ const download = (format) => emit("download", format);
239
+ return {
240
+ draft, advanced, palettes, places, has, swatches, layoutError, scaleText, shown, readout, put, clear, fit,
241
+ typed, opened, resetAll, download,
242
+ };
243
+ },
244
+ template: `
245
+ <aside class="chart-panel" aria-label="chart settings">
246
+ <section class="chart-group">
247
+ <div class="chart-group-title">axes</div>
248
+ <div v-if="has.x" class="chart-row">
249
+ <span class="chart-label">x</span>
250
+ <select v-model="settings.xscale" aria-label="x scale">
251
+ <option value="">{{ scaleText(auto.logx) }}</option>
252
+ <option value="linear">linear</option>
253
+ <option value="log">log</option>
254
+ </select>
255
+ <input type="number" step="any" placeholder="min" aria-label="x minimum" v-model.lazy="settings.xmin">
256
+ <input type="number" step="any" placeholder="max" aria-label="x maximum" v-model.lazy="settings.xmax">
257
+ </div>
258
+ <div v-if="has.y" class="chart-row">
259
+ <span class="chart-label">y</span>
260
+ <select v-model="settings.yscale" aria-label="y scale">
261
+ <option value="">{{ scaleText(auto.logy) }}</option>
262
+ <option value="linear">linear</option>
263
+ <option value="log">log</option>
264
+ </select>
265
+ <input type="number" step="any" placeholder="min" aria-label="y minimum" v-model.lazy="settings.ymin">
266
+ <input type="number" step="any" placeholder="max" aria-label="y maximum" v-model.lazy="settings.ymax">
267
+ </div>
268
+ <div v-if="has.x || has.y" class="chart-hint">an empty range end follows the data</div>
269
+ <div class="chart-row">
270
+ <span class="chart-label">titles</span>
271
+ <input type="text" spellcheck="false" aria-label="x axis title" :placeholder="auto.xlabel || 'x axis'"
272
+ v-model.lazy="settings.xtitle">
273
+ <input type="text" spellcheck="false" aria-label="y axis title" :placeholder="auto.ylabel || 'y axis'"
274
+ v-model.lazy="settings.ytitle">
275
+ </div>
276
+ <label v-if="has.grid" class="chart-check"><input type="checkbox" v-model="settings.grid"> grid</label>
277
+ </section>
278
+
279
+ <section class="chart-group">
280
+ <div class="chart-group-title">{{ has.line ? 'lines and markers' : 'marks' }}</div>
281
+ <div v-if="has.line" class="chart-row">
282
+ <span class="chart-label">line width</span>
283
+ <input type="range" min="0.5" max="8" step="0.5" aria-label="line width" :value="shown('width')"
284
+ @input="put('width', $event)">
285
+ <button type="button" class="chart-auto" :disabled="settings.width === ''" title="back to auto"
286
+ @click="clear('width')">{{ readout('width') }}</button>
287
+ </div>
288
+ <div v-if="has.line" class="chart-row">
289
+ <span class="chart-label">line style</span>
290
+ <select v-model="settings.dash" aria-label="line style">
291
+ <option value="">auto</option>
292
+ <option value="solid">solid</option>
293
+ <option value="dash">dashed</option>
294
+ <option value="dot">dotted</option>
295
+ </select>
296
+ </div>
297
+ <div v-if="has.line" class="chart-row">
298
+ <span class="chart-label">draw</span>
299
+ <select v-model="settings.mode" aria-label="lines or markers">
300
+ <option value="auto">auto</option>
301
+ <option value="lines">lines</option>
302
+ <option value="markers">markers</option>
303
+ <option value="lines+markers">lines and markers</option>
304
+ </select>
305
+ </div>
306
+ <div v-if="has.marker" class="chart-row">
307
+ <span class="chart-label">marker size</span>
308
+ <input type="range" min="1" max="20" step="1" aria-label="marker size" :value="shown('size')"
309
+ @input="put('size', $event)">
310
+ <button type="button" class="chart-auto" :disabled="settings.size === ''" title="back to auto"
311
+ @click="clear('size')">{{ readout('size') }}</button>
312
+ </div>
313
+ <div class="chart-row">
314
+ <span class="chart-label">opacity</span>
315
+ <input type="range" min="0.05" max="1" step="0.05" aria-label="opacity" :value="shown('opacity')"
316
+ @input="put('opacity', $event)">
317
+ <button type="button" class="chart-auto" :disabled="settings.opacity === ''" title="back to auto"
318
+ @click="clear('opacity')">{{ readout('opacity') }}</button>
319
+ </div>
320
+ <div v-if="has.line" class="chart-row">
321
+ <span class="chart-label">smoothing</span>
322
+ <input type="range" min="0" max="0.99" step="0.01" aria-label="smoothing" v-model.number="settings.smoothing">
323
+ <button type="button" class="chart-auto" :disabled="!settings.smoothing" title="turn smoothing off"
324
+ @click="settings.smoothing = 0">{{ settings.smoothing ? settings.smoothing : 'off' }}</button>
325
+ </div>
326
+ <div v-if="has.line && settings.smoothing" class="chart-hint">exponential moving average; the raw line stays faint</div>
327
+ <div v-if="has.bins" class="chart-row">
328
+ <span class="chart-label">bins</span>
329
+ <input type="number" min="1" max="1000" step="1" placeholder="auto" aria-label="bins"
330
+ v-model.lazy="settings.bins">
331
+ </div>
332
+ <div v-if="has.bins" class="chart-hint">the server counts the rows again into this many bins</div>
333
+ </section>
334
+
335
+ <section class="chart-group">
336
+ <div class="chart-group-title">look</div>
337
+ <div class="chart-row">
338
+ <span class="chart-label">title</span>
339
+ <input type="text" spellcheck="false" aria-label="title on the chart" placeholder="none"
340
+ v-model.lazy="settings.title">
341
+ </div>
342
+ <div class="chart-row">
343
+ <span class="chart-label">legend</span>
344
+ <select v-model="settings.legend" aria-label="legend">
345
+ <option value="auto">auto</option>
346
+ <option value="on">on</option>
347
+ <option value="off">off</option>
348
+ </select>
349
+ <select v-model="settings.place" aria-label="legend place" :disabled="settings.legend === 'off'">
350
+ <option v-for="place in places" :key="place" :value="place">{{ place }}</option>
351
+ </select>
352
+ </div>
353
+ <div class="chart-row">
354
+ <span class="chart-label">palette</span>
355
+ <select v-model="settings.palette" aria-label="palette">
356
+ <option v-for="name in palettes" :key="name" :value="name">{{ name }}</option>
357
+ </select>
358
+ </div>
359
+ <div class="chart-swatches">
360
+ <span v-for="(color, index) in swatches" :key="index" class="chart-swatch" :style="{ background: color }"></span>
361
+ </div>
362
+ </section>
363
+
364
+ <section class="chart-group">
365
+ <div class="chart-group-title">this view</div>
366
+ <div class="chart-row">
367
+ <span class="chart-label">width</span>
368
+ <input type="number" min="200" step="10" aria-label="view width" :placeholder="fit(size.width)"
369
+ v-model.lazy="settings.boxWidth">
370
+ <span class="chart-unit">px</span>
371
+ </div>
372
+ <div class="chart-row">
373
+ <span class="chart-label">height</span>
374
+ <input type="number" min="200" step="10" aria-label="view height" :placeholder="fit(size.height)"
375
+ v-model.lazy="settings.boxHeight">
376
+ <span class="chart-unit">px</span>
377
+ </div>
378
+ </section>
379
+
380
+ <section class="chart-group">
381
+ <div class="chart-group-title">download</div>
382
+ <div class="chart-row">
383
+ <span class="chart-label">size</span>
384
+ <input type="number" min="100" step="10" aria-label="image width" :placeholder="String(size.width || 900)"
385
+ v-model.lazy="settings.imageWidth">
386
+ <span class="chart-unit">by</span>
387
+ <input type="number" min="100" step="10" aria-label="image height" :placeholder="String(size.height || 600)"
388
+ v-model.lazy="settings.imageHeight">
389
+ </div>
390
+ <div class="chart-row">
391
+ <span class="chart-label">scale</span>
392
+ <input type="number" min="0.25" max="8" step="0.25" aria-label="image scale" placeholder="2 for png, 1 for svg"
393
+ v-model.lazy="settings.imageScale">
394
+ </div>
395
+ <div class="chart-row chart-actions">
396
+ <span class="chart-label"></span>
397
+ <button type="button" class="tool" @click="download('png')">png</button>
398
+ <button type="button" class="tool" @click="download('svg')">svg</button>
399
+ </div>
400
+ </section>
401
+
402
+ <details class="chart-group" :open="advanced" @toggle="opened">
403
+ <summary class="chart-group-title">advanced</summary>
404
+ <div class="chart-hint">Plotly layout as JSON, merged over the layout of this chart</div>
405
+ <textarea rows="6" spellcheck="false" aria-label="Plotly layout JSON" :value="draft" @input="typed"
406
+ placeholder='{"yaxis": {"tickformat": ".1e"}, "margin": {"l": 80}}'></textarea>
407
+ <div v-if="layoutError" class="chart-error">{{ layoutError }}</div>
408
+ </details>
409
+
410
+ <div class="chart-row chart-actions">
411
+ <button type="button" class="tool chart-reset" @click="resetAll">reset</button>
412
+ <span class="chart-hint">saved in this browser for this chart</span>
413
+ </div>
414
+ <div class="chart-hint">Drag a box to zoom, double click to zoom out, click a legend entry to hide its series.</div>
415
+ </aside>`,
416
+ };
417
+
418
+ export default ChartSettings;
@@ -0,0 +1,43 @@
1
+ import { computed } from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
2
+
3
+
4
+ export function sparkPoints(values, width, height, count) {
5
+ const tail = (Array.isArray(values) ? values : []).slice(-count);
6
+ const finite = tail.map((value, index) => [index, value])
7
+ .filter((pair) => typeof pair[1] === "number" && Number.isFinite(pair[1]));
8
+ if (finite.length < 2) return "";
9
+ const numbers = finite.map((pair) => pair[1]);
10
+ const low = Math.min(...numbers);
11
+ const high = Math.max(...numbers);
12
+ const span = high - low || 1;
13
+ const last = Math.max(tail.length - 1, 1);
14
+ return finite.map((pair) => {
15
+ const x = 1 + (pair[0] / last) * (width - 2);
16
+ const y = height - 1 - ((pair[1] - low) / span) * (height - 2);
17
+ return x.toFixed(1) + "," + y.toFixed(1);
18
+ }).join(" ");
19
+ }
20
+
21
+ export const Spark = {
22
+ name: "KSpark",
23
+ props: {
24
+ values: { type: Array, default: () => [] },
25
+ width: { type: Number, default: 80 },
26
+ height: { type: Number, default: 20 },
27
+ count: { type: Number, default: 40 },
28
+ title: { type: String, default: "" },
29
+ },
30
+ setup(props) {
31
+ const points = computed(() => sparkPoints(props.values, props.width, props.height, props.count));
32
+ const box = computed(() => "0 0 " + props.width + " " + props.height);
33
+ return { points, box };
34
+ },
35
+ template: `
36
+ <svg class="spark" :width="width" :height="height" :viewBox="box" preserveAspectRatio="none" role="img">
37
+ <title v-if="title">{{ title }}</title>
38
+ <polyline v-if="points" :points="points" fill="none" stroke="currentColor" stroke-width="1.5"
39
+ stroke-linejoin="round" stroke-linecap="round"></polyline>
40
+ </svg>`,
41
+ };
42
+
43
+ export default Spark;
@@ -0,0 +1,156 @@
1
+ import { computed, ref } from "https://cdn.jsdelivr.net/npm/vue@3.4.38/dist/vue.esm-browser.prod.js";
2
+
3
+ import { EMPTY, number, text } from "../format.js";
4
+ import { Badge } from "./badge.js";
5
+ import { Progress } from "./progress.js";
6
+ import { Spark } from "./spark.js";
7
+
8
+
9
+ export function missing(value) {
10
+ return value === null || value === undefined || value === "" || (typeof value === "number" && Number.isNaN(value));
11
+ }
12
+
13
+ export function compareValues(first, second) {
14
+ if (typeof first === "number" && typeof second === "number") return first - second;
15
+ return String(first).localeCompare(String(second), undefined, { numeric: true, sensitivity: "base" });
16
+ }
17
+
18
+ export function valueOf(row, column) {
19
+ if (column.value) return column.value(row);
20
+ return row ? row[column.key] : undefined;
21
+ }
22
+
23
+ export function textOf(row, column) {
24
+ if (column.text) return column.text(row);
25
+ const value = valueOf(row, column);
26
+ if (missing(value)) return EMPTY;
27
+ return typeof value === "number" ? number(value) : text(value);
28
+ }
29
+
30
+ export function sortRows(rows, column, descending) {
31
+ if (!column) return rows;
32
+ const sign = descending ? -1 : 1;
33
+ return [...rows].sort((first, second) => {
34
+ const left = valueOf(first, column);
35
+ const right = valueOf(second, column);
36
+ const leftMissing = missing(left);
37
+ const rightMissing = missing(right);
38
+ if (leftMissing || rightMissing) return leftMissing === rightMissing ? 0 : leftMissing ? 1 : -1;
39
+ return compareValues(left, right) * sign;
40
+ });
41
+ }
42
+
43
+ export const Table = {
44
+ name: "KTable",
45
+ components: { KBadge: Badge, KProgress: Progress, KSpark: Spark },
46
+ props: {
47
+ columns: { type: Array, default: () => [] },
48
+ rows: { type: Array, default: () => [] },
49
+ rowKey: { type: String, default: "path" },
50
+ selectable: { type: Boolean, default: false },
51
+ canSelect: { type: Function, default: null },
52
+ selected: { type: Array, default: () => [] },
53
+ filterable: { type: Boolean, default: true },
54
+ placeholder: { type: String, default: "filter" },
55
+ sortKey: { type: String, default: "" },
56
+ sortDesc: { type: Boolean, default: false },
57
+ empty: { type: String, default: "nothing to show" },
58
+ rowClass: { type: Function, default: null },
59
+ },
60
+ emits: ["update:selected", "select"],
61
+ setup(props, { emit }) {
62
+ const needle = ref("");
63
+ const key = ref(props.sortKey);
64
+ const descending = ref(props.sortDesc);
65
+ const keyOf = (row) => String(row ? row[props.rowKey] : "");
66
+ const matches = (row, wanted) => props.columns.some((column) => String(textOf(row, column)).toLowerCase()
67
+ .includes(wanted));
68
+ const shown = computed(() => {
69
+ const wanted = needle.value.trim().toLowerCase();
70
+ const rows = (props.rows || []).filter((row) => !wanted || matches(row, wanted));
71
+ return sortRows(rows, props.columns.find((column) => column.key === key.value), descending.value);
72
+ });
73
+ const sortBy = (column) => {
74
+ if (column.sortable === false) return;
75
+ if (key.value === column.key) descending.value = !descending.value;
76
+ else {
77
+ key.value = column.key;
78
+ descending.value = Boolean(column.descending);
79
+ }
80
+ };
81
+ const arrow = (column) => (key.value === column.key ? (descending.value ? "▼" : "▲") : "");
82
+ const selectableRow = (row) => (props.canSelect ? Boolean(props.canSelect(row)) : true);
83
+ const isSelected = (row) => props.selected.includes(keyOf(row));
84
+ const publish = (keys) => {
85
+ const unique = [...new Set(keys)];
86
+ emit("update:selected", unique);
87
+ emit("select", unique);
88
+ };
89
+ const toggle = (row) => {
90
+ const id = keyOf(row);
91
+ publish(isSelected(row) ? props.selected.filter((item) => item !== id) : [...props.selected, id]);
92
+ };
93
+ const pickable = computed(() => shown.value.filter(selectableRow));
94
+ const allSelected = computed(() => pickable.value.length > 0 && pickable.value.every(isSelected));
95
+ const toggleAll = () => {
96
+ const ids = pickable.value.map(keyOf);
97
+ if (allSelected.value) publish(props.selected.filter((item) => !ids.includes(item)));
98
+ else publish([...props.selected, ...ids]);
99
+ };
100
+ const href = (row, column) => (column.href ? column.href(row) : "");
101
+ const cellClass = (column) => [column.numeric ? "num" : "", column.className || ""].join(" ").trim();
102
+ const classOf = (row) => [props.rowClass ? props.rowClass(row) : "", isSelected(row) ? "selected" : ""]
103
+ .join(" ").trim();
104
+ const progressOf = (row, column) => column.progress(row) || { value: 0, total: null };
105
+ const titleOf = (row, column) => (column.title ? column.title(row) : "");
106
+ const count = computed(() => shown.value.length);
107
+ return {
108
+ needle, shown, sortBy, arrow, selectableRow, isSelected, toggle, allSelected, toggleAll, href, cellClass,
109
+ classOf, progressOf, titleOf, count, keyOf, valueOf, textOf,
110
+ };
111
+ },
112
+ template: `
113
+ <div class="table-box">
114
+ <div class="table-tools" v-if="filterable">
115
+ <input class="filter" type="search" v-model="needle" :placeholder="placeholder">
116
+ <span class="muted small">{{ count }} of {{ rows.length }}</span>
117
+ <slot name="tools"></slot>
118
+ </div>
119
+ <div class="table-wrap">
120
+ <table class="data">
121
+ <thead>
122
+ <tr>
123
+ <th v-if="selectable" class="check">
124
+ <input type="checkbox" :checked="allSelected" @change="toggleAll" title="select every shown row">
125
+ </th>
126
+ <th v-for="column in columns" :key="column.key" :class="[cellClass(column), column.sortable === false ? '' : 'sortable']"
127
+ @click="sortBy(column)" :title="column.hint || ''">
128
+ {{ column.label || column.key }} <span class="arrow">{{ arrow(column) }}</span>
129
+ </th>
130
+ </tr>
131
+ </thead>
132
+ <tbody>
133
+ <tr v-for="row in shown" :key="keyOf(row)" :class="classOf(row)">
134
+ <td v-if="selectable" class="check">
135
+ <input v-if="selectableRow(row)" type="checkbox" :checked="isSelected(row)" @change="toggle(row)">
136
+ </td>
137
+ <td v-for="column in columns" :key="column.key" :class="cellClass(column)" :title="titleOf(row, column)">
138
+ <k-badge v-if="column.badge" :status="valueOf(row, column)"></k-badge>
139
+ <k-progress v-else-if="column.progress" :value="progressOf(row, column).value"
140
+ :total="progressOf(row, column).total" :label="progressOf(row, column).label || ''"
141
+ :part="progressOf(row, column).part"></k-progress>
142
+ <k-spark v-else-if="column.spark" :values="column.spark(row)"></k-spark>
143
+ <a v-else-if="href(row, column)" class="link" :href="href(row, column)">{{ textOf(row, column) }}</a>
144
+ <span v-else>{{ textOf(row, column) }}</span>
145
+ </td>
146
+ </tr>
147
+ <tr v-if="!shown.length">
148
+ <td class="empty" :colspan="columns.length + (selectable ? 1 : 0)">{{ empty }}</td>
149
+ </tr>
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+ </div>`,
154
+ };
155
+
156
+ export default Table;