@cfasim-ui/docs 0.3.18 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/charts/BarChart/BarChart.md +196 -0
- package/charts/BarChart/BarChart.vue +844 -0
- package/charts/ChartMenu/ChartMenu.vue +11 -4
- package/charts/ChoroplethMap/ChoroplethMap.md +42 -0
- package/charts/ChoroplethMap/ChoroplethMap.vue +22 -2
- package/charts/DataTable/DataTable.md +39 -9
- package/charts/DataTable/DataTable.vue +45 -59
- package/charts/LineChart/LineChart.md +3 -2
- package/charts/LineChart/LineChart.vue +86 -291
- package/charts/_shared/axes.ts +69 -0
- package/charts/_shared/computeTicks.ts +42 -0
- package/charts/_shared/index.ts +20 -0
- package/charts/_shared/seriesCsv.ts +68 -0
- package/charts/_shared/useChartMenu.ts +72 -0
- package/charts/_shared/useChartPadding.ts +37 -0
- package/charts/_shared/useChartSize.ts +49 -0
- package/charts/_shared/useChartTooltip.ts +152 -0
- package/charts/index.ts +5 -0
- package/index.json +18 -1
- package/package.json +1 -1
- package/pyodide/index.ts +2 -0
- package/pyodide/pyodide.worker.ts +109 -72
- package/pyodide/pyodideWorkerApi.ts +157 -63
- package/pyodide/useModel.ts +10 -21
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { computed, ref, type Ref } from "vue";
|
|
2
|
+
import type { ChartMenuItem } from "../ChartMenu/ChartMenu.vue";
|
|
3
|
+
import { saveSvg, savePng, downloadCsv } from "../ChartMenu/download.js";
|
|
4
|
+
|
|
5
|
+
export interface ChartMenuOptions {
|
|
6
|
+
filename: () => string | undefined;
|
|
7
|
+
/** Used as the menu's filename fallback when `filename` is unset. */
|
|
8
|
+
legacyMenuLabel: () => boolean | string | undefined;
|
|
9
|
+
/** Builds the CSV content for downloads. */
|
|
10
|
+
getCsv: () => string;
|
|
11
|
+
/** Whether a separate download link is rendered (and the CSV menu item should be hidden). */
|
|
12
|
+
downloadLink: () => boolean | string | undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Computes the standard chart menu items (SVG / PNG / CSV) plus the
|
|
17
|
+
* CSV-download-link state shared by every chart.
|
|
18
|
+
*/
|
|
19
|
+
export function useChartMenu(opts: ChartMenuOptions) {
|
|
20
|
+
const svgRef = ref<SVGSVGElement | null>(null);
|
|
21
|
+
|
|
22
|
+
function resolvedFilename(): string {
|
|
23
|
+
const f = opts.filename();
|
|
24
|
+
if (f) return f;
|
|
25
|
+
const menu = opts.legacyMenuLabel();
|
|
26
|
+
return typeof menu === "string" ? menu : "chart";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const items = computed<ChartMenuItem[]>(() => {
|
|
30
|
+
const fname = resolvedFilename();
|
|
31
|
+
const out: ChartMenuItem[] = [
|
|
32
|
+
{
|
|
33
|
+
label: "Save as SVG",
|
|
34
|
+
action: () => {
|
|
35
|
+
if (svgRef.value) saveSvg(svgRef.value, fname);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
label: "Save as PNG",
|
|
40
|
+
action: () => {
|
|
41
|
+
if (svgRef.value) savePng(svgRef.value, fname);
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
if (!opts.downloadLink()) {
|
|
46
|
+
out.push({
|
|
47
|
+
label: "Download CSV",
|
|
48
|
+
action: () => downloadCsv(opts.getCsv(), fname),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const downloadLinkText = computed<string | null>(() => {
|
|
55
|
+
const v = opts.downloadLink();
|
|
56
|
+
if (!v) return null;
|
|
57
|
+
return typeof v === "string" ? v : "Download data (CSV)";
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const csvHref = computed<string | null>(() => {
|
|
61
|
+
if (!opts.downloadLink()) return null;
|
|
62
|
+
return `data:text/csv;charset=utf-8,${encodeURIComponent(opts.getCsv())}`;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
svgRef: svgRef as Ref<SVGSVGElement | null>,
|
|
67
|
+
items,
|
|
68
|
+
downloadLinkText,
|
|
69
|
+
csvHref,
|
|
70
|
+
resolvedFilename,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
|
|
3
|
+
/** Vertical space reserved at the top of the chart for inline legend swatches. */
|
|
4
|
+
export const INLINE_LEGEND_HEIGHT = 20;
|
|
5
|
+
|
|
6
|
+
export interface ChartPaddingOptions {
|
|
7
|
+
title: () => string | undefined;
|
|
8
|
+
xLabel: () => string | undefined;
|
|
9
|
+
yLabel: () => string | undefined;
|
|
10
|
+
hasInlineLegend: () => boolean;
|
|
11
|
+
width: () => number;
|
|
12
|
+
height: () => number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Computes the standard chart padding (top/right/bottom/left) and the
|
|
17
|
+
* derived inner plotting region (innerW, innerH). Shared by LineChart
|
|
18
|
+
* and BarChart so the axis label spacing and inline legend strip stay
|
|
19
|
+
* consistent.
|
|
20
|
+
*/
|
|
21
|
+
export function useChartPadding(opts: ChartPaddingOptions) {
|
|
22
|
+
const padding = computed(() => ({
|
|
23
|
+
top:
|
|
24
|
+
(opts.title() ? 30 : 10) +
|
|
25
|
+
(opts.hasInlineLegend() ? INLINE_LEGEND_HEIGHT : 0),
|
|
26
|
+
right: 10,
|
|
27
|
+
bottom: opts.xLabel() ? 46 : 30,
|
|
28
|
+
left: opts.yLabel() ? 66 : 50,
|
|
29
|
+
}));
|
|
30
|
+
const innerW = computed(
|
|
31
|
+
() => opts.width() - padding.value.left - padding.value.right,
|
|
32
|
+
);
|
|
33
|
+
const innerH = computed(
|
|
34
|
+
() => opts.height() - padding.value.top - padding.value.bottom,
|
|
35
|
+
);
|
|
36
|
+
return { padding, innerW, innerH };
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ref, onMounted, onUnmounted, type Ref } from "vue";
|
|
2
|
+
|
|
3
|
+
export interface ChartSizeOptions {
|
|
4
|
+
/** Fallback width when measurement is not yet available. */
|
|
5
|
+
fallbackWidth?: number;
|
|
6
|
+
/** Optional debounce in ms applied to ResizeObserver updates. */
|
|
7
|
+
debounce?: () => number | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Watches an element ref for size changes and exposes the measured width.
|
|
12
|
+
* Mirrors the per-chart ResizeObserver wiring previously inlined in each
|
|
13
|
+
* chart component.
|
|
14
|
+
*/
|
|
15
|
+
export function useChartSize(opts: ChartSizeOptions = {}) {
|
|
16
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
17
|
+
const measuredWidth = ref(0);
|
|
18
|
+
let observer: ResizeObserver | null = null;
|
|
19
|
+
let resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
20
|
+
|
|
21
|
+
onMounted(() => {
|
|
22
|
+
if (!containerRef.value) return;
|
|
23
|
+
measuredWidth.value = containerRef.value.clientWidth;
|
|
24
|
+
observer = new ResizeObserver((entries) => {
|
|
25
|
+
const entry = entries[0];
|
|
26
|
+
if (!entry) return;
|
|
27
|
+
const debounce = opts.debounce?.();
|
|
28
|
+
if (debounce) {
|
|
29
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
30
|
+
resizeTimeout = setTimeout(() => {
|
|
31
|
+
measuredWidth.value = entry.contentRect.width;
|
|
32
|
+
}, debounce);
|
|
33
|
+
} else {
|
|
34
|
+
measuredWidth.value = entry.contentRect.width;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
observer.observe(containerRef.value);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
onUnmounted(() => {
|
|
41
|
+
observer?.disconnect();
|
|
42
|
+
if (resizeTimeout) clearTimeout(resizeTimeout);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
containerRef,
|
|
47
|
+
measuredWidth,
|
|
48
|
+
} as { containerRef: Ref<HTMLElement | null>; measuredWidth: Ref<number> };
|
|
49
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ref, computed, watch, type Ref } from "vue";
|
|
2
|
+
import { placeTooltip, type TooltipClamp } from "../tooltip-position.js";
|
|
3
|
+
|
|
4
|
+
export interface ChartTooltipOptions {
|
|
5
|
+
/** Whether tooltip interactions are wired up at all. */
|
|
6
|
+
enabled: () => boolean;
|
|
7
|
+
/** Tooltip activation mode. */
|
|
8
|
+
trigger?: () => "hover" | "click" | undefined;
|
|
9
|
+
/** Boundary for tooltip flip/clamp. */
|
|
10
|
+
clamp?: () => TooltipClamp;
|
|
11
|
+
/**
|
|
12
|
+
* Maps a client (x, y) pointer location to a data index, or null when
|
|
13
|
+
* the pointer is outside the chart. Most charts only need `clientX`
|
|
14
|
+
* (categorical or x-indexed), but horizontal bar charts also need
|
|
15
|
+
* `clientY`.
|
|
16
|
+
*/
|
|
17
|
+
pointerToIndex: (clientX: number, clientY: number) => number | null;
|
|
18
|
+
/** The chart's container element (used to compute relative position). */
|
|
19
|
+
containerRef: Ref<HTMLElement | null>;
|
|
20
|
+
/** Pointer-vertical offset applied for touch interactions. */
|
|
21
|
+
touchYOffset?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Emit hover events. The first arg is `{ index }` while hovering and
|
|
24
|
+
* `null` when leaving.
|
|
25
|
+
*/
|
|
26
|
+
onHover?: (payload: { index: number } | null) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Shared tooltip state + pointer/touch handlers used by chart components.
|
|
31
|
+
* The caller wires the returned `handlers` to its hit-test overlay and
|
|
32
|
+
* places the floating tooltip with `tooltipPos`.
|
|
33
|
+
*/
|
|
34
|
+
export function useChartTooltip(opts: ChartTooltipOptions) {
|
|
35
|
+
const TOUCH_Y_OFFSET = opts.touchYOffset ?? 50;
|
|
36
|
+
const hoverIndex = ref<number | null>(null);
|
|
37
|
+
const isTouching = ref(false);
|
|
38
|
+
const tooltipRef = ref<HTMLElement | null>(null);
|
|
39
|
+
const pointer = ref<{ clientX: number; clientY: number } | null>(null);
|
|
40
|
+
const tooltipPos = ref<{ left: number; top: number } | null>(null);
|
|
41
|
+
|
|
42
|
+
function pointerFromEvent(
|
|
43
|
+
event: MouseEvent | TouchEvent,
|
|
44
|
+
): { clientX: number; clientY: number } | null {
|
|
45
|
+
if ("touches" in event) {
|
|
46
|
+
return event.touches[0] ?? null;
|
|
47
|
+
}
|
|
48
|
+
return event;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function updateHover(event: MouseEvent | TouchEvent) {
|
|
52
|
+
const pt = pointerFromEvent(event);
|
|
53
|
+
if (!pt) return;
|
|
54
|
+
const idx = opts.pointerToIndex(pt.clientX, pt.clientY);
|
|
55
|
+
if (idx === null) return;
|
|
56
|
+
hoverIndex.value = idx;
|
|
57
|
+
pointer.value = { clientX: pt.clientX, clientY: pt.clientY };
|
|
58
|
+
opts.onHover?.({ index: idx });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
watch(
|
|
62
|
+
[pointer, hoverIndex],
|
|
63
|
+
() => {
|
|
64
|
+
if (hoverIndex.value === null || !pointer.value) {
|
|
65
|
+
tooltipPos.value = null;
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const el = tooltipRef.value;
|
|
69
|
+
const container = opts.containerRef.value;
|
|
70
|
+
if (!el || !container) return;
|
|
71
|
+
const rect = container.getBoundingClientRect();
|
|
72
|
+
const offset = isTouching.value ? TOUCH_Y_OFFSET : 0;
|
|
73
|
+
const clamp = opts.clamp?.() ?? "chart";
|
|
74
|
+
const { left, top } = placeTooltip(
|
|
75
|
+
pointer.value.clientX,
|
|
76
|
+
pointer.value.clientY - offset,
|
|
77
|
+
el.offsetWidth,
|
|
78
|
+
el.offsetHeight,
|
|
79
|
+
clamp,
|
|
80
|
+
rect,
|
|
81
|
+
);
|
|
82
|
+
tooltipPos.value = { left: left - rect.left, top: top - rect.top };
|
|
83
|
+
},
|
|
84
|
+
{ flush: "post" },
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
function onMouseMove(event: MouseEvent) {
|
|
88
|
+
if (!opts.enabled()) return;
|
|
89
|
+
updateHover(event);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onMouseLeave() {
|
|
93
|
+
if (!opts.enabled()) return;
|
|
94
|
+
if (opts.trigger?.() !== "click") {
|
|
95
|
+
hoverIndex.value = null;
|
|
96
|
+
opts.onHover?.(null);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function onClick(event: MouseEvent) {
|
|
101
|
+
if (!opts.enabled()) return;
|
|
102
|
+
if (opts.trigger?.() !== "click") return;
|
|
103
|
+
const pt = pointerFromEvent(event);
|
|
104
|
+
if (!pt) return;
|
|
105
|
+
const idx = opts.pointerToIndex(pt.clientX, pt.clientY);
|
|
106
|
+
if (idx === null) return;
|
|
107
|
+
hoverIndex.value = hoverIndex.value === idx ? null : idx;
|
|
108
|
+
opts.onHover?.(hoverIndex.value !== null ? { index: idx } : null);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function onTouchStart(event: TouchEvent) {
|
|
112
|
+
if (!opts.enabled()) return;
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
isTouching.value = true;
|
|
115
|
+
updateHover(event);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onTouchMove(event: TouchEvent) {
|
|
119
|
+
if (!opts.enabled()) return;
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
updateHover(event);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function onTouchEnd() {
|
|
125
|
+
if (!opts.enabled()) return;
|
|
126
|
+
isTouching.value = false;
|
|
127
|
+
hoverIndex.value = null;
|
|
128
|
+
opts.onHover?.(null);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Note: when binding via `v-on="handlers"`, Vue expects event names
|
|
132
|
+
// *without* the `on` prefix. Touch events default to passive in some
|
|
133
|
+
// contexts; consumers using touch overlays should still bind the
|
|
134
|
+
// touch handlers individually with `.prevent`.
|
|
135
|
+
const handlers = {
|
|
136
|
+
mousemove: onMouseMove,
|
|
137
|
+
mouseleave: onMouseLeave,
|
|
138
|
+
click: onClick,
|
|
139
|
+
touchstart: onTouchStart,
|
|
140
|
+
touchmove: onTouchMove,
|
|
141
|
+
touchend: onTouchEnd,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
hoverIndex,
|
|
146
|
+
isTouching,
|
|
147
|
+
pointer,
|
|
148
|
+
tooltipRef,
|
|
149
|
+
tooltipPos,
|
|
150
|
+
handlers,
|
|
151
|
+
};
|
|
152
|
+
}
|
package/charts/index.ts
CHANGED
package/index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.4.1",
|
|
3
3
|
"package": "@cfasim-ui/docs",
|
|
4
4
|
"content": {
|
|
5
5
|
"components": [
|
|
@@ -120,6 +120,23 @@
|
|
|
120
120
|
}
|
|
121
121
|
],
|
|
122
122
|
"charts": [
|
|
123
|
+
{
|
|
124
|
+
"name": "BarChart",
|
|
125
|
+
"slug": "bar-chart",
|
|
126
|
+
"docs": "charts/BarChart/BarChart.md",
|
|
127
|
+
"source": "charts/BarChart/BarChart.vue",
|
|
128
|
+
"keywords": [
|
|
129
|
+
"bar",
|
|
130
|
+
"column",
|
|
131
|
+
"chart",
|
|
132
|
+
"categorical",
|
|
133
|
+
"grouped",
|
|
134
|
+
"stacked",
|
|
135
|
+
"vertical",
|
|
136
|
+
"horizontal",
|
|
137
|
+
"svg"
|
|
138
|
+
]
|
|
139
|
+
},
|
|
123
140
|
{
|
|
124
141
|
"name": "ChoroplethMap",
|
|
125
142
|
"slug": "choropleth-map",
|
package/package.json
CHANGED
package/pyodide/index.ts
CHANGED
|
@@ -5,13 +5,25 @@ import {
|
|
|
5
5
|
} from "@cfasim-ui/shared";
|
|
6
6
|
import type { ColumnDescriptor, ModelOutputsWire } from "@cfasim-ui/shared";
|
|
7
7
|
|
|
8
|
-
interface
|
|
8
|
+
interface RunMessage {
|
|
9
9
|
id: number;
|
|
10
|
-
type?: "run"
|
|
11
|
-
python
|
|
12
|
-
module?: string;
|
|
10
|
+
type?: "run";
|
|
11
|
+
python: string;
|
|
13
12
|
context?: Record<string, unknown>;
|
|
14
13
|
}
|
|
14
|
+
interface CallMessage {
|
|
15
|
+
id: number;
|
|
16
|
+
type: "call";
|
|
17
|
+
module: string;
|
|
18
|
+
fn: string;
|
|
19
|
+
kwargs?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
interface LoadModuleMessage {
|
|
22
|
+
id: number;
|
|
23
|
+
type: "loadModule";
|
|
24
|
+
module: string;
|
|
25
|
+
}
|
|
26
|
+
type WorkerMessage = RunMessage | CallMessage | LoadModuleMessage;
|
|
15
27
|
|
|
16
28
|
let wheelMap: Record<string, string> = {};
|
|
17
29
|
|
|
@@ -92,24 +104,29 @@ function installAllWheels(): Promise<void> {
|
|
|
92
104
|
return installPromise;
|
|
93
105
|
}
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
|
+
const modulePromises = new Map<string, Promise<any>>();
|
|
96
109
|
|
|
97
110
|
function ensureModule(
|
|
98
111
|
pyodide: Awaited<typeof pyodideReadyPromise>,
|
|
99
112
|
moduleName: string,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
): Promise<any> {
|
|
115
|
+
let p = modulePromises.get(moduleName);
|
|
116
|
+
if (!p) {
|
|
117
|
+
if (!wheelMap[moduleName]) {
|
|
118
|
+
return Promise.reject(new Error(`Unknown module: ${moduleName}`));
|
|
119
|
+
}
|
|
120
|
+
p = (async () => {
|
|
104
121
|
await installAllWheels();
|
|
105
|
-
pyodide.pyimport(moduleName);
|
|
122
|
+
return pyodide.pyimport(moduleName);
|
|
106
123
|
})();
|
|
107
|
-
|
|
124
|
+
p.catch(() => {
|
|
108
125
|
modulePromises.delete(moduleName);
|
|
109
126
|
});
|
|
110
|
-
modulePromises.set(moduleName,
|
|
127
|
+
modulePromises.set(moduleName, p);
|
|
111
128
|
}
|
|
112
|
-
return
|
|
129
|
+
return p;
|
|
113
130
|
}
|
|
114
131
|
|
|
115
132
|
// Map Python struct format characters to TypedArray constructors
|
|
@@ -127,18 +144,20 @@ const FORMAT_TO_TYPED_ARRAY: Record<
|
|
|
127
144
|
d: Float64Array,
|
|
128
145
|
};
|
|
129
146
|
|
|
147
|
+
// Copy a Pyodide getBuffer() result into a JS-owned typed array. Releases the
|
|
148
|
+
// underlying Python buffer; the returned view's ArrayBuffer is safe to transfer.
|
|
130
149
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
-
function
|
|
150
|
+
function copyPyBuffer(proxy: any): ArrayBufferView {
|
|
132
151
|
const pyBuffer = proxy.getBuffer();
|
|
133
152
|
const Ctor = FORMAT_TO_TYPED_ARRAY[pyBuffer.format] ?? Float64Array;
|
|
134
|
-
const
|
|
153
|
+
const view = new Ctor(
|
|
135
154
|
pyBuffer.data.buffer.slice(
|
|
136
155
|
pyBuffer.data.byteOffset,
|
|
137
156
|
pyBuffer.data.byteOffset + pyBuffer.data.byteLength,
|
|
138
157
|
),
|
|
139
158
|
);
|
|
140
159
|
pyBuffer.release();
|
|
141
|
-
return
|
|
160
|
+
return view;
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -161,7 +180,7 @@ function convertModelOutputs(jsResult: any): ModelOutputsWire | null {
|
|
|
161
180
|
for (const buf of wire.buffers) {
|
|
162
181
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
163
182
|
if (buf && typeof buf === "object" && (buf as any).getBuffer) {
|
|
164
|
-
buffers.push(
|
|
183
|
+
buffers.push(copyPyBuffer(buf).buffer as ArrayBuffer);
|
|
165
184
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
166
185
|
if ((buf as any).destroy) (buf as any).destroy();
|
|
167
186
|
} else if (buf instanceof ArrayBuffer) {
|
|
@@ -187,76 +206,94 @@ function convertModelOutputs(jsResult: any): ModelOutputsWire | null {
|
|
|
187
206
|
return { __modelOutputs: true, outputs };
|
|
188
207
|
}
|
|
189
208
|
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
210
|
+
function convertResult(rawResult: any): unknown {
|
|
211
|
+
if (!rawResult || typeof rawResult.destroy !== "function") return rawResult;
|
|
212
|
+
try {
|
|
213
|
+
if (typeof rawResult.toJs === "function") {
|
|
214
|
+
return rawResult.toJs({ dict_converter: Object.fromEntries });
|
|
215
|
+
}
|
|
216
|
+
if (typeof rawResult.getBuffer === "function") {
|
|
217
|
+
return copyPyBuffer(rawResult);
|
|
218
|
+
}
|
|
219
|
+
return rawResult.toString();
|
|
220
|
+
} finally {
|
|
221
|
+
rawResult.destroy();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function send(id: number, result: unknown) {
|
|
226
|
+
const modelOutputs = convertModelOutputs(result);
|
|
227
|
+
if (modelOutputs) {
|
|
228
|
+
postModelOutputsWithTransfer(self, id, modelOutputs);
|
|
229
|
+
} else {
|
|
230
|
+
postWithTransfer(self, id, result);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
190
234
|
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
191
235
|
const pyodide = await pyodideReadyPromise;
|
|
192
|
-
const
|
|
236
|
+
const msg = event.data;
|
|
237
|
+
const { id } = msg;
|
|
193
238
|
|
|
194
239
|
try {
|
|
195
|
-
if (type === "loadModule"
|
|
196
|
-
await ensureModule(pyodide,
|
|
240
|
+
if (msg.type === "loadModule") {
|
|
241
|
+
await ensureModule(pyodide, msg.module);
|
|
197
242
|
postWithTransfer(self, id, true);
|
|
198
243
|
return;
|
|
199
244
|
}
|
|
200
245
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
|
|
246
|
+
if (msg.type === "call") {
|
|
247
|
+
const mod = await ensureModule(pyodide, msg.module);
|
|
248
|
+
const t0 = performance.now();
|
|
249
|
+
// mod[fn] creates a fresh PyProxy on every access — release in finally.
|
|
250
|
+
const pyFn = mod[msg.fn];
|
|
251
|
+
let result: unknown;
|
|
252
|
+
try {
|
|
253
|
+
if (typeof pyFn !== "function") {
|
|
254
|
+
throw new Error(`Module ${msg.module} has no function ${msg.fn}`);
|
|
255
|
+
}
|
|
256
|
+
const rawResult = msg.kwargs ? pyFn.callKwargs(msg.kwargs) : pyFn();
|
|
257
|
+
result = convertResult(rawResult);
|
|
258
|
+
} finally {
|
|
259
|
+
if (pyFn && typeof pyFn.destroy === "function") pyFn.destroy();
|
|
260
|
+
}
|
|
261
|
+
const tEnd = performance.now();
|
|
262
|
+
console.log(
|
|
263
|
+
`[pyodide-worker] ${msg.module}.${msg.fn} ${
|
|
264
|
+
Math.round((tEnd - t0) * 10) / 10
|
|
265
|
+
}ms`,
|
|
266
|
+
);
|
|
267
|
+
send(id, result);
|
|
268
|
+
return;
|
|
205
269
|
}
|
|
206
270
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const tPython = performance.now();
|
|
216
|
-
|
|
217
|
-
// Destroy PyProxy if returned to prevent memory leaks
|
|
218
|
-
let result = rawResult;
|
|
219
|
-
if (rawResult && typeof rawResult === "object" && rawResult.destroy) {
|
|
220
|
-
if (rawResult.toJs) {
|
|
221
|
-
result = rawResult.toJs({ dict_converter: Object.fromEntries });
|
|
222
|
-
} else if (rawResult.getBuffer) {
|
|
223
|
-
// Single numpy array: use getBuffer() for direct typed array access
|
|
224
|
-
const pyBuffer = rawResult.getBuffer();
|
|
225
|
-
const Ctor = FORMAT_TO_TYPED_ARRAY[pyBuffer.format] ?? Float64Array;
|
|
226
|
-
result = new Ctor(
|
|
227
|
-
pyBuffer.data.buffer.slice(
|
|
228
|
-
pyBuffer.data.byteOffset,
|
|
229
|
-
pyBuffer.data.byteOffset + pyBuffer.data.byteLength,
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
pyBuffer.release();
|
|
233
|
-
} else {
|
|
234
|
-
result = rawResult.toString();
|
|
271
|
+
// type === "run" or omitted
|
|
272
|
+
let globals;
|
|
273
|
+
if (msg.context) {
|
|
274
|
+
const dict = pyodide.globals.get("dict");
|
|
275
|
+
try {
|
|
276
|
+
globals = dict(Object.entries(msg.context));
|
|
277
|
+
} finally {
|
|
278
|
+
dict.destroy();
|
|
235
279
|
}
|
|
236
|
-
rawResult.destroy();
|
|
237
280
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
281
|
+
const t0 = performance.now();
|
|
282
|
+
let result: unknown;
|
|
283
|
+
try {
|
|
284
|
+
const rawResult = pyodide.runPython(
|
|
285
|
+
msg.python,
|
|
286
|
+
globals ? { globals } : undefined,
|
|
287
|
+
);
|
|
288
|
+
result = convertResult(rawResult);
|
|
289
|
+
} finally {
|
|
290
|
+
if (globals && globals.destroy) globals.destroy();
|
|
242
291
|
}
|
|
243
|
-
|
|
244
|
-
const tConvert = performance.now();
|
|
245
|
-
const bench = {
|
|
246
|
-
python_ms: Math.round((tPython - t0) * 10) / 10,
|
|
247
|
-
convert_ms: Math.round((tConvert - tPython) * 10) / 10,
|
|
248
|
-
};
|
|
292
|
+
const tEnd = performance.now();
|
|
249
293
|
console.log(
|
|
250
|
-
`[pyodide-worker]
|
|
294
|
+
`[pyodide-worker] runPython ${Math.round((tEnd - t0) * 10) / 10}ms`,
|
|
251
295
|
);
|
|
252
|
-
|
|
253
|
-
// Check for ModelOutputs wire format
|
|
254
|
-
const modelOutputs = convertModelOutputs(result);
|
|
255
|
-
if (modelOutputs) {
|
|
256
|
-
postModelOutputsWithTransfer(self, id, modelOutputs);
|
|
257
|
-
} else {
|
|
258
|
-
postWithTransfer(self, id, result);
|
|
259
|
-
}
|
|
296
|
+
send(id, result);
|
|
260
297
|
} catch (error) {
|
|
261
298
|
postErrorWithTransfer(self, id, error);
|
|
262
299
|
}
|