From c5fcf6acd29c5844de79a43cfda9129e419322f0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Loi=CC=88c=20Correnson?= <loic.correnson@cea.fr>
Date: Fri, 24 May 2024 13:41:39 +0200
Subject: [PATCH] [dome/diagram] selection callback

---
 ivette/.dome-pkg-app.lock                  |  2 +-
 ivette/.dome-pkg-dev.lock                  |  2 +-
 ivette/package.json                        |  4 +-
 ivette/src/dome/renderer/graph/diagram.tsx | 50 +++++++++++++++-------
 ivette/src/dome/template/makefile.packages | 12 +++---
 ivette/src/sandbox/dotdiagram.tsx          |  4 ++
 ivette/yarn.lock                           |  9 +++-
 7 files changed, 57 insertions(+), 26 deletions(-)

diff --git a/ivette/.dome-pkg-app.lock b/ivette/.dome-pkg-app.lock
index da523751673..d10dcc94cc5 100644
--- a/ivette/.dome-pkg-app.lock
+++ b/ivette/.dome-pkg-app.lock
@@ -1 +1 @@
-react@^18 react-dom@^18 source-map-support lodash@^4 react-virtualized@9.22.5 react-draggable react-fast-compare diff@^5 codemirror@5.65.2 @codemirror/language@6.10.0 @codemirror/search@6.5.5 @codemirror/state@6.4.0 @codemirror/view@6.23.1 @lezer/cpp@^1 react-virtualized-auto-sizer@^1.0.22 react-force-graph-2d@^1.25.4 react-force-graph-3d@^1.24.2 d3-graphviz
+react@^18 react-dom@^18 source-map-support lodash@^4 react-virtualized@9.22.5 react-draggable react-fast-compare diff@^5 codemirror@5.65.2 @codemirror/language@6.10.0 @codemirror/search@6.5.5 @codemirror/state@6.4.0 @codemirror/view@6.23.1 @lezer/cpp@^1 react-virtualized-auto-sizer@^1.0.22 react-force-graph-2d@^1.25.4 react-force-graph-3d@^1.24.2 d3-selection@^3 d3-graphviz@^5
diff --git a/ivette/.dome-pkg-dev.lock b/ivette/.dome-pkg-dev.lock
index c77ef156115..5f7937b83c1 100644
--- a/ivette/.dome-pkg-dev.lock
+++ b/ivette/.dome-pkg-dev.lock
@@ -1 +1 @@
-electron@^28 electron-builder@^24 electron-vite@^2 electron-devtools-installer @types/lodash@^4 @types/react@^18 @types/node@^18 @types/react-dom@^18 @types/react-virtualized@^9.21.8 @types/diff@^5 @types/d3-graphviz typescript@^5
+electron@^28 electron-builder@^24 electron-vite@^2 electron-devtools-installer @types/lodash@^4 @types/react@^18 @types/node@^18 @types/react-dom@^18 @types/react-virtualized@^9.21.8 @types/diff@^5 @types/d3-selection @types/d3-graphviz typescript@^5
diff --git a/ivette/package.json b/ivette/package.json
index 57176f28b25..e44cabd9e3d 100644
--- a/ivette/package.json
+++ b/ivette/package.json
@@ -39,7 +39,8 @@
     "cytoscape-klay": "",
     "cytoscape-panzoom": "",
     "cytoscape-popper": "",
-    "d3-graphviz": "^5.4.0",
+    "d3-graphviz": "^5",
+    "d3-selection": "^3",
     "diff": "^5",
     "lodash": "^4",
     "react": "^18",
@@ -63,6 +64,7 @@
     "@playwright/test": "^1.41.2",
     "@types/cytoscape": "^3.19.16",
     "@types/d3-graphviz": "^2.6.10",
+    "@types/d3-selection": "^3.0.10",
     "@types/diff": "^5",
     "@types/lodash": "^4",
     "@types/node": "^18",
diff --git a/ivette/src/dome/renderer/graph/diagram.tsx b/ivette/src/dome/renderer/graph/diagram.tsx
index 88e04b89e4b..871b34e94ee 100644
--- a/ivette/src/dome/renderer/graph/diagram.tsx
+++ b/ivette/src/dome/renderer/graph/diagram.tsx
@@ -24,7 +24,8 @@ import React from 'react';
 import { Catch } from 'dome/errors';
 import { classes } from 'dome/misc/utils';
 import { Size } from 'react-virtualized';
-import * as d3 from 'd3-graphviz';
+import { select, selectAll } from 'd3-selection';
+import { graphviz } from 'd3-graphviz';
 import AutoSizer from 'react-virtualized-auto-sizer';
 import './style.css';
 
@@ -97,7 +98,7 @@ export interface DiagramProps {
 
   /**
      Element to focus on.
-     The graph is scrolled to make this node visible if necessary.
+     The default color for this element is `'selected'`.
    */
   selected?: string;
 
@@ -105,7 +106,7 @@ export interface DiagramProps {
   direction?: Direction;
 
   /** Invoked when a node is selected. */
-  onSelection?: (node: string, evt: MouseEvent) => void;
+  onSelection?: (node: string | undefined) => void;
 
   /** Whether the Graph shall be displayed or not (defaults to true). */
   display?: boolean;
@@ -158,10 +159,11 @@ type edgeSpec = { source: string, target: string };
 const edgeKey = (e: edgeSpec): string => `${e.source} -> ${e.target}`;
 
 class DotModel {
-
-  // --- Basics
+  private selected: string | undefined;
   private spec = 'digraph {\n';
 
+  constructor(s: string | undefined) { this.selected = s; }
+
   print(...text: string[]): DotModel {
     this.spec = this.spec.concat(...text);
     return this;
@@ -222,8 +224,9 @@ class DotModel {
         .attr('label', n.label)
         .attr('shape', n.shape);
     }
-    const color = n.color ?? 'white';
+    const color = n.color ?? (n.id === this.selected ? 'selected' : 'white');
     this
+      .attr('id', n.id)
       .attr('tooltip', n.title)
       .attr('fontcolor', FGCOLOR[color])
       .attr('fillcolor', BGCOLOR[color])
@@ -256,18 +259,16 @@ const byEdge = (a: Edge, b: Edge): number => byStr(edgeKey(a), edgeKey(b));
 /* -------------------------------------------------------------------------- */
 
 let divId = 0;
-const newDivId = (): string => `dome_d3gv_${++divId}`;
+const newDivId = (): string => `dome_xDiagram_g${++divId}`;
 
 interface GraphvizProps extends DiagramProps { size: Size }
 
 function GraphvizView(props: GraphvizProps): JSX.Element {
 
   // --- Model Generation
-  const {
-    direction = 'LR', nodes, edges
-  } = props;
+  const { direction = 'LR', nodes, edges, selected } = props;
   const model = React.useMemo(() => {
-    const dot = new DotModel();
+    const dot = new DotModel(selected);
     dot
       .attr('rankdir', direction)
       .attr('bgcolor', 'none')
@@ -276,7 +277,7 @@ function GraphvizView(props: GraphvizProps): JSX.Element {
     nodes.concat().sort(byNode).forEach(n => dot.node(n));
     edges.concat().sort(byEdge).forEach(e => dot.edge(e));
     return dot.flush();
-  }, [direction, nodes, edges]);
+  }, [direction, nodes, edges, selected]);
 
   // --- Model Update Callback
   const { onModelChanged } = props;
@@ -284,19 +285,36 @@ function GraphvizView(props: GraphvizProps): JSX.Element {
     if (onModelChanged) onModelChanged(model);
   }, [model, onModelChanged]);
 
+
   // --- Rendering & Remote
   const id = React.useMemo(newDivId, []);
+  const { onSelection } = props;
   const { width, height } = props.size;
   React.useEffect(() => {
-    d3.graphviz(`#${id}`, {
+    graphviz(`#${id}`, {
       useWorker: false,
       fit: true, zoom: true, width, height,
-    }).renderDot(model);
-  }, [id, model, width, height]);
+    }).renderDot(model).on('end', function () {
+      if (onSelection) {
+        selectAll('.node')
+          .on('click', function (evt: PointerEvent) {
+            const s = select(this).attr('id');
+            if (s) {
+              evt.stopPropagation();
+              onSelection(s);
+            }
+          });
+      }
+    });
+  }, [id, model, width, height, onSelection]);
+
+  const onClick = React.useCallback((): void => {
+    if (onSelection) onSelection(undefined);
+  }, [onSelection]);
 
   return (
     <Catch label='Graphviz Error'>
-      <div id={id} className={props.className} />
+      <div id={id} className={props.className} onClick={onClick} />
     </Catch>
   );
 }
diff --git a/ivette/src/dome/template/makefile.packages b/ivette/src/dome/template/makefile.packages
index c252920ad78..ba3fa7df3f6 100644
--- a/ivette/src/dome/template/makefile.packages
+++ b/ivette/src/dome/template/makefile.packages
@@ -35,7 +35,8 @@ DOME_DEV_PACKAGES= \
 	@types/react-dom@^18 \
 	@types/react-virtualized@^9.21.8 \
 	@types/diff@^5 \
-	@types/d3-graphviz\
+	@types/d3-selection \
+	@types/d3-graphviz \
 	typescript@^5
 
 DOME_APP_PACKAGES= \
@@ -53,9 +54,10 @@ DOME_APP_PACKAGES= \
     @codemirror/state@6.4.0 \
     @codemirror/view@6.23.1 \
 	@lezer/cpp@^1 \
-	react-virtualized-auto-sizer@^1.0.22\
-	react-force-graph-2d@^1.25.4\
-	react-force-graph-3d@^1.24.2\
-	d3-graphviz
+	react-virtualized-auto-sizer@^1.0.22 \
+	react-force-graph-2d@^1.25.4 \
+	react-force-graph-3d@^1.24.2 \
+	d3-selection@^3 \
+	d3-graphviz@^5
 
 # --------------------------------------------------------------------------
diff --git a/ivette/src/sandbox/dotdiagram.tsx b/ivette/src/sandbox/dotdiagram.tsx
index 921acdf25e4..8eeaa2198c8 100644
--- a/ivette/src/sandbox/dotdiagram.tsx
+++ b/ivette/src/sandbox/dotdiagram.tsx
@@ -72,9 +72,11 @@ const edges: Edge[] = [
 
 function DiagramSample(): JSX.Element {
   const [model, setModel] = React.useState('');
+  const [selected, setSelected] = React.useState<string>();
   return (
     <HSplit settings='sandbox.diagram.split'>
       <Scroll>
+        <pre>Selected: {selected}</pre>
         <pre>
           {model}
         </pre>
@@ -82,7 +84,9 @@ function DiagramSample(): JSX.Element {
       <Diagram
         nodes={nodes}
         edges={edges}
+        selected={selected}
         onModelChanged={setModel}
+        onSelection={setSelected}
       />
     </HSplit >
   );
diff --git a/ivette/yarn.lock b/ivette/yarn.lock
index 3e3c1ce2443..573ecf8a41a 100644
--- a/ivette/yarn.lock
+++ b/ivette/yarn.lock
@@ -943,6 +943,11 @@
   resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-1.4.6.tgz#3e6056117b19d8bb6c729c872ca7234622099fb6"
   integrity sha512-0MhJ/LzJe6/vQVxiYJnvNq5CD/MF6Qy0dLp4BEQ6Dz8oOaB0EMXfx1GGeBFSW+3VzgjaUrxK6uECDQj9VLa/Mg==
 
+"@types/d3-selection@^3.0.10":
+  version "3.0.10"
+  resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe"
+  integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==
+
 "@types/d3-transition@^1":
   version "1.3.5"
   resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-1.3.5.tgz#5ef69ea917d6935b0a1db895a7e5698ba7a08af1"
@@ -2271,7 +2276,7 @@ d3-drag@^1.0.4:
   resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz"
   integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
 
-d3-graphviz@^5.4.0:
+d3-graphviz@^5:
   version "5.4.0"
   resolved "https://registry.yarnpkg.com/d3-graphviz/-/d3-graphviz-5.4.0.tgz#a63ecb4345ff31aacb8813c74533fdd1ec4304ed"
   integrity sha512-e/kvvdfIfARiB4bF9/vDgY6WwvLxGCny2tS6ozUaOwgbL/CfaBWT7EwvCH5PiDQuvdx+xscnxjCsoUjw2CR86A==
@@ -2336,7 +2341,7 @@ d3-selection@1:
   resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz"
   integrity sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==
 
-"d3-selection@2 - 3", d3-selection@3:
+"d3-selection@2 - 3", d3-selection@3, d3-selection@^3:
   version "3.0.0"
   resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
   integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
-- 
GitLab