From d205888eb24fa96dd5acd65bf514a088217d951b Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Fri, 8 Nov 2024 14:50:51 +0000
Subject: [PATCH] feat(Vis1D): changes style and adds 2D options

---
 .../lib/vis/visualizations/vis1D/Vis1D.tsx    |  89 +++++++----
 .../vis1D/components/CustomChartPlotly.tsx    | 150 +++++++++++++++---
 2 files changed, 189 insertions(+), 50 deletions(-)

diff --git a/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx b/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx
index 739c992f5..846ab862b 100644
--- a/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx
+++ b/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx
@@ -4,27 +4,31 @@ import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/confi
 import html2canvas from 'html2canvas';
 import { CustomChartPlotly, plotTypeOptions } from './components/CustomChartPlotly';
 import { Input } from '@graphpolaris/shared/lib/components/inputs';
-import { DropdownTextAndIcon } from '@graphpolaris/shared/lib/components/selectors/textAndIcon';
 import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
 import { Button } from '@graphpolaris/shared/lib/components/buttons';
 
 export interface Vis1DProps {
-  plotType: (typeof plotTypeOptions)[number];
-  title: string;
-  attribute?: string;
-  nodeLabel: string;
+  plotType: (typeof plotTypeOptions)[number]; // plotly plot type
+  title: string; // title of the plot
+  nodeLabel: string; // node label to plot
+  xAxisLabel?: string;
+  yAxisLabel?: string;
+  showAxis: boolean;
 }
 
 const defaultSettings: Vis1DProps = {
   plotType: 'bar',
   title: '',
-  attribute: '',
   nodeLabel: '',
+  xAxisLabel: '',
+  yAxisLabel: '',
+  showAxis: true,
 };
 
 export interface Vis1DVisHandle {
   exportImageInternal: () => void;
 }
+
 const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({ data, settings }, refExternal) => {
   const internalRef = useRef<HTMLDivElement>(null);
 
@@ -63,48 +67,52 @@ const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({
     },
   }));
 
-  const attributeValues = useMemo(() => {
-    if (!settings.nodeLabel || !settings.attribute) {
+  const getAttributeValues = (attributeKey: string | number | undefined) => {
+    if (!settings.nodeLabel || !attributeKey) {
       return [];
     }
 
     return data.nodes
-      .filter((item) => item.label === settings.nodeLabel && item.attributes && settings.attribute! in item.attributes)
-      .map((item) => item.attributes[settings.attribute!] as string | number);
-  }, [data, settings.nodeLabel, settings.attribute]);
+      .filter((item) => item.label === settings.nodeLabel && item.attributes && attributeKey in item.attributes)
+      .map((item) => item.attributes[attributeKey]);
+  };
+
+  const xAxisData = useMemo(() => getAttributeValues(settings.xAxisLabel), [data, settings.nodeLabel, settings.xAxisLabel]);
+  const yAxisData = useMemo(() => getAttributeValues(settings.yAxisLabel), [data, settings.nodeLabel, settings.yAxisLabel]);
 
   return (
     <div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
-      <CustomChartPlotly data={attributeValues as string[] | number[]} plotType={settings.plotType} title={settings.title} />
+      <CustomChartPlotly
+        xAxisData={xAxisData as string[] | number[]}
+        yAxisData={yAxisData as string[] | number[]}
+        plotType={settings.plotType}
+        title={settings.title}
+        showAxis={settings.showAxis}
+        xAxisLabel={settings.xAxisLabel}
+        yAxisLabel={settings.yAxisLabel}
+      />
     </div>
   );
 });
 
 const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<Vis1DProps>) => {
   const mutablePlotTypes = [...plotTypeOptions];
-  const [attributeOptions, setAttributeOptions] = useState<{ name: string; type: string }[]>([]);
-  const [selectedOption, setSelectedOption] = useState<{ name: string; type: string } | null>(null);
+  const [attributeOptions, setAttributeOptions] = useState<string[]>([]);
 
-  const handleChange = (option: { name: string; type: string }) => {
-    setSelectedOption(option);
-    updateSettings({ attribute: option.name });
-  };
   useEffect(() => {
-    if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
+    if (settings.nodeLabel === '' && graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0) {
       const nodeLabelTemp = graphMetadata.nodes.labels[0];
       updateSettings({ nodeLabel: nodeLabelTemp });
     }
-  }, [graphMetadata]);
+  }, [settings.nodeLabel, graphMetadata]);
 
   useEffect(() => {
     if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.nodeLabel != '') {
-      const newAttributeOptions = Object.entries(graphMetadata.nodes.types[settings.nodeLabel].attributes).map(([key, value]) => ({
-        name: key,
-        type: value.attributeType,
-      }));
-      updateSettings({ attribute: newAttributeOptions[0].name });
+      const newAttributeOptions = Object.keys(graphMetadata.nodes.types[settings.nodeLabel].attributes);
+      if (settings.xAxisLabel === '') {
+        updateSettings({ xAxisLabel: newAttributeOptions[0] });
+      }
       // initialize the selected option for creating the dropdown and plots
-      setSelectedOption(newAttributeOptions[0]);
       setAttributeOptions(newAttributeOptions);
     }
   }, [graphMetadata, settings.nodeLabel]);
@@ -129,14 +137,13 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
             />
           }
         />
-
         <div className="mb-2">
           <Input type="text" label="Title" value={settings.title} onChange={(value) => updateSettings({ title: value as string })} />
         </div>
         <div className="mb-2">
           <Input
             type="dropdown"
-            label="Type Chart"
+            label="Chart"
             value={settings.plotType}
             options={mutablePlotTypes}
             onChange={(value: string | number) => {
@@ -145,7 +152,31 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
           />
         </div>
         <div className="mb-2">
-          <DropdownTextAndIcon value={selectedOption} onChange={handleChange} options={attributeOptions} />
+          <Input
+            type="dropdown"
+            label="X-axis:"
+            value={settings.xAxisLabel}
+            options={attributeOptions}
+            onChange={(value) => {
+              updateSettings({ xAxisLabel: value as string });
+            }}
+          />
+        </div>
+        {(settings.plotType === 'scatter' || settings.plotType === 'line') && (
+          <div className="mb-2">
+            <Input
+              type="dropdown"
+              label="Y-axis:"
+              value={settings.yAxisLabel}
+              options={attributeOptions}
+              onChange={(value) => {
+                updateSettings({ yAxisLabel: value as string });
+              }}
+            />
+          </div>
+        )}
+        <div className="mb-2">
+          <Input type="boolean" label="Show axis" value={settings.showAxis} onChange={(val) => updateSettings({ showAxis: val })} />
         </div>
       </div>
     </SettingsContainer>
diff --git a/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx b/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx
index 796bde78d..04229b1d0 100644
--- a/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx
+++ b/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx
@@ -1,18 +1,53 @@
 import { visualizationColors } from 'config';
 import React, { useRef, useEffect, useState } from 'react';
 import Plot from 'react-plotly.js';
+import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip';
 
+const getCSSVariableHSL = (varName: string) => {
+  const rootStyles = getComputedStyle(document.documentElement);
+  const hslValue = rootStyles.getPropertyValue(varName).trim().replace('deg', '');
+  return `hsl(${hslValue})`;
+};
 export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
 
 export interface CustomChartPlotlyProps {
-  data: string[] | number[];
+  xAxisData: string[] | number[];
   plotType: (typeof plotTypeOptions)[number];
   title: string;
+  showAxis: boolean;
+  yAxisData: string[] | number[];
+  xAxisLabel?: string;
+  yAxisLabel?: string;
 }
 
-export const getPlotData = (data: (string | number)[], plotType: (typeof plotTypeOptions)[number]): Partial<Plotly.PlotData>[] => {
+export const getPlotData = (
+  xAxisData: (string | number)[],
+  plotType: (typeof plotTypeOptions)[number],
+  yAxisData?: (string | number)[],
+): Partial<Plotly.PlotData>[] => {
   const mainColors = visualizationColors.GPCat.colors[14];
-  const xValues = data.map((_, index) => index + 1);
+
+  const primaryColor = getCSSVariableHSL('--clr-sec--400');
+  let xValues: (string | number)[] = [];
+  let yValues: (string | number)[] = [];
+
+  if (plotType === 'scatter' || plotType === 'line') {
+    if (xAxisData.length != 0 && yAxisData && yAxisData.length != 0) {
+      xValues = xAxisData;
+      yValues = yAxisData;
+    } else if (xAxisData.length != 0 && yAxisData && yAxisData.length == 0) {
+      xValues = xAxisData;
+      yValues = xAxisData.map((_, index) => index + 1);
+    } else if (xAxisData.length == 0 && yAxisData && yAxisData.length != 0) {
+      xValues = yAxisData.map((_, index) => index + 1);
+      yValues = yAxisData;
+    } else if (xAxisData.length == 0 && yAxisData && yAxisData.length == 0) {
+    } else {
+    }
+  } else {
+    xValues = xAxisData;
+    yValues = xAxisData.map((_, index) => index + 1);
+  }
 
   switch (plotType) {
     case 'bar':
@@ -20,8 +55,9 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
         {
           type: 'bar',
           x: xValues,
-          y: data,
-          marker: { color: mainColors },
+          y: yValues,
+          marker: { color: primaryColor },
+          hoverinfo: 'none',
         },
       ];
     case 'scatter':
@@ -29,9 +65,10 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
         {
           type: 'scatter',
           x: xValues,
-          y: data,
+          y: yValues,
           mode: 'markers',
-          marker: { color: mainColors, size: 12 },
+          marker: { color: primaryColor, size: 12 },
+          hoverinfo: 'none',
         },
       ];
     case 'line':
@@ -39,17 +76,19 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
         {
           type: 'scatter',
           x: xValues,
-          y: data,
+          y: yValues,
           mode: 'lines',
-          line: { color: mainColors },
+          line: { color: primaryColor },
+          hoverinfo: 'none',
         },
       ];
     case 'histogram':
       return [
         {
           type: 'histogram',
-          x: data,
-          marker: { color: mainColors },
+          x: xAxisData,
+          marker: { color: primaryColor },
+          hoverinfo: 'none',
         },
       ];
     case 'pie':
@@ -57,18 +96,29 @@ export const getPlotData = (data: (string | number)[], plotType: (typeof plotTyp
         {
           type: 'pie',
           labels: xValues.map(String),
-          values: data,
+          values: xAxisData,
           marker: { colors: mainColors },
+          hoverinfo: 'none',
         },
       ];
+
     default:
       return [];
   }
 };
 
-export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ data, plotType, title }) => {
+export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
+  xAxisData,
+  plotType,
+  title,
+  showAxis,
+  yAxisData,
+  xAxisLabel,
+  yAxisLabel,
+}) => {
   const internalRef = useRef<HTMLDivElement>(null);
   const [divSize, setDivSize] = useState({ width: 0, height: 0 });
+  const [hoveredPoint, setHoveredPoint] = useState<{ left: number; top: number; value: number } | null>(null);
 
   useEffect(() => {
     const handleResize = () => {
@@ -78,7 +128,7 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ data, plot
       }
     };
 
-    handleResize(); // Set initial size
+    handleResize();
     window.addEventListener('resize', handleResize);
     if (internalRef.current) {
       new ResizeObserver(handleResize).observe(internalRef.current);
@@ -89,28 +139,86 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({ data, plot
     };
   }, []);
 
+  const handleHover = (event: any) => {
+    const { points } = event;
+
+    if (points.length) {
+      const point = points[0];
+      const plotRect = internalRef.current?.getBoundingClientRect(); // Get the plot's bounding box
+
+      if (plotRect) {
+        // Calculate the position of the tooltip
+        const xIndex = point.xaxis.d2p(point.x); // Convert x value to pixel position
+        const yIndex = point.yaxis.d2p(point.y); // Convert y value to pixel position
+
+        setHoveredPoint({
+          left: xIndex, // Center tooltip above the point
+          top: plotRect.top + yIndex, // Position below the point
+          value: point.y, // Value to display
+        });
+      }
+    }
+  };
+
   return (
-    <div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
+    <div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
       <Plot
-        data={getPlotData(data, plotType)}
-        config={{ responsive: true, displayModeBar: false }}
+        data={getPlotData(xAxisData, plotType, yAxisData)}
+        config={{
+          responsive: true,
+          scrollZoom: false,
+          displayModeBar: false,
+          staticPlot: true,
+          displaylogo: false,
+        }}
         layout={{
           width: divSize.width,
           height: divSize.height,
           title: title,
+          dragmode: false,
           font: {
             family: 'Inter, sans-serif',
-            size: 16,
-            color: '#374151',
+            size: 12,
+            color: '#374151', // change to gp default color
           },
           xaxis: {
-            title: 'Category',
+            title: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
+            showgrid: false,
+            visible: showAxis,
+            showline: true,
+            zeroline: false,
           },
           yaxis: {
-            title: 'Value',
+            title: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
+            showgrid: false,
+            visible: showAxis,
+            showline: true,
+            zeroline: false,
           },
         }}
+        onHover={handleHover}
+        onUnhover={() => setHoveredPoint(null)}
       />
+
+      {hoveredPoint && (
+        <div>
+          <Tooltip open={true} showArrow={true}>
+            <TooltipTrigger />
+            <TooltipContent
+              style={{
+                position: 'absolute',
+                left: hoveredPoint.left,
+                top: hoveredPoint.top,
+                transform: 'translate(-50%, -100%)',
+              }}
+            >
+              <div>
+                <strong>Value:</strong> {hoveredPoint.value} <br />
+              </div>
+            </TooltipContent>
+          </Tooltip>
+        </div>
+      )}
     </div>
   );
 };
-- 
GitLab