From b47b593ac7c8696d594c3d1944d0bda39ceb7336 Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Wed, 4 Sep 2024 15:07:01 +0000
Subject: [PATCH] Feat/export paoh vis

---
 .../vis/visualizations/paohvis/paohvis.tsx    | 1527 +++++++++--------
 1 file changed, 788 insertions(+), 739 deletions(-)

diff --git a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx
index 9b88cc33f..011beea25 100644
--- a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx
+++ b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState, useMemo } from 'react';
+import React, { useEffect, useRef, useState, useMemo, forwardRef, useImperativeHandle } from 'react';
 import { PaohvisDataPaginated, RowInformation, LinesHyperEdges } from './types';
 import { parseQueryResult } from './utils/dataProcessing';
 
@@ -15,6 +15,11 @@ import { Button } from '@graphpolaris/shared/lib/components/buttons';
 import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
 import { cloneDeep } from 'lodash-es';
 import { useImmer } from 'use-immer';
+import html2canvas from 'html2canvas';
+
+export interface PaohVisHandle {
+  exportImageInternal: () => void;
+}
 
 export type PaohVisProps = {
   rowHeight: number;
@@ -43,320 +48,322 @@ const settings: PaohVisProps = {
   mergeData: false,
 };
 
-export const PaohVis = ({ data, graphMetadata, schema, settings, updateSettings }: VisualizationPropTypes<PaohVisProps>) => {
-  // general
-  const [loading, setLoading] = useState(true);
-
-  // row states
-  const [informationRow, setInformationRow] = useState<RowInformation>([]); // rows that will be rendered, sorted and sliced by pagination
-  const [informationRowAllData, setInformationRowAllData] = useState<RowInformation>([]); // rows that will be rendered, sorted but not sliced used by pagination
-  const [informationRowOriginal, setInformationRowOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination
-
-  const [sortingOrderRow, setSortingOrderRow] = useState<'asc' | 'desc' | 'original'>('original');
-  const [originalPermutationIndicesRow, setOriginalPermutationIndicesRow] = useState<number[]>([]);
-  const [permutationIndicesRow, setPermutationIndicesRow] = useState<number[]>([]);
-  const [previousHeaderRow, setPreviousHeaderRow] = useState<string>('none');
-
-  // rows visible, even without rows selected
-  const [numRowsVisible, setNumRowsVisible] = useState<number>(0);
-  const [numColsVisible, setNumColsVisible] = useState<number>(0);
-
-  // columns states
-  const [informationColumn, setInformationColumn] = useState<RowInformation>([]);
-  const [informationColumnAllData, setInformationColumnAllData] = useState<RowInformation>([]);
-  const [informationColumnOriginal, setInformationColumnOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination
-  const [sortingOrderColumn, setSortingOrderColumn] = useState<'asc' | 'desc' | 'original'>('original');
-
-  const [originalPermutationIndicesColumn, setOriginalPermutationIndicesColumn] = useState<number[]>([]);
-  const [permutationIndicesColumn, setPermutationIndicesColumn] = useState<number[]>([]);
-  const [previousHeaderColumn, setPreviousHeaderColumn] = useState<string>('none');
-
-  // lines hyperEdge
-  const [lineHyperEdges, setLineHyperEdges] = useState<LinesHyperEdges[]>([]);
-
-  //
-  const [indicesRowsForColumnSort, setIndicesRowsForColumnSort] = useState<number[]>([]);
-  const [indicesColumnForRowSort, setIndicesColumnForRowSort] = useState<number[]>([]);
-
-  // render states
-  const svgRef = useRef<SVGSVGElement>(null);
-
-  // states track order headers attributes
-  const prevDisplayAttributesColumns = useRef<string[]>();
-
-  // information hyperedgesBlock
-  // dataModel renders bounded by pagination
-  const [dataModel, setDataModel] = useImmer<PaohvisDataPaginated>({
-    pageData: {
-      rowLabels: [],
-      hyperEdgeRanges: [],
-      rowDegrees: {},
-      nodes: [],
-      edges: [],
-    },
-    data: {
-      rowLabels: [],
-      hyperEdgeRanges: [],
-      rowDegrees: {},
-      nodes: [],
-      edges: [],
-    },
-    originalData: {
-      rowLabels: [],
-      hyperEdgeRanges: [],
-      rowDegrees: {},
-      nodes: [],
-      edges: [],
-    },
-  });
-
-  const [widthTotalRowInformation, setWidthTotalRowInformation] = useState<number>(0);
-  const [widthTotalColumnInformation, setWidthTotalColumnInformation] = useState<number>(0);
-
-  const classTopTextColumns = 'font-mono text-secondary-800 mx-1 overflow-hidden whitespace-nowrap text-ellipsis';
-
-  const configStyle: { [key: string]: string } = {
-    colorText: 'hsl(var(--clr-sec--800))',
-    colorTextUnselect: 'hsl(var(--clr-sec--400))',
-    colorLinesHyperEdge: 'hsl(var(--clr-black))',
-    colorLinesGrid: 'hsl(var(--clr-sec--300))',
-    colorLinesGridByClass: 'fill-secondary-300',
-  };
+const PaohVis = forwardRef<PaohVisHandle, VisualizationPropTypes<PaohVisProps>>(
+  ({ data, graphMetadata, schema, settings, updateSettings }, refExternal) => {
+    // general
+    const [loading, setLoading] = useState(true);
 
-  let configPaohvis = useMemo(
-    () => ({
-      rowHeight: 30,
-      hyperEdgeRanges: 30,
-      rowsMaxPerPage: settings.numRowsDisplay,
-      columnsMaxPerPage: settings.numColumnsDisplay,
-      maxSizeTextColumns: 120,
-      maxSizeTextRows: 120,
-      maxSizeTextID: 70,
-      sizeIcons: 16,
-    }),
-    [settings],
-  );
+    // row states
+    const [informationRow, setInformationRow] = useState<RowInformation>([]); // rows that will be rendered, sorted and sliced by pagination
+    const [informationRowAllData, setInformationRowAllData] = useState<RowInformation>([]); // rows that will be rendered, sorted but not sliced used by pagination
+    const [informationRowOriginal, setInformationRowOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination
 
-  //
-  // Methods
-  //
-
-  const onMouseEnterRowLabels = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
-    const targetClassList = (event.currentTarget as SVGGElement).classList;
-    // all elements - unselect
-    selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect);
-
-    selectAll('.' + targetClassList[1])
-      .selectAll('span')
-      .style('color', configStyle.colorText);
-
-    // all hyperedges - unselect
-    const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
-    hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3');
-    hyperEdgeBlock.selectAll('line').attr('opacity', '.3');
-    selectAll('.text-columns').selectAll('span').style('color', configStyle.colorTextUnselect);
-
-    // get row selected
-    const rowSelection: number = parseInt(targetClassList[1].substring('row-'.length), 10);
-
-    // get circles on the same row
-    selectAll('.circle-' + (rowSelection + (currentPageRows?.startIndexRow ?? 0))).each(function (d, i) {
-      // get all hyperedges which are connected those circles
-      const hyperEdge = (select(this).node() as Element)?.parentNode?.parentNode;
-      if (hyperEdge instanceof Element) {
-        const classList = Array.from(hyperEdge.classList);
-        // text columns
-        selectAll('.col-' + classList[1].substring('hyperEdge-col-'.length))
-          .selectAll('span')
-          .style('color', configStyle.colorText);
-
-        // hypererdge
-        select('.' + classList[1])
-          .selectAll('circle')
-          .attr('fill', 'hsl(var(--clr-acc))')
-          .attr('stroke-opacity', '1');
-
-        select('.' + classList[1])
-          .selectAll('line')
-          .attr('opacity', '1');
-
-        // text rows
-        selectAll('.' + classList[1])
-          .select('.hyperEdgeBlockCircles')
-          .selectAll('circle')
-          .each(function () {
-            const circleInside: number = parseInt(select(this).attr('class').substring('circle-'.length), 10);
-            selectAll('.row-' + (circleInside - (currentPageRows?.startIndexRow ?? 0)))
-              .selectAll('span')
-              .style('color', configStyle.colorText);
-          });
-      }
-    });
-  };
+    const [sortingOrderRow, setSortingOrderRow] = useState<'asc' | 'desc' | 'original'>('original');
+    const [originalPermutationIndicesRow, setOriginalPermutationIndicesRow] = useState<number[]>([]);
+    const [permutationIndicesRow, setPermutationIndicesRow] = useState<number[]>([]);
+    const [previousHeaderRow, setPreviousHeaderRow] = useState<string>('none');
 
-  const onMouseLeaveRowLabels = () => {
-    selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText);
-    const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
-    hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1');
-    hyperEdgeBlock.selectAll('circle').attr('fill', 'white');
-    hyperEdgeBlock.selectAll('line').attr('opacity', '1');
-    selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText);
-  };
+    // rows visible, even without rows selected
+    const [numRowsVisible, setNumRowsVisible] = useState<number>(0);
+    const [numColsVisible, setNumColsVisible] = useState<number>(0);
 
-  const onMouseEnterHyperEdge = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
-    const targetClassList = (event.currentTarget as SVGGElement).classList;
-    // all elements
-    const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
-    // all elements: hyperedges
-    hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3');
-    hyperEdgeBlock.selectAll('line').attr('opacity', '.3');
-    // all elements: column text and row text
-    selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorTextUnselect);
-    selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect);
-
-    // selected elements
-    const hyperEdgeSelected = select('.' + targetClassList[1]);
-    hyperEdgeSelected.selectAll('circle').attr('fill', 'hsl(var(--clr-acc))');
-    hyperEdgeSelected.selectAll('circle').attr('stroke-opacity', '1');
-    hyperEdgeSelected.selectAll('line').attr('opacity', '1');
-
-    // selected elements col text
-    const columnSelection = targetClassList[1].substring('hyperEdge-'.length);
-
-    selectAll('.' + columnSelection)
-      .selectAll('span')
-      .style('color', configStyle.colorText);
-
-    // selected elements nodes text
-    hyperEdgeSelected.selectAll('circle').each(function (d, i) {
-      const className = select(this).attr('class');
-
-      const index = className.split('circle-')[1];
-      if (currentPageRows) {
-        const indexNumber = parseInt(index);
-        const rowSelector = `.row-${indexNumber - currentPageRows.startIndexRow}`;
-        select(rowSelector).selectAll('span').style('color', configStyle.colorText);
-      }
+    // columns states
+    const [informationColumn, setInformationColumn] = useState<RowInformation>([]);
+    const [informationColumnAllData, setInformationColumnAllData] = useState<RowInformation>([]);
+    const [informationColumnOriginal, setInformationColumnOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination
+    const [sortingOrderColumn, setSortingOrderColumn] = useState<'asc' | 'desc' | 'original'>('original');
+
+    const [originalPermutationIndicesColumn, setOriginalPermutationIndicesColumn] = useState<number[]>([]);
+    const [permutationIndicesColumn, setPermutationIndicesColumn] = useState<number[]>([]);
+    const [previousHeaderColumn, setPreviousHeaderColumn] = useState<string>('none');
+
+    // lines hyperEdge
+    const [lineHyperEdges, setLineHyperEdges] = useState<LinesHyperEdges[]>([]);
+
+    //
+    const [indicesRowsForColumnSort, setIndicesRowsForColumnSort] = useState<number[]>([]);
+    const [indicesColumnForRowSort, setIndicesColumnForRowSort] = useState<number[]>([]);
+
+    // render states
+    const svgRef = useRef<SVGSVGElement>(null);
+    const divRef = useRef<HTMLDivElement>(null);
+
+    // states track order headers attributes
+    const prevDisplayAttributesColumns = useRef<string[]>();
+
+    // information hyperedgesBlock
+    // dataModel renders bounded by pagination
+    const [dataModel, setDataModel] = useImmer<PaohvisDataPaginated>({
+      pageData: {
+        rowLabels: [],
+        hyperEdgeRanges: [],
+        rowDegrees: {},
+        nodes: [],
+        edges: [],
+      },
+      data: {
+        rowLabels: [],
+        hyperEdgeRanges: [],
+        rowDegrees: {},
+        nodes: [],
+        edges: [],
+      },
+      originalData: {
+        rowLabels: [],
+        hyperEdgeRanges: [],
+        rowDegrees: {},
+        nodes: [],
+        edges: [],
+      },
     });
-  };
 
-  const onMouseLeaveHyperEdge = () => {
-    // all elements
-    selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText);
-    selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText);
+    const [widthTotalRowInformation, setWidthTotalRowInformation] = useState<number>(0);
+    const [widthTotalColumnInformation, setWidthTotalColumnInformation] = useState<number>(0);
 
-    const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
-    hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1');
-    hyperEdgeBlock.selectAll('circle').attr('fill', 'white');
-    hyperEdgeBlock.selectAll('line').attr('opacity', '1');
-  };
+    const classTopTextColumns = 'font-mono text-secondary-800 mx-1 overflow-hidden whitespace-nowrap text-ellipsis';
 
-  const handleClickHeaderSorting = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
-    // get target header
-    let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersRows-', '');
-    targeHeader = targeHeader == '#' ? '# Connections' : targeHeader;
+    const configStyle: { [key: string]: string } = {
+      colorText: 'hsl(var(--clr-sec--800))',
+      colorTextUnselect: 'hsl(var(--clr-sec--400))',
+      colorLinesHyperEdge: 'hsl(var(--clr-black))',
+      colorLinesGrid: 'hsl(var(--clr-sec--300))',
+      colorLinesGridByClass: 'fill-secondary-300',
+    };
 
-    // set sorting orders. Tracks header change, new header changes to asc
-    let newSortingOrder: 'asc' | 'desc' | 'original';
+    let configPaohvis = useMemo(
+      () => ({
+        rowHeight: 30,
+        hyperEdgeRanges: 30,
+        rowsMaxPerPage: settings.numRowsDisplay,
+        columnsMaxPerPage: settings.numColumnsDisplay,
+        maxSizeTextColumns: 120,
+        maxSizeTextRows: 120,
+        maxSizeTextID: 70,
+        sizeIcons: 16,
+      }),
+      [settings],
+    );
 
-    if (targeHeader !== previousHeaderRow) {
-      newSortingOrder = 'desc';
-    } else {
-      switch (sortingOrderRow) {
+    //
+    // Methods
+    //
+
+    const onMouseEnterRowLabels = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
+      const targetClassList = (event.currentTarget as SVGGElement).classList;
+      // all elements - unselect
+      selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect);
+
+      selectAll('.' + targetClassList[1])
+        .selectAll('span')
+        .style('color', configStyle.colorText);
+
+      // all hyperedges - unselect
+      const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
+      hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3');
+      hyperEdgeBlock.selectAll('line').attr('opacity', '.3');
+      selectAll('.text-columns').selectAll('span').style('color', configStyle.colorTextUnselect);
+
+      // get row selected
+      const rowSelection: number = parseInt(targetClassList[1].substring('row-'.length), 10);
+
+      // get circles on the same row
+      selectAll('.circle-' + (rowSelection + (currentPageRows?.startIndexRow ?? 0))).each(function (d, i) {
+        // get all hyperedges which are connected those circles
+        const hyperEdge = (select(this).node() as Element)?.parentNode?.parentNode;
+        if (hyperEdge instanceof Element) {
+          const classList = Array.from(hyperEdge.classList);
+          // text columns
+          selectAll('.col-' + classList[1].substring('hyperEdge-col-'.length))
+            .selectAll('span')
+            .style('color', configStyle.colorText);
+
+          // hypererdge
+          select('.' + classList[1])
+            .selectAll('circle')
+            .attr('fill', 'hsl(var(--clr-acc))')
+            .attr('stroke-opacity', '1');
+
+          select('.' + classList[1])
+            .selectAll('line')
+            .attr('opacity', '1');
+
+          // text rows
+          selectAll('.' + classList[1])
+            .select('.hyperEdgeBlockCircles')
+            .selectAll('circle')
+            .each(function () {
+              const circleInside: number = parseInt(select(this).attr('class').substring('circle-'.length), 10);
+              selectAll('.row-' + (circleInside - (currentPageRows?.startIndexRow ?? 0)))
+                .selectAll('span')
+                .style('color', configStyle.colorText);
+            });
+        }
+      });
+    };
+
+    const onMouseLeaveRowLabels = () => {
+      selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText);
+      const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
+      hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1');
+      hyperEdgeBlock.selectAll('circle').attr('fill', 'white');
+      hyperEdgeBlock.selectAll('line').attr('opacity', '1');
+      selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText);
+    };
+
+    const onMouseEnterHyperEdge = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
+      const targetClassList = (event.currentTarget as SVGGElement).classList;
+      // all elements
+      const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
+      // all elements: hyperedges
+      hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3');
+      hyperEdgeBlock.selectAll('line').attr('opacity', '.3');
+      // all elements: column text and row text
+      selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorTextUnselect);
+      selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect);
+
+      // selected elements
+      const hyperEdgeSelected = select('.' + targetClassList[1]);
+      hyperEdgeSelected.selectAll('circle').attr('fill', 'hsl(var(--clr-acc))');
+      hyperEdgeSelected.selectAll('circle').attr('stroke-opacity', '1');
+      hyperEdgeSelected.selectAll('line').attr('opacity', '1');
+
+      // selected elements col text
+      const columnSelection = targetClassList[1].substring('hyperEdge-'.length);
+
+      selectAll('.' + columnSelection)
+        .selectAll('span')
+        .style('color', configStyle.colorText);
+
+      // selected elements nodes text
+      hyperEdgeSelected.selectAll('circle').each(function (d, i) {
+        const className = select(this).attr('class');
+
+        const index = className.split('circle-')[1];
+        if (currentPageRows) {
+          const indexNumber = parseInt(index);
+          const rowSelector = `.row-${indexNumber - currentPageRows.startIndexRow}`;
+          select(rowSelector).selectAll('span').style('color', configStyle.colorText);
+        }
+      });
+    };
+
+    const onMouseLeaveHyperEdge = () => {
+      // all elements
+      selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText);
+      selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText);
+
+      const hyperEdgeBlock = selectAll('.hyperEdgeBlock');
+      hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1');
+      hyperEdgeBlock.selectAll('circle').attr('fill', 'white');
+      hyperEdgeBlock.selectAll('line').attr('opacity', '1');
+    };
+
+    const handleClickHeaderSorting = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
+      // get target header
+      let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersRows-', '');
+      targeHeader = targeHeader == '#' ? '# Connections' : targeHeader;
+
+      // set sorting orders. Tracks header change, new header changes to asc
+      let newSortingOrder: 'asc' | 'desc' | 'original';
+
+      if (targeHeader !== previousHeaderRow) {
+        newSortingOrder = 'desc';
+      } else {
+        switch (sortingOrderRow) {
+          case 'asc':
+            newSortingOrder = 'original';
+            break;
+          case 'desc':
+            newSortingOrder = 'asc';
+            break;
+          case 'original':
+            newSortingOrder = 'desc';
+            break;
+        }
+      }
+      if (newSortingOrder == 'original') {
+        // reset previous state
+        setPreviousHeaderRow('none');
+      } else {
+        setPreviousHeaderRow(targeHeader);
+      }
+
+      setSortingOrderRow(newSortingOrder);
+
+      // get permutations indices
+      let sortedIndices: number[];
+      switch (newSortingOrder) {
         case 'asc':
-          newSortingOrder = 'original';
+          sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'asc');
           break;
         case 'desc':
-          newSortingOrder = 'asc';
+          sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'desc');
           break;
         case 'original':
-          newSortingOrder = 'desc';
+          sortedIndices = originalPermutationIndicesRow;
           break;
-      }
-    }
-    if (newSortingOrder == 'original') {
-      // reset previous state
-      setPreviousHeaderRow('none');
-    } else {
-      setPreviousHeaderRow(targeHeader);
-    }
-
-    setSortingOrderRow(newSortingOrder);
-
-    // get permutations indices
-    let sortedIndices: number[];
-    switch (newSortingOrder) {
-      case 'asc':
-        sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'asc');
-        break;
-      case 'desc':
-        sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'desc');
-        break;
-      case 'original':
-        sortedIndices = originalPermutationIndicesRow;
-        break;
-
-      default:
-        sortedIndices = [];
-        break;
-    }
-
-    setIndicesRowsForColumnSort(sortedIndices);
-    // sort according permutations
-    const sortedRowInformation = sortRowInformation(informationRowOriginal, sortedIndices);
-    const sortedRowInformationSliced = sortedRowInformation.map((row) => ({
-      ...row,
-      data: row.data.slice(currentPageRows?.startIndexRow, currentPageRows?.endIndexRow),
-    }));
-
-    // update rows
-    const sortedRowInformationSlicedFiltered = sortedRowInformationSliced.filter((row) => settings.attributeRowShow.includes(row.header));
-
-    setInformationRow(sortedRowInformationSlicedFiltered);
-    setInformationRowAllData(sortedRowInformation);
 
-    // hyperEdge - sort according permutations indices
-    const dataModelOriginalTemporal = cloneDeep(dataModel.originalData);
-
-    for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) {
-      for (
-        let indexRowsIndices = 0;
-        indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length;
-        indexRowsIndices++
-      ) {
-        dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = sortedIndices.indexOf(
-          dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices],
-        );
+        default:
+          sortedIndices = [];
+          break;
       }
 
-      // sort indices - correct render
-      dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[
-        indexColOrder
-      ].hyperEdges.indices.sort((a: number, b: number) => a - b);
-    }
+      setIndicesRowsForColumnSort(sortedIndices);
+      // sort according permutations
+      const sortedRowInformation = sortRowInformation(informationRowOriginal, sortedIndices);
+      const sortedRowInformationSliced = sortedRowInformation.map((row) => ({
+        ...row,
+        data: row.data.slice(currentPageRows?.startIndexRow, currentPageRows?.endIndexRow),
+      }));
+
+      // update rows
+      const sortedRowInformationSlicedFiltered = sortedRowInformationSliced.filter((row) => settings.attributeRowShow.includes(row.header));
+
+      setInformationRow(sortedRowInformationSlicedFiltered);
+      setInformationRowAllData(sortedRowInformation);
+
+      // hyperEdge - sort according permutations indices
+      const dataModelOriginalTemporal = cloneDeep(dataModel.originalData);
+
+      for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) {
+        for (
+          let indexRowsIndices = 0;
+          indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length;
+          indexRowsIndices++
+        ) {
+          dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = sortedIndices.indexOf(
+            dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices],
+          );
+        }
 
-    const dataModelOriginalTemporalSorted = indicesColumnForRowSort
-      .map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index])
-      .filter((d) => !!d);
-    const sortedArrayDataModelFiltered = dataModelOriginalTemporalSorted.slice(
-      currentPageColumns?.startIndexColumn,
-      currentPageColumns?.endIndexColumn,
-    );
+        // sort indices - correct render
+        dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[
+          indexColOrder
+        ].hyperEdges.indices.sort((a: number, b: number) => a - b);
+      }
 
-    setDataModel((draft) => {
-      draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered;
-      draft.data.hyperEdgeRanges = dataModelOriginalTemporalSorted;
-    });
+      const dataModelOriginalTemporalSorted = indicesColumnForRowSort
+        .map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index])
+        .filter((d) => !!d);
+      const sortedArrayDataModelFiltered = dataModelOriginalTemporalSorted.slice(
+        currentPageColumns?.startIndexColumn,
+        currentPageColumns?.endIndexColumn,
+      );
 
-    // Update lines
-    if (currentPageRows) {
-      const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => {
-        return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices);
+      setDataModel((draft) => {
+        draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered;
+        draft.data.hyperEdgeRanges = dataModelOriginalTemporalSorted;
       });
-      setLineHyperEdges(newLinePositions);
-    }
 
-    /*
+      // Update lines
+      if (currentPageRows) {
+        const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => {
+          return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices);
+        });
+        setLineHyperEdges(newLinePositions);
+      }
+
+      /*
     setDataModel({
       rowLabels: dataModelOriginalTemporal.rowLabels.slice(currentPageRows?.startIndexRow, currentPageRows?.endIndexRow),
       hyperEdgeRanges: dataModelOriginalTemporalSorted.hyperEdgeRanges.slice(
@@ -370,533 +377,569 @@ export const PaohVis = ({ data, graphMetadata, schema, settings, updateSettings
       hyperEdgeRanges: dataModelOriginalTemporal.hyperEdgeRanges,
     });
     */
-  };
+    };
 
-  const handleClickHeaderSortingColumns = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
-    // get target header
-    let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersCols-', '');
+    const handleClickHeaderSortingColumns = (event: React.MouseEvent<SVGGElement, MouseEvent>) => {
+      // get target header
+      let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersCols-', '');
+
+      targeHeader = targeHeader == '#' ? '# Connections' : targeHeader;
+      // set sorting orders. Tracks header change, new header changes to asc
+      let newSortingOrder: 'asc' | 'desc' | 'original';
+
+      if (targeHeader !== previousHeaderColumn) {
+        newSortingOrder = 'desc';
+      } else {
+        switch (sortingOrderColumn) {
+          case 'asc':
+            newSortingOrder = 'original';
+            break;
+          case 'desc':
+            newSortingOrder = 'asc';
+            break;
+          case 'original':
+            newSortingOrder = 'desc';
+            break;
+        }
+      }
+      if (newSortingOrder == 'original') {
+        // reset previos state
+        setPreviousHeaderColumn('none');
+      } else {
+        setPreviousHeaderColumn(targeHeader);
+      }
 
-    targeHeader = targeHeader == '#' ? '# Connections' : targeHeader;
-    // set sorting orders. Tracks header change, new header changes to asc
-    let newSortingOrder: 'asc' | 'desc' | 'original';
+      setSortingOrderColumn(newSortingOrder);
 
-    if (targeHeader !== previousHeaderColumn) {
-      newSortingOrder = 'desc';
-    } else {
-      switch (sortingOrderColumn) {
+      // get permutations indices
+      let sortedIndices: number[];
+      switch (newSortingOrder) {
         case 'asc':
-          newSortingOrder = 'original';
+          sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'asc');
           break;
         case 'desc':
-          newSortingOrder = 'asc';
+          sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'desc');
           break;
         case 'original':
-          newSortingOrder = 'desc';
+          sortedIndices = originalPermutationIndicesColumn;
+          break;
+
+        default:
+          sortedIndices = [];
           break;
       }
-    }
-    if (newSortingOrder == 'original') {
-      // reset previos state
-      setPreviousHeaderColumn('none');
-    } else {
-      setPreviousHeaderColumn(targeHeader);
-    }
 
-    setSortingOrderColumn(newSortingOrder);
-
-    // get permutations indices
-    let sortedIndices: number[];
-    switch (newSortingOrder) {
-      case 'asc':
-        sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'asc');
-        break;
-      case 'desc':
-        sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'desc');
-        break;
-      case 'original':
-        sortedIndices = originalPermutationIndicesColumn;
-        break;
-
-      default:
-        sortedIndices = [];
-        break;
-    }
+      // sort according permutations
+      const sortedColumnInformation = sortRowInformation(informationColumnOriginal, sortedIndices);
 
-    // sort according permutations
-    const sortedColumnInformation = sortRowInformation(informationColumnOriginal, sortedIndices);
+      // slice according pagination
+      const sortedColumnInformationSliced = sortedColumnInformation.map((row) => ({
+        ...row,
+        data: row.data.slice(currentPageColumns?.startIndexColumn, currentPageColumns?.endIndexColumn),
+      }));
 
-    // slice according pagination
-    const sortedColumnInformationSliced = sortedColumnInformation.map((row) => ({
-      ...row,
-      data: row.data.slice(currentPageColumns?.startIndexColumn, currentPageColumns?.endIndexColumn),
-    }));
+      // update rows
+      const sortedColumnInformationSlicedFiltered = sortedColumnInformationSliced.filter((row) =>
+        settings.attributeColumnShow.includes(row.header),
+      );
 
-    // update rows
-    const sortedColumnInformationSlicedFiltered = sortedColumnInformationSliced.filter((row) =>
-      settings.attributeColumnShow.includes(row.header),
-    );
+      setInformationColumn(sortedColumnInformationSlicedFiltered);
+      setInformationColumnAllData(sortedColumnInformation);
+      // hyperEdge - sort according permutations indices
+      const dataModelOriginalTemporal = cloneDeep(dataModel.originalData);
+      for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) {
+        for (
+          let indexRowsIndices = 0;
+          indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length;
+          indexRowsIndices++
+        ) {
+          dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = indicesRowsForColumnSort.indexOf(
+            dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices],
+          );
+        }
 
-    setInformationColumn(sortedColumnInformationSlicedFiltered);
-    setInformationColumnAllData(sortedColumnInformation);
-    // hyperEdge - sort according permutations indices
-    const dataModelOriginalTemporal = cloneDeep(dataModel.originalData);
-    for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) {
-      for (
-        let indexRowsIndices = 0;
-        indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length;
-        indexRowsIndices++
-      ) {
-        dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = indicesRowsForColumnSort.indexOf(
-          dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices],
-        );
+        // sort indices - correct render
+        dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[
+          indexColOrder
+        ].hyperEdges.indices.sort((a: number, b: number) => a - b);
       }
 
-      // sort indices - correct render
-      dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[
-        indexColOrder
-      ].hyperEdges.indices.sort((a: number, b: number) => a - b);
-    }
-
-    setIndicesColumnForRowSort(sortedIndices);
-    const sortedArrayDataModel = sortedIndices.map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index]).filter((d) => !!d);
-
-    const sortedArrayDataModelFiltered = sortedArrayDataModel.slice(
-      currentPageColumns?.startIndexColumn,
-      currentPageColumns?.endIndexColumn,
-    );
+      setIndicesColumnForRowSort(sortedIndices);
+      const sortedArrayDataModel = sortedIndices.map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index]).filter((d) => !!d);
 
-    setDataModel((draft) => {
-      draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered;
-      draft.data.hyperEdgeRanges = sortedArrayDataModel;
-    });
+      const sortedArrayDataModelFiltered = sortedArrayDataModel.slice(
+        currentPageColumns?.startIndexColumn,
+        currentPageColumns?.endIndexColumn,
+      );
 
-    // Update lines hyperedges
-    if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) {
-      const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => {
-        return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices);
+      setDataModel((draft) => {
+        draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered;
+        draft.data.hyperEdgeRanges = sortedArrayDataModel;
       });
-      setLineHyperEdges(newLinePositions);
-    }
-  };
-
-  useEffect(() => {
-    if (!svgRef.current) return;
-    const resizeObserver = new ResizeObserver(() => {});
-    resizeObserver.observe(svgRef.current);
-    return () => resizeObserver.disconnect(); // clean up
-  }, []);
 
-  useEffect(() => {
-    if (
-      graphMetadata &&
-      settings.columnNode !== '' &&
-      settings.rowNode !== '' &&
-      graphMetadata.nodes.types[settings.rowNode] &&
-      graphMetadata.nodes.types[settings.columnNode]
-    ) {
-      const firstColumnLabels = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes).slice(0, 2);
-      const firstRowLabels = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes).slice(0, 2);
+      // Update lines hyperedges
+      if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) {
+        const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => {
+          return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices);
+        });
+        setLineHyperEdges(newLinePositions);
+      }
+    };
 
-      if (firstColumnLabels && firstRowLabels) {
-        if (settings.attributeColumnShow.includes('_id')) {
-          updateSettings({ attributeColumnShow: [...firstColumnLabels, '# Connections'] });
-        }
-        if (settings.attributeRowShow.includes('_id')) {
-          updateSettings({ attributeRowShow: [...firstRowLabels, '# Connections'] });
+    useEffect(() => {
+      if (!svgRef.current) return;
+      const resizeObserver = new ResizeObserver(() => {});
+      resizeObserver.observe(svgRef.current);
+      return () => resizeObserver.disconnect(); // clean up
+    }, []);
+
+    useEffect(() => {
+      if (
+        graphMetadata &&
+        settings.columnNode !== '' &&
+        settings.rowNode !== '' &&
+        graphMetadata.nodes.types[settings.rowNode] &&
+        graphMetadata.nodes.types[settings.columnNode]
+      ) {
+        const firstColumnLabels = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes).slice(0, 2);
+        const firstRowLabels = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes).slice(0, 2);
+
+        if (firstColumnLabels && firstRowLabels) {
+          if (settings.attributeColumnShow.includes('_id')) {
+            updateSettings({ attributeColumnShow: [...firstColumnLabels, '# Connections'] });
+          }
+          if (settings.attributeRowShow.includes('_id')) {
+            updateSettings({ attributeRowShow: [...firstRowLabels, '# Connections'] });
+          }
+          setTimeout(() => setLoading(false), 100); // wait for the settings to update first
         }
-        setTimeout(() => setLoading(false), 100); // wait for the settings to update first
       }
-    }
-  }, [graphMetadata, settings]);
+    }, [graphMetadata, settings]);
+
+    useEffect(() => {
+      if (loading || settings.rowNode === '' || settings.columnNode === '') return;
+      // set new data
+      // for dev env
+
+      let labelEdge = '';
+      let edgeSchema;
+
+      let toNode = '';
+
+      let columnNodeAttributes;
+      let rowNodeAttributes;
+
+      if (graphMetadata != undefined) {
+        labelEdge = graphMetadata.edges.labels[0];
+        edgeSchema = schema.edges.find((obj) => String(obj.key).includes(labelEdge));
+        toNode = edgeSchema?.target as string;
+        columnNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes);
+        rowNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes);
+      } else {
+        if ('to' in schema.edges[0]) toNode = schema.edges[0].to as string;
+        columnNodeAttributes = schema.nodes
+          .find((node: any) => {
+            return node.name === settings.columnNode;
+          })
+          ?.attributes?.map((attributesStructure: any) => attributesStructure.name);
+
+        rowNodeAttributes = schema.nodes
+          .find((node: any) => {
+            return node.name === settings.rowNode;
+          })
+          ?.attributes?.map((attributesStructure: any) => attributesStructure.name);
+      }
+      const newData = parseQueryResult(data, settings as PaohVisProps, toNode, settings.mergeData);
+      // original data without slicing
+      setNumRowsVisible(Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length));
+      setNumColsVisible(Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length));
+
+      const rowNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.rowNode));
+      const columnNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.columnNode));
+      // to keep order of new attributes
+      prevDisplayAttributesColumns.current = [...columnNodeAttributes];
+
+      setDataModel({
+        pageData: {
+          rowLabels: newData.rowLabels.slice(0, configPaohvis.rowsMaxPerPage),
+          hyperEdgeRanges: newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage),
+          rowDegrees: newData.rowDegrees,
+          nodes: newData.nodes,
+          edges: newData.edges,
+        },
+        data: newData,
+        originalData: newData,
+      });
 
-  useEffect(() => {
-    if (loading || settings.rowNode === '' || settings.columnNode === '') return;
-    // set new data
-    // for dev env
+      // Update lines hyperedges
+      const newLinePositions = newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage).map((hyperEdgeRange) => {
+        return intersectionElements(
+          [0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)],
+          hyperEdgeRange.hyperEdges.indices,
+        );
+      });
+      setLineHyperEdges(newLinePositions);
 
-    let labelEdge = '';
-    let edgeSchema;
+      const originalIndicesColumns = Array.from({ length: newData.hyperEdgeRanges.length + 1 }, (_, index) => index);
 
-    let toNode = '';
+      setIndicesColumnForRowSort(originalIndicesColumns);
+      setCurrentPageColumns({
+        startIndexColumn: 0,
+        endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length),
+      });
 
-    let columnNodeAttributes;
-    let rowNodeAttributes;
+      const hyperEdgeRangesKeys = [...newData.hyperEdgeRanges.keys()];
+      setPermutationIndicesColumn(hyperEdgeRangesKeys);
+      setOriginalPermutationIndicesColumn(hyperEdgeRangesKeys);
+      setCurrentPageRows({
+        startIndexRow: 0,
+        endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length),
+      });
 
-    if (graphMetadata != undefined) {
-      labelEdge = graphMetadata.edges.labels[0];
-      edgeSchema = schema.edges.find((obj) => String(obj.key).includes(labelEdge));
-      toNode = edgeSchema?.target as string;
-      columnNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes);
-      rowNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes);
-    } else {
-      if ('to' in schema.edges[0]) toNode = schema.edges[0].to as string;
-      columnNodeAttributes = schema.nodes
-        .find((node: any) => {
-          return node.name === settings.columnNode;
-        })
-        ?.attributes?.map((attributesStructure: any) => attributesStructure.name);
-
-      rowNodeAttributes = schema.nodes
-        .find((node: any) => {
-          return node.name === settings.rowNode;
-        })
-        ?.attributes?.map((attributesStructure: any) => attributesStructure.name);
-    }
-    const newData = parseQueryResult(data, settings as PaohVisProps, toNode, settings.mergeData);
-    // original data without slicing
-    setNumRowsVisible(Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length));
-    setNumColsVisible(Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length));
+      // columnInformation
+      const informationColumnTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries(
+        graphMetadata.nodes.types[settings.columnNode].attributes,
+      ).map(([k, v]) => {
+        const mappedData = columnNodes.map((node) => node.attributes[k]);
+        return {
+          header: k,
+          data: mappedData,
+          width: configPaohvis.maxSizeTextRows,
+        };
+      });
 
-    const rowNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.rowNode));
-    const columnNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.columnNode));
-    // to keep order of new attributes
-    prevDisplayAttributesColumns.current = [...columnNodeAttributes];
+      const columnsIdDegree: { [_id: string]: number } = newData.hyperEdgeRanges.reduce((acc: { [_id: string]: number }, node) => {
+        acc[node._id] = node.degree;
+        return acc;
+      }, {});
 
-    setDataModel({
-      pageData: {
-        rowLabels: newData.rowLabels.slice(0, configPaohvis.rowsMaxPerPage),
-        hyperEdgeRanges: newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage),
-        rowDegrees: newData.rowDegrees,
-        nodes: newData.nodes,
-        edges: newData.edges,
-      },
-      data: newData,
-      originalData: newData,
-    });
+      informationColumnTemporalOriginal.push({
+        header: '# Connections',
+        data: Object.values(columnsIdDegree),
+        width: configPaohvis.maxSizeTextColumns,
+      });
 
-    // Update lines hyperedges
-    const newLinePositions = newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage).map((hyperEdgeRange) => {
-      return intersectionElements([0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)], hyperEdgeRange.hyperEdges.indices);
-    });
-    setLineHyperEdges(newLinePositions);
+      informationColumnTemporalOriginal.push({
+        header: '_id',
+        data: columnNodes.map((node) => node._id),
+        width: configPaohvis.maxSizeTextID,
+      });
 
-    const originalIndicesColumns = Array.from({ length: newData.hyperEdgeRanges.length + 1 }, (_, index) => index);
+      const informationColumnTemporal: { header: string; data: any[]; width: number }[] = informationColumnTemporalOriginal.map((d) => ({
+        ...d,
+        data: d.data.slice(0, Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length)),
+      }));
 
-    setIndicesColumnForRowSort(originalIndicesColumns);
-    setCurrentPageColumns({
-      startIndexColumn: 0,
-      endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length),
-    });
+      setPermutationIndicesRow(originalPermutationIndicesRow);
 
-    const hyperEdgeRangesKeys = [...newData.hyperEdgeRanges.keys()];
-    setPermutationIndicesColumn(hyperEdgeRangesKeys);
-    setOriginalPermutationIndicesColumn(hyperEdgeRangesKeys);
-    setCurrentPageRows({
-      startIndexRow: 0,
-      endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length),
-    });
+      const columnsAttrOrder = prevDisplayAttributesColumns.current;
 
-    // columnInformation
-    const informationColumnTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries(
-      graphMetadata.nodes.types[settings.columnNode].attributes,
-    ).map(([k, v]) => {
-      const mappedData = columnNodes.map((node) => node.attributes[k]);
-      return {
-        header: k,
-        data: mappedData,
-        width: configPaohvis.maxSizeTextRows,
-      };
-    });
+      if (columnsAttrOrder) {
+        // sort them according order of showing them
+        informationColumnTemporal.sort((a, b) => {
+          const indexA = columnsAttrOrder.indexOf(a.header);
+          const indexB = columnsAttrOrder.indexOf(b.header);
+          return indexA - indexB;
+        });
 
-    const columnsIdDegree: { [_id: string]: number } = newData.hyperEdgeRanges.reduce((acc: { [_id: string]: number }, node) => {
-      acc[node._id] = node.degree;
-      return acc;
-    }, {});
+        informationColumnTemporalOriginal.sort((a, b) => {
+          const indexA = columnsAttrOrder.indexOf(a.header);
+          const indexB = columnsAttrOrder.indexOf(b.header);
+          return indexA - indexB;
+        });
 
-    informationColumnTemporalOriginal.push({
-      header: '# Connections',
-      data: Object.values(columnsIdDegree),
-      width: configPaohvis.maxSizeTextColumns,
-    });
+        // select necessary variables to show
+        const filteredInformationColumnTemporal = informationColumnTemporal.filter((row) =>
+          settings.attributeColumnShow.includes(row.header),
+        );
 
-    informationColumnTemporalOriginal.push({
-      header: '_id',
-      data: columnNodes.map((node) => node._id),
-      width: configPaohvis.maxSizeTextID,
-    });
+        // set data
+        setInformationColumn(filteredInformationColumnTemporal);
+        setInformationColumnOriginal(informationColumnTemporalOriginal);
+        setInformationColumnAllData(informationColumnTemporalOriginal);
+        const totalWidthColumnInformation = filteredInformationColumnTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight;
+        setWidthTotalColumnInformation(totalWidthColumnInformation);
+      } else {
+        console.error(`Nodes for entity  are undefined or empty.`);
+      }
 
-    const informationColumnTemporal: { header: string; data: any[]; width: number }[] = informationColumnTemporalOriginal.map((d) => ({
-      ...d,
-      data: d.data.slice(0, Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length)),
-    }));
+      // rowInformation - entityVertical
+      // build
+
+      const informationRowTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries(
+        graphMetadata.nodes.types[settings.rowNode].attributes,
+      ).map(([k, v]) => {
+        const mappedData = rowNodes.map((node) => node.attributes[k]);
 
-    setPermutationIndicesRow(originalPermutationIndicesRow);
+        return {
+          header: k,
+          data: mappedData,
+          width: configPaohvis.maxSizeTextRows,
+        };
+      });
 
-    const columnsAttrOrder = prevDisplayAttributesColumns.current;
+      const idsRows = rowNodes.map((obj) => obj._id);
 
-    if (columnsAttrOrder) {
-      // sort them according order of showing them
-      informationColumnTemporal.sort((a, b) => {
-        const indexA = columnsAttrOrder.indexOf(a.header);
-        const indexB = columnsAttrOrder.indexOf(b.header);
-        return indexA - indexB;
+      informationRowTemporalOriginal.push({
+        header: '# Connections',
+        data: idsRows.map((id) => newData.rowDegrees[id]),
+        width: configPaohvis.maxSizeTextRows,
       });
 
-      informationColumnTemporalOriginal.sort((a, b) => {
-        const indexA = columnsAttrOrder.indexOf(a.header);
-        const indexB = columnsAttrOrder.indexOf(b.header);
-        return indexA - indexB;
+      informationRowTemporalOriginal.push({
+        header: '_id',
+        data: idsRows,
+        width: configPaohvis.maxSizeTextID, //configPaohvis.maxSizeTextRows,
       });
 
-      // select necessary variables to show
-      const filteredInformationColumnTemporal = informationColumnTemporal.filter((row) =>
-        settings.attributeColumnShow.includes(row.header),
-      );
+      const informationRowTemporal: { header: string; data: any[]; width: number }[] = informationRowTemporalOriginal.map((d) => ({
+        ...d,
+        data: d.data.slice(0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)),
+      }));
 
-      // set data
-      setInformationColumn(filteredInformationColumnTemporal);
-      setInformationColumnOriginal(informationColumnTemporalOriginal);
-      setInformationColumnAllData(informationColumnTemporalOriginal);
-      const totalWidthColumnInformation = filteredInformationColumnTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight;
-      setWidthTotalColumnInformation(totalWidthColumnInformation);
-    } else {
-      console.error(`Nodes for entity  are undefined or empty.`);
-    }
+      const objectWithIdHeader = informationRowTemporalOriginal.find((obj) => obj.header === '_id');
+      let originalIndices: number[] = [];
 
-    // rowInformation - entityVertical
-    // build
+      if (objectWithIdHeader) {
+        originalIndices = [...objectWithIdHeader.data.keys()];
+      } else {
+        console.error("Row Object with header 'id' not found");
+      }
+      setOriginalPermutationIndicesRow(originalIndices);
+      setIndicesRowsForColumnSort(originalIndices);
 
-    const informationRowTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries(
-      graphMetadata.nodes.types[settings.rowNode].attributes,
-    ).map(([k, v]) => {
-      const mappedData = rowNodes.map((node) => node.attributes[k]);
+      // select necessary variables to show
+      const filteredInformationRowTemporal = informationRowTemporal.filter((row) => settings.attributeRowShow.includes(row.header));
 
-      return {
-        header: k,
-        data: mappedData,
-        width: configPaohvis.maxSizeTextRows,
-      };
+      // set data
+      setInformationRow(filteredInformationRowTemporal);
+      setInformationRowOriginal(informationRowTemporalOriginal);
+      setInformationRowAllData(informationRowTemporalOriginal);
+      const totalWidthRowInformation = filteredInformationRowTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight;
+
+      setWidthTotalRowInformation(totalWidthRowInformation);
+    }, [
+      settings.rowNode,
+      settings.columnNode,
+      configPaohvis,
+      settings.attributeRowShow,
+      settings.attributeColumnShow,
+      settings.mergeData,
+      loading,
+    ]);
+
+    const [currentPageColumns, setCurrentPageColumns] = useState<{
+      startIndexColumn: number;
+      endIndexColumn: number;
+    } | null>({
+      startIndexColumn: 0,
+      endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length),
     });
 
-    const idsRows = rowNodes.map((obj) => obj._id);
-
-    informationRowTemporalOriginal.push({
-      header: '# Connections',
-      data: idsRows.map((id) => newData.rowDegrees[id]),
-      width: configPaohvis.maxSizeTextRows,
+    const [currentPageRows, setCurrentPageRows] = useState<{
+      startIndexRow: number;
+      endIndexRow: number;
+    } | null>({
+      startIndexRow: 0,
+      endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length),
     });
 
-    informationRowTemporalOriginal.push({
-      header: '_id',
-      data: idsRows,
-      width: configPaohvis.maxSizeTextID, //configPaohvis.maxSizeTextRows,
-    });
+    const computedSizesSvg = useMemo(() => {
+      let tableWidth = 0;
+      let tableWidthWithExtraColumnLabelWidth = 0;
 
-    const informationRowTemporal: { header: string; data: any[]; width: number }[] = informationRowTemporalOriginal.map((d) => ({
-      ...d,
-      data: d.data.slice(0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)),
-    }));
+      dataModel.pageData.hyperEdgeRanges.forEach((hyperEdgeRange) => {
+        const columnWidth = 1 * settings.rowHeight;
+        tableWidth += columnWidth;
 
-    const objectWithIdHeader = informationRowTemporalOriginal.find((obj) => obj.header === '_id');
-    let originalIndices: number[] = [];
+        if (tableWidth > tableWidthWithExtraColumnLabelWidth) tableWidthWithExtraColumnLabelWidth = tableWidth;
+      });
 
-    if (objectWithIdHeader) {
-      originalIndices = [...objectWithIdHeader.data.keys()];
-    } else {
-      console.error("Row Object with header 'id' not found");
-    }
-    setOriginalPermutationIndicesRow(originalIndices);
-    setIndicesRowsForColumnSort(originalIndices);
-
-    // select necessary variables to show
-    const filteredInformationRowTemporal = informationRowTemporal.filter((row) => settings.attributeRowShow.includes(row.header));
-
-    // set data
-    setInformationRow(filteredInformationRowTemporal);
-    setInformationRowOriginal(informationRowTemporalOriginal);
-    setInformationRowAllData(informationRowTemporalOriginal);
-    const totalWidthRowInformation = filteredInformationRowTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight;
-
-    setWidthTotalRowInformation(totalWidthRowInformation);
-  }, [
-    settings.rowNode,
-    settings.columnNode,
-    configPaohvis,
-    settings.attributeRowShow,
-    settings.attributeColumnShow,
-    settings.mergeData,
-    loading,
-  ]);
-
-  const [currentPageColumns, setCurrentPageColumns] = useState<{
-    startIndexColumn: number;
-    endIndexColumn: number;
-  } | null>({
-    startIndexColumn: 0,
-    endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length),
-  });
-
-  const [currentPageRows, setCurrentPageRows] = useState<{
-    startIndexRow: number;
-    endIndexRow: number;
-  } | null>({
-    startIndexRow: 0,
-    endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length),
-  });
-
-  const computedSizesSvg = useMemo(() => {
-    let tableWidth = 0;
-    let tableWidthWithExtraColumnLabelWidth = 0;
-
-    dataModel.pageData.hyperEdgeRanges.forEach((hyperEdgeRange) => {
-      const columnWidth = 1 * settings.rowHeight;
-      tableWidth += columnWidth;
-
-      if (tableWidth > tableWidthWithExtraColumnLabelWidth) tableWidthWithExtraColumnLabelWidth = tableWidth;
-    });
+      let finalTableWidth = tableWidthWithExtraColumnLabelWidth;
+      finalTableWidth += widthTotalRowInformation;
+      finalTableWidth += finalTableWidth * 0.01;
 
-    let finalTableWidth = tableWidthWithExtraColumnLabelWidth;
-    finalTableWidth += widthTotalRowInformation;
-    finalTableWidth += finalTableWidth * 0.01;
+      return {
+        tableWidthWithExtraColumnLabelWidth: finalTableWidth,
+        colWidth: widthTotalColumnInformation,
+      };
+    }, [
+      dataModel,
+      settings.rowHeight,
+      widthTotalColumnInformation,
+      widthTotalRowInformation,
+      settings.attributeColumnShow,
+      settings.attributeRowShow,
+    ]);
+
+    const onWheel = (event: React.WheelEvent<SVGSVGElement>) => {
+      if (event.deltaY !== 0) {
+        if (event.shiftKey) onPageChangeColumns(event.deltaY > 0 ? settings.colJumpAmount : -settings.colJumpAmount);
+        else onPageChangeRows(event.deltaY > 0 ? settings.rowJumpAmount : -settings.rowJumpAmount);
+      }
 
-    return {
-      tableWidthWithExtraColumnLabelWidth: finalTableWidth,
-      colWidth: widthTotalColumnInformation,
+      if (event.deltaX !== 0) {
+        onPageChangeColumns(event.deltaX > 0 ? settings.colJumpAmount : -settings.colJumpAmount);
+      }
     };
-  }, [
-    dataModel,
-    settings.rowHeight,
-    widthTotalColumnInformation,
-    widthTotalRowInformation,
-    settings.attributeColumnShow,
-    settings.attributeRowShow,
-  ]);
-
-  const onWheel = (event: React.WheelEvent<SVGSVGElement>) => {
-    if (event.deltaY !== 0) {
-      if (event.shiftKey) onPageChangeColumns(event.deltaY > 0 ? settings.colJumpAmount : -settings.colJumpAmount);
-      else onPageChangeRows(event.deltaY > 0 ? settings.rowJumpAmount : -settings.rowJumpAmount);
-    }
 
-    if (event.deltaX !== 0) {
-      onPageChangeColumns(event.deltaX > 0 ? settings.colJumpAmount : -settings.colJumpAmount);
-    }
-  };
+    const onPageChangeColumns = (delta: number) => {
+      const startIndexColumn = (currentPageColumns?.startIndexColumn || 0) + delta;
+      if (startIndexColumn < 0 || startIndexColumn + 10 > dataModel.data.hyperEdgeRanges.length) return;
+      const endIndexColumn = Math.min(startIndexColumn + configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length);
+      setNumColsVisible(endIndexColumn - startIndexColumn);
 
-  const onPageChangeColumns = (delta: number) => {
-    const startIndexColumn = (currentPageColumns?.startIndexColumn || 0) + delta;
-    if (startIndexColumn < 0 || startIndexColumn + 10 > dataModel.data.hyperEdgeRanges.length) return;
-    const endIndexColumn = Math.min(startIndexColumn + configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length);
-    setNumColsVisible(endIndexColumn - startIndexColumn);
+      const slicedHyperEdgeRanges = dataModel.data.hyperEdgeRanges.slice(startIndexColumn, endIndexColumn);
 
-    const slicedHyperEdgeRanges = dataModel.data.hyperEdgeRanges.slice(startIndexColumn, endIndexColumn);
+      setDataModel((draft) => {
+        draft.pageData.hyperEdgeRanges = slicedHyperEdgeRanges;
+      });
 
-    setDataModel((draft) => {
-      draft.pageData.hyperEdgeRanges = slicedHyperEdgeRanges;
-    });
+      // Update lines hyperedges
+      if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) {
+        const newLinePositions = slicedHyperEdgeRanges.map((hyperEdgeRange) => {
+          return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices);
+        });
+        setLineHyperEdges(newLinePositions);
+      }
 
-    // Update lines hyperedges
-    if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) {
-      const newLinePositions = slicedHyperEdgeRanges.map((hyperEdgeRange) => {
-        return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices);
-      });
-      setLineHyperEdges(newLinePositions);
-    }
+      // columns
 
-    // columns
+      const dataColumnsSorted = cloneDeep(informationColumnAllData);
 
-    const dataColumnsSorted = cloneDeep(informationColumnAllData);
+      const dataColumnsSortedSliced = dataColumnsSorted.map((table) => ({
+        ...table,
+        data: table.data.slice(startIndexColumn, endIndexColumn),
+      }));
 
-    const dataColumnsSortedSliced = dataColumnsSorted.map((table) => ({
-      ...table,
-      data: table.data.slice(startIndexColumn, endIndexColumn),
-    }));
+      const filteredInformationColumnsTemporal = dataColumnsSortedSliced.filter((row) => settings.attributeColumnShow.includes(row.header));
 
-    const filteredInformationColumnsTemporal = dataColumnsSortedSliced.filter((row) => settings.attributeColumnShow.includes(row.header));
+      setInformationColumn(filteredInformationColumnsTemporal);
 
-    setInformationColumn(filteredInformationColumnsTemporal);
+      setCurrentPageColumns({
+        startIndexColumn: startIndexColumn,
+        endIndexColumn: endIndexColumn,
+      });
+    };
 
-    setCurrentPageColumns({
-      startIndexColumn: startIndexColumn,
-      endIndexColumn: endIndexColumn,
-    });
-  };
+    const onPageChangeRows = (delta: number) => {
+      const startIndexRow = (currentPageRows?.startIndexRow || 0) + delta;
+      if (startIndexRow < 0 || startIndexRow + 10 > dataModel.data.rowLabels.length) return;
+      const endIndexRow = Math.min(startIndexRow + configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length);
 
-  const onPageChangeRows = (delta: number) => {
-    const startIndexRow = (currentPageRows?.startIndexRow || 0) + delta;
-    if (startIndexRow < 0 || startIndexRow + 10 > dataModel.data.rowLabels.length) return;
-    const endIndexRow = Math.min(startIndexRow + configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length);
+      setNumRowsVisible(endIndexRow - startIndexRow);
 
-    setNumRowsVisible(endIndexRow - startIndexRow);
+      // slice data rows
+      const dataRowsSorted = cloneDeep(informationRowAllData);
 
-    // slice data rows
-    const dataRowsSorted = cloneDeep(informationRowAllData);
+      const dataRowsSortedSliced = dataRowsSorted.map((table) => ({
+        ...table,
+        data: table.data.slice(startIndexRow, endIndexRow),
+      }));
 
-    const dataRowsSortedSliced = dataRowsSorted.map((table) => ({
-      ...table,
-      data: table.data.slice(startIndexRow, endIndexRow),
-    }));
+      const filteredInformationRowTemporal = dataRowsSortedSliced.filter((row) => settings.attributeRowShow.includes(row.header));
 
-    const filteredInformationRowTemporal = dataRowsSortedSliced.filter((row) => settings.attributeRowShow.includes(row.header));
+      setInformationRow(filteredInformationRowTemporal);
 
-    setInformationRow(filteredInformationRowTemporal);
+      // set pagination rows
+      setCurrentPageRows({
+        startIndexRow: startIndexRow,
+        endIndexRow: endIndexRow,
+      });
 
-    // set pagination rows
-    setCurrentPageRows({
-      startIndexRow: startIndexRow,
-      endIndexRow: endIndexRow,
-    });
+      // Update lines hyperedges
+      const newLinePositions = dataModel.pageData.hyperEdgeRanges.map((hyperEdgeRange) => {
+        return intersectionElements([startIndexRow, endIndexRow], hyperEdgeRange.hyperEdges.indices);
+      });
+      setLineHyperEdges(newLinePositions);
+    };
 
-    // Update lines hyperedges
-    const newLinePositions = dataModel.pageData.hyperEdgeRanges.map((hyperEdgeRange) => {
-      return intersectionElements([startIndexRow, endIndexRow], hyperEdgeRange.hyperEdges.indices);
-    });
-    setLineHyperEdges(newLinePositions);
-  };
+    const exportImageInternal = () => {
+      if (divRef.current) {
+        const options = {
+          backgroundColor: '#FFFFFF',
+          foreignObjectRendering: false,
+          removeContainer: false,
+        };
+        html2canvas(divRef.current, options).then((canvas) => {
+          const pngData = canvas.toDataURL('image/png');
+          const a = document.createElement('a');
+          a.href = pngData;
+          a.download = 'paohvis.png';
+          a.click();
+        });
+      } else {
+        console.error('The referenced div is null.');
+      }
+    };
 
-  return (
-    <svg
-      className="m-1 overflow-hidden"
-      ref={svgRef}
-      style={{
-        width: '100%',
-        height: '99%',
-      }}
-      onWheel={onWheel}
-    >
-      <defs>
-        <pattern id="diagonal-lines" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)">
-          <rect width="6" height="8" fill="transparent"></rect>
-          <rect x="6" width="2" height="8" fill="#eaeaea"></rect>
-        </pattern>
-      </defs>
-
-      <RowLabels
-        dataRows={informationRow}
-        rowHeight={settings.rowHeight}
-        yOffset={computedSizesSvg.colWidth}
-        rowLabelColumnWidth={widthTotalRowInformation}
-        classTopTextColumns={classTopTextColumns}
-        onMouseEnterRowLabels={onMouseEnterRowLabels}
-        onMouseLeaveRowLabels={onMouseLeaveRowLabels}
-        handleClickHeaderSorting={handleClickHeaderSorting}
-        sortState={sortingOrderRow}
-        headerState={previousHeaderRow}
-        configStyle={configStyle}
-        numColumns={numColsVisible}
-      />
-
-      <HyperEdgeRangesBlock
-        dataModel={dataModel}
-        dataLinesHyperEdges={lineHyperEdges}
-        rowHeight={settings.rowHeight}
-        yOffset={computedSizesSvg.colWidth}
-        rowLabelColumnWidth={widthTotalRowInformation}
-        classTopTextColumns={classTopTextColumns}
-        currentPageRows={currentPageRows}
-        widthColumns={widthTotalColumnInformation}
-        columnHeaderInformation={informationColumn}
-        configStyle={configStyle}
-        onMouseEnterHyperEdge={onMouseEnterHyperEdge}
-        onMouseLeaveHyperEdge={onMouseLeaveHyperEdge}
-        numRows={numRowsVisible}
-        sortState={sortingOrderColumn}
-        handleClickHeaderSorting={handleClickHeaderSortingColumns}
-        headerState={previousHeaderColumn}
-      />
-    </svg>
-  );
-};
+    useImperativeHandle(refExternal, () => ({
+      exportImageInternal,
+    }));
+
+    return (
+      <div
+        className="overflow-hidden"
+        ref={divRef}
+        style={{
+          width: '100%',
+          height: '99%',
+        }}
+      >
+        <svg
+          className="m-1 overflow-hidden"
+          ref={svgRef}
+          style={{
+            width: '100%',
+            height: '99%',
+          }}
+          onWheel={onWheel}
+        >
+          <defs>
+            <pattern id="diagonal-lines" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)">
+              <rect width="6" height="8" fill="transparent"></rect>
+              <rect x="6" width="2" height="8" fill="#eaeaea"></rect>
+            </pattern>
+          </defs>
+
+          <RowLabels
+            dataRows={informationRow}
+            rowHeight={settings.rowHeight}
+            yOffset={computedSizesSvg.colWidth}
+            rowLabelColumnWidth={widthTotalRowInformation}
+            classTopTextColumns={classTopTextColumns}
+            onMouseEnterRowLabels={onMouseEnterRowLabels}
+            onMouseLeaveRowLabels={onMouseLeaveRowLabels}
+            handleClickHeaderSorting={handleClickHeaderSorting}
+            sortState={sortingOrderRow}
+            headerState={previousHeaderRow}
+            configStyle={configStyle}
+            numColumns={numColsVisible}
+          />
+
+          <HyperEdgeRangesBlock
+            dataModel={dataModel}
+            dataLinesHyperEdges={lineHyperEdges}
+            rowHeight={settings.rowHeight}
+            yOffset={computedSizesSvg.colWidth}
+            rowLabelColumnWidth={widthTotalRowInformation}
+            classTopTextColumns={classTopTextColumns}
+            currentPageRows={currentPageRows}
+            widthColumns={widthTotalColumnInformation}
+            columnHeaderInformation={informationColumn}
+            configStyle={configStyle}
+            onMouseEnterHyperEdge={onMouseEnterHyperEdge}
+            onMouseLeaveHyperEdge={onMouseLeaveHyperEdge}
+            numRows={numRowsVisible}
+            sortState={sortingOrderColumn}
+            handleClickHeaderSorting={handleClickHeaderSortingColumns}
+            headerState={previousHeaderColumn}
+          />
+        </svg>
+      </div>
+    );
+  },
+);
 
 const PaohSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<PaohVisProps>) => {
   const [areCollapsedAttrRows, setAreCollapsedAttrRows] = useState<boolean>(true);
@@ -1103,14 +1146,20 @@ const PaohSettings = ({ settings, graphMetadata, updateSettings }: Visualization
   );
 };
 
+const paohvisRef = React.createRef<{ exportImageInternal: () => void }>();
+
 export const PaohVisComponent: VISComponentType<PaohVisProps> = {
   displayName: 'PaohVis',
   description: 'Paths and Connection',
-  component: PaohVis,
+  component: React.forwardRef((props: VisualizationPropTypes<PaohVisProps>, ref) => <PaohVis {...props} ref={paohvisRef} />),
   settingsComponent: PaohSettings,
   settings: settings,
   exportImage: () => {
-    alert('Not yet supported');
+    if (paohvisRef.current) {
+      paohvisRef.current.exportImageInternal();
+    } else {
+      console.error('Paohvis reference is not set.');
+    }
   },
 };
 
-- 
GitLab