From 23b18987eb8f82e2d7492ece51f9a72c25623fc3 Mon Sep 17 00:00:00 2001
From: "Vink, S.A. (Sjoerd)" <s.a.vink@uu.nl>
Date: Thu, 5 Sep 2024 12:39:15 +0000
Subject: [PATCH] feat: improvement of nodelink map visualization

---
 .../colorComponents/colorPicker/index.tsx     |   1 -
 .../layers/nodelink-layer/NodeLinkLayer.tsx   |  96 ++--
 .../layers/nodelink-layer/NodeLinkOptions.tsx | 524 +++++++++---------
 .../layers/nodelink-layer/shapeFactory.tsx    |  45 +-
 .../lib/vis/visualizations/mapvis/mapvis.tsx  |   4 +-
 .../lib/vis/visualizations/mapvis/utils.ts    |  10 +
 6 files changed, 363 insertions(+), 317 deletions(-)
 create mode 100644 libs/shared/lib/vis/visualizations/mapvis/utils.ts

diff --git a/libs/shared/lib/components/colorComponents/colorPicker/index.tsx b/libs/shared/lib/components/colorComponents/colorPicker/index.tsx
index afe153a85..ef0e49e5e 100644
--- a/libs/shared/lib/components/colorComponents/colorPicker/index.tsx
+++ b/libs/shared/lib/components/colorComponents/colorPicker/index.tsx
@@ -53,7 +53,6 @@ export default function ColorPicker({ value, updateValue }: Props) {
               triangle="top-right"
               color={{ r: value[0], g: value[1], b: value[2] }}
               onChangeComplete={(color) => {
-                console.log(color);
                 const rgb = color.rgb;
                 const newValue: [number, number, number] = [rgb.r, rgb.g, rgb.b];
                 updateValue(newValue);
diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
index 81f943b88..dd407682b 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx
@@ -1,10 +1,8 @@
 import React from 'react';
 import { CompositeLayer, Layer } from 'deck.gl';
-import { IconLayer, LineLayer, TextLayer } from '@deck.gl/layers';
+import { LineLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
 import { CompositeLayerType, LayerProps } from '../../mapvis.types';
 import { BrushingExtension, CollisionFilterExtension } from '@deck.gl/extensions';
-import { Node } from '@graphpolaris/shared/lib/data-access';
-import { createIcon } from './shapeFactory';
 
 export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
   static type = 'nodelink';
@@ -19,27 +17,40 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
   }
 
   renderLayers() {
-    const { data, settings, getNodeLocation, ml, graphMetadata } = this.props;
+    const { data, settings, getNodeLocation, ml, graphMetadata, selected } = this.props;
     const layerSettings = settings[NodeLinkLayer.type];
 
     const brushingExtension = new BrushingExtension();
     const collisionFilter = new CollisionFilterExtension();
 
+    const nodeLocations = data.nodes.reduce((acc: Record<string, [number, number]>, node: any) => {
+      const pos = getNodeLocation(node._id);
+      if (pos && (pos[0] !== 0 || pos[1] !== 0)) {
+        acc[node._id] = pos;
+      }
+      return acc;
+    }, {});
+
     graphMetadata.edges.labels.forEach((label: string) => {
       const layerId = `${label}-edges-line`;
-      const edgeData = data.edges;
+
+      const edgeData = data.edges.filter((edge: any) => {
+        const from = nodeLocations[edge.from];
+        const to = nodeLocations[edge.to];
+        return from && to;
+      });
 
       this._layers[layerId] = new LineLayer({
         id: layerId,
         data: edgeData,
-        visible: !layerSettings.edges[label].hidden,
+        visible: !layerSettings?.edges[label]?.hidden,
         pickable: true,
-        getWidth: layerSettings.edges[label].width,
+        getWidth: layerSettings?.edges[label]?.width,
         getSourcePosition: (d) => getNodeLocation(d.from),
         getTargetPosition: (d) => getNodeLocation(d.to),
-        getColor: (d) => layerSettings.edges[d.label].color,
+        getColor: (d) => layerSettings?.edges[d.label]?.color,
         extensions: [brushingExtension],
-        brushingEnabled: layerSettings.enableBrushing,
+        brushingEnabled: layerSettings?.enableBrushing,
       });
     });
 
@@ -57,44 +68,49 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> {
 
     graphMetadata.nodes.labels.forEach((label: string) => {
       const layerId = `${label}-nodes-scatterplot`;
+      const textLayerId = `${label}-label-target`;
 
-      this._layers[layerId] = new IconLayer({
+      const nodes = data.nodes.filter((node: any) => nodeLocations[node._id] && node.label === label);
+
+      this._layers[layerId] = new ScatterplotLayer({
         id: layerId,
-        visible: !layerSettings.nodes[label].hidden,
-        data: data.nodes.filter((node: Node) => node.label === label),
+        visible: !layerSettings?.nodes[label]?.hidden,
+        data: nodes,
         pickable: true,
-        getColor: (d) => [200, 140, 0],
-        getSize: (d) => layerSettings.nodes[label].size,
+        getFillColor: (d) => layerSettings?.nodes[label]?.color,
         getPosition: (d) => getNodeLocation(d._id),
-        getIcon: (d: any) => {
-          return {
-            url: createIcon(layerSettings.nodes[label].shape, layerSettings.nodes[label].color),
-            width: 24,
-            height: 24,
-          };
+        getRadius: (d) => layerSettings?.nodes[label]?.size,
+        radiusMinPixels: 5,
+        getLineWidth: (d: any) => (selected && selected.some((sel) => sel._id === d._id) ? 2 : 1),
+        lineWidthUnits: 'pixels',
+        stroked: true,
+        updateTriggers: {
+          getIcon: [selected],
         },
       });
-    });
 
-    const textLayerId = 'label-target';
-
-    this._layers[textLayerId] = new TextLayer({
-      id: textLayerId,
-      data: data.nodes,
-      getPosition: (d: any) => getNodeLocation(d._id),
-      getText: (d: any) => d.id,
-      getSize: 15,
-      visible: true,
-      getAlignmentBaseline: 'top',
-      background: true,
-      getPixelOffset: [10, 10],
-      extensions: [collisionFilter],
-      collisionEnabled: true,
-      getCollisionPriority: (d: any) => d.id,
-      collisionTestProps: { sizeScale: 10 },
-      getRadius: 10,
-      radiusUnits: 'pixels',
-      collisionGroup: 'text',
+      this._layers[textLayerId] = new TextLayer({
+        id: textLayerId,
+        data: nodes,
+        getPosition: (d: any) => getNodeLocation(d._id),
+        getText: (d: any) => d.label,
+        getSize: (d: any) => (layerSettings?.nodes[label]?.size * 2) / d.label.length,
+        getAlignmentBaseline: 'center',
+        getRadius: 10,
+        radiusScale: 20,
+        getColor: [255, 255, 255],
+        sizeUnits: 'meters',
+        sizeMaxPixels: 64,
+        characterSet: 'auto',
+        fontFamily: 'monospace',
+        billboard: false,
+        getAngle: () => 0,
+        collisionGroup: 'textLabels',
+        extensions: [collisionFilter],
+        collisionEnabled: layerSettings?.collisionEnabled ?? true,
+        getCollisionPriority: () => 100,
+        collisionTestProps: { sizeScale: 5 },
+      });
     });
 
     return Object.values(this._layers);
diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
index 306774b91..566817294 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx
@@ -3,6 +3,25 @@ import ColorPicker from '@graphpolaris/shared/lib/components/colorComponents/col
 import { Button, DropdownColorLegend, EntityPill, Icon, Input, RelationPill } from '@graphpolaris/shared/lib/components';
 import { MapProps } from '../../mapvis';
 import { LayerSettingsComponentType } from '../../mapvis.types';
+import { nodeColorHex } from '../../utils';
+
+const defaultNodeSettings = (index: number) => ({
+  colorByAttribute: false,
+  colorAttribute: undefined,
+  colorAttributeType: undefined,
+  hidden: false,
+  shape: 'circle',
+  color: nodeColorHex(index),
+  size: 40,
+});
+
+const defaultEdgeSettings = () => ({
+  hidden: false,
+  width: 1,
+  sizeAttribute: '',
+  fixed: true,
+  color: [132, 150, 155],
+});
 
 export function NodeLinkOptions({
   settings,
@@ -12,43 +31,36 @@ export function NodeLinkOptions({
   updateSpatialAttribute,
 }: LayerSettingsComponentType<MapProps>) {
   const layerType = 'nodelink';
-  const layerSettings = settings[layerType];
+  const layerSettings = settings[layerType] || { enableBrushing: false, nodes: {}, edges: {} };
 
   useEffect(() => {
-    if (!layerSettings) {
-      const initialSettingsObject = { enableBrushing: false, nodes: {}, edges: {} };
+    const nodes = layerSettings.nodes || {};
+    const edges = layerSettings.edges || {};
 
-      graphMetadata.nodes.labels.forEach((node) => {
-        initialSettingsObject.nodes = {
-          ...initialSettingsObject.nodes,
-          [node]: {
-            colorByAttribute: false,
-            colorAttribute: undefined,
-            colorAttributeType: undefined,
-            hidden: false,
-            shape: 'circle',
-            color: [Math.floor(Math.random() * 251), Math.floor(Math.random() * 251), 0],
-            size: 10,
-          },
-        };
-      });
+    const newNodes = graphMetadata.nodes.labels.reduce(
+      (acc, node, index) => {
+        acc[node] = nodes[node] || defaultNodeSettings(index);
+        return acc;
+      },
+      {} as typeof nodes,
+    );
 
-      graphMetadata.edges.labels.forEach((edge) => {
-        initialSettingsObject.edges = {
-          ...initialSettingsObject.edges,
-          [edge]: {
-            hidden: false,
-            width: 1,
-            sizeAttribute: '',
-            fixed: true,
-            color: [0, 0, 0],
-          },
-        };
-      });
+    const newEdges = graphMetadata.edges.labels.reduce(
+      (acc, edge) => {
+        acc[edge] = edges[edge] || defaultEdgeSettings();
+        return acc;
+      },
+      {} as typeof edges,
+    );
 
-      updateLayerSettings({ ...initialSettingsObject });
+    if (JSON.stringify(newNodes) !== JSON.stringify(nodes) || JSON.stringify(newEdges) !== JSON.stringify(edges)) {
+      updateLayerSettings({
+        ...layerSettings,
+        nodes: newNodes,
+        edges: newEdges,
+      });
     }
-  }, [graphMetadata, settings, updateLayerSettings]);
+  }, [graphMetadata]);
 
   const handleCollapseToggle = (type: string, itemType: 'nodes' | 'edges') => {
     if (layerSettings) {
@@ -71,266 +83,274 @@ export function NodeLinkOptions({
           const nodeSettings = layerSettings?.nodes?.[nodeType] || {};
 
           return (
-            <div className="mt-2" key={nodeType}>
-              <div className="flex items-center">
-                <Button
-                  size="2xs"
-                  iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'}
-                  variant="ghost"
-                  onClick={() => handleCollapseToggle(nodeType, 'nodes')}
-                />
-                <div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}>
-                  <EntityPill title={nodeType} />
+            layerSettings?.nodes?.[nodeType] && (
+              <div className="mt-2" key={nodeType}>
+                <div className="flex items-center">
+                  <Button
+                    size="2xs"
+                    iconComponent={nodeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'}
+                    variant="ghost"
+                    onClick={() => handleCollapseToggle(nodeType, 'nodes')}
+                  />
+                  <div className="flex flex-grow mr-2 cursor-pointer" onClick={() => handleCollapseToggle(nodeType, 'nodes')}>
+                    <EntityPill title={nodeType} />
+                  </div>
                 </div>
-              </div>
 
-              {!nodeSettings.collapsed && (
-                <div>
-                  <Input
-                    label="Hidden"
-                    type="boolean"
-                    value={nodeSettings.hidden}
-                    onChange={(val) =>
-                      updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } })
-                    }
-                  />
-                  <div className="border-t-2 my-2">
-                    <span className="font-bold mt-2">Location attributes</span>
-                    <Input
-                      inline
-                      label="Latitude"
-                      type="dropdown"
-                      value={settings?.location[nodeType]?.lat}
-                      options={[...spatialAttributes[nodeType]]}
-                      disabled={spatialAttributes[nodeType].length < 1}
-                      onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)}
-                    />
+                {!nodeSettings.collapsed && (
+                  <div>
                     <Input
-                      inline
-                      label="Longitude"
-                      type="dropdown"
-                      value={settings?.location[nodeType]?.lon}
-                      options={[...spatialAttributes[nodeType]]}
-                      disabled={spatialAttributes[nodeType].length < 1}
-                      onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)}
+                      label="Hidden"
+                      type="boolean"
+                      value={nodeSettings.hidden}
+                      onChange={(val) =>
+                        updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, hidden: val } } })
+                      }
                     />
-                  </div>
+                    <div className="border-t-2 my-2">
+                      <span className="font-bold mt-2">Location attributes</span>
+                      <Input
+                        inline
+                        label="Latitude"
+                        type="dropdown"
+                        value={settings?.location[nodeType]?.lat}
+                        options={[...spatialAttributes[nodeType]]}
+                        disabled={spatialAttributes[nodeType].length < 1}
+                        onChange={(val) => updateSpatialAttribute(nodeType, 'lat', val as string)}
+                      />
+                      <Input
+                        inline
+                        label="Longitude"
+                        type="dropdown"
+                        value={settings?.location[nodeType]?.lon}
+                        options={[...spatialAttributes[nodeType]]}
+                        disabled={spatialAttributes[nodeType].length < 1}
+                        onChange={(val) => updateSpatialAttribute(nodeType, 'lon', val as string)}
+                      />
+                    </div>
 
-                  <div className="border-t-2 my-2">
-                    <div className="flex justify-between">
-                      <span className="font-bold">Color</span>
-                      {!nodeSettings.colorByAttribute && (
-                        <ColorPicker
-                          value={nodeSettings.color}
-                          updateValue={(val) => {
-                            updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } });
-                          }}
+                    <div className="border-t-2 my-2">
+                      <div className="flex justify-between">
+                        <span className="font-bold">Color</span>
+                        {!nodeSettings.colorByAttribute && (
+                          <ColorPicker
+                            value={nodeSettings.color}
+                            updateValue={(val) => {
+                              updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, color: val } } });
+                            }}
+                          />
+                        )}
+                      </div>
+
+                      <div>
+                        <Input
+                          label="By attribute"
+                          type="boolean"
+                          value={nodeSettings.colorByAttribute ?? false}
+                          onChange={(val) =>
+                            updateLayerSettings({
+                              nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorByAttribute: val } },
+                            })
+                          }
                         />
-                      )}
+                        {nodeSettings.colorByAttribute && (
+                          <div>
+                            <Input
+                              inline
+                              label="Color based on"
+                              type="dropdown"
+                              value={nodeSettings.colorAttribute}
+                              options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)}
+                              disabled={!settings.nodes}
+                              onChange={(val) =>
+                                updateLayerSettings({
+                                  nodes: {
+                                    ...layerSettings.nodes,
+                                    [nodeType]: {
+                                      ...nodeSettings,
+                                      colorAttribute: String(val),
+                                      colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension,
+                                    },
+                                  },
+                                })
+                              }
+                            />
+                            {nodeSettings.colorAttributeType === 'numerical' ? (
+                              <div>
+                                <DropdownColorLegend
+                                  value={settings?.colorScale}
+                                  onChange={(val) =>
+                                    updateLayerSettings({
+                                      nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } },
+                                    })
+                                  }
+                                />
+                              </div>
+                            ) : (
+                              <div>Categorical</div>
+                            )}
+                          </div>
+                        )}
+                      </div>
                     </div>
 
-                    <div>
+                    <div className="border-t-2 my-2">
+                      <span className="font-bold mt-2">Shape & Size</span>
                       <Input
-                        label="By attribute"
-                        type="boolean"
-                        value={nodeSettings.colorByAttribute ?? false}
+                        inline
+                        label="Shape"
+                        type="dropdown"
+                        value={nodeSettings.shape}
+                        options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']}
+                        disabled={true}
                         onChange={(val) =>
-                          updateLayerSettings({ nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorByAttribute: val } } })
+                          updateLayerSettings({
+                            nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) } },
+                          })
+                        }
+                      />
+                      <Input
+                        label="Size"
+                        type="slider"
+                        min={0}
+                        max={80}
+                        step={5}
+                        value={nodeSettings.size}
+                        onChange={(val) =>
+                          updateLayerSettings({
+                            nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } },
+                          })
                         }
                       />
-                      {nodeSettings.colorByAttribute && (
-                        <div>
-                          <Input
-                            inline
-                            label="Color based on"
-                            type="dropdown"
-                            value={nodeSettings.colorAttribute}
-                            options={Object.keys(graphMetadata.nodes.types[nodeType]?.attributes)}
-                            disabled={!settings.nodes}
-                            onChange={(val) =>
-                              updateLayerSettings({
-                                nodes: {
-                                  ...layerSettings.nodes,
-                                  [nodeType]: {
-                                    ...nodeSettings,
-                                    colorAttribute: String(val),
-                                    colorAttributeType: graphMetadata.nodes.types[nodeType].attributes[val].dimension,
-                                  },
-                                },
-                              })
-                            }
-                          />
-                          {nodeSettings.colorAttributeType === 'numerical' ? (
-                            <div>
-                              <DropdownColorLegend
-                                value={settings?.colorScale}
-                                onChange={(val) =>
-                                  updateLayerSettings({
-                                    nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, colorScale: val } },
-                                  })
-                                }
-                              />
-                            </div>
-                          ) : (
-                            <div>Categorical</div>
-                          )}
-                        </div>
-                      )}
                     </div>
                   </div>
-
-                  <div className="border-t-2 my-2">
-                    <span className="font-bold mt-2">Shape & Size</span>
-                    <Input
-                      inline
-                      label="Shape"
-                      type="dropdown"
-                      value={nodeSettings.shape}
-                      options={['circle', 'square', 'triangle', 'diamond', 'location', 'star']}
-                      disabled={!settings.shape}
-                      onChange={(val) =>
-                        updateLayerSettings({
-                          nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, shape: String(val) } },
-                        })
-                      }
-                    />
-                    <Input
-                      label="Size"
-                      type="slider"
-                      min={0}
-                      max={40}
-                      step={1}
-                      value={nodeSettings.size}
-                      onChange={(val) =>
-                        updateLayerSettings({
-                          nodes: { ...layerSettings.nodes, [nodeType]: { ...nodeSettings, size: Number(val) } },
-                        })
-                      }
-                    />
-                  </div>
-                </div>
-              )}
-            </div>
+                )}
+              </div>
+            )
           );
         })}
         {graphMetadata.edges.labels.map((edgeType) => {
           const edgeSettings = layerSettings?.edges?.[edgeType] || {};
 
           return (
-            <div className="mt-2" key={edgeType}>
-              <div className="flex items-center">
-                <Button
-                  size="2xs"
-                  iconComponent={edgeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'}
-                  variant="ghost"
-                  onClick={() => handleCollapseToggle(edgeType, 'edges')}
-                />
-                <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType, 'edges')}>
-                  <RelationPill title={edgeType} />
-                </div>
-              </div>
-
-              {!edgeSettings.collapsed && (
-                <div>
-                  <Input
-                    label="Hidden"
-                    type="boolean"
-                    value={edgeSettings.hidden ?? false}
-                    onChange={(val) => {
-                      updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } });
-                    }}
+            layerSettings?.edges?.[edgeType] && (
+              <div className="mt-2" key={edgeType}>
+                <div className="flex items-center">
+                  <Button
+                    size="2xs"
+                    iconComponent={edgeSettings.collapsed ? 'icon-[ic--baseline-arrow-right]' : 'icon-[ic--baseline-arrow-drop-down]'}
+                    variant="ghost"
+                    onClick={() => handleCollapseToggle(edgeType, 'edges')}
                   />
-
-                  <div className="flex justify-between">
-                    <span className="font-bold">Color</span>
-                    <ColorPicker
-                      value={edgeSettings.color}
-                      updateValue={(val) =>
-                        updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } })
-                      }
-                    />
+                  <div className="w-3/4 mr-6 cursor-pointer" onClick={() => handleCollapseToggle(edgeType, 'edges')}>
+                    <RelationPill title={edgeType} />
                   </div>
+                </div>
 
-                  <Input
-                    label="Enable brushing"
-                    type="boolean"
-                    value={settings.enableBrushing}
-                    onChange={(val) => {
-                      updateLayerSettings({ enableBrushing: val as boolean });
-                    }}
-                  />
-
+                {!edgeSettings.collapsed && (
                   <div>
-                    <div className="flex items-center gap-1">
-                      <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
-                      <span>Width</span>
+                    <Input
+                      label="Hidden"
+                      type="boolean"
+                      value={edgeSettings.hidden ?? false}
+                      onChange={(val) => {
+                        updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, hidden: val } } });
+                      }}
+                    />
+
+                    <div className="flex justify-between">
+                      <span className="font-bold">Color</span>
+                      <ColorPicker
+                        value={edgeSettings.color}
+                        updateValue={(val) =>
+                          updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, color: val } } })
+                        }
+                      />
                     </div>
+
                     <Input
-                      label="Fixed"
+                      label="Enable brushing"
                       type="boolean"
-                      value={edgeSettings.fixed}
-                      onChange={(val) => updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } })}
+                      value={settings.enableBrushing}
+                      onChange={(val) => {
+                        updateLayerSettings({ enableBrushing: val as boolean });
+                      }}
                     />
-                    {!edgeSettings.fixed ? (
-                      <div>
-                        <Input
-                          label="Based on"
-                          type="dropdown"
-                          size="xs"
-                          options={
-                            graphMetadata.edges.types[edgeType]?.attributes
-                              ? Object.keys(graphMetadata.edges.types[edgeType].attributes).filter(
-                                  (key) => graphMetadata.edges.types[edgeType].attributes[key].dimension === 'numerical',
-                                )
-                              : []
-                          }
-                          value={edgeSettings.sizeAttribute}
-                          onChange={(val) =>
-                            updateLayerSettings({
-                              edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } },
-                            })
-                          }
-                        />
-                        <div className="flex">
+
+                    <div>
+                      <div className="flex items-center gap-1">
+                        <Icon component="icon-[ic--baseline-subdirectory-arrow-right]" size={16} color="text-secondary-300" />
+                        <span>Width</span>
+                      </div>
+                      <Input
+                        label="Fixed"
+                        type="boolean"
+                        value={edgeSettings.fixed}
+                        onChange={(val) =>
+                          updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, fixed: val } } })
+                        }
+                      />
+                      {!edgeSettings.fixed ? (
+                        <div>
                           <Input
-                            type="number"
-                            label="min"
+                            label="Based on"
+                            type="dropdown"
                             size="xs"
-                            value={edgeSettings.min}
+                            options={
+                              graphMetadata.edges.types[edgeType]?.attributes
+                                ? Object.keys(graphMetadata.edges.types[edgeType].attributes).filter(
+                                    (key) => graphMetadata.edges.types[edgeType].attributes[key].dimension === 'numerical',
+                                  )
+                                : []
+                            }
+                            value={edgeSettings.sizeAttribute}
                             onChange={(val) =>
-                              updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } })
+                              updateLayerSettings({
+                                edges: { ...settings.edges, [edgeType]: { ...edgeSettings, sizeAttribute: String(val) } },
+                              })
                             }
                           />
+                          <div className="flex">
+                            <Input
+                              type="number"
+                              label="min"
+                              size="xs"
+                              value={edgeSettings.min}
+                              onChange={(val) =>
+                                updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, min: val } } })
+                              }
+                            />
+                            <Input
+                              type="number"
+                              label="max"
+                              size="xs"
+                              value={edgeSettings.max}
+                              onChange={(val) =>
+                                updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } })
+                              }
+                            />
+                          </div>
+                        </div>
+                      ) : (
+                        <div>
                           <Input
-                            type="number"
-                            label="max"
-                            size="xs"
-                            value={edgeSettings.max}
+                            type="slider"
+                            label="Width"
+                            min={0}
+                            max={10}
+                            step={0.2}
+                            value={edgeSettings.width}
                             onChange={(val) =>
-                              updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, max: val } } })
+                              updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } })
                             }
                           />
                         </div>
-                      </div>
-                    ) : (
-                      <div>
-                        <Input
-                          type="slider"
-                          label="Width"
-                          min={0}
-                          max={10}
-                          step={0.2}
-                          value={edgeSettings.width}
-                          onChange={(val) =>
-                            updateLayerSettings({ edges: { ...settings.edges, [edgeType]: { ...edgeSettings, width: Number(val) } } })
-                          }
-                        />
-                      </div>
-                    )}
+                      )}
+                    </div>
                   </div>
-                </div>
-              )}
-            </div>
+                )}
+              </div>
+            )
           );
         })}
       </div>
diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx
index 92e78c54d..0bafa0ec6 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/shapeFactory.tsx
@@ -2,44 +2,46 @@ function svgToDataURL(svg: string): string {
   return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
 }
 
-export const createIcon = (type: string, color: [number, number, number]) => {
+export const createIcon = (type: string, color: [number, number, number], selected: boolean = false) => {
+  const strokeWidth = selected ? 8 : 2;
+
   switch (type) {
     case 'diamond':
-      return svgToDataURL(diamond(color));
+      return svgToDataURL(diamond(color, strokeWidth));
 
     case 'star':
-      return svgToDataURL(star(color));
+      return svgToDataURL(star(color, strokeWidth));
 
     case 'circle':
-      return svgToDataURL(circle(color));
+      return svgToDataURL(circle(color, strokeWidth));
 
     case 'square':
-      return svgToDataURL(square(color));
+      return svgToDataURL(square(color, strokeWidth));
 
     case 'triangle':
-      return svgToDataURL(triangle(color));
+      return svgToDataURL(triangle(color, strokeWidth));
 
     case 'location':
-      return svgToDataURL(location(color));
+      return svgToDataURL(location(color, strokeWidth));
 
     default:
-      return svgToDataURL(diamond(color));
+      return svgToDataURL(diamond(color, strokeWidth));
   }
 };
 
-const diamond = (rgbColor: [number, number, number]) => {
+const diamond = (rgbColor: [number, number, number], strokeWidth: number) => {
   return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
-            <polygon points="12,2 22,12 12,22 2,12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" />
+            <polygon points="12,2 22,12 12,22 2,12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" />
           </svg>`;
 };
 
-const star = (rgbColor: [number, number, number]) => {
+const star = (rgbColor: [number, number, number], strokeWidth: number) => {
   return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
-            <polygon points="12,2 14,8 20,8 15,12 18,18 12,14 6,18 9,12 4,8 10,8" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" />
+            <polygon points="12,2 14,8 20,8 15,12 18,18 12,14 6,18 9,12 4,8 10,8" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" />
           </svg>`;
 };
 
-const triangle = (rgbColor: [number, number, number]) => {
+const triangle = (rgbColor: [number, number, number], strokeWidth: number) => {
   return `<svg
         width="24"
         height="24"
@@ -47,38 +49,37 @@ const triangle = (rgbColor: [number, number, number]) => {
         fill-rule="evenodd"
         clip-rule="evenodd"
       >
-        <polygon points="12,2 22,22 2,22" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" />
+        <polygon points="12,2 22,22 2,22" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" />
       </svg>`;
 };
 
-const circle = (rgbColor: [number, number, number]) => {
+const circle = (rgbColor: [number, number, number], strokeWidth: number) => {
   return `<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg">
-            <circle cx="12" cy="12" r="12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})"/>
+            <circle cx="12" cy="12" r="12" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" />
           </svg>`;
 };
 
-const square = (rgbColor: [number, number, number]) => {
+const square = (rgbColor: [number, number, number], strokeWidth: number) => {
   return `<svg
         width="24"
         height="24"
         xmlns="http://www.w3.org/2000/svg"
-        xmlns:serif="http://www.serif.com/"
         fill-rule="evenodd"
         clip-rule="evenodd"
       >
-        <path serif:id="shape 1" d="M0 0h24v24h-24z" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" />
+        <path d="M0 0h24v24h-24z" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}" />
       </svg>`;
 };
 
-const location = (rgbColor: [number, number, number]) => {
+const location = (rgbColor: [number, number, number], strokeWidth: number) => {
   return `<svg width="34" height="34" xmlns="http://www.w3.org/2000/svg">
             <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
                 <g transform="translate(-125.000000, -643.000000)">
                     <g transform="translate(37.000000, 169.000000)">
                         <g transform="translate(78.000000, 468.000000)">
                             <g transform="translate(10.000000, 6.000000)">
-                                <path d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z" id="Shape" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})"></path>
-                                <circle id="Oval" fill="#0C0058" fill-rule="nonzero" cx="14" cy="14" r="7"></circle>
+                                <path d="M14,0 C21.732,0 28,5.641 28,12.6 C28,23.963 14,36 14,36 C14,36 0,24.064 0,12.6 C0,5.641 6.268,0 14,0 Z" fill="rgb(${rgbColor[0]}, ${rgbColor[1]}, ${rgbColor[2]})" stroke="black" stroke-width="${strokeWidth}"></path>
+                                <circle fill="#0C0058" fill-rule="nonzero" cx="14" cy="14" r="7"></circle>
                             </g>
                         </g>
                     </g>
diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
index c37a2cae6..e48b49d6f 100644
--- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
+++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx
@@ -1,6 +1,6 @@
 import React, { useEffect, useCallback, useState, useRef, forwardRef, useImperativeHandle } from 'react';
 import DeckGL, { DeckGLProps, DeckGLRef } from '@deck.gl/react';
-import { CompositeLayer, FlyToInterpolator, WebMercatorViewport } from '@deck.gl/core';
+import { CompositeLayer, FlyToInterpolator, MapViewState, MapViewState, WebMercatorViewport } from '@deck.gl/core';
 import { CompositeLayerType, Coordinate, LayerSettingsType, LocationInfo, SearchResultType } from './mapvis.types';
 import { VISComponentType, VisualizationPropTypes } from '../../common';
 import { layerTypes, createBaseMap, LayerTypes } from './layers';
@@ -20,7 +20,7 @@ const settings: MapProps = {
   location: {},
 };
 
-const INITIAL_VIEW_STATE = {
+const INITIAL_VIEW_STATE: MapViewState = {
   latitude: 52.1006,
   longitude: 5.6464,
   zoom: 6,
diff --git a/libs/shared/lib/vis/visualizations/mapvis/utils.ts b/libs/shared/lib/vis/visualizations/mapvis/utils.ts
new file mode 100644
index 000000000..d45003bd9
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/mapvis/utils.ts
@@ -0,0 +1,10 @@
+import { visualizationColors } from 'config';
+
+export function nodeColorHex(num: number) {
+  const colorVal = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length];
+  const hex = colorVal.replace(/^#/, '');
+  const r = parseInt(hex.substring(0, 2), 16);
+  const g = parseInt(hex.substring(2, 4), 16);
+  const b = parseInt(hex.substring(4, 6), 16);
+  return [r, g, b];
+}
-- 
GitLab