From 1f58267d71879a01c7108a4daa7dc0cf4d9be29d Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Wed, 4 Sep 2024 17:08:49 +0000
Subject: [PATCH] Feat/export semantic substrates

---
 .../semanticsubstratesvis.tsx                 | 1364 +++++++++--------
 1 file changed, 700 insertions(+), 664 deletions(-)

diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx
index 246e02fe1..4fbd594bb 100644
--- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx
+++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx
@@ -1,4 +1,4 @@
-import React, { useRef, useState, useMemo, useEffect } from 'react';
+import React, { useRef, useState, useMemo, useEffect, forwardRef, useImperativeHandle } from 'react';
 import { Scatterplot, KeyedScatterplotProps } from './components/Scatterplot';
 import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
 import { Button } from '@graphpolaris/shared/lib/components/buttons';
@@ -19,6 +19,7 @@ import {
   DataFromPanel,
   VisualRegionConfig,
 } from './components/types';
+import html2canvas from 'html2canvas';
 
 import EdgesLayer, { KeyedEdgesLayerProps } from './components/EdgesLayer';
 import { MultiGraph } from 'graphology';
@@ -26,6 +27,10 @@ import { buildGraphology, config, numColorsCategorical, marginAxis, isColorCircl
 import { SemSubsConfigPanel } from './configPanel';
 import { nodeColorHex } from './components/utils';
 
+export interface VisSemanticSubstratesHandle {
+  exportImageInternal: () => void;
+}
+
 export type SemSubstrProps = {
   showColor: boolean;
   dataPanels: DataFromPanel[];
@@ -38,199 +43,149 @@ const settings: SemSubstrProps = {
 
 const displayName = 'SemanticSubstratesVis';
 
-export const VisSemanticSubstrates = ({ data, graphMetadata, settings, updateSettings }: VisualizationPropTypes<SemSubstrProps>) => {
-  // for sizing the vis
-  const divRef = useRef<HTMLDivElement>(null);
-  const [divSize, setDivSize] = useState({ width: 0, height: 0 });
-
-  // avoid mount iteration
-  const isMounted = useRef(false);
-
-  // to render scatterplots
-  const [appState, setAppState] = useState({
-    scatterplots: [] as KeyedScatterplotProps[],
-    dataRegions: [] as RegionData[],
-  });
-  // to render edges layer
-  const [stateEdges, setStateEdges] = useState({
-    edgePlotted: [] as KeyedEdgesLayerProps[],
-  });
-
-  // for connecting with config panel
-  const idScatterplotsFromConfig = useRef<number[]>([]);
-  const prevDataPanelsRef = useRef<DataFromPanel[]>([]);
-
-  // for preserve brush information
-  //const idBrushed = useRef<string[][]>([]);
-  const idBrushed = useRef<{ idScatterplot: number; data: string[] }[]>([]);
-
-  // keep track what is the new scatterplot
-  const newScatterplotIs = useRef<string>('');
-
-  // To know the correspondence between scatterplot IDs and edge IDs
-  const IDScatterplot2IDEdge = useRef<number[]>([]);
-  const indexNewEdge = useRef<number>(0);
-  const IDEdgeUpdating = useRef<number>(0);
-  const IDScatterplotR0 = useRef<number>(0);
-  const arrayIDScatterplotR0 = useRef<string[]>([]);
-
-  const IDScatterplotUpdating = useRef<number>(0);
-  // information about edges, useful for brushing
-  const informationEdges = useRef<{ nameEdges: string[]; idEdges: string[][] }>({
-    nameEdges: [],
-    idEdges: [],
-  });
-
-  // information about edges
-  const arrayConnections = useRef<connectionFromTo[][]>([]);
-
-  // to syncronized the data from scatterplots simulation and create the edges
-  const [computedData, setComputedData] = useState({
-    region1: [] as DataPoint[], // always R0
-    region2: [] as DataPoint[], // last scatterplot added
-  });
-
-  // data structure to handle node/edge information
-  const augmentedNodes: AugmentedNodeAttributes[] = useMemo(() => {
-    return data.nodes.map((node: Node) => ({
-      _id: node._id,
-      attributes: node.attributes,
-      label: node.label,
-    }));
-  }, [data]);
-
-  const augmentedEdges: AugmentedEdgeAttributes[] = useMemo(() => {
-    return data.edges.map((edge: any) => ({
-      id: edge.id,
-      to: edge.to,
-      from: edge.from,
-      attributes: edge.attributes,
-      label: edge.label,
-    }));
-  }, [data]);
-
-  const graphologyGraph: MultiGraph = useMemo(
-    () => buildGraphology({ nodes: Object.values(augmentedNodes), edges: Object.values(augmentedEdges) }),
-    [augmentedNodes, augmentedEdges],
-  );
-
-  const configVisualRegion = useMemo<VisualRegionConfig>(() => {
-    const baseConfig = {
-      marginPercentage: { top: 0.14, right: 0.15, bottom: 0.2, left: 0.15 },
-      width: divSize.width,
-      widthPercentage: 0.4,
-      height: 300,
-    };
-
-    const margin = {
-      top: baseConfig.marginPercentage.top * baseConfig.height,
-      right: baseConfig.marginPercentage.right * baseConfig.width,
-      bottom: baseConfig.marginPercentage.bottom * baseConfig.height,
-      left: baseConfig.marginPercentage.left * baseConfig.width,
-    };
-
-    const widthMargin = baseConfig.width - margin.right - margin.left;
-    const heightMargin = baseConfig.height - margin.top - margin.bottom;
-
-    return {
-      ...baseConfig,
-      margin,
-      widthMargin,
-      heightMargin,
-    };
-  }, [divSize]);
-
-  const configVisualEdges = useRef<VisualEdgesConfig>({
-    width: divSize.width,
-    height: configVisualRegion.height,
-    configRegion: configVisualRegion,
-    offsetY: 0,
-    stroke: config.edges.stroke,
-    strokeWidth: config.edges.strokeWidth,
-    strokeOpacity: config.edges.strokeOpacity,
-  });
-
-  const handleBrushClear = useMemo(() => {
-    return (selectedElement: string): void => {
-      let modifiedString: number = +selectedElement.replace('region_', '');
-      //idBrushed.current[modifiedString] = [];
-
-      const indexBrushedR0 = idBrushed.current.findIndex((obj) => obj.idScatterplot === modifiedString);
-      idBrushed.current[indexBrushedR0].data = [];
-
-      informationEdges.current.idEdges.forEach((edgesR0RN, index) => {
-        if (edgesR0RN.length !== 0) {
-          //if (idBrushed.current[0]?.length === 0 && idBrushed.current[index + 1]?.length === 0) {
-          if (
-            idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 &&
-            idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0
-          ) {
-            select(`.edge_region0_to_region_${index + 1}`)
-              .selectAll('path')
-              .style('stroke-opacity', 1);
-          } else {
-            //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_');
+const VisSemanticSubstrates = forwardRef<VisSemanticSubstratesHandle, VisualizationPropTypes<SemSubstrProps>>(
+  ({ data, graphMetadata, settings, updateSettings }, refExternal) => {
+    // for sizing the vis
+    const divRef = useRef<HTMLDivElement>(null);
+    const [divSize, setDivSize] = useState({ width: 0, height: 0 });
+
+    // avoid mount iteration
+    const isMounted = useRef(false);
+
+    // to render scatterplots
+    const [appState, setAppState] = useState({
+      scatterplots: [] as KeyedScatterplotProps[],
+      dataRegions: [] as RegionData[],
+    });
+    // to render edges layer
+    const [stateEdges, setStateEdges] = useState({
+      edgePlotted: [] as KeyedEdgesLayerProps[],
+    });
+
+    // for connecting with config panel
+    const idScatterplotsFromConfig = useRef<number[]>([]);
+    const prevDataPanelsRef = useRef<DataFromPanel[]>([]);
+
+    // for preserve brush information
+    //const idBrushed = useRef<string[][]>([]);
+    const idBrushed = useRef<{ idScatterplot: number; data: string[] }[]>([]);
+
+    // keep track what is the new scatterplot
+    const newScatterplotIs = useRef<string>('');
+
+    // To know the correspondence between scatterplot IDs and edge IDs
+    const IDScatterplot2IDEdge = useRef<number[]>([]);
+    const indexNewEdge = useRef<number>(0);
+    const IDEdgeUpdating = useRef<number>(0);
+    const IDScatterplotR0 = useRef<number>(0);
+    const arrayIDScatterplotR0 = useRef<string[]>([]);
+
+    const IDScatterplotUpdating = useRef<number>(0);
+    // information about edges, useful for brushing
+    const informationEdges = useRef<{ nameEdges: string[]; idEdges: string[][] }>({
+      nameEdges: [],
+      idEdges: [],
+    });
+
+    // information about edges
+    const arrayConnections = useRef<connectionFromTo[][]>([]);
+
+    // to syncronized the data from scatterplots simulation and create the edges
+    const [computedData, setComputedData] = useState({
+      region1: [] as DataPoint[], // always R0
+      region2: [] as DataPoint[], // last scatterplot added
+    });
+
+    // data structure to handle node/edge information
+    const augmentedNodes: AugmentedNodeAttributes[] = useMemo(() => {
+      return data.nodes.map((node: Node) => ({
+        _id: node._id,
+        attributes: node.attributes,
+        label: node.label,
+      }));
+    }, [data]);
+
+    const augmentedEdges: AugmentedEdgeAttributes[] = useMemo(() => {
+      return data.edges.map((edge: any) => ({
+        id: edge.id,
+        to: edge.to,
+        from: edge.from,
+        attributes: edge.attributes,
+        label: edge.label,
+      }));
+    }, [data]);
+
+    const graphologyGraph: MultiGraph = useMemo(
+      () => buildGraphology({ nodes: Object.values(augmentedNodes), edges: Object.values(augmentedEdges) }),
+      [augmentedNodes, augmentedEdges],
+    );
+
+    const configVisualRegion = useMemo<VisualRegionConfig>(() => {
+      const baseConfig = {
+        marginPercentage: { top: 0.14, right: 0.15, bottom: 0.2, left: 0.15 },
+        width: divSize.width,
+        widthPercentage: 0.4,
+        height: 300,
+      };
 
-            const edgesVisible = filterArray(
-              idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [],
-              idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [],
-              edgesR0RN,
-              '_fromto_',
-            );
+      const margin = {
+        top: baseConfig.marginPercentage.top * baseConfig.height,
+        right: baseConfig.marginPercentage.right * baseConfig.width,
+        bottom: baseConfig.marginPercentage.bottom * baseConfig.height,
+        left: baseConfig.marginPercentage.left * baseConfig.width,
+      };
 
-            const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_'));
+      const widthMargin = baseConfig.width - margin.right - margin.left;
+      const heightMargin = baseConfig.height - margin.top - margin.bottom;
 
-            const edgesVisibleSlashNotNum = edgesVisibleSlash.map((item) => {
-              if (/^\d/.test(item)) {
-                return 'idAdd_' + item;
-              } else {
-                return item;
-              }
-            });
+      return {
+        ...baseConfig,
+        margin,
+        widthMargin,
+        heightMargin,
+      };
+    }, [divSize]);
 
-            const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(',');
-            select(`.edge_region0_to_region_${index + 1}`)
-              .selectAll('path')
-              .style('stroke-opacity', 0);
-            selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1);
-          }
-        }
-      });
-    };
-  }, []);
-
-  const handleBrushUpdate = useMemo(() => {
-    return (idElements: string[], selectedElement: string): void => {
-      let modifiedString: number = +selectedElement.replace('region_', '');
-
-      //idBrushed.current[modifiedString] = idElements;
-      const indexBrushedR0 = idBrushed.current.findIndex((obj) => obj.idScatterplot === modifiedString);
-      idBrushed.current[indexBrushedR0].data = idElements;
-
-      // iterate over the region pairs: r0-r1, r0-r2, and update visibility of edges based on registered brushed ids
-      informationEdges.current.idEdges.forEach((edgesR0RN, index) => {
-        if (modifiedString == 0) {
-          //if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) {
-
-          if (
-            idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 &&
-            idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0
-          ) {
-            select(`.edge_region0_to_region_${index + 1}`)
-              .selectAll('path')
-              .style('stroke-opacity', 0);
-          } else {
-            //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_');
+    const configVisualEdges = useRef<VisualEdgesConfig>({
+      width: divSize.width,
+      height: configVisualRegion.height,
+      configRegion: configVisualRegion,
+      offsetY: 0,
+      stroke: config.edges.stroke,
+      strokeWidth: config.edges.strokeWidth,
+      strokeOpacity: config.edges.strokeOpacity,
+    });
+
+    const handleBrushClear = useMemo(() => {
+      return (selectedElement: string): void => {
+        let modifiedString: number = +selectedElement.replace('region_', '');
+        //idBrushed.current[modifiedString] = [];
+
+        const indexBrushedR0 = idBrushed.current.findIndex((obj) => obj.idScatterplot === modifiedString);
+        idBrushed.current[indexBrushedR0].data = [];
+
+        informationEdges.current.idEdges.forEach((edgesR0RN, index) => {
+          if (edgesR0RN.length !== 0) {
+            //if (idBrushed.current[0]?.length === 0 && idBrushed.current[index + 1]?.length === 0) {
+            if (
+              idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 &&
+              idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0
+            ) {
+              select(`.edge_region0_to_region_${index + 1}`)
+                .selectAll('path')
+                .style('stroke-opacity', 1);
+            } else {
+              //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_');
 
-            const edgesVisible = filterArray(
-              idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [],
-              idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [],
-              edgesR0RN,
-              '_fromto_',
-            );
+              const edgesVisible = filterArray(
+                idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [],
+                idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [],
+                edgesR0RN,
+                '_fromto_',
+              );
 
-            if (edgesVisible.length != 0) {
               const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_'));
+
               const edgesVisibleSlashNotNum = edgesVisibleSlash.map((item) => {
                 if (/^\d/.test(item)) {
                   return 'idAdd_' + item;
@@ -244,225 +199,186 @@ export const VisSemanticSubstrates = ({ data, graphMetadata, settings, updateSet
                 .selectAll('path')
                 .style('stroke-opacity', 0);
               selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1);
-            } else {
-              select(`.edge_region0_to_region_${index + 1}`)
-                .selectAll('path')
-                .style('stroke-opacity', 0);
             }
           }
-        } else if (modifiedString == index + 1) {
-          //if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) {
-
-          if (
-            idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 &&
-            idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0
-          ) {
-            select(`.edge_region0_to_region_${index + 1}`)
-              .selectAll('path')
-              .style('stroke-opacity', 0);
-          } else {
-            //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_');
-            const edgesVisible = filterArray(
-              idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [],
-              idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [],
-              edgesR0RN,
-              '_fromto_',
-            );
-            if (edgesVisible.length != 0) {
-              const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_'));
-              const edgesVisibleSlashNotNum = edgesVisibleSlash.map((item) => {
-                if (/^\d/.test(item)) {
-                  return 'idAdd_' + item;
-                } else {
-                  return item;
-                }
-              });
+        });
+      };
+    }, []);
 
-              const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(',');
+    const handleBrushUpdate = useMemo(() => {
+      return (idElements: string[], selectedElement: string): void => {
+        let modifiedString: number = +selectedElement.replace('region_', '');
+
+        //idBrushed.current[modifiedString] = idElements;
+        const indexBrushedR0 = idBrushed.current.findIndex((obj) => obj.idScatterplot === modifiedString);
+        idBrushed.current[indexBrushedR0].data = idElements;
 
+        // iterate over the region pairs: r0-r1, r0-r2, and update visibility of edges based on registered brushed ids
+        informationEdges.current.idEdges.forEach((edgesR0RN, index) => {
+          if (modifiedString == 0) {
+            //if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) {
+
+            if (
+              idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 &&
+              idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0
+            ) {
               select(`.edge_region0_to_region_${index + 1}`)
                 .selectAll('path')
                 .style('stroke-opacity', 0);
-              selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1);
             } else {
+              //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_');
+
+              const edgesVisible = filterArray(
+                idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [],
+                idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [],
+                edgesR0RN,
+                '_fromto_',
+              );
+
+              if (edgesVisible.length != 0) {
+                const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_'));
+                const edgesVisibleSlashNotNum = edgesVisibleSlash.map((item) => {
+                  if (/^\d/.test(item)) {
+                    return 'idAdd_' + item;
+                  } else {
+                    return item;
+                  }
+                });
+
+                const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(',');
+                select(`.edge_region0_to_region_${index + 1}`)
+                  .selectAll('path')
+                  .style('stroke-opacity', 0);
+                selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1);
+              } else {
+                select(`.edge_region0_to_region_${index + 1}`)
+                  .selectAll('path')
+                  .style('stroke-opacity', 0);
+              }
+            }
+          } else if (modifiedString == index + 1) {
+            //if (idBrushed.current[0].length == 0 && idBrushed.current[index + 1].length == 0) {
+
+            if (
+              idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length === 0 &&
+              idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data.length === 0
+            ) {
               select(`.edge_region0_to_region_${index + 1}`)
                 .selectAll('path')
                 .style('stroke-opacity', 0);
+            } else {
+              //const edgesVisible = filterArray(idBrushed.current[0], idBrushed.current[index + 1], edgesR0RN, '_fromto_');
+              const edgesVisible = filterArray(
+                idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [],
+                idBrushed.current.find((obj) => obj.idScatterplot === index + 1)?.data ?? [],
+                edgesR0RN,
+                '_fromto_',
+              );
+              if (edgesVisible.length != 0) {
+                const edgesVisibleSlash = edgesVisible.map((element) => element.replace(/\//g, '_'));
+                const edgesVisibleSlashNotNum = edgesVisibleSlash.map((item) => {
+                  if (/^\d/.test(item)) {
+                    return 'idAdd_' + item;
+                  } else {
+                    return item;
+                  }
+                });
+
+                const edgesVisibleSlashDot: string = edgesVisibleSlashNotNum.map((edgeClass) => `.${edgeClass}`).join(',');
+
+                select(`.edge_region0_to_region_${index + 1}`)
+                  .selectAll('path')
+                  .style('stroke-opacity', 0);
+                selectAll(edgesVisibleSlashDot).style('stroke-opacity', 1);
+              } else {
+                select(`.edge_region0_to_region_${index + 1}`)
+                  .selectAll('path')
+                  .style('stroke-opacity', 0);
+              }
             }
           }
-        }
-      });
-    };
-  }, []);
-
-  const handleResultJitter = (data: DataPoint[], idScatterplot: number) => {
-    //console.log('DONE JITTER ', idScatterplot);
-    if (idScatterplot == IDScatterplotR0.current) {
-      setComputedData((prevData) => ({
-        ...prevData,
-        region1: data,
-      }));
-    } else {
-      setComputedData((prevData) => ({
-        ...prevData,
-        region2: data,
-      }));
-    }
-  };
-
-  useEffect(() => {
-    function handleResize() {
-      if (divRef.current) {
-        setDivSize({ width: divRef.current.getBoundingClientRect().width, height: divRef.current.getBoundingClientRect().height });
+        });
+      };
+    }, []);
+
+    const handleResultJitter = (data: DataPoint[], idScatterplot: number) => {
+      //console.log('DONE JITTER ', idScatterplot);
+      if (idScatterplot == IDScatterplotR0.current) {
+        setComputedData((prevData) => ({
+          ...prevData,
+          region1: data,
+        }));
+      } else {
+        setComputedData((prevData) => ({
+          ...prevData,
+          region2: data,
+        }));
       }
-    }
-
-    window.addEventListener('resize', handleResize);
-    if (divRef.current) new ResizeObserver(handleResize).observe(divRef.current);
-
-    return () => {
-      window.removeEventListener('resize', handleResize);
     };
-  }, []);
 
-  useEffect(() => {
-    if (idBrushed.current.length != 0) {
-      if (idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length != 0) {
-        handleBrushUpdate(idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [], 'region_0');
+    useEffect(() => {
+      function handleResize() {
+        if (divRef.current) {
+          setDivSize({ width: divRef.current.getBoundingClientRect().width, height: divRef.current.getBoundingClientRect().height });
+        }
       }
-    }
-  }, [stateEdges]);
 
-  // manages the addition of edges elements
-  useEffect(() => {
-    //console.log('computedData', computedData.region1.length > 0 && computedData.region2.length > 0, computedData, newScatterplotIs.current);
-    if (computedData.region1.length > 0 && computedData.region2.length > 0) {
-      if (newScatterplotIs.current === 'add' || !stateEdges.edgePlotted[IDEdgeUpdating.current]) {
-        // !FIXME !stateEdges.edgePlotted[IDEdgeUpdating.current] was added to solve bug, but now only last scatter shows edges
-
-        //console.log('ADD EDGES ');
-        const temporalConfigVisualEdges: VisualEdgesConfig = {
-          ...configVisualEdges.current,
-          height: configVisualRegion.height * appState.scatterplots.length,
-          offsetY: configVisualRegion.height * (appState.scatterplots.length - 2),
-        };
-
-        const visualConfigRef = { current: temporalConfigVisualEdges } as React.MutableRefObject<VisualEdgesConfig>;
-
-        const newEdgePlot: KeyedEdgesLayerProps = {
-          key: appState.scatterplots.length.toString(),
-          dataConnections: arrayConnections.current[indexNewEdge.current - 1],
-          visualConfig: visualConfigRef,
-          visualScatterplot: configVisualRegion,
-          data1: computedData.region1,
-          data2: computedData.region2,
-          nameEdges: informationEdges.current.nameEdges[indexNewEdge.current - 1],
-        };
+      window.addEventListener('resize', handleResize);
+      if (divRef.current) new ResizeObserver(handleResize).observe(divRef.current);
 
-        //console.log('newEdgePlot', newEdgePlot);
-        setStateEdges({
-          edgePlotted: [...stateEdges.edgePlotted, newEdgePlot],
-        });
+      return () => {
+        window.removeEventListener('resize', handleResize);
+      };
+    }, []);
 
-        // update edges
-      } else if (IDScatterplotUpdating.current === IDScatterplotR0.current) {
-        //console.log('UPDATE EDGES of R0 ');
-        const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
+    useEffect(() => {
+      if (idBrushed.current.length != 0) {
+        if (idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data.length != 0) {
+          handleBrushUpdate(idBrushed.current.find((obj) => obj.idScatterplot === 0)?.data ?? [], 'region_0');
+        }
+      }
+    }, [stateEdges]);
 
-        const modifiedStateEdges = stateEdgesTemporal.map((obj, index) => {
-          const data1 = (stateEdgesTemporal[index].data1 = computedData.region1);
-          const dataConnections = arrayConnections.current[index];
+    // manages the addition of edges elements
+    useEffect(() => {
+      //console.log('computedData', computedData.region1.length > 0 && computedData.region2.length > 0, computedData, newScatterplotIs.current);
+      if (computedData.region1.length > 0 && computedData.region2.length > 0) {
+        if (newScatterplotIs.current === 'add' || !stateEdges.edgePlotted[IDEdgeUpdating.current]) {
+          // !FIXME !stateEdges.edgePlotted[IDEdgeUpdating.current] was added to solve bug, but now only last scatter shows edges
 
+          //console.log('ADD EDGES ');
           const temporalConfigVisualEdges: VisualEdgesConfig = {
             ...configVisualEdges.current,
-            height: configVisualRegion.height * (index + 2),
-            offsetY: configVisualRegion.height * index,
+            height: configVisualRegion.height * appState.scatterplots.length,
+            offsetY: configVisualRegion.height * (appState.scatterplots.length - 2),
           };
 
           const visualConfigRef = { current: temporalConfigVisualEdges } as React.MutableRefObject<VisualEdgesConfig>;
 
-          return {
-            ...obj,
+          const newEdgePlot: KeyedEdgesLayerProps = {
+            key: appState.scatterplots.length.toString(),
+            dataConnections: arrayConnections.current[indexNewEdge.current - 1],
             visualConfig: visualConfigRef,
-            data1,
-            dataConnections,
+            visualScatterplot: configVisualRegion,
+            data1: computedData.region1,
+            data2: computedData.region2,
+            nameEdges: informationEdges.current.nameEdges[indexNewEdge.current - 1],
           };
-        });
-
-        setStateEdges({
-          edgePlotted: modifiedStateEdges,
-        });
-      } else if (stateEdges.edgePlotted[IDEdgeUpdating.current]) {
-        //console.log('UPDATE EDGES NO R0');
-
-        //console.log('informationEdges.current.nameEdges ', informationEdges.current.nameEdges);
 
-        const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
-
-        //console.log('IDEdgeUpdating', IDEdgeUpdating.current, stateEdges.edgePlotted, stateEdgesTemporal[IDEdgeUpdating.current]);
-        stateEdgesTemporal[IDEdgeUpdating.current].data1 = computedData.region1; // necessary ?
-        stateEdgesTemporal[IDEdgeUpdating.current].data2 = computedData.region2;
-        stateEdgesTemporal[IDEdgeUpdating.current].dataConnections = arrayConnections.current[IDEdgeUpdating.current];
-        setStateEdges({
-          edgePlotted: stateEdgesTemporal,
-        });
-      } else {
-        console.error('Error: Edge not found', IDScatterplotUpdating.current, stateEdges.edgePlotted);
-      }
-    }
-  }, [computedData.region2, computedData.region1]);
-
-  function createNewScatterplot(newPanel: DataFromPanel) {
-    //console.log('createNewScatterplot:', newPanel, 'IDScatterplotR0: ', IDScatterplotR0.current);
-    if (idScatterplotsFromConfig.current.length === 0 && !IDScatterplotR0.current) {
-      IDScatterplotR0.current = newPanel.id;
-      //console.log('newPanel ', newPanel);
-    }
-
-    idScatterplotsFromConfig.current.push(newPanel.id);
-    newScatterplotIs.current = 'add';
-    return handleNewDataRegion(newPanel.data, newPanel.id);
-  }
-
-  function removeScatterplot(deletedPanel?: DataFromPanel) {
-    if (deletedPanel) {
-      // checks if deleted one is R0
-      if (deletedPanel.id === IDScatterplotR0.current) {
-        // !FIXME: dont allow delete of R0
-        /*
-        setComputedData((prevState) => ({
-          ...prevState,
-          region1: [],
-        }));
-        */
-      } else {
-        // checks if deleted one is RX
-
-        // Delete scatterplot and associated variables
-        idScatterplotsFromConfig.current = idScatterplotsFromConfig.current.filter((item) => item !== deletedPanel.id);
-        setAppState((prevState) => ({
-          scatterplots: prevState.scatterplots.filter((scatterplot) => scatterplot.key !== deletedPanel.id),
-          dataRegions: prevState.dataRegions.filter((_, index) => prevState.scatterplots[index].key !== deletedPanel.id),
-        }));
-
-        idBrushed.current = idBrushed.current.filter((item) => item.idScatterplot !== deletedPanel.id);
-
-        // Delete edges
-        const removeIndexEdge = informationEdges.current.nameEdges.findIndex(
-          (name) => name === `edge_region0_to_region_${deletedPanel.id}`,
-        );
-        const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
-        const stateEdgesTemporalFiltered = stateEdgesTemporal.filter((_, index) => index !== removeIndexEdge);
+          //console.log('newEdgePlot', newEdgePlot);
+          setStateEdges({
+            edgePlotted: [...stateEdges.edgePlotted, newEdgePlot],
+          });
 
-        if (removeIndexEdge != -1) {
-          // check if it need it
-          IDScatterplot2IDEdge.current = IDScatterplot2IDEdge.current.filter((_, index) => index !== removeIndexEdge);
+          // update edges
+        } else if (IDScatterplotUpdating.current === IDScatterplotR0.current) {
+          //console.log('UPDATE EDGES of R0 ');
+          const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
 
-          informationEdges.current.idEdges = informationEdges.current.idEdges.filter((_, index) => index !== removeIndexEdge);
-          informationEdges.current.nameEdges = informationEdges.current.nameEdges.filter((_, index) => index !== removeIndexEdge);
-          arrayConnections.current = arrayConnections.current.filter((_, index) => index !== removeIndexEdge);
+          const modifiedStateEdges = stateEdgesTemporal.map((obj, index) => {
+            const data1 = (stateEdgesTemporal[index].data1 = computedData.region1);
+            const dataConnections = arrayConnections.current[index];
 
-          const modifiedStateEdges = stateEdgesTemporalFiltered.map((obj, index) => {
             const temporalConfigVisualEdges: VisualEdgesConfig = {
               ...configVisualEdges.current,
               height: configVisualRegion.height * (index + 2),
@@ -474,359 +390,471 @@ export const VisSemanticSubstrates = ({ data, graphMetadata, settings, updateSet
             return {
               ...obj,
               visualConfig: visualConfigRef,
+              data1,
+              dataConnections,
             };
           });
 
           setStateEdges({
             edgePlotted: modifiedStateEdges,
           });
+        } else if (stateEdges.edgePlotted[IDEdgeUpdating.current]) {
+          //console.log('UPDATE EDGES NO R0');
+
+          //console.log('informationEdges.current.nameEdges ', informationEdges.current.nameEdges);
+
+          const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
+
+          //console.log('IDEdgeUpdating', IDEdgeUpdating.current, stateEdges.edgePlotted, stateEdgesTemporal[IDEdgeUpdating.current]);
+          stateEdgesTemporal[IDEdgeUpdating.current].data1 = computedData.region1; // necessary ?
+          stateEdgesTemporal[IDEdgeUpdating.current].data2 = computedData.region2;
+          stateEdgesTemporal[IDEdgeUpdating.current].dataConnections = arrayConnections.current[IDEdgeUpdating.current];
+          setStateEdges({
+            edgePlotted: stateEdgesTemporal,
+          });
+        } else {
+          console.error('Error: Edge not found', IDScatterplotUpdating.current, stateEdges.edgePlotted);
         }
+      }
+    }, [computedData.region2, computedData.region1]);
 
-        indexNewEdge.current--;
+    function createNewScatterplot(newPanel: DataFromPanel) {
+      //console.log('createNewScatterplot:', newPanel, 'IDScatterplotR0: ', IDScatterplotR0.current);
+      if (idScatterplotsFromConfig.current.length === 0 && !IDScatterplotR0.current) {
+        IDScatterplotR0.current = newPanel.id;
+        //console.log('newPanel ', newPanel);
       }
+
+      idScatterplotsFromConfig.current.push(newPanel.id);
+      newScatterplotIs.current = 'add';
+      return handleNewDataRegion(newPanel.data, newPanel.id);
     }
-  }
-
-  function updateScatterplot(updatedPanel?: DataFromPanel) {
-    if (updatedPanel) {
-      //console.log('updatedPanel', updatedPanel, appState.scatterplots);
-
-      const idScatterplotUpdate = appState.scatterplots.findIndex((item) => item.key === updatedPanel.id);
-      newScatterplotIs.current = 'update';
-      if (idScatterplotUpdate !== -1) {
-        //handleBrushClear(`region_${indexOnState}`);
-
-        if (!updatedPanel.data.entitySelected) return;
-        //console.log('UPDATE: appState ', idScatterplotUpdate);
-
-        const regionUserSelection: UserSelection = {
-          name: appState.dataRegions[idScatterplotUpdate].name,
-          nodeName: updatedPanel.data.entitySelected,
-          attributeAsRegion: updatedPanel.data.attributeSelected,
-          attributeAsRegionSelection: updatedPanel.data.attributeValueSelected,
-          placement: {
-            xAxis: updatedPanel.data.xAxisSelected,
-            yAxis: updatedPanel.data.yAxisSelected,
-            colorNodes: appState.dataRegions[idScatterplotUpdate].colorNodes,
-            colorNodesStroke: appState.dataRegions[idScatterplotUpdate].colorNodesStroke,
-            colorFillBrush: config.brush.fillClr,
-            colorStrokeBrush: config.brush.strokeClr,
-          },
-        };
 
-        const regionDataUser: RegionData = getRegionData(augmentedNodes, regionUserSelection);
+    function removeScatterplot(deletedPanel?: DataFromPanel) {
+      if (deletedPanel) {
+        // checks if deleted one is R0
+        if (deletedPanel.id === IDScatterplotR0.current) {
+          // !FIXME: dont allow delete of R0
+          /*
+        setComputedData((prevState) => ({
+          ...prevState,
+          region1: [],
+        }));
+        */
+        } else {
+          // checks if deleted one is RX
+
+          // Delete scatterplot and associated variables
+          idScatterplotsFromConfig.current = idScatterplotsFromConfig.current.filter((item) => item !== deletedPanel.id);
+          setAppState((prevState) => ({
+            scatterplots: prevState.scatterplots.filter((scatterplot) => scatterplot.key !== deletedPanel.id),
+            dataRegions: prevState.dataRegions.filter((_, index) => prevState.scatterplots[index].key !== deletedPanel.id),
+          }));
 
-        let xScaleShared: any;
-        let yScaleShared: any;
+          idBrushed.current = idBrushed.current.filter((item) => item.idScatterplot !== deletedPanel.id);
 
-        if (!regionDataUser.xAxisName && !regionDataUser.yAxisName) {
-          xScaleShared = noDataRange;
-          yScaleShared = noDataRange;
-        } else if (regionDataUser.xAxisName && regionDataUser.yAxisName) {
-          if (typeof regionDataUser.xData[0] != 'number') {
-            const xExtent = getUniqueValues(regionDataUser.xData);
-            xScaleShared = xExtent;
-          } else {
-            const xExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
-            xScaleShared = xExtent;
-          }
+          // Delete edges
+          const removeIndexEdge = informationEdges.current.nameEdges.findIndex(
+            (name) => name === `edge_region0_to_region_${deletedPanel.id}`,
+          );
+          const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
+          const stateEdgesTemporalFiltered = stateEdgesTemporal.filter((_, index) => index !== removeIndexEdge);
 
-          if (typeof regionDataUser.yData[0] != 'number') {
-            const yExtent = getUniqueValues(regionDataUser.yData);
-            yScaleShared = yExtent;
-          } else {
-            const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
-            yScaleShared = yExtent;
-          }
-        } else if (regionDataUser.xAxisName) {
-          if (typeof regionDataUser.xData[0] != 'number') {
-            const xExtent = getUniqueValues(regionDataUser.xData);
-            xScaleShared = xExtent;
+          if (removeIndexEdge != -1) {
+            // check if it need it
+            IDScatterplot2IDEdge.current = IDScatterplot2IDEdge.current.filter((_, index) => index !== removeIndexEdge);
 
-            yScaleShared = noDataRange;
-          } else {
-            const xExtent: string[] | number[] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
-            xScaleShared = xExtent;
+            informationEdges.current.idEdges = informationEdges.current.idEdges.filter((_, index) => index !== removeIndexEdge);
+            informationEdges.current.nameEdges = informationEdges.current.nameEdges.filter((_, index) => index !== removeIndexEdge);
+            arrayConnections.current = arrayConnections.current.filter((_, index) => index !== removeIndexEdge);
 
-            yScaleShared = noDataRange;
+            const modifiedStateEdges = stateEdgesTemporalFiltered.map((obj, index) => {
+              const temporalConfigVisualEdges: VisualEdgesConfig = {
+                ...configVisualEdges.current,
+                height: configVisualRegion.height * (index + 2),
+                offsetY: configVisualRegion.height * index,
+              };
+
+              const visualConfigRef = { current: temporalConfigVisualEdges } as React.MutableRefObject<VisualEdgesConfig>;
+
+              return {
+                ...obj,
+                visualConfig: visualConfigRef,
+              };
+            });
+
+            setStateEdges({
+              edgePlotted: modifiedStateEdges,
+            });
           }
-        } else if (regionDataUser.yAxisName) {
-          if (typeof regionDataUser.yData[0] != 'number') {
-            const yExtent = getUniqueValues(regionDataUser.yData);
-            yScaleShared = yExtent;
 
-            xScaleShared = noDataRange;
-          } else {
-            const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
+          indexNewEdge.current--;
+        }
+      }
+    }
 
-            yScaleShared = yExtent;
+    function updateScatterplot(updatedPanel?: DataFromPanel) {
+      if (updatedPanel) {
+        //console.log('updatedPanel', updatedPanel, appState.scatterplots);
+
+        const idScatterplotUpdate = appState.scatterplots.findIndex((item) => item.key === updatedPanel.id);
+        newScatterplotIs.current = 'update';
+        if (idScatterplotUpdate !== -1) {
+          //handleBrushClear(`region_${indexOnState}`);
+
+          if (!updatedPanel.data.entitySelected) return;
+          //console.log('UPDATE: appState ', idScatterplotUpdate);
+
+          const regionUserSelection: UserSelection = {
+            name: appState.dataRegions[idScatterplotUpdate].name,
+            nodeName: updatedPanel.data.entitySelected,
+            attributeAsRegion: updatedPanel.data.attributeSelected,
+            attributeAsRegionSelection: updatedPanel.data.attributeValueSelected,
+            placement: {
+              xAxis: updatedPanel.data.xAxisSelected,
+              yAxis: updatedPanel.data.yAxisSelected,
+              colorNodes: appState.dataRegions[idScatterplotUpdate].colorNodes,
+              colorNodesStroke: appState.dataRegions[idScatterplotUpdate].colorNodesStroke,
+              colorFillBrush: config.brush.fillClr,
+              colorStrokeBrush: config.brush.strokeClr,
+            },
+          };
 
+          const regionDataUser: RegionData = getRegionData(augmentedNodes, regionUserSelection);
+
+          let xScaleShared: any;
+          let yScaleShared: any;
+
+          if (!regionDataUser.xAxisName && !regionDataUser.yAxisName) {
             xScaleShared = noDataRange;
+            yScaleShared = noDataRange;
+          } else if (regionDataUser.xAxisName && regionDataUser.yAxisName) {
+            if (typeof regionDataUser.xData[0] != 'number') {
+              const xExtent = getUniqueValues(regionDataUser.xData);
+              xScaleShared = xExtent;
+            } else {
+              const xExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
+              xScaleShared = xExtent;
+            }
+
+            if (typeof regionDataUser.yData[0] != 'number') {
+              const yExtent = getUniqueValues(regionDataUser.yData);
+              yScaleShared = yExtent;
+            } else {
+              const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
+              yScaleShared = yExtent;
+            }
+          } else if (regionDataUser.xAxisName) {
+            if (typeof regionDataUser.xData[0] != 'number') {
+              const xExtent = getUniqueValues(regionDataUser.xData);
+              xScaleShared = xExtent;
+
+              yScaleShared = noDataRange;
+            } else {
+              const xExtent: string[] | number[] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
+              xScaleShared = xExtent;
+
+              yScaleShared = noDataRange;
+            }
+          } else if (regionDataUser.yAxisName) {
+            if (typeof regionDataUser.yData[0] != 'number') {
+              const yExtent = getUniqueValues(regionDataUser.yData);
+              yScaleShared = yExtent;
+
+              xScaleShared = noDataRange;
+            } else {
+              const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
+
+              yScaleShared = yExtent;
+
+              xScaleShared = noDataRange;
+            }
           }
-        }
 
-        const updatedScatterplot = {
-          key: appState.scatterplots[idScatterplotUpdate].key,
-          data: regionDataUser,
-          visualConfig: configVisualRegion,
-          xScaleRange: xScaleShared,
-          yScaleRange: yScaleShared,
-          onBrushUpdate: handleBrushUpdate,
-          onResultJitter: handleResultJitter,
-          onBrushClear: handleBrushClear,
-        };
+          const updatedScatterplot = {
+            key: appState.scatterplots[idScatterplotUpdate].key,
+            data: regionDataUser,
+            visualConfig: configVisualRegion,
+            xScaleRange: xScaleShared,
+            yScaleRange: yScaleShared,
+            onBrushUpdate: handleBrushUpdate,
+            onResultJitter: handleResultJitter,
+            onBrushClear: handleBrushClear,
+          };
 
-        // modify scatterplot
-        setAppState((prevState) => {
-          const scatterplots = [...prevState.scatterplots];
-          scatterplots[idScatterplotUpdate] = updatedScatterplot;
-          return { ...prevState, scatterplots };
-        });
+          // modify scatterplot
+          setAppState((prevState) => {
+            const scatterplots = [...prevState.scatterplots];
+            scatterplots[idScatterplotUpdate] = updatedScatterplot;
+            return { ...prevState, scatterplots };
+          });
 
-        // modify edge layer
-        const arrayIDs = regionDataUser.idData.map((item) => item);
-        IDScatterplotUpdating.current = idScatterplotUpdate;
-        //console.log('IDScatterplotUpdating', IDScatterplotUpdating.current, IDScatterplotR0.current, idScatterplotUpdate);
-        if (idScatterplotUpdate === IDScatterplotR0.current) {
-          const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
+          // modify edge layer
+          const arrayIDs = regionDataUser.idData.map((item) => item);
+          IDScatterplotUpdating.current = idScatterplotUpdate;
+          //console.log('IDScatterplotUpdating', IDScatterplotUpdating.current, IDScatterplotR0.current, idScatterplotUpdate);
+          if (idScatterplotUpdate === IDScatterplotR0.current) {
+            const stateEdgesTemporal = cloneDeep(stateEdges.edgePlotted);
+
+            stateEdgesTemporal.forEach((obj, index) => {
+              const IDsConnectedScatterplot = obj.data2.map((value) => value.id);
+              const [connectedD, connectedD2] = findConnectionsNodes(
+                arrayIDs,
+                IDsConnectedScatterplot,
+                graphologyGraph,
+                regionDataUser.label,
+                true,
+              );
 
-          stateEdgesTemporal.forEach((obj, index) => {
-            const IDsConnectedScatterplot = obj.data2.map((value) => value.id);
+              informationEdges.current.idEdges[index] = connectedD2;
+              arrayConnections.current[index] = connectedD;
+            });
+          } else {
+            // get indices to modify
+            const IDEdgeBasedOnIDscatterplot = IDScatterplot2IDEdge.current[idScatterplotUpdate - 1];
+            IDEdgeUpdating.current = IDEdgeBasedOnIDscatterplot;
+
+            const indexEdgeInvolved = informationEdges.current.nameEdges.findIndex(
+              (name) => name === `edge_region0_to_region_${idScatterplotUpdate}`,
+            );
+            //console.log('edge involved: ', IDEdgeBasedOnIDscatterplot, indexEdgeInvolved);
+            // modify data structures
             const [connectedD, connectedD2] = findConnectionsNodes(
               arrayIDs,
-              IDsConnectedScatterplot,
+              appState.dataRegions[IDScatterplotR0.current].idData,
               graphologyGraph,
               regionDataUser.label,
-              true,
             );
 
-            informationEdges.current.idEdges[index] = connectedD2;
-            arrayConnections.current[index] = connectedD;
-          });
+            informationEdges.current.idEdges[IDEdgeBasedOnIDscatterplot] = connectedD2;
+            arrayConnections.current[IDEdgeBasedOnIDscatterplot] = connectedD;
+          }
+
+          //
         } else {
-          // get indices to modify
-          const IDEdgeBasedOnIDscatterplot = IDScatterplot2IDEdge.current[idScatterplotUpdate - 1];
-          IDEdgeUpdating.current = IDEdgeBasedOnIDscatterplot;
+          console.log('Error: Scatterplot not found', idScatterplotUpdate, updatedPanel.id, appState.scatterplots);
+        }
+      }
+    }
 
-          const indexEdgeInvolved = informationEdges.current.nameEdges.findIndex(
-            (name) => name === `edge_region0_to_region_${idScatterplotUpdate}`,
+    // manages when the settingsPanel changes
+    useEffect(() => {
+      if (isMounted.current && configVisualRegion.width > 0 && !isEqual(settings.dataPanels, prevDataPanelsRef.current)) {
+        const prevDataPanels = prevDataPanelsRef.current;
+        const currentDataPanels = settings.dataPanels;
+
+        if (currentDataPanels.length > prevDataPanels.length) {
+          // Element added
+          //console.log('ADD SCATTERPLOT');
+          const newPanel = currentDataPanels.filter(
+            (panel: DataFromPanel) => !prevDataPanels.some((prevPanel) => prevPanel.id === panel.id),
           );
-          //console.log('edge involved: ', IDEdgeBasedOnIDscatterplot, indexEdgeInvolved);
-          // modify data structures
-          const [connectedD, connectedD2] = findConnectionsNodes(
-            arrayIDs,
-            appState.dataRegions[IDScatterplotR0.current].idData,
-            graphologyGraph,
-            regionDataUser.label,
+          const newDataRegions = [];
+          const newScatterplots = [];
+          for (const panel of newPanel) {
+            const { dataRegions, scatterplot } = createNewScatterplot(panel);
+            newDataRegions.push(dataRegions);
+            newScatterplots.push(scatterplot);
+          }
+          setAppState({
+            scatterplots: [...appState.scatterplots, ...newScatterplots],
+            dataRegions: [...appState.dataRegions, ...newDataRegions],
+          });
+        } else if (currentDataPanels.length < prevDataPanels.length) {
+          // Element deleted
+          const deletedPanel = prevDataPanels.find(
+            (prevPanel) => !currentDataPanels.some((panel: DataFromPanel) => panel.id === prevPanel.id),
           );
 
-          informationEdges.current.idEdges[IDEdgeBasedOnIDscatterplot] = connectedD2;
-          arrayConnections.current[IDEdgeBasedOnIDscatterplot] = connectedD;
-        }
+          //console.log('DELETE SCATTERPLOT: ', deletedPanel);
 
-        //
+          removeScatterplot(deletedPanel);
+        } else {
+          // Element updated
+          const updatedPanel = currentDataPanels.find((panel: DataFromPanel) => {
+            const prevPanel = prevDataPanels.find((prevPanel) => prevPanel.id === panel.id);
+            return prevPanel && !isEqual(prevPanel.data, panel.data);
+          });
+          //console.log('UPDATE SCATTERPLOT: ', updatedPanel);
+          if (updatedPanel !== undefined) {
+            updateScatterplot(updatedPanel);
+          }
+        }
+        prevDataPanelsRef.current = currentDataPanels;
       } else {
-        console.log('Error: Scatterplot not found', idScatterplotUpdate, updatedPanel.id, appState.scatterplots);
+        isMounted.current = true;
       }
-    }
-  }
+    }, [settings.dataPanels, configVisualRegion]);
+    //}, [settings.dataPanels]);
 
-  // manages when the settingsPanel changes
-  useEffect(() => {
-    if (isMounted.current && configVisualRegion.width > 0 && !isEqual(settings.dataPanels, prevDataPanelsRef.current)) {
-      const prevDataPanels = prevDataPanelsRef.current;
-      const currentDataPanels = settings.dataPanels;
-
-      if (currentDataPanels.length > prevDataPanels.length) {
-        // Element added
-        //console.log('ADD SCATTERPLOT');
-        const newPanel = currentDataPanels.filter((panel: DataFromPanel) => !prevDataPanels.some((prevPanel) => prevPanel.id === panel.id));
-        const newDataRegions = [];
-        const newScatterplots = [];
-        for (const panel of newPanel) {
-          const { dataRegions, scatterplot } = createNewScatterplot(panel);
-          newDataRegions.push(dataRegions);
-          newScatterplots.push(scatterplot);
-        }
-        setAppState({
-          scatterplots: [...appState.scatterplots, ...newScatterplots],
-          dataRegions: [...appState.dataRegions, ...newDataRegions],
-        });
-      } else if (currentDataPanels.length < prevDataPanels.length) {
-        // Element deleted
-        const deletedPanel = prevDataPanels.find(
-          (prevPanel) => !currentDataPanels.some((panel: DataFromPanel) => panel.id === prevPanel.id),
-        );
-
-        //console.log('DELETE SCATTERPLOT: ', deletedPanel);
+    const handleNewDataRegion = (data: DataPanelConfig, idNew: number) => {
+      let colorCircle: string;
+      let strokeCircle: string;
 
-        removeScatterplot(deletedPanel);
+      if (isColorCircleFix) {
+        colorCircle = config.circle.fillClr;
+        strokeCircle = config.circle.strokeClr;
       } else {
-        // Element updated
-        const updatedPanel = currentDataPanels.find((panel: DataFromPanel) => {
-          const prevPanel = prevDataPanels.find((prevPanel) => prevPanel.id === panel.id);
-          return prevPanel && !isEqual(prevPanel.data, panel.data);
-        });
-        //console.log('UPDATE SCATTERPLOT: ', updatedPanel);
-        if (updatedPanel !== undefined) {
-          updateScatterplot(updatedPanel);
+        if (idNew < numColorsCategorical) {
+          colorCircle = nodeColorHex(idNew + 1);
+        } else {
+          colorCircle = nodeColorHex(idNew + 1 - numColorsCategorical);
         }
-      }
-      prevDataPanelsRef.current = currentDataPanels;
-    } else {
-      isMounted.current = true;
-    }
-  }, [settings.dataPanels, configVisualRegion]);
-  //}, [settings.dataPanels]);
-
-  const handleNewDataRegion = (data: DataPanelConfig, idNew: number) => {
-    let colorCircle: string;
-    let strokeCircle: string;
 
-    if (isColorCircleFix) {
-      colorCircle = config.circle.fillClr;
-      strokeCircle = config.circle.strokeClr;
-    } else {
-      if (idNew < numColorsCategorical) {
-        colorCircle = nodeColorHex(idNew + 1);
-      } else {
-        colorCircle = nodeColorHex(idNew + 1 - numColorsCategorical);
+        strokeCircle = config.circle.strokeClr;
       }
 
-      strokeCircle = config.circle.strokeClr;
-    }
-
-    const regionUserSelection: UserSelection = {
-      name: `region_${idNew}`,
-      nodeName: data.entitySelected,
-      attributeAsRegion: data.attributeSelected,
-      attributeAsRegionSelection: data.attributeValueSelected,
-      placement: {
-        xAxis: data.xAxisSelected,
-        yAxis: data.yAxisSelected,
-        colorNodes: colorCircle,
-        colorNodesStroke: strokeCircle,
-        colorFillBrush: config.brush.fillClr,
-        colorStrokeBrush: config.brush.strokeClr,
-      },
-    };
+      const regionUserSelection: UserSelection = {
+        name: `region_${idNew}`,
+        nodeName: data.entitySelected,
+        attributeAsRegion: data.attributeSelected,
+        attributeAsRegionSelection: data.attributeValueSelected,
+        placement: {
+          xAxis: data.xAxisSelected,
+          yAxis: data.yAxisSelected,
+          colorNodes: colorCircle,
+          colorNodesStroke: strokeCircle,
+          colorFillBrush: config.brush.fillClr,
+          colorStrokeBrush: config.brush.strokeClr,
+        },
+      };
 
-    const regionDataUser: RegionData = getRegionData(augmentedNodes, regionUserSelection);
+      const regionDataUser: RegionData = getRegionData(augmentedNodes, regionUserSelection);
 
-    let xScaleShared: any;
-    let yScaleShared: any;
+      let xScaleShared: any;
+      let yScaleShared: any;
 
-    if (!regionDataUser.xAxisName && !regionDataUser.yAxisName) {
-      xScaleShared = noDataRange;
-      yScaleShared = noDataRange;
-    } else if (regionDataUser.xAxisName && regionDataUser.yAxisName) {
-      if (typeof regionDataUser.xData[0] != 'number') {
-        const xExtent = getUniqueValues(regionDataUser.xData);
-        xScaleShared = xExtent;
-      } else {
-        const xExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
-        xScaleShared = xExtent;
-      }
-      if (typeof regionDataUser.yData[0] != 'number') {
-        const yExtent = getUniqueValues(regionDataUser.yData);
-        yScaleShared = yExtent;
-      } else {
-        const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
-        yScaleShared = yExtent;
-      }
-    } else if (regionDataUser.xAxisName) {
-      if (typeof regionDataUser.xData[0] != 'number') {
-        const xExtent = getUniqueValues(regionDataUser.xData);
-        xScaleShared = xExtent;
-        yScaleShared = noDataRange;
-      } else {
-        const xExtent: string[] | number[] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
-        xScaleShared = xExtent;
-        yScaleShared = noDataRange;
-      }
-    } else if (regionDataUser.yAxisName) {
-      if (typeof regionDataUser.yData[0] != 'number') {
-        const yExtent = getUniqueValues(regionDataUser.yData);
-        yScaleShared = yExtent;
-        xScaleShared = noDataRange;
-      } else {
-        const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
-        yScaleShared = yExtent;
+      if (!regionDataUser.xAxisName && !regionDataUser.yAxisName) {
         xScaleShared = noDataRange;
+        yScaleShared = noDataRange;
+      } else if (regionDataUser.xAxisName && regionDataUser.yAxisName) {
+        if (typeof regionDataUser.xData[0] != 'number') {
+          const xExtent = getUniqueValues(regionDataUser.xData);
+          xScaleShared = xExtent;
+        } else {
+          const xExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
+          xScaleShared = xExtent;
+        }
+        if (typeof regionDataUser.yData[0] != 'number') {
+          const yExtent = getUniqueValues(regionDataUser.yData);
+          yScaleShared = yExtent;
+        } else {
+          const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
+          yScaleShared = yExtent;
+        }
+      } else if (regionDataUser.xAxisName) {
+        if (typeof regionDataUser.xData[0] != 'number') {
+          const xExtent = getUniqueValues(regionDataUser.xData);
+          xScaleShared = xExtent;
+          yScaleShared = noDataRange;
+        } else {
+          const xExtent: string[] | number[] = setExtension(marginAxis, [...regionDataUser.xData, ...regionDataUser.xData]);
+          xScaleShared = xExtent;
+          yScaleShared = noDataRange;
+        }
+      } else if (regionDataUser.yAxisName) {
+        if (typeof regionDataUser.yData[0] != 'number') {
+          const yExtent = getUniqueValues(regionDataUser.yData);
+          yScaleShared = yExtent;
+          xScaleShared = noDataRange;
+        } else {
+          const yExtent: [number, number] = setExtension(marginAxis, [...regionDataUser.yData, ...regionDataUser.yData]);
+          yScaleShared = yExtent;
+          xScaleShared = noDataRange;
+        }
       }
-    }
 
-    const arrayIDs = regionDataUser.idData.map((item) => item);
+      const arrayIDs = regionDataUser.idData.map((item) => item);
 
-    // update scatterplot state
+      // update scatterplot state
 
-    const newScatterplot: KeyedScatterplotProps = {
-      key: idNew,
-      data: regionDataUser,
-      visualConfig: configVisualRegion,
-      xScaleRange: xScaleShared,
-      yScaleRange: yScaleShared,
-      onBrushUpdate: handleBrushUpdate,
-      onResultJitter: handleResultJitter,
-      onBrushClear: handleBrushClear,
-    };
+      const newScatterplot: KeyedScatterplotProps = {
+        key: idNew,
+        data: regionDataUser,
+        visualConfig: configVisualRegion,
+        xScaleRange: xScaleShared,
+        yScaleRange: yScaleShared,
+        onBrushUpdate: handleBrushUpdate,
+        onResultJitter: handleResultJitter,
+        onBrushClear: handleBrushClear,
+      };
 
-    idBrushed.current.push({ idScatterplot: idNew, data: [] });
+      idBrushed.current.push({ idScatterplot: idNew, data: [] });
 
-    if (IDScatterplotR0.current === idNew) {
-      arrayIDScatterplotR0.current = arrayIDs;
-    } else {
-      if (graphologyGraph) {
-        if (IDScatterplot2IDEdge.current) {
-          IDScatterplot2IDEdge.current[idNew - 1] = indexNewEdge.current;
+      if (IDScatterplotR0.current === idNew) {
+        arrayIDScatterplotR0.current = arrayIDs;
+      } else {
+        if (graphologyGraph) {
+          if (IDScatterplot2IDEdge.current) {
+            IDScatterplot2IDEdge.current[idNew - 1] = indexNewEdge.current;
+          }
+          const [connectedD, connectedD2] = findConnectionsNodes(
+            arrayIDs,
+            arrayIDScatterplotR0.current,
+            graphologyGraph,
+            regionDataUser.label,
+          );
+
+          informationEdges.current.nameEdges.push(`edge_region0_to_${regionUserSelection.name}`);
+          //console.log('informationEdges ', informationEdges.current.nameEdges);
+          informationEdges.current.idEdges.push(connectedD2);
+          arrayConnections.current.push(connectedD);
+          indexNewEdge.current++;
         }
-        const [connectedD, connectedD2] = findConnectionsNodes(
-          arrayIDs,
-          arrayIDScatterplotR0.current,
-          graphologyGraph,
-          regionDataUser.label,
-        );
-
-        informationEdges.current.nameEdges.push(`edge_region0_to_${regionUserSelection.name}`);
-        //console.log('informationEdges ', informationEdges.current.nameEdges);
-        informationEdges.current.idEdges.push(connectedD2);
-        arrayConnections.current.push(connectedD);
-        indexNewEdge.current++;
       }
-    }
 
-    return {
-      scatterplot: newScatterplot,
-      dataRegions: regionDataUser,
+      return {
+        scatterplot: newScatterplot,
+        dataRegions: regionDataUser,
+      };
+    };
+
+    const exportImageInternal = () => {
+      if (divRef.current) {
+        // Check if divRef.current is not null
+        html2canvas(divRef.current).then((canvas) => {
+          const pngData = canvas.toDataURL('image/png');
+          const a = document.createElement('a');
+          a.href = pngData;
+          a.download = 'semanticSubstrates.png';
+          a.click();
+        });
+      } else {
+        console.error('The referenced div is null.');
+      }
     };
-  };
 
-  return (
-    <div className="w-full font-inter overflow-x-hidden ">
-      <div className="w-full relative" ref={divRef}>
-        {configVisualRegion.width > 0 && appState.scatterplots.some((s) => s.visualConfig.width > 0) && (
-          <>
-            <div className="w-full regionContainer z-0">
-              {appState.scatterplots.map((scatterplot) => (
-                <Scatterplot {...scatterplot} />
-              ))}
-            </div>
-            <div className="pointer-events-none absolute top-0 w-full flex flex-row justify-center">
-              {stateEdges.edgePlotted.map(
-                (edegePlot, index) =>
-                  edegePlot.dataConnections && (
-                    <div key={index} className="absolute">
-                      <EdgesLayer {...edegePlot} />
-                    </div>
-                  ),
-              )}
-            </div>
-          </>
-        )}
+    useImperativeHandle(refExternal, () => ({
+      exportImageInternal,
+    }));
+
+    return (
+      <div className="w-full font-inter overflow-x-hidden ">
+        <div className="w-full relative" ref={divRef}>
+          {configVisualRegion.width > 0 && appState.scatterplots.some((s) => s.visualConfig.width > 0) && (
+            <>
+              <div className="w-full regionContainer z-0">
+                {appState.scatterplots.map((scatterplot) => (
+                  <Scatterplot {...scatterplot} />
+                ))}
+              </div>
+              <div className="pointer-events-none absolute top-0 w-full flex flex-row justify-center">
+                {stateEdges.edgePlotted.map(
+                  (edegePlot, index) =>
+                    edegePlot.dataConnections && (
+                      <div key={index} className="absolute">
+                        <EdgesLayer {...edegePlot} />
+                      </div>
+                    ),
+                )}
+              </div>
+            </>
+          )}
+        </div>
       </div>
-    </div>
-  );
-};
+    );
+  },
+);
 
 const SemSubstrSettings = ({ settings, updateSettings, graphMetadata }: VisualizationSettingsPropTypes<SemSubstrProps>) => {
   useEffect(() => {
@@ -927,14 +955,22 @@ const SemSubstrSettings = ({ settings, updateSettings, graphMetadata }: Visualiz
   );
 };
 
+const SemSubstrVisRef = React.createRef<{ exportImageInternal: () => void }>();
+
 export const SemSubstrVisComponent: VISComponentType<SemSubstrProps> = {
   displayName: displayName,
   description: 'Node/Edge Attribute Exploration',
-  component: VisSemanticSubstrates,
+  component: React.forwardRef((props: VisualizationPropTypes<SemSubstrProps>, ref) => (
+    <VisSemanticSubstrates {...props} ref={SemSubstrVisRef} />
+  )),
   settingsComponent: SemSubstrSettings,
   settings: settings,
   exportImage: () => {
-    alert('Not yet supported');
+    if (SemSubstrVisRef.current) {
+      SemSubstrVisRef.current.exportImageInternal();
+    } else {
+      console.error('Map reference is not set.');
+    }
   },
 };
 
-- 
GitLab