Spaces:
Running
Running
Merge pull request #73 from biggraph/darabos-rdkit
Browse files- .github/workflows/test.yaml +1 -1
- README.md +2 -1
- examples/Image processing +1 -1
- examples/NetworkX demo +0 -0
- examples/PyTorch demo +1 -1
- lynxkite-app/src/lynxkite_app/crdt.py +0 -1
- lynxkite-app/web/package-lock.json +21 -4
- lynxkite-app/web/package.json +1 -1
- lynxkite-app/web/src/index.css +18 -16
- lynxkite-app/web/src/workspace/Workspace.tsx +16 -2
- lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx +1 -1
- lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx +3 -3
- lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx +36 -36
- lynxkite-app/web/tests/examples.spec.ts +5 -0
- lynxkite-app/web/tests/lynxkite.ts +1 -1
- lynxkite-core/pyproject.toml +1 -1
- lynxkite-core/src/lynxkite/core/workspace.py +3 -1
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/__init__.py +1 -1
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py +60 -14
- lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py +1 -1
.github/workflows/test.yaml
CHANGED
@@ -24,7 +24,7 @@ jobs:
|
|
24 |
run: |
|
25 |
eval `ssh-agent -s`
|
26 |
ssh-add - <<< '${{ secrets.LYNXSCRIBE_DEPLOY_KEY }}'
|
27 |
-
uv pip install -e lynxkite-core/[dev] lynxkite-app/[dev] lynxkite-graph-analytics/[dev] lynxkite-lynxscribe/ lynxkite-pillow-example/
|
28 |
env:
|
29 |
UV_SYSTEM_PYTHON: 1
|
30 |
|
|
|
24 |
run: |
|
25 |
eval `ssh-agent -s`
|
26 |
ssh-add - <<< '${{ secrets.LYNXSCRIBE_DEPLOY_KEY }}'
|
27 |
+
uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-analytics/[dev] -e lynxkite-bio -e lynxkite-lynxscribe/ -e lynxkite-pillow-example/
|
28 |
env:
|
29 |
UV_SYSTEM_PYTHON: 1
|
30 |
|
README.md
CHANGED
@@ -14,6 +14,7 @@ original LynxKite. The primary goals of this rewrite are:
|
|
14 |
- `lynxkite-graph-analytics`: Graph analytics plugin. The classical LynxKite experience!
|
15 |
- `lynxkite-pillow`: A simple example plugin.
|
16 |
- `lynxkite-lynxscribe`: A plugin for building and running LynxScribe applications.
|
|
|
17 |
- `docs`: User-facing documentation. It's shared between all packages.
|
18 |
|
19 |
## Development
|
@@ -25,7 +26,7 @@ uv venv
|
|
25 |
source .venv/bin/activate
|
26 |
uvx pre-commit install
|
27 |
# The [dev] tag is only needed if you intend on running tests
|
28 |
-
uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-analytics/[dev] -e lynxkite-lynxscribe/ -e lynxkite-pillow-example/
|
29 |
```
|
30 |
|
31 |
This also builds the frontend, hopefully very quickly. To run it:
|
|
|
14 |
- `lynxkite-graph-analytics`: Graph analytics plugin. The classical LynxKite experience!
|
15 |
- `lynxkite-pillow`: A simple example plugin.
|
16 |
- `lynxkite-lynxscribe`: A plugin for building and running LynxScribe applications.
|
17 |
+
- `lynxkite-bio`: Bioinformatics additions for LynxKite Graph Analytics.
|
18 |
- `docs`: User-facing documentation. It's shared between all packages.
|
19 |
|
20 |
## Development
|
|
|
26 |
source .venv/bin/activate
|
27 |
uvx pre-commit install
|
28 |
# The [dev] tag is only needed if you intend on running tests
|
29 |
+
uv pip install -e lynxkite-core/[dev] -e lynxkite-app/[dev] -e lynxkite-graph-analytics/[dev] -e lynxkite-bio -e lynxkite-lynxscribe/ -e lynxkite-pillow-example/
|
30 |
```
|
31 |
|
32 |
This also builds the frontend, hopefully very quickly. To run it:
|
examples/Image processing
CHANGED
@@ -281,4 +281,4 @@
|
|
281 |
"targetHandle": "image"
|
282 |
}
|
283 |
]
|
284 |
-
}
|
|
|
281 |
"targetHandle": "image"
|
282 |
}
|
283 |
]
|
284 |
+
}
|
examples/NetworkX demo
CHANGED
The diff for this file is too large to render.
See raw diff
|
|
examples/PyTorch demo
CHANGED
@@ -620,4 +620,4 @@
|
|
620 |
"targetHandle": "x"
|
621 |
}
|
622 |
]
|
623 |
-
}
|
|
|
620 |
"targetHandle": "x"
|
621 |
}
|
622 |
]
|
623 |
+
}
|
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3 |
import asyncio
|
4 |
import contextlib
|
5 |
import enum
|
6 |
-
import pathlib
|
7 |
import fastapi
|
8 |
import os.path
|
9 |
import pycrdt
|
|
|
3 |
import asyncio
|
4 |
import contextlib
|
5 |
import enum
|
|
|
6 |
import fastapi
|
7 |
import os.path
|
8 |
import pycrdt
|
lynxkite-app/web/package-lock.json
CHANGED
@@ -8,7 +8,7 @@
|
|
8 |
"name": "lynxkite",
|
9 |
"version": "0.0.0",
|
10 |
"dependencies": {
|
11 |
-
"@esbuild/linux-x64": "^0.
|
12 |
"@iconify-json/tabler": "^1.2.10",
|
13 |
"@svgr/core": "^8.1.0",
|
14 |
"@svgr/plugin-jsx": "^8.1.0",
|
@@ -632,9 +632,9 @@
|
|
632 |
}
|
633 |
},
|
634 |
"node_modules/@esbuild/linux-x64": {
|
635 |
-
"version": "0.
|
636 |
-
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.
|
637 |
-
"integrity": "sha512-
|
638 |
"cpu": [
|
639 |
"x64"
|
640 |
],
|
@@ -3055,6 +3055,23 @@
|
|
3055 |
"@esbuild/win32-x64": "0.24.2"
|
3056 |
}
|
3057 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3058 |
"node_modules/escalade": {
|
3059 |
"version": "3.2.0",
|
3060 |
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
|
|
8 |
"name": "lynxkite",
|
9 |
"version": "0.0.0",
|
10 |
"dependencies": {
|
11 |
+
"@esbuild/linux-x64": "^0.25.0",
|
12 |
"@iconify-json/tabler": "^1.2.10",
|
13 |
"@svgr/core": "^8.1.0",
|
14 |
"@svgr/plugin-jsx": "^8.1.0",
|
|
|
632 |
}
|
633 |
},
|
634 |
"node_modules/@esbuild/linux-x64": {
|
635 |
+
"version": "0.25.0",
|
636 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
|
637 |
+
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
|
638 |
"cpu": [
|
639 |
"x64"
|
640 |
],
|
|
|
3055 |
"@esbuild/win32-x64": "0.24.2"
|
3056 |
}
|
3057 |
},
|
3058 |
+
"node_modules/esbuild/node_modules/@esbuild/linux-x64": {
|
3059 |
+
"version": "0.24.2",
|
3060 |
+
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
3061 |
+
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
3062 |
+
"cpu": [
|
3063 |
+
"x64"
|
3064 |
+
],
|
3065 |
+
"dev": true,
|
3066 |
+
"license": "MIT",
|
3067 |
+
"optional": true,
|
3068 |
+
"os": [
|
3069 |
+
"linux"
|
3070 |
+
],
|
3071 |
+
"engines": {
|
3072 |
+
"node": ">=18"
|
3073 |
+
}
|
3074 |
+
},
|
3075 |
"node_modules/escalade": {
|
3076 |
"version": "3.2.0",
|
3077 |
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
lynxkite-app/web/package.json
CHANGED
@@ -11,7 +11,7 @@
|
|
11 |
"preview": "npx vite preview"
|
12 |
},
|
13 |
"dependencies": {
|
14 |
-
"@esbuild/linux-x64": "^0.
|
15 |
"@iconify-json/tabler": "^1.2.10",
|
16 |
"@svgr/core": "^8.1.0",
|
17 |
"@svgr/plugin-jsx": "^8.1.0",
|
|
|
11 |
"preview": "npx vite preview"
|
12 |
},
|
13 |
"dependencies": {
|
14 |
+
"@esbuild/linux-x64": "^0.25.0",
|
15 |
"@iconify-json/tabler": "^1.2.10",
|
16 |
"@svgr/core": "^8.1.0",
|
17 |
"@svgr/plugin-jsx": "^8.1.0",
|
lynxkite-app/web/src/index.css
CHANGED
@@ -286,13 +286,19 @@ body {
|
|
286 |
.entry-list .entry {
|
287 |
display: flex;
|
288 |
border-bottom: 1px solid whitesmoke;
|
289 |
-
padding-left: 10px;
|
290 |
color: #004165;
|
291 |
cursor: pointer;
|
292 |
user-select: none;
|
293 |
-
|
294 |
-
|
295 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
296 |
}
|
297 |
|
298 |
.entry-list .open .entry,
|
@@ -316,11 +322,6 @@ body {
|
|
316 |
}
|
317 |
}
|
318 |
|
319 |
-
path.react-flow__edge-path {
|
320 |
-
stroke-width: 2;
|
321 |
-
stroke: black;
|
322 |
-
}
|
323 |
-
|
324 |
.react-flow__edge.selected path.react-flow__edge-path {
|
325 |
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
326 |
outline-offset: 10px;
|
@@ -354,13 +355,14 @@ path.react-flow__edge-path {
|
|
354 |
margin-top: 10px;
|
355 |
}
|
356 |
|
357 |
-
.graph-tables,
|
358 |
-
|
359 |
-
|
360 |
-
|
|
|
361 |
}
|
362 |
|
363 |
-
.graph-table-header{
|
364 |
display: flex;
|
365 |
justify-content: space-between;
|
366 |
font-weight: bold;
|
@@ -400,7 +402,7 @@ path.react-flow__edge-path {
|
|
400 |
font-weight: bold;
|
401 |
display: block;
|
402 |
margin-bottom: 2px;
|
403 |
-
color: #666;
|
404 |
}
|
405 |
|
406 |
.graph-relation-attributes input {
|
@@ -428,4 +430,4 @@ path.react-flow__edge-path {
|
|
428 |
|
429 |
.add-relationship-button:hover {
|
430 |
background-color: #218838;
|
431 |
-
}
|
|
|
286 |
.entry-list .entry {
|
287 |
display: flex;
|
288 |
border-bottom: 1px solid whitesmoke;
|
|
|
289 |
color: #004165;
|
290 |
cursor: pointer;
|
291 |
user-select: none;
|
292 |
+
|
293 |
+
a {
|
294 |
+
text-decoration: none;
|
295 |
+
flex: 1;
|
296 |
+
padding-left: 10px;
|
297 |
+
}
|
298 |
+
|
299 |
+
button {
|
300 |
+
padding-right: 10px;
|
301 |
+
}
|
302 |
}
|
303 |
|
304 |
.entry-list .open .entry,
|
|
|
322 |
}
|
323 |
}
|
324 |
|
|
|
|
|
|
|
|
|
|
|
325 |
.react-flow__edge.selected path.react-flow__edge-path {
|
326 |
outline: var(--xy-selection-border, var(--xy-selection-border-default));
|
327 |
outline-offset: 10px;
|
|
|
355 |
margin-top: 10px;
|
356 |
}
|
357 |
|
358 |
+
.graph-tables,
|
359 |
+
.graph-relations {
|
360 |
+
flex: 1;
|
361 |
+
padding-left: 10px;
|
362 |
+
padding-right: 10px;
|
363 |
}
|
364 |
|
365 |
+
.graph-table-header {
|
366 |
display: flex;
|
367 |
justify-content: space-between;
|
368 |
font-weight: bold;
|
|
|
402 |
font-weight: bold;
|
403 |
display: block;
|
404 |
margin-bottom: 2px;
|
405 |
+
color: #666; /* Lighter text for labels */
|
406 |
}
|
407 |
|
408 |
.graph-relation-attributes input {
|
|
|
430 |
|
431 |
.add-relationship-button:hover {
|
432 |
background-color: #218838;
|
433 |
+
}
|
lynxkite-app/web/src/workspace/Workspace.tsx
CHANGED
@@ -41,11 +41,11 @@ import NodeSearch, {
|
|
41 |
type Catalog,
|
42 |
type Catalogs,
|
43 |
} from "./NodeSearch.tsx";
|
|
|
44 |
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
45 |
import NodeWithParams from "./nodes/NodeWithParams";
|
46 |
import NodeWithTableView from "./nodes/NodeWithTableView.tsx";
|
47 |
import NodeWithVisualization from "./nodes/NodeWithVisualization.tsx";
|
48 |
-
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
49 |
|
50 |
export default function (props: any) {
|
51 |
return (
|
@@ -78,6 +78,9 @@ function LynxKiteFlow() {
|
|
78 |
if (!state.workspace) return;
|
79 |
if (!state.workspace.nodes) return;
|
80 |
if (!state.workspace.edges) return;
|
|
|
|
|
|
|
81 |
setNodes([...state.workspace.nodes] as Node[]);
|
82 |
setEdges([...state.workspace.edges] as Edge[]);
|
83 |
for (const node of state.workspace.nodes) {
|
@@ -284,7 +287,18 @@ function LynxKiteFlow() {
|
|
284 |
proOptions={{ hideAttribution: true }}
|
285 |
maxZoom={3}
|
286 |
minZoom={0.3}
|
287 |
-
defaultEdgeOptions={{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
288 |
>
|
289 |
<Controls />
|
290 |
<MiniMap />
|
|
|
41 |
type Catalog,
|
42 |
type Catalogs,
|
43 |
} from "./NodeSearch.tsx";
|
44 |
+
import NodeWithGraphCreationView from "./nodes/GraphCreationNode.tsx";
|
45 |
import NodeWithImage from "./nodes/NodeWithImage.tsx";
|
46 |
import NodeWithParams from "./nodes/NodeWithParams";
|
47 |
import NodeWithTableView from "./nodes/NodeWithTableView.tsx";
|
48 |
import NodeWithVisualization from "./nodes/NodeWithVisualization.tsx";
|
|
|
49 |
|
50 |
export default function (props: any) {
|
51 |
return (
|
|
|
78 |
if (!state.workspace) return;
|
79 |
if (!state.workspace.nodes) return;
|
80 |
if (!state.workspace.edges) return;
|
81 |
+
for (const n of state.workspace.nodes) {
|
82 |
+
n.dragHandle = ".bg-primary";
|
83 |
+
}
|
84 |
setNodes([...state.workspace.nodes] as Node[]);
|
85 |
setEdges([...state.workspace.edges] as Edge[]);
|
86 |
for (const node of state.workspace.nodes) {
|
|
|
287 |
proOptions={{ hideAttribution: true }}
|
288 |
maxZoom={3}
|
289 |
minZoom={0.3}
|
290 |
+
defaultEdgeOptions={{
|
291 |
+
markerEnd: {
|
292 |
+
type: MarkerType.ArrowClosed,
|
293 |
+
color: "black",
|
294 |
+
width: 15,
|
295 |
+
height: 15,
|
296 |
+
},
|
297 |
+
style: {
|
298 |
+
strokeWidth: 2,
|
299 |
+
stroke: "black",
|
300 |
+
},
|
301 |
+
}}
|
302 |
>
|
303 |
<Controls />
|
304 |
<MiniMap />
|
lynxkite-app/web/src/workspace/nodes/LynxKiteNode.tsx
CHANGED
@@ -64,7 +64,7 @@ export default function LynxKiteNode(props: LynxKiteNodeProps) {
|
|
64 |
|
65 |
return (
|
66 |
<div
|
67 |
-
className={`node-container
|
68 |
style={{
|
69 |
width: props.width || 200,
|
70 |
height: expanded ? props.height || 200 : undefined,
|
|
|
64 |
|
65 |
return (
|
66 |
<div
|
67 |
+
className={`node-container ${expanded ? "expanded" : "collapsed"} `}
|
68 |
style={{
|
69 |
width: props.width || 200,
|
70 |
height: expanded ? props.height || 200 : undefined,
|
lynxkite-app/web/src/workspace/nodes/NodeParameter.tsx
CHANGED
@@ -31,7 +31,7 @@ export default function NodeParameter({
|
|
31 |
<>
|
32 |
<ParamName name={name} />
|
33 |
<textarea
|
34 |
-
className="textarea textarea-bordered w-full
|
35 |
rows={6}
|
36 |
value={value}
|
37 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
@@ -42,7 +42,7 @@ export default function NodeParameter({
|
|
42 |
<>
|
43 |
<ParamName name={name} />
|
44 |
<select
|
45 |
-
className="select select-bordered w-full
|
46 |
value={value || meta.type.enum[0]}
|
47 |
onChange={(evt) => onChange(evt.currentTarget.value)}
|
48 |
>
|
@@ -69,7 +69,7 @@ export default function NodeParameter({
|
|
69 |
<>
|
70 |
<ParamName name={name} />
|
71 |
<input
|
72 |
-
className="input input-bordered w-full
|
73 |
value={value || ""}
|
74 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
75 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
|
|
31 |
<>
|
32 |
<ParamName name={name} />
|
33 |
<textarea
|
34 |
+
className="textarea textarea-bordered w-full"
|
35 |
rows={6}
|
36 |
value={value}
|
37 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
|
|
42 |
<>
|
43 |
<ParamName name={name} />
|
44 |
<select
|
45 |
+
className="select select-bordered w-full"
|
46 |
value={value || meta.type.enum[0]}
|
47 |
onChange={(evt) => onChange(evt.currentTarget.value)}
|
48 |
>
|
|
|
69 |
<>
|
70 |
<ParamName name={name} />
|
71 |
<input
|
72 |
+
className="input input-bordered w-full"
|
73 |
value={value || ""}
|
74 |
onChange={(evt) => onChange(evt.currentTarget.value, { delay: 2 })}
|
75 |
onBlur={(evt) => onChange(evt.currentTarget.value, { delay: 0 })}
|
lynxkite-app/web/src/workspace/nodes/NodeWithTableView.tsx
CHANGED
@@ -19,45 +19,45 @@ export default function NodeWithTableView(props: any) {
|
|
19 |
const display = props.data.display?.value;
|
20 |
const single =
|
21 |
display?.dataframes && Object.keys(display?.dataframes).length === 1;
|
|
|
|
|
22 |
return (
|
23 |
<LynxKiteNode {...props}>
|
24 |
{display && [
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
46 |
-
{
|
47 |
-
<
|
48 |
-
|
49 |
-
<
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
)
|
56 |
-
|
57 |
-
|
58 |
-
|
59 |
-
),
|
60 |
-
),
|
61 |
Object.entries(display.others || {}).map(([name, o]) => (
|
62 |
<>
|
63 |
<div
|
|
|
19 |
const display = props.data.display?.value;
|
20 |
const single =
|
21 |
display?.dataframes && Object.keys(display?.dataframes).length === 1;
|
22 |
+
const dfs = Object.entries(display?.dataframes || {});
|
23 |
+
dfs.sort();
|
24 |
return (
|
25 |
<LynxKiteNode {...props}>
|
26 |
{display && [
|
27 |
+
dfs.map(([name, df]: [string, any]) => (
|
28 |
+
<React.Fragment key={name}>
|
29 |
+
{!single && (
|
30 |
+
<div
|
31 |
+
key={`${name}-header`}
|
32 |
+
className="df-head"
|
33 |
+
onClick={() => setOpen({ ...open, [name]: !open[name] })}
|
34 |
+
>
|
35 |
+
{name}
|
36 |
+
</div>
|
37 |
+
)}
|
38 |
+
{(single || open[name]) &&
|
39 |
+
(df.data.length > 1 ? (
|
40 |
+
<Table
|
41 |
+
key={`${name}-table`}
|
42 |
+
columns={df.columns}
|
43 |
+
data={df.data}
|
44 |
+
/>
|
45 |
+
) : df.data.length ? (
|
46 |
+
<dl key={`${name}-dl`}>
|
47 |
+
{df.columns.map((c: string, i: number) => (
|
48 |
+
<React.Fragment key={`${name}-${c}`}>
|
49 |
+
<dt>{c}</dt>
|
50 |
+
<dd>
|
51 |
+
<Markdown>{toMD(df.data[0][i])}</Markdown>
|
52 |
+
</dd>
|
53 |
+
</React.Fragment>
|
54 |
+
))}
|
55 |
+
</dl>
|
56 |
+
) : (
|
57 |
+
JSON.stringify(df.data)
|
58 |
+
))}
|
59 |
+
</React.Fragment>
|
60 |
+
)),
|
|
|
|
|
61 |
Object.entries(display.others || {}).map(([name, o]) => (
|
62 |
<>
|
63 |
<div
|
lynxkite-app/web/tests/examples.spec.ts
CHANGED
@@ -7,6 +7,11 @@ test("LynxKite Graph Analytics example", async ({ page }) => {
|
|
7 |
expect(await ws.isErrorFree(process.env.CI ? 2000 : 1000)).toBeTruthy();
|
8 |
});
|
9 |
|
|
|
|
|
|
|
|
|
|
|
10 |
test("Pytorch example", async ({ page }) => {
|
11 |
const ws = await Workspace.open(page, "PyTorch demo");
|
12 |
expect(await ws.isErrorFree()).toBeTruthy();
|
|
|
7 |
expect(await ws.isErrorFree(process.env.CI ? 2000 : 1000)).toBeTruthy();
|
8 |
});
|
9 |
|
10 |
+
test("Bio example", async ({ page }) => {
|
11 |
+
const ws = await Workspace.open(page, "Bio demo");
|
12 |
+
expect(await ws.isErrorFree()).toBeTruthy();
|
13 |
+
});
|
14 |
+
|
15 |
test("Pytorch example", async ({ page }) => {
|
16 |
const ws = await Workspace.open(page, "PyTorch demo");
|
17 |
expect(await ws.isErrorFree()).toBeTruthy();
|
lynxkite-app/web/tests/lynxkite.ts
CHANGED
@@ -113,7 +113,7 @@ export class Workspace {
|
|
113 |
targetPosition?: { x: number; y: number },
|
114 |
) {
|
115 |
// Move a box around, it is a best effort operation, the exact target position may not be reached
|
116 |
-
const box = await this.getBox(boxId).boundingBox();
|
117 |
if (!box) {
|
118 |
return;
|
119 |
}
|
|
|
113 |
targetPosition?: { x: number; y: number },
|
114 |
) {
|
115 |
// Move a box around, it is a best effort operation, the exact target position may not be reached
|
116 |
+
const box = await this.getBox(boxId).locator(".title").boundingBox();
|
117 |
if (!box) {
|
118 |
return;
|
119 |
}
|
lynxkite-core/pyproject.toml
CHANGED
@@ -10,4 +10,4 @@ dependencies = [
|
|
10 |
[project.optional-dependencies]
|
11 |
dev = [
|
12 |
"pytest",
|
13 |
-
]
|
|
|
10 |
[project.optional-dependencies]
|
11 |
dev = [
|
12 |
"pytest",
|
13 |
+
]
|
lynxkite-core/src/lynxkite/core/workspace.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1 |
"""For working with LynxKite workspaces."""
|
2 |
|
|
|
3 |
from typing import Optional
|
4 |
import dataclasses
|
5 |
import os
|
@@ -65,7 +66,8 @@ async def execute(ws: Workspace):
|
|
65 |
|
66 |
def save(ws: Workspace, path: str):
|
67 |
"""Persist a workspace to a local file in JSON format."""
|
68 |
-
j = ws.
|
|
|
69 |
dirname, basename = os.path.split(path)
|
70 |
os.makedirs(dirname, exist_ok=True)
|
71 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
|
|
1 |
"""For working with LynxKite workspaces."""
|
2 |
|
3 |
+
import json
|
4 |
from typing import Optional
|
5 |
import dataclasses
|
6 |
import os
|
|
|
66 |
|
67 |
def save(ws: Workspace, path: str):
|
68 |
"""Persist a workspace to a local file in JSON format."""
|
69 |
+
j = ws.model_dump()
|
70 |
+
j = json.dumps(j, indent=2, sort_keys=True) + "\n"
|
71 |
dirname, basename = os.path.split(path)
|
72 |
os.makedirs(dirname, exist_ok=True)
|
73 |
# Create temp file in the same directory to make sure it's on the same filesystem.
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/__init__.py
CHANGED
@@ -1,3 +1,3 @@
|
|
1 |
-
from . import
|
2 |
from . import networkx_ops # noqa (imported to trigger registration)
|
3 |
from . import pytorch_model_ops # noqa (imported to trigger registration)
|
|
|
1 |
+
from .lynxkite_ops import * # noqa (imported to trigger registration)
|
2 |
from . import networkx_ops # noqa (imported to trigger registration)
|
3 |
from . import pytorch_model_ops # noqa (imported to trigger registration)
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/lynxkite_ops.py
CHANGED
@@ -83,12 +83,26 @@ class Bundle:
|
|
83 |
# TODO: Use relations.
|
84 |
graph = nx.DiGraph()
|
85 |
if "nodes" in self.dfs:
|
86 |
-
|
87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
88 |
)
|
89 |
-
graph.add_edges_from(
|
90 |
-
self.dfs["edges"][["source", "target"]].itertuples(index=False, name=None)
|
91 |
-
)
|
92 |
return graph
|
93 |
|
94 |
def copy(self):
|
@@ -104,7 +118,7 @@ class Bundle:
|
|
104 |
"dataframes": {
|
105 |
name: {
|
106 |
"columns": [str(c) for c in df.columns],
|
107 |
-
"data":
|
108 |
}
|
109 |
for name, df in self.dfs.items()
|
110 |
},
|
@@ -336,8 +350,14 @@ def _map_color(value):
|
|
336 |
|
337 |
|
338 |
@op("Visualize graph", view="visualization")
|
339 |
-
def visualize_graph(
|
340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
341 |
if color_nodes_by:
|
342 |
nodes["color"] = _map_color(nodes[color_nodes_by])
|
343 |
for cols in ["x y", "long lat"]:
|
@@ -367,15 +387,21 @@ def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
|
|
367 |
)
|
368 |
curveness = 0.3
|
369 |
nodes = nodes.to_records()
|
370 |
-
edges =
|
|
|
|
|
|
|
|
|
371 |
edges = edges.to_records()
|
372 |
v = {
|
373 |
"animationDuration": 500,
|
374 |
"animationEasingUpdate": "quinticInOut",
|
|
|
375 |
"series": [
|
376 |
{
|
377 |
"type": "graph",
|
378 |
-
|
|
|
379 |
"lineStyle": {
|
380 |
"color": "gray",
|
381 |
"curveness": curveness,
|
@@ -386,6 +412,7 @@ def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
|
|
386 |
"width": 10,
|
387 |
},
|
388 |
},
|
|
|
389 |
"data": [
|
390 |
{
|
391 |
"id": str(n.id),
|
@@ -394,11 +421,24 @@ def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
|
|
394 |
# Adjust node size to cover the same area no matter how many nodes there are.
|
395 |
"symbolSize": 50 / len(nodes) ** 0.5,
|
396 |
"itemStyle": {"color": n.color} if color_nodes_by else {},
|
|
|
|
|
|
|
|
|
|
|
397 |
}
|
398 |
for n in nodes
|
399 |
],
|
400 |
"links": [
|
401 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
402 |
],
|
403 |
},
|
404 |
],
|
@@ -406,12 +446,18 @@ def visualize_graph(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
|
|
406 |
return v
|
407 |
|
408 |
|
409 |
-
def
|
|
|
|
|
410 |
if isinstance(df, pl.LazyFrame):
|
411 |
df = df.collect()
|
412 |
if isinstance(df, pl.DataFrame):
|
413 |
-
|
414 |
-
|
|
|
|
|
|
|
|
|
415 |
|
416 |
|
417 |
@op("View tables", view="table_view")
|
|
|
83 |
# TODO: Use relations.
|
84 |
graph = nx.DiGraph()
|
85 |
if "nodes" in self.dfs:
|
86 |
+
df = self.dfs["nodes"]
|
87 |
+
if df.index.name != "id":
|
88 |
+
df = df.set_index("id")
|
89 |
+
graph.add_nodes_from(df.to_dict("index").items())
|
90 |
+
if "edges" in self.dfs:
|
91 |
+
edges = self.dfs["edges"]
|
92 |
+
graph.add_edges_from(
|
93 |
+
[
|
94 |
+
(
|
95 |
+
e["source"],
|
96 |
+
e["target"],
|
97 |
+
{
|
98 |
+
k: e[k]
|
99 |
+
for k in edges.columns
|
100 |
+
if k not in ["source", "target"]
|
101 |
+
},
|
102 |
+
)
|
103 |
+
for e in edges.to_records()
|
104 |
+
]
|
105 |
)
|
|
|
|
|
|
|
106 |
return graph
|
107 |
|
108 |
def copy(self):
|
|
|
118 |
"dataframes": {
|
119 |
name: {
|
120 |
"columns": [str(c) for c in df.columns],
|
121 |
+
"data": df_for_frontend(df, limit).values.tolist(),
|
122 |
}
|
123 |
for name, df in self.dfs.items()
|
124 |
},
|
|
|
350 |
|
351 |
|
352 |
@op("Visualize graph", view="visualization")
|
353 |
+
def visualize_graph(
|
354 |
+
graph: Bundle,
|
355 |
+
*,
|
356 |
+
color_nodes_by: ops.NodeAttribute = None,
|
357 |
+
label_by: ops.NodeAttribute = None,
|
358 |
+
color_edges_by: ops.EdgeAttribute = None,
|
359 |
+
):
|
360 |
+
nodes = df_for_frontend(graph.dfs["nodes"], 10_000)
|
361 |
if color_nodes_by:
|
362 |
nodes["color"] = _map_color(nodes[color_nodes_by])
|
363 |
for cols in ["x y", "long lat"]:
|
|
|
387 |
)
|
388 |
curveness = 0.3
|
389 |
nodes = nodes.to_records()
|
390 |
+
edges = df_for_frontend(
|
391 |
+
graph.dfs["edges"].drop_duplicates(["source", "target"]), 10_000
|
392 |
+
)
|
393 |
+
if color_edges_by:
|
394 |
+
edges["color"] = _map_color(edges[color_edges_by])
|
395 |
edges = edges.to_records()
|
396 |
v = {
|
397 |
"animationDuration": 500,
|
398 |
"animationEasingUpdate": "quinticInOut",
|
399 |
+
"tooltip": {"show": True},
|
400 |
"series": [
|
401 |
{
|
402 |
"type": "graph",
|
403 |
+
# Mouse zoom/panning is disabled for now. It interacts badly with ReactFlow.
|
404 |
+
# "roam": True,
|
405 |
"lineStyle": {
|
406 |
"color": "gray",
|
407 |
"curveness": curveness,
|
|
|
412 |
"width": 10,
|
413 |
},
|
414 |
},
|
415 |
+
"label": {"position": "top", "formatter": "{b}"},
|
416 |
"data": [
|
417 |
{
|
418 |
"id": str(n.id),
|
|
|
421 |
# Adjust node size to cover the same area no matter how many nodes there are.
|
422 |
"symbolSize": 50 / len(nodes) ** 0.5,
|
423 |
"itemStyle": {"color": n.color} if color_nodes_by else {},
|
424 |
+
"label": {"show": label_by is not None},
|
425 |
+
"name": str(getattr(n, label_by, "")) if label_by else None,
|
426 |
+
"value": str(getattr(n, color_nodes_by, ""))
|
427 |
+
if color_nodes_by
|
428 |
+
else None,
|
429 |
}
|
430 |
for n in nodes
|
431 |
],
|
432 |
"links": [
|
433 |
+
{
|
434 |
+
"source": str(r.source),
|
435 |
+
"target": str(r.target),
|
436 |
+
"lineStyle": {"color": r.color} if color_edges_by else {},
|
437 |
+
"value": str(getattr(r, color_edges_by, ""))
|
438 |
+
if color_edges_by
|
439 |
+
else None,
|
440 |
+
}
|
441 |
+
for r in edges
|
442 |
],
|
443 |
},
|
444 |
],
|
|
|
446 |
return v
|
447 |
|
448 |
|
449 |
+
def df_for_frontend(df: pd.DataFrame, limit: int) -> pd.DataFrame:
|
450 |
+
"""Returns a DataFrame with values that are safe to send to the frontend."""
|
451 |
+
df = df[:limit]
|
452 |
if isinstance(df, pl.LazyFrame):
|
453 |
df = df.collect()
|
454 |
if isinstance(df, pl.DataFrame):
|
455 |
+
df = df.to_pandas()
|
456 |
+
# Convert non-numeric columns to strings.
|
457 |
+
for c in df.columns:
|
458 |
+
if not pd.api.types.is_numeric_dtype(df[c]):
|
459 |
+
df[c] = df[c].astype(str)
|
460 |
+
return df
|
461 |
|
462 |
|
463 |
@op("View tables", view="table_view")
|
lynxkite-graph-analytics/src/lynxkite_graph_analytics/pytorch_model_ops.py
CHANGED
@@ -72,4 +72,4 @@ ops.register_passive_op(
|
|
72 |
inputs=[ops.Input(name="input", position="top", type="tensor")],
|
73 |
outputs=[ops.Output(name="output", position="bottom", type="tensor")],
|
74 |
params=[ops.Parameter.basic("times", 1, int)],
|
75 |
-
)
|
|
|
72 |
inputs=[ops.Input(name="input", position="top", type="tensor")],
|
73 |
outputs=[ops.Output(name="output", position="bottom", type="tensor")],
|
74 |
params=[ops.Parameter.basic("times", 1, int)],
|
75 |
+
)
|