darabos commited on
Commit
50a7b2a
·
2 Parent(s): bf8f4f1 cf99292

Merge pull request #73 from biggraph/darabos-rdkit

Browse files
.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.24.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.24.2",
636
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
637
- "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
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.24.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
- text-decoration: none;
294
- justify-content: space-between;
295
- padding-right: 10px;
 
 
 
 
 
 
 
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, .graph-relations {
358
- flex: 1;
359
- padding-left: 10px;
360
- padding-right: 10px;
 
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; /* Lighter text for labels */
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={{ markerEnd: { type: MarkerType.Arrow } }}
 
 
 
 
 
 
 
 
 
 
 
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·${expanded ? "expanded" : "collapsed"} `}
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 max-w-xs"
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 max-w-xs"
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 max-w-xs"
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
- Object.entries(display.dataframes || {}).map(
26
- ([name, df]: [string, any]) => (
27
- <React.Fragment key={name}>
28
- {!single && (
29
- <div
30
- key={`${name}-header`}
31
- className="df-head"
32
- onClick={() => setOpen({ ...open, [name]: !open[name] })}
33
- >
34
- {name}
35
- </div>
36
- )}
37
- {(single || open[name]) &&
38
- (df.data.length > 1 ? (
39
- <Table
40
- key={`${name}-table`}
41
- columns={df.columns}
42
- data={df.data}
43
- />
44
- ) : df.data.length ? (
45
- <dl key={`${name}-dl`}>
46
- {df.columns.map((c: string, i: number) => (
47
- <React.Fragment key={`${name}-${c}`}>
48
- <dt>{c}</dt>
49
- <dd>
50
- <Markdown>{toMD(df.data[0][i])}</Markdown>
51
- </dd>
52
- </React.Fragment>
53
- ))}
54
- </dl>
55
- ) : (
56
- JSON.stringify(df.data)
57
- ))}
58
- </React.Fragment>
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.model_dump_json(indent=2) + "\n"
 
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 lynxkite_ops # 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)
 
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
- graph.add_nodes_from(
87
- self.dfs["nodes"].set_index("id").to_dict("index").items()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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": collect(df)[:limit],
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(graph: Bundle, *, color_nodes_by: ops.NodeAttribute = None):
340
- nodes = graph.dfs["nodes"].copy()
 
 
 
 
 
 
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 = graph.dfs["edges"].drop_duplicates(["source", "target"])
 
 
 
 
371
  edges = edges.to_records()
372
  v = {
373
  "animationDuration": 500,
374
  "animationEasingUpdate": "quinticInOut",
 
375
  "series": [
376
  {
377
  "type": "graph",
378
- "roam": True,
 
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
- {"source": str(r.source), "target": str(r.target)} for r in edges
 
 
 
 
 
 
 
 
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 collect(df: pd.DataFrame):
 
 
410
  if isinstance(df, pl.LazyFrame):
411
  df = df.collect()
412
  if isinstance(df, pl.DataFrame):
413
- return [[d[c] for c in df.columns] for d in df.to_dicts()]
414
- return df.values.tolist()
 
 
 
 
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
+ )