From fbbf8d2ebe8346b199f642d146bbe6096ccb32e6 Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Fri, 8 Nov 2024 12:57:08 +0000
Subject: [PATCH] feat: vis0D persistent, stats and relationship

---
 .../lib/vis/components/VisualizationPanel.tsx |   2 +-
 .../lib/vis/visualizations/Vis0D/Vis0D.tsx    |  95 ------
 libs/shared/lib/vis/visualizations/index.tsx  |   2 +-
 .../lib/vis/visualizations/vis0D/Vis0D.tsx    | 284 ++++++++++++++++++
 .../visualizations/{Vis0D => vis0D}/index.ts  |   0
 .../{Vis0D => vis0D}/vis0D.stories.tsx        |   0
 6 files changed, 286 insertions(+), 97 deletions(-)
 delete mode 100644 libs/shared/lib/vis/visualizations/Vis0D/Vis0D.tsx
 create mode 100644 libs/shared/lib/vis/visualizations/vis0D/Vis0D.tsx
 rename libs/shared/lib/vis/visualizations/{Vis0D => vis0D}/index.ts (100%)
 rename libs/shared/lib/vis/visualizations/{Vis0D => vis0D}/vis0D.stories.tsx (100%)

diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx
index 16e539086..b84ba8f6b 100644
--- a/libs/shared/lib/vis/components/VisualizationPanel.tsx
+++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx
@@ -30,7 +30,7 @@ export const Visualizations: Record<string, PromiseFunc> = {
     SemanticSubstratesVis: () => import('../visualizations/semanticsubstratesvis/semanticsubstratesvis'),
   }),
   ...(isVisualizationReleased('MapVis') && { MapVis: () => import('../visualizations/mapvis/mapvis') }),
-  ...(isVisualizationReleased('Vis0D') && { Vis0D: () => import('../visualizations/Vis0D/Vis0D') }),
+  ...(isVisualizationReleased('Vis0D') && { Vis0D: () => import('../visualizations/vis0D/Vis0D') }),
   ...(isVisualizationReleased('Vis1D') && { Vis1D: () => import('../visualizations/vis1D/Vis1D') }),
 };
 
diff --git a/libs/shared/lib/vis/visualizations/Vis0D/Vis0D.tsx b/libs/shared/lib/vis/visualizations/Vis0D/Vis0D.tsx
deleted file mode 100644
index b3c56b11d..000000000
--- a/libs/shared/lib/vis/visualizations/Vis0D/Vis0D.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import React, { useRef, useImperativeHandle, forwardRef } from 'react';
-import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
-import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
-import html2canvas from 'html2canvas';
-import { Input } from '@graphpolaris/shared/lib/components/inputs';
-
-export interface Vis0DProps {
-  title: string;
-}
-
-const settings: Vis0DProps = {
-  title: '',
-};
-
-export interface Vis0DVisHandle {
-  exportImageInternal: () => void;
-}
-
-const formatNumber = (number: number) => {
-  return number.toLocaleString('de-DE');
-};
-const Vis0D = forwardRef<Vis0DVisHandle, VisualizationPropTypes<Vis0DProps>>(({ data, settings }, refExternal) => {
-  const internalRef = useRef<HTMLDivElement>(null);
-  useImperativeHandle(refExternal, () => ({
-    exportImageInternal() {
-      const captureImage = () => {
-        const element = internalRef.current;
-        if (element) {
-          html2canvas(element, {
-            backgroundColor: '#FFFFFF',
-          })
-            .then((canvas) => {
-              const finalImage = canvas.toDataURL('image/png');
-              const link = document.createElement('a');
-              link.href = finalImage;
-              link.download = 'Vis0D.png';
-              document.body.appendChild(link);
-              link.click();
-              document.body.removeChild(link);
-            })
-            .catch((error) => {
-              console.error('Error capturing image:', error);
-            });
-        } else {
-          console.error('Container element not found');
-        }
-      };
-
-      const renderCanvas = () => {
-        requestAnimationFrame(() => {
-          captureImage();
-        });
-      };
-
-      renderCanvas();
-    },
-  }));
-
-  // !FIXME: When stats pills are ready, substitue results accordingly
-  return (
-    <div className="h-full w-full flex flex-col items-center justify-center overflow-hidden" ref={internalRef}>
-      {settings.title && <span className="text-3xl text-center mb-4">{settings.title}</span>}
-      {data?.nodes?.length > 0 ? (
-        <span className="text-4xl text-center">Select 0D data</span>
-      ) : (
-        <span className="text-8xl text-center">{formatNumber(1231312)}</span>
-      )}{' '}
-    </div>
-  );
-});
-
-const Vis0DSettings = ({ settings, updateSettings }: VisualizationSettingsPropTypes<Vis0DProps>) => {
-  return (
-    <SettingsContainer>
-      <Input type="text" label="Title" value={settings.title} onChange={(value) => updateSettings({ title: value as string })} />
-    </SettingsContainer>
-  );
-};
-
-const Vis0DRef = React.createRef<Vis0DVisHandle>();
-
-export const Vis0DComponent: VISComponentType<Vis0DProps> = {
-  component: React.forwardRef((props: VisualizationPropTypes<Vis0DProps>, ref) => <Vis0D {...props} ref={Vis0DRef} />),
-  settingsComponent: Vis0DSettings,
-  settings: settings,
-  exportImage: () => {
-    if (Vis0DRef.current) {
-      Vis0DRef.current.exportImageInternal();
-    } else {
-      console.error('0Dvis reference is not set.');
-    }
-  },
-};
-
-export default Vis0DComponent;
diff --git a/libs/shared/lib/vis/visualizations/index.tsx b/libs/shared/lib/vis/visualizations/index.tsx
index 974612dec..ca8bf58d6 100644
--- a/libs/shared/lib/vis/visualizations/index.tsx
+++ b/libs/shared/lib/vis/visualizations/index.tsx
@@ -5,4 +5,4 @@ export * from './tablevis/tablevis';
 export * from './matrixvis/matrixvis';
 export * from './semanticsubstratesvis/semanticsubstratesvis';
 export * from './vis1D/Vis1D';
-export * from './Vis0D/Vis0D';
+export * from './vis0D/Vis0D';
diff --git a/libs/shared/lib/vis/visualizations/vis0D/Vis0D.tsx b/libs/shared/lib/vis/visualizations/vis0D/Vis0D.tsx
new file mode 100644
index 000000000..d43d86256
--- /dev/null
+++ b/libs/shared/lib/vis/visualizations/vis0D/Vis0D.tsx
@@ -0,0 +1,284 @@
+import React, { useRef, useImperativeHandle, forwardRef, useEffect, useState, Fragment } from 'react';
+import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common';
+import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config';
+import html2canvas from 'html2canvas';
+import { Input } from '@graphpolaris/shared/lib/components/inputs';
+import { EntityPill, RelationPill } from '@graphpolaris/shared/lib/components/pills/Pill';
+import { Button } from '@graphpolaris/shared/lib/components/buttons';
+export interface Vis0DProps {
+  title: string;
+  selectedEntity: string;
+  selectedAttribute: string;
+  selectedStat: string;
+}
+
+const settings: Vis0DProps = {
+  title: '',
+  selectedEntity: '',
+  selectedAttribute: '',
+  selectedStat: '',
+};
+
+export interface Vis0DVisHandle {
+  exportImageInternal: () => void;
+}
+
+const formatNumber = (number: number) => {
+  return number.toLocaleString('de-DE');
+};
+
+const Vis0D = forwardRef<Vis0DVisHandle, VisualizationPropTypes<Vis0DProps>>(({ settings, graphMetadata }, refExternal) => {
+  const [statRender, setStatRender] = useState<number | undefined>(undefined);
+  const internalRef = useRef<HTMLDivElement>(null);
+  useImperativeHandle(refExternal, () => ({
+    exportImageInternal() {
+      const captureImage = () => {
+        const element = internalRef.current;
+        if (element) {
+          html2canvas(element, {
+            backgroundColor: '#FFFFFF',
+          })
+            .then((canvas) => {
+              const finalImage = canvas.toDataURL('image/png');
+              const link = document.createElement('a');
+              link.href = finalImage;
+              link.download = 'Vis0D.png';
+              document.body.appendChild(link);
+              link.click();
+              document.body.removeChild(link);
+            })
+            .catch((error) => {
+              console.error('Error capturing image:', error);
+            });
+        } else {
+          console.error('Container element not found');
+        }
+      };
+
+      const renderCanvas = () => {
+        requestAnimationFrame(() => {
+          captureImage();
+        });
+      };
+
+      renderCanvas();
+    },
+  }));
+
+  useEffect(() => {
+    if (settings.selectedEntity != '' && graphMetadata.nodes.types && settings.selectedAttribute != '' && settings.selectedStat != '') {
+      const nodesLabels = graphMetadata.nodes.labels;
+
+      let attributes = [];
+      if (nodesLabels.includes(settings.selectedEntity)) {
+        attributes = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
+      } else {
+        attributes = Object.keys(graphMetadata.edges.types[settings.selectedEntity].attributes);
+      }
+
+      if (attributes.includes(settings.selectedAttribute)) {
+        let statsAvailable = [];
+
+        if (nodesLabels.includes(settings.selectedEntity)) {
+          statsAvailable = Object.keys(
+            graphMetadata.nodes.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics,
+          );
+        } else {
+          statsAvailable = Object.keys(
+            graphMetadata.edges.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics,
+          );
+        }
+
+        if (statsAvailable.includes(settings.selectedStat)) {
+          let statValue = 0;
+          if (nodesLabels.includes(settings.selectedEntity)) {
+            statValue = (
+              graphMetadata.nodes.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics as Record<string, number>
+            )[settings.selectedStat];
+          } else {
+            statValue = (
+              graphMetadata.edges.types[settings.selectedEntity].attributes[settings.selectedAttribute].statistics as Record<string, number>
+            )[settings.selectedStat];
+          }
+
+          setStatRender(statValue);
+        } else {
+          setStatRender(undefined);
+        }
+      } else {
+        setStatRender(undefined);
+      }
+    }
+  }, [settings.selectedEntity, settings.selectedAttribute, settings.selectedStat]);
+
+  return (
+    <div className="h-full w-full flex flex-col items-center justify-center overflow-hidden" ref={internalRef}>
+      {settings.title && <span className="text-3xl text-center mb-4">{settings.title}</span>}
+      {statRender === undefined ? (
+        <span className="text-4xl text-center">Select 0D data</span>
+      ) : (
+        <span className="text-8xl text-center">{formatNumber(statRender)}</span>
+      )}
+    </div>
+  );
+});
+
+const Vis0DSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<Vis0DProps>) => {
+  const [attributeOptions, setAttributeOptions] = useState<string[]>([]);
+  const [statsOptions, setStatsOptions] = useState<string[]>([]);
+
+  useEffect(() => {
+    if (settings.selectedEntity === '' && graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
+      const firstEntity = graphMetadata.nodes.labels[0];
+
+      const attributesFirstEntity = Object.keys(graphMetadata.nodes.types[firstEntity].attributes);
+      setAttributeOptions(attributesFirstEntity);
+      const selectedAttribute = attributesFirstEntity[0];
+
+      const attributeSelectedStatistics = graphMetadata.nodes.types[firstEntity].attributes[selectedAttribute].statistics;
+
+      const notNaNStats = Object.keys(attributeSelectedStatistics).filter((key) => {
+        const value = attributeSelectedStatistics[key as keyof typeof attributeSelectedStatistics];
+        return typeof value === 'number' && !isNaN(value);
+      });
+
+      setStatsOptions(notNaNStats as string[]);
+      updateSettings({ selectedEntity: firstEntity, selectedAttribute: selectedAttribute, selectedStat: notNaNStats[0] });
+    }
+  }, [graphMetadata]);
+
+  useEffect(() => {
+    if (
+      settings.selectedEntity != '' &&
+      settings.selectedAttribute != '' &&
+      graphMetadata &&
+      graphMetadata.nodes &&
+      graphMetadata.nodes.labels.length > 0
+    ) {
+      const nodesLabels = graphMetadata.nodes.labels;
+
+      // attribute management
+      let attributesFirstEntity = [];
+      if (nodesLabels.includes(settings.selectedEntity)) {
+        attributesFirstEntity = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
+      } else {
+        attributesFirstEntity = Object.keys(graphMetadata.edges.types[settings.selectedEntity].attributes);
+      }
+      setAttributeOptions(attributesFirstEntity);
+      let selectedAttribute = '';
+
+      if (settings.selectedAttribute === '' || !attributesFirstEntity.includes(settings.selectedAttribute)) {
+        selectedAttribute = attributesFirstEntity[0];
+        updateSettings({ selectedAttribute: selectedAttribute });
+      } else {
+        selectedAttribute = settings.selectedAttribute;
+      }
+
+      // stat management
+      let attributeSelectedStatistics: Record<string, number> = {};
+
+      if (nodesLabels.includes(settings.selectedEntity)) {
+        attributeSelectedStatistics = graphMetadata.nodes.types[settings.selectedEntity].attributes[selectedAttribute].statistics as Record<
+          string,
+          number
+        >;
+      } else {
+        attributeSelectedStatistics = graphMetadata.edges.types[settings.selectedEntity].attributes[selectedAttribute].statistics as Record<
+          string,
+          number
+        >;
+      }
+
+      const notNaNStats = Object.keys(attributeSelectedStatistics).filter((key) => {
+        const value = attributeSelectedStatistics[key as keyof typeof attributeSelectedStatistics];
+        // !TODO: include string stats
+        return !isNaN(value);
+      });
+
+      setStatsOptions(notNaNStats as string[]);
+
+      if (settings.selectedStat == '' || !notNaNStats.includes(settings.selectedStat)) {
+        updateSettings({ selectedStat: notNaNStats[0] });
+      } else {
+        updateSettings({ selectedStat: settings.selectedStat });
+      }
+    }
+  }, [settings.selectedEntity, settings.selectedAttribute]);
+
+  return (
+    <SettingsContainer>
+      <div className="p-1">
+        <Input
+          className="mb-2"
+          type="text"
+          label="Title"
+          value={settings.title}
+          onChange={(value) => updateSettings({ title: value as string })}
+        />
+        <Input
+          className="w-full text-justify justify-start mb-2"
+          type="dropdown"
+          value={settings.selectedEntity}
+          options={[...graphMetadata.nodes.labels, ...graphMetadata.edges.labels]}
+          onChange={(val) => updateSettings({ selectedEntity: val as string })}
+          overrideRender={
+            graphMetadata.nodes.labels.includes(settings.selectedEntity) ? (
+              <EntityPill
+                title={
+                  <div className="flex flex-row justify-between items-center cursor-pointer">
+                    <span>{settings.selectedEntity || ''}</span>
+                    <Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
+                  </div>
+                }
+              />
+            ) : (
+              <RelationPill
+                title={
+                  <div className="flex flex-row justify-between items-center cursor-pointer">
+                    <span>{settings.selectedEntity || ''}</span>
+                    <Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" />
+                  </div>
+                }
+              />
+            )
+          }
+        ></Input>
+        <div className="mb-2 w-full">
+          <Input
+            className="w-full text-justify justify-start"
+            type="dropdown"
+            value={settings.selectedAttribute}
+            options={attributeOptions}
+            onChange={(val) => updateSettings({ selectedAttribute: val as string })}
+          ></Input>
+        </div>
+        <div className="mb-2">
+          <Input
+            className="w-full text-justify justify-start"
+            type="dropdown"
+            value={settings.selectedStat}
+            options={statsOptions}
+            onChange={(val) => updateSettings({ selectedStat: val as string })}
+          ></Input>
+        </div>
+      </div>
+    </SettingsContainer>
+  );
+};
+
+const Vis0DRef = React.createRef<Vis0DVisHandle>();
+
+export const Vis0DComponent: VISComponentType<Vis0DProps> = {
+  component: React.forwardRef((props: VisualizationPropTypes<Vis0DProps>, ref) => <Vis0D {...props} ref={Vis0DRef} />),
+  settingsComponent: Vis0DSettings,
+  settings: settings,
+  exportImage: () => {
+    if (Vis0DRef.current) {
+      Vis0DRef.current.exportImageInternal();
+    } else {
+      console.error('0Dvis reference is not set.');
+    }
+  },
+};
+
+export default Vis0DComponent;
diff --git a/libs/shared/lib/vis/visualizations/Vis0D/index.ts b/libs/shared/lib/vis/visualizations/vis0D/index.ts
similarity index 100%
rename from libs/shared/lib/vis/visualizations/Vis0D/index.ts
rename to libs/shared/lib/vis/visualizations/vis0D/index.ts
diff --git a/libs/shared/lib/vis/visualizations/Vis0D/vis0D.stories.tsx b/libs/shared/lib/vis/visualizations/vis0D/vis0D.stories.tsx
similarity index 100%
rename from libs/shared/lib/vis/visualizations/Vis0D/vis0D.stories.tsx
rename to libs/shared/lib/vis/visualizations/vis0D/vis0D.stories.tsx
-- 
GitLab