Skip to content

Commit de6f46a

Browse files
authored
feat: support marimo poc version (#647)
* feat: support marimo poc version * chore: bump to v0.4.9.11
1 parent 8cc152d commit de6f46a

File tree

9 files changed

+244
-6
lines changed

9 files changed

+244
-6
lines changed

app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"serve": "vite preview"
1616
},
1717
"dependencies": {
18+
"@anywidget/react": "^0.0.8",
1819
"@headlessui/react": "^1.7.14",
1920
"@heroicons/react": "^2.0.8",
2021
"@kanaries/graphic-walker": "0.4.70",

app/src/index.tsx

+39-3
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import type { VizSpecStore } from '@kanaries/graphic-walker/store/visualSpecStor
77
import type { IGWHandler, IViewField, ISegmentKey, IDarkMode, IChatMessage, IRow } from '@kanaries/graphic-walker/interfaces';
88
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
99
import { Streamlit, withStreamlitConnection } from "streamlit-component-lib"
10+
import { createRender, useModel } from "@anywidget/react";
1011

1112
import Options from './components/options';
1213
import { IAppProps } from './interfaces';
1314

1415
import { loadDataSource, postDataService, finishDataService, getDatasFromKernelBySql, getDatasFromKernelByPayload } from './dataSource';
1516

1617
import commonStore from "./store/common";
17-
import { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback } from "./utils/communication";
18+
import { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback, initAnywidgetCommunication } from "./utils/communication";
1819
import communicationStore from "./store/communication"
1920
import { setConfig } from './utils/userConfig';
2021
import CodeExportModal from './components/codeExportModal';
@@ -209,7 +210,7 @@ const ExploreApp: React.FC<IAppProps & {initChartFlag: boolean}> = (props) => {
209210
const exportTool = getExportTool(setExportOpen);
210211

211212
const tools = [exportTool];
212-
if ((props.env === "jupyter_widgets" || props.env === "streamlit" || props.env === "gradio") && props.useSaveTool) {
213+
if (props.env && ["jupyter_widgets", "streamlit", "gradio", "marimo"].indexOf(props.env) !== -1 && props.useSaveTool) {
213214
const saveTool = getSaveTool(props, gwRef, storeRef, isChanged, setIsChanged);
214215
tools.push(saveTool);
215216
}
@@ -368,6 +369,16 @@ const initOnHttpCommunication = async(props: IAppProps) => {
368369
await initDslParser();
369370
}
370371

372+
const initOnAnywidgetCommunication = async(props: IAppProps, model: import("@anywidget/types").AnyModel) => {
373+
const comm = await initAnywidgetCommunication(props.id, model);
374+
communicationStore.setComm(comm);
375+
if ((props.gwMode === "explore" || props.gwMode === "filter_renderer") && props.needLoadLastSpec) {
376+
const visSpecResp = await comm.sendMsg("get_latest_vis_spec", {});
377+
props.visSpec = visSpecResp["data"]["visSpec"];
378+
}
379+
await initDslParser();
380+
}
381+
371382
const defaultInit = async(props: IAppProps) => {}
372383

373384
function GWalkerComponent(props: IAppProps) {
@@ -568,4 +579,29 @@ const StreamlitGWalker = () => {
568579
)
569580
}
570581

571-
export default { GWalker, PreviewApp, ChartPreviewApp, StreamlitGWalker }
582+
function AnywidgetGWalkerApp() {
583+
const [inited, setInited] = useState(false);
584+
const model = useModel();
585+
const props = JSON.parse(model.get("props")) as IAppProps;
586+
props.visSpec = FormatSpec(props.visSpec, props.rawFields);
587+
588+
useEffect(() => {
589+
initOnAnywidgetCommunication(props, model).then(() => {
590+
setInited(true);
591+
})
592+
}, []);
593+
594+
return (
595+
<React.StrictMode>
596+
{!inited && <div>Loading...</div>}
597+
{inited && (
598+
<MainApp darkMode={props.dark}>
599+
<GWalkerComponent {...props} />
600+
</MainApp>
601+
)}
602+
</React.StrictMode>
603+
);
604+
}
605+
606+
607+
export default { GWalker, PreviewApp, ChartPreviewApp, StreamlitGWalker, render: createRender(AnywidgetGWalkerApp) }

app/src/utils/communication.tsx

+59-1
Original file line numberDiff line numberDiff line change
@@ -255,5 +255,63 @@ const streamlitComponentCallback = (data: any) => {
255255
}
256256
}
257257

258+
const initAnywidgetCommunication = async(gid: string, model: import("@anywidget/types").AnyModel) => {
259+
const bufferMap = new Map<string, any>();
260+
261+
const onMessage = (msg: string) => {
262+
const data = JSON.parse(msg);
263+
const action = data.action;
264+
if (action === "finish_request") {
265+
bufferMap.set(data.rid, data.data);
266+
document.dispatchEvent(new CustomEvent(getSignalName(data.rid)));
267+
return
268+
}
269+
}
270+
271+
model.on("msg:custom", msg => {
272+
if (msg.type !== "pyg_response") {
273+
return;
274+
}
275+
onMessage(msg.data);
276+
});
277+
278+
const sendMsg = async(action: string, data: any, timeout: number = 30_000) => {
279+
const rid = uuidv4();
280+
const promise = new Promise<any>((resolve, reject) => {
281+
setTimeout(() => {
282+
sendMsgAsync(action, data, rid);
283+
}, 0);
284+
const timer = setTimeout(() => {
285+
raiseRequestError("communication timeout", 0);
286+
reject(new Error("get result timeout"));
287+
}, timeout);
288+
document.addEventListener(getSignalName(rid), (_) => {
289+
clearTimeout(timer);
290+
const resp = bufferMap.get(rid);
291+
if (resp.code !== 0) {
292+
raiseRequestError(resp.message, resp.code);
293+
reject(new Error(resp.message));
294+
}
295+
resolve(resp);
296+
});
297+
});
298+
299+
return promise;
300+
}
301+
302+
const sendMsgAsync = (action: string, data: any, rid: string | null) => {
303+
rid = rid ?? uuidv4();
304+
model.send({type: "pyg_request", msg: { gid, rid, action, data }});
305+
}
306+
307+
const registerEndpoint = (_: string, __: (data: any) => any) => {}
308+
309+
return {
310+
sendMsg,
311+
registerEndpoint,
312+
sendMsgAsync,
313+
}
314+
}
315+
258316
export type { ICommunication };
259-
export { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback };
317+
export { initJupyterCommunication, initHttpCommunication, streamlitComponentCallback, initAnywidgetCommunication };

app/vite.config.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default defineConfig((config: ConfigEnv) => {
1717
entry: path.resolve(__dirname, './src/index.tsx'),
1818
name: 'PyGWalkerApp',
1919
fileName: (format) => `pygwalker-app.${format}.js`,
20-
formats: ['iife']
20+
formats: ['iife', "es"]
2121
},
2222
minify: 'esbuild',
2323
sourcemap: false,
@@ -77,6 +77,8 @@ export default defineConfig((config: ConfigEnv) => {
7777
rollupOptions: {
7878
external: modulesNotToBundle,
7979
output: {
80+
manualChunks: undefined,
81+
inlineDynamicImports: true,
8082
globals: {
8183
'react': 'React',
8284
'react-dom': 'ReactDOM',

app/yarn.lock

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
"@jridgewell/gen-mapping" "^0.3.0"
1616
"@jridgewell/trace-mapping" "^0.3.9"
1717

18+
"@anywidget/react@^0.0.8":
19+
version "0.0.8"
20+
resolved "https://registry.yarnpkg.com/@anywidget/react/-/react-0.0.8.tgz#64b73cf9c9bad7bad180c8ba44134f09cc6eb44f"
21+
integrity sha512-obr4EasXgWra485u+G4V3Msn7A1EOnowarvR62FRjpv2Rz6AyOoLMz2B03Z9j3DrWdD0634fMGu5ZAeRyjuV4w==
22+
dependencies:
23+
"@anywidget/types" "^0.2.0"
24+
25+
"@anywidget/types@^0.2.0":
26+
version "0.2.0"
27+
resolved "https://registry.yarnpkg.com/@anywidget/types/-/types-0.2.0.tgz#6bae4e4fa36d193f565b0b78dc7eac50bb71beb4"
28+
integrity sha512-+XtK4uwxRd4JpuevUMhirrbvC0V4yCA/i0lEjhmSAtOaxiXIg/vBKzaSonDuoZ1a9LEjUXTW2+m7w+ULgsJYvg==
29+
1830
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.22.13":
1931
version "7.22.13"
2032
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"

pygwalker/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pygwalker.services.global_var import GlobalVarManager
1111
from pygwalker.services.kaggle import show_tips_user_kaggle as __show_tips_user_kaggle
1212

13-
__version__ = "0.4.9.10"
13+
__version__ = "0.4.9.11"
1414
__hash__ = __rand_str()
1515

1616
from pygwalker.api.jupyter import walk, render, table

pygwalker/api/marimo.py

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from typing import Union, List, Optional
2+
import inspect
3+
import json
4+
import pathlib
5+
6+
from typing_extensions import Literal
7+
8+
from .pygwalker import PygWalker
9+
from pygwalker.data_parsers.base import FieldSpec
10+
from pygwalker.data_parsers.database_parser import Connector
11+
from pygwalker._typing import DataFrame, IAppearance, IThemeKey
12+
from pygwalker.services.format_invoke_walk_code import get_formated_spec_params_code_from_frame
13+
from pygwalker.communications.anywidget_comm import AnywidgetCommunication
14+
import marimo as mo
15+
import anywidget
16+
import traitlets
17+
18+
19+
class _WalkerWidget(anywidget.AnyWidget):
20+
"""WalkerWidget"""
21+
_esm = (pathlib.Path(__file__).parent.parent / "templates" / "dist" / "pygwalker-app.es.js").read_text()
22+
props = traitlets.Unicode("").tag(sync=True)
23+
24+
25+
def walk(
26+
dataset: Union[DataFrame, Connector, str],
27+
gid: Union[int, str] = None,
28+
*,
29+
field_specs: Optional[List[FieldSpec]] = None,
30+
theme_key: IThemeKey = 'g2',
31+
appearance: IAppearance = 'media',
32+
spec: str = "",
33+
show_cloud_tool: bool = True,
34+
kanaries_api_key: str = "",
35+
default_tab: Literal["data", "vis"] = "vis",
36+
**kwargs
37+
):
38+
"""Walk through pandas.DataFrame df with Graphic Walker
39+
40+
Args:
41+
- dataset (pl.DataFrame | pd.DataFrame | Connector, optional): dataframe.
42+
- gid (Union[int, str], optional): GraphicWalker container div's id ('gwalker-{gid}')
43+
44+
Kargs:
45+
- field_specs (List[FieldSpec], optional): Specifications of some fields. They'll been automatically inferred from `df` if some fields are not specified.
46+
- theme_key ('vega' | 'g2' | 'streamlit'): theme type.
47+
- appearance (Literal['media' | 'light' | 'dark']): 'media': auto detect OS theme.
48+
- spec (str): chart config data. config id, json, remote file url
49+
- kanaries_api_key (str): kanaries api key, Default to "".
50+
- default_tab (Literal["data", "vis"]): default tab to show. Default to "vis"
51+
"""
52+
if field_specs is None:
53+
field_specs = []
54+
55+
source_invoke_code = get_formated_spec_params_code_from_frame(
56+
inspect.stack()[1].frame
57+
)
58+
59+
widget = _WalkerWidget()
60+
walker = PygWalker(
61+
gid=gid,
62+
dataset=dataset,
63+
field_specs=field_specs,
64+
spec=spec,
65+
source_invoke_code=source_invoke_code,
66+
theme_key=theme_key,
67+
appearance=appearance,
68+
show_cloud_tool=show_cloud_tool,
69+
use_preview=False,
70+
kernel_computation=True,
71+
use_save_tool=True,
72+
gw_mode="explore",
73+
is_export_dataframe=True,
74+
kanaries_api_key=kanaries_api_key,
75+
default_tab=default_tab,
76+
cloud_computation=False,
77+
**kwargs
78+
)
79+
comm = AnywidgetCommunication(walker.gid)
80+
81+
widget.props = json.dumps(walker._get_props("marimo", []))
82+
comm.register_widget(widget)
83+
walker._init_callback(comm)
84+
85+
return mo.ui.anywidget(widget)
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from typing import Any, Dict, Optional, List
2+
import uuid
3+
import json
4+
5+
import anywidget
6+
7+
from .base import BaseCommunication
8+
from pygwalker.utils.encode import DataFrameEncoder
9+
10+
11+
class AnywidgetCommunication(BaseCommunication):
12+
"""communication class for anywidget"""
13+
def register_widget(self, widget: anywidget.AnyWidget) -> None:
14+
"""register widget"""
15+
self.widget = widget
16+
self.widget.on_msg(self._on_mesage)
17+
18+
def send_msg_async(self, action: str, data: Dict[str, Any], rid: Optional[str] = None):
19+
"""send message base on anywidget"""
20+
if rid is None:
21+
rid = uuid.uuid1().hex
22+
msg = {
23+
"gid": self.gid,
24+
"rid": rid,
25+
"action": action,
26+
"data": data
27+
}
28+
self.widget.send({"type": "pyg_response", "data": json.dumps(msg, cls=DataFrameEncoder)})
29+
30+
def _on_mesage(self, _: anywidget.AnyWidget, data: Dict[str, Any], buffers: List[Any]):
31+
if data.get("type", "") != "pyg_request":
32+
return
33+
34+
msg = data["msg"]
35+
action = msg["action"]
36+
rid = msg["rid"]
37+
38+
if action == "finish_request":
39+
return
40+
41+
resp = self._receive_msg(action, msg["data"])
42+
self.send_msg_async("finish_request", resp, rid)

pyproject.toml

+2
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ dependencies = [
3636
"numpy<2.0.0",
3737
"ipylab<=1.0.0",
3838
"quickjs",
39+
"traitlets",
40+
"anywidget",
3941
]
4042
[project.urls]
4143
homepage = "https://kanaries.net/pygwalker"

0 commit comments

Comments
 (0)