@cfasim-ui/docs 0.4.8 → 0.4.10
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/charts/LineChart/LineChart.md +59 -0
- package/index.json +3 -1
- package/package.json +1 -1
- package/pyodide/pyodide.worker.ts +7 -2
- package/pyodide/pyodideWorkerApi.ts +79 -30
- package/wasm/wasm.worker.ts +1 -1
- package/wasm/wasmWorkerApi.ts +45 -5
|
@@ -355,6 +355,53 @@ floor, and `yMin <= 0` is ignored.
|
|
|
355
355
|
</template>
|
|
356
356
|
</ComponentDemo>
|
|
357
357
|
|
|
358
|
+
### Confidence band
|
|
359
|
+
|
|
360
|
+
Use the `areas` prop to fill a band between two y-series — useful for
|
|
361
|
+
confidence intervals or min/max envelopes around a mean line. Each `Area`
|
|
362
|
+
takes parallel `upper` and `lower` arrays (and an optional `x` array, just
|
|
363
|
+
like `Series`).
|
|
364
|
+
|
|
365
|
+
<ComponentDemo>
|
|
366
|
+
<LineChart
|
|
367
|
+
:data="[0, 4, 8, 15, 22, 30, 28, 20, 12, 5, 2]"
|
|
368
|
+
:areas="[
|
|
369
|
+
{
|
|
370
|
+
upper: [2, 8, 14, 22, 30, 38, 36, 28, 19, 11, 7],
|
|
371
|
+
lower: [0, 1, 3, 9, 15, 22, 21, 13, 6, 1, 0],
|
|
372
|
+
color: '#0057b7',
|
|
373
|
+
opacity: 0.15,
|
|
374
|
+
},
|
|
375
|
+
]"
|
|
376
|
+
:height="220"
|
|
377
|
+
x-label="Days"
|
|
378
|
+
y-label="Cases"
|
|
379
|
+
tooltip-trigger="hover"
|
|
380
|
+
/>
|
|
381
|
+
|
|
382
|
+
<template #code>
|
|
383
|
+
|
|
384
|
+
```vue
|
|
385
|
+
<LineChart
|
|
386
|
+
:data="mean"
|
|
387
|
+
:areas="[
|
|
388
|
+
{
|
|
389
|
+
upper: ci95Hi,
|
|
390
|
+
lower: ci95Lo,
|
|
391
|
+
color: '#0057b7',
|
|
392
|
+
opacity: 0.15,
|
|
393
|
+
},
|
|
394
|
+
]"
|
|
395
|
+
:height="220"
|
|
396
|
+
x-label="Days"
|
|
397
|
+
y-label="Cases"
|
|
398
|
+
tooltip-trigger="hover"
|
|
399
|
+
/>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
</template>
|
|
403
|
+
</ComponentDemo>
|
|
404
|
+
|
|
358
405
|
### Area sections
|
|
359
406
|
|
|
360
407
|
Highlight a range of a series line by filling the area between the line and the x-axis. Labels are rendered below the chart and automatically stack when they overlap.
|
|
@@ -749,6 +796,18 @@ interface Series {
|
|
|
749
796
|
}
|
|
750
797
|
```
|
|
751
798
|
|
|
799
|
+
### Area
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
interface Area {
|
|
803
|
+
upper: LineChartData;
|
|
804
|
+
lower: LineChartData;
|
|
805
|
+
x?: LineChartData; // optional parallel x-values
|
|
806
|
+
color?: string;
|
|
807
|
+
opacity?: number;
|
|
808
|
+
}
|
|
809
|
+
```
|
|
810
|
+
|
|
752
811
|
### AreaSection
|
|
753
812
|
|
|
754
813
|
```ts
|
package/index.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
postWithTransfer,
|
|
3
3
|
postModelOutputsWithTransfer,
|
|
4
4
|
postErrorWithTransfer,
|
|
5
|
-
} from "@cfasim-ui/shared";
|
|
5
|
+
} from "@cfasim-ui/shared/transfer";
|
|
6
6
|
import type { ColumnDescriptor, ModelOutputsWire } from "@cfasim-ui/shared";
|
|
7
7
|
|
|
8
8
|
interface RunMessage {
|
|
@@ -231,12 +231,17 @@ function send(id: number, result: unknown) {
|
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
// Silence the unhandled-rejection warning if init fails before any message
|
|
235
|
+
// arrives. Each message's onmessage handler still awaits the promise inside
|
|
236
|
+
// its own try/catch, so the rejection is reported back to the caller.
|
|
237
|
+
pyodideReadyPromise.catch(() => {});
|
|
238
|
+
|
|
234
239
|
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
235
|
-
const pyodide = await pyodideReadyPromise;
|
|
236
240
|
const msg = event.data;
|
|
237
241
|
const { id } = msg;
|
|
238
242
|
|
|
239
243
|
try {
|
|
244
|
+
const pyodide = await pyodideReadyPromise;
|
|
240
245
|
if (msg.type === "loadModule") {
|
|
241
246
|
await ensureModule(pyodide, msg.module);
|
|
242
247
|
postWithTransfer(self, id, true);
|
|
@@ -30,24 +30,22 @@ type OutgoingMessage =
|
|
|
30
30
|
| Omit<CallMessage, "id">
|
|
31
31
|
| Omit<LoadModuleMessage, "id">;
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
worker.postMessage({ id, ...msg } as WorkerMessage);
|
|
50
|
-
});
|
|
33
|
+
export type PyodideResponse = { result?: unknown; error?: string };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Convert a worker `ErrorEvent` into a human-readable message. Cross-origin
|
|
37
|
+
* worker failures sanitize the event so every field is null/empty (e.g. a
|
|
38
|
+
* dynamic `import("https://cdn…")` that fails at module evaluation): in
|
|
39
|
+
* that case we fall back to a hint about likely causes instead of an
|
|
40
|
+
* empty string, which would otherwise surface as a blank error.
|
|
41
|
+
*/
|
|
42
|
+
function describeWorkerError(e: ErrorEvent): string {
|
|
43
|
+
const msg = e.message || e.error?.message || "";
|
|
44
|
+
if (msg) {
|
|
45
|
+
if (e.filename) return `${msg} (${e.filename}:${e.lineno})`;
|
|
46
|
+
return msg;
|
|
47
|
+
}
|
|
48
|
+
return "Worker failed to load (no error details available — most often a cross-origin script load failure or a syntax error in the worker module)";
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/**
|
|
@@ -61,7 +59,11 @@ const DEFAULT_WORKER = "default";
|
|
|
61
59
|
|
|
62
60
|
interface WorkerEntry {
|
|
63
61
|
worker: Worker;
|
|
64
|
-
modules: Map<string, Promise<
|
|
62
|
+
modules: Map<string, Promise<PyodideResponse>>;
|
|
63
|
+
/** Pending request id → resolver, so a worker-level crash can drain them all. */
|
|
64
|
+
pending: Map<number, (r: PyodideResponse) => void>;
|
|
65
|
+
/** Set once the worker has fired an error event — any further call short-circuits to this error. */
|
|
66
|
+
deadError: string | null;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
const workers = new Map<string, WorkerEntry>();
|
|
@@ -74,7 +76,31 @@ function getWorker(name: string): WorkerEntry {
|
|
|
74
76
|
const worker = new Worker(new URL("./pyodide.worker.ts", import.meta.url), {
|
|
75
77
|
type: "module",
|
|
76
78
|
});
|
|
77
|
-
|
|
79
|
+
const newEntry: WorkerEntry = {
|
|
80
|
+
worker,
|
|
81
|
+
modules: new Map(),
|
|
82
|
+
pending: new Map(),
|
|
83
|
+
deadError: null,
|
|
84
|
+
};
|
|
85
|
+
function failAll(message: string) {
|
|
86
|
+
if (newEntry.deadError) return;
|
|
87
|
+
newEntry.deadError = message;
|
|
88
|
+
// Drop cached module promises so a future loadModule on a respawned
|
|
89
|
+
// worker (if the caller chooses to retry) re-installs.
|
|
90
|
+
newEntry.modules.clear();
|
|
91
|
+
const pending = Array.from(newEntry.pending.values());
|
|
92
|
+
newEntry.pending.clear();
|
|
93
|
+
for (const resolve of pending) resolve({ error: message });
|
|
94
|
+
}
|
|
95
|
+
worker.addEventListener("error", (e: ErrorEvent) => {
|
|
96
|
+
failAll(describeWorkerError(e));
|
|
97
|
+
});
|
|
98
|
+
worker.addEventListener("messageerror", () => {
|
|
99
|
+
failAll(
|
|
100
|
+
"Worker received a message it could not deserialize (messageerror)",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
entry = newEntry;
|
|
78
104
|
workers.set(name, entry);
|
|
79
105
|
for (const mod of sharedModules) {
|
|
80
106
|
ensureModuleOn(entry, mod);
|
|
@@ -83,13 +109,38 @@ function getWorker(name: string): WorkerEntry {
|
|
|
83
109
|
return entry;
|
|
84
110
|
}
|
|
85
111
|
|
|
112
|
+
function requestResponse(
|
|
113
|
+
entry: WorkerEntry,
|
|
114
|
+
msg: OutgoingMessage,
|
|
115
|
+
): Promise<PyodideResponse> {
|
|
116
|
+
if (entry.deadError) {
|
|
117
|
+
return Promise.resolve({ error: entry.deadError });
|
|
118
|
+
}
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
const id = getId();
|
|
121
|
+
entry.pending.set(id, resolve);
|
|
122
|
+
function listener(event: MessageEvent<TransferableResponse>) {
|
|
123
|
+
if (event.data?.id !== id) return;
|
|
124
|
+
entry.worker.removeEventListener("message", listener);
|
|
125
|
+
entry.pending.delete(id);
|
|
126
|
+
if (event.data.error) {
|
|
127
|
+
resolve({ error: event.data.error });
|
|
128
|
+
} else {
|
|
129
|
+
resolve({ result: unwrapResponse(event.data) });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
entry.worker.addEventListener("message", listener);
|
|
133
|
+
entry.worker.postMessage({ id, ...msg } as WorkerMessage);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
86
137
|
function ensureModuleOn(
|
|
87
138
|
entry: WorkerEntry,
|
|
88
139
|
moduleName: string,
|
|
89
|
-
): Promise<
|
|
140
|
+
): Promise<PyodideResponse> {
|
|
90
141
|
let p = entry.modules.get(moduleName);
|
|
91
142
|
if (!p) {
|
|
92
|
-
p = requestResponse(entry
|
|
143
|
+
p = requestResponse(entry, {
|
|
93
144
|
type: "loadModule",
|
|
94
145
|
module: moduleName,
|
|
95
146
|
});
|
|
@@ -106,9 +157,7 @@ function ensureModuleOn(
|
|
|
106
157
|
* worker, and on any future worker the moment it spawns. Returns once the
|
|
107
158
|
* default worker has finished installing.
|
|
108
159
|
*/
|
|
109
|
-
export async function loadModule(
|
|
110
|
-
moduleName: string,
|
|
111
|
-
): Promise<{ result?: unknown; error?: string }> {
|
|
160
|
+
export async function loadModule(moduleName: string): Promise<PyodideResponse> {
|
|
112
161
|
sharedModules.add(moduleName);
|
|
113
162
|
// Always install on the default worker — that's what callers expect when
|
|
114
163
|
// the function name has no "OnWorker" suffix.
|
|
@@ -129,7 +178,7 @@ export async function loadModule(
|
|
|
129
178
|
export async function loadModuleOnWorker(
|
|
130
179
|
moduleName: string,
|
|
131
180
|
worker: WorkerName,
|
|
132
|
-
): Promise<
|
|
181
|
+
): Promise<PyodideResponse> {
|
|
133
182
|
return ensureModuleOn(getWorker(worker), moduleName);
|
|
134
183
|
}
|
|
135
184
|
|
|
@@ -142,8 +191,8 @@ export async function asyncRunPython(
|
|
|
142
191
|
script: string,
|
|
143
192
|
context?: Record<string, unknown>,
|
|
144
193
|
worker: WorkerName = DEFAULT_WORKER,
|
|
145
|
-
): Promise<
|
|
146
|
-
return requestResponse(getWorker(worker)
|
|
194
|
+
): Promise<PyodideResponse> {
|
|
195
|
+
return requestResponse(getWorker(worker), {
|
|
147
196
|
type: "run",
|
|
148
197
|
python: script,
|
|
149
198
|
context,
|
|
@@ -160,11 +209,11 @@ export async function callPython(
|
|
|
160
209
|
fn: string,
|
|
161
210
|
kwargs?: Record<string, unknown>,
|
|
162
211
|
worker: WorkerName = DEFAULT_WORKER,
|
|
163
|
-
): Promise<
|
|
212
|
+
): Promise<PyodideResponse> {
|
|
164
213
|
const entry = getWorker(worker);
|
|
165
214
|
const installResult = await ensureModuleOn(entry, module);
|
|
166
215
|
if (installResult.error) return installResult;
|
|
167
|
-
return requestResponse(entry
|
|
216
|
+
return requestResponse(entry, {
|
|
168
217
|
type: "call",
|
|
169
218
|
module,
|
|
170
219
|
fn,
|
package/wasm/wasm.worker.ts
CHANGED
package/wasm/wasmWorkerApi.ts
CHANGED
|
@@ -10,23 +10,62 @@ interface WorkerMessage {
|
|
|
10
10
|
|
|
11
11
|
let lastId = 0;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Convert a worker `ErrorEvent` into a human-readable message. Cross-origin
|
|
15
|
+
* worker failures sanitize the event so every field is null/empty (e.g.
|
|
16
|
+
* the worker's module fails to evaluate); fall back to a hint rather than
|
|
17
|
+
* an empty string.
|
|
18
|
+
*/
|
|
19
|
+
function describeWorkerError(e: ErrorEvent): string {
|
|
20
|
+
const msg = e.message || e.error?.message || "";
|
|
21
|
+
if (msg) {
|
|
22
|
+
if (e.filename) return `${msg} (${e.filename}:${e.lineno})`;
|
|
23
|
+
return msg;
|
|
24
|
+
}
|
|
25
|
+
return "Worker failed to load (no error details available — most often a cross-origin script load failure or a syntax error in the worker module)";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Rejects keyed by request id so `cancelWasm` can fail in-flight promises
|
|
29
|
+
// when we terminate the worker mid-run, and so worker-level errors can drain
|
|
30
|
+
// every pending call at once.
|
|
31
|
+
const pendingRejects = new Map<number, (reason: Error) => void>();
|
|
32
|
+
|
|
33
|
+
// Set once the worker fires an `error` / `messageerror` event. Subsequent
|
|
34
|
+
// `runWasm` calls short-circuit with this error so the caller sees a real
|
|
35
|
+
// rejection instead of waiting forever on a worker that will never reply.
|
|
36
|
+
let workerDeadError: Error | null = null;
|
|
37
|
+
|
|
13
38
|
function newWorker(): Worker {
|
|
14
|
-
|
|
39
|
+
const w = new Worker(new URL("./wasm.worker.ts", import.meta.url), {
|
|
15
40
|
type: "module",
|
|
16
41
|
});
|
|
42
|
+
function failAll(message: string) {
|
|
43
|
+
workerDeadError = new Error(message);
|
|
44
|
+
const rejects = Array.from(pendingRejects.values());
|
|
45
|
+
pendingRejects.clear();
|
|
46
|
+
for (const reject of rejects) reject(workerDeadError);
|
|
47
|
+
}
|
|
48
|
+
w.addEventListener("error", (e: ErrorEvent) => {
|
|
49
|
+
failAll(describeWorkerError(e));
|
|
50
|
+
});
|
|
51
|
+
w.addEventListener("messageerror", () => {
|
|
52
|
+
failAll(
|
|
53
|
+
"Worker received a message it could not deserialize (messageerror)",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
return w;
|
|
17
57
|
}
|
|
18
58
|
|
|
19
59
|
let worker = newWorker();
|
|
20
60
|
|
|
21
|
-
// Rejects keyed by request id so `cancelWasm` can fail in-flight promises
|
|
22
|
-
// when we terminate the worker mid-run.
|
|
23
|
-
const pendingRejects = new Map<number, (reason: Error) => void>();
|
|
24
|
-
|
|
25
61
|
export function runWasm(
|
|
26
62
|
model: string,
|
|
27
63
|
fn: string,
|
|
28
64
|
...args: string[]
|
|
29
65
|
): Promise<unknown> {
|
|
66
|
+
if (workerDeadError) {
|
|
67
|
+
return Promise.reject(workerDeadError);
|
|
68
|
+
}
|
|
30
69
|
return new Promise((resolve, reject) => {
|
|
31
70
|
const id = ++lastId;
|
|
32
71
|
// Capture the current worker so a later `cancelWasm` that swaps the
|
|
@@ -63,6 +102,7 @@ export function cancelWasm(): void {
|
|
|
63
102
|
worker.terminate();
|
|
64
103
|
const rejects = Array.from(pendingRejects.values());
|
|
65
104
|
pendingRejects.clear();
|
|
105
|
+
workerDeadError = null;
|
|
66
106
|
worker = newWorker();
|
|
67
107
|
for (const reject of rejects) {
|
|
68
108
|
reject(new Error("cancelled"));
|