From dd2c47673ffe27d470d0ef5820bb1f3850e0adbb Mon Sep 17 00:00:00 2001
From: Marcos Pieras <pieras.marcos@gmail.com>
Date: Wed, 4 Dec 2024 14:49:40 +0000
Subject: [PATCH] feat: updates to 1DVis: color selection, stacked bar chart,
 date grouping

---
 .../textEditor/plugins/PreviewPlugin.tsx      |  66 +-
 .../lib/vis/visualizations/vis1D/Vis1D.tsx    | 104 ++-
 .../vis1D/components/CustomChartPlotly.tsx    | 677 +++++++++++++++---
 3 files changed, 706 insertions(+), 141 deletions(-)

diff --git a/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx b/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx
index 9867fb865..a84ebe221 100644
--- a/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx
+++ b/libs/shared/lib/components/textEditor/plugins/PreviewPlugin.tsx
@@ -5,8 +5,8 @@ import { $generateHtmlFromNodes } from '@lexical/html';
 import { VariableType } from '../VariableNode';
 import { useVisualization, useGraphQueryResult } from '@graphpolaris/shared/lib/data-access';
 import { Visualizations } from '@graphpolaris/shared/lib/vis/components/VisualizationPanel';
-import { Vis1DComponent, type Vis1DProps } from '@graphpolaris/shared/lib/vis/visualizations/vis1D';
-import { getPlotData } from '@graphpolaris/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly';
+import { Vis1DComponent, type Vis1DProps, getAttributeValues } from '@graphpolaris/shared/lib/vis/visualizations/vis1D';
+import { preparePlotData } from '@graphpolaris/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly';
 import { VisualizationSettingsType } from '@graphpolaris/shared/lib/vis/common';
 // @ts-ignore
 import { newPlot, toImage } from 'plotly.js/dist/plotly';
@@ -25,6 +25,8 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
   }
 
   const result = useGraphQueryResult();
+
+  /*
   const getAttributeValues = useCallback(
     (settings: Vis1DProps & VisualizationSettingsType, attributeKey: string | number) => {
       if (!settings.nodeLabel || !attributeKey) {
@@ -37,6 +39,7 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
     },
     [result],
   );
+  */
   const vis = useVisualization();
 
   async function replaceAllAsync(string: string, regexp: RegExp, replacerFunction: CallableFunction) {
@@ -61,34 +64,57 @@ export function PreviewPlugin({ contentEditable }: { contentEditable: RefObject<
           return ` ${value} `;
 
         case VariableType.visualization:
-          const activeVisualization = vis.openVisualizationArray.find(x => x.name == name) as Vis1DProps & VisualizationSettingsType;
+          const activeVisualization = vis.openVisualizationArray.find((x) => x.name == name) as Vis1DProps & VisualizationSettingsType;
 
           if (!activeVisualization) {
             throw new Error('Tried to render non-existing visualization');
           }
+          let xAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.xAxisLabel!);
+          let yAxisData: (string | number)[] = [];
+          let zAxisData: (string | number)[] = [];
+
+          if (activeVisualization.yAxisLabel != null) {
+            yAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.yAxisLabel);
+          }
+
+          if (activeVisualization.zAxisLabel != null) {
+            zAxisData = getAttributeValues(result, activeVisualization.selectedEntity, activeVisualization.zAxisLabel);
+          }
+
+          //debugger;
+          const groupBy = activeVisualization.groupData;
+          const stack = activeVisualization.stack;
+          const showAxis = true;
+
+          const xAxisLabel = '';
+          const yAxisLabel = '';
+          const zAxisLabel = '';
 
-          const xAxisData = getAttributeValues(activeVisualization, activeVisualization.xAxisLabel!);
-          const yAxisData = getAttributeValues(activeVisualization, activeVisualization.yAxisLabel!);
-          debugger;
           const plotType = activeVisualization.plotType;
-          const plotData = getPlotData(xAxisData, plotType, yAxisData);
 
-          const plot = await newPlot(document.createElement('div'), plotData, {
+          const { plotData, layout } = preparePlotData(
+            xAxisData,
+            plotType,
+            yAxisData,
+            zAxisData,
+            xAxisLabel,
+            yAxisLabel,
+            zAxisLabel,
+            showAxis,
+            groupBy,
+            stack,
+          );
+
+          const layout2 = {
+            ...layout,
             width: 600,
             height: 400,
             title: activeVisualization.title,
-            font: {
-              family: 'Inter, sans-serif',
-              size: 16,
-              color: '#374151',
-            },
-            xaxis: {
-              title: 'Category',
-            },
-            yaxis: {
-              title: 'Value',
-            },
-          });
+          };
+
+          // Generate the plot
+          const plot = await newPlot(document.createElement('div'), plotData, layout2);
+
           const dataURI = await toImage(plot);
           return `<img src="${dataURI}" width="300" height="200" alt="${activeVisualization.title}" />`;
       }
diff --git a/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx b/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx
index ebfc2423f..8c27baa8e 100644
--- a/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx
+++ b/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx
@@ -6,6 +6,7 @@ import { CustomChartPlotly, plotTypeOptions } from './components/CustomChartPlot
 import { Input } from '@graphpolaris/shared/lib/components/inputs';
 import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill';
 import { Button } from '@graphpolaris/shared/lib/components/buttons';
+import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access';
 
 export interface Vis1DProps {
   plotType: (typeof plotTypeOptions)[number]; // plotly plot type
@@ -13,7 +14,10 @@ export interface Vis1DProps {
   selectedEntity: string; // node label to plot
   xAxisLabel?: string;
   yAxisLabel?: string;
+  zAxisLabel?: string;
   showAxis: boolean;
+  groupData?: string;
+  stack: boolean;
 }
 
 const defaultSettings: Vis1DProps = {
@@ -22,13 +26,34 @@ const defaultSettings: Vis1DProps = {
   selectedEntity: '',
   xAxisLabel: '',
   yAxisLabel: '',
+  zAxisLabel: '',
   showAxis: true,
+  groupData: undefined,
+  stack: false,
 };
 
 export interface Vis1DVisHandle {
   exportImageInternal: () => void;
 }
 
+export const getAttributeValues = (query: GraphQueryResult, selectedEntity: string, attributeKey: string | number | undefined): any[] => {
+  if (!selectedEntity || !attributeKey) {
+    return [];
+  }
+
+  if (attributeKey == ' ') {
+    return [];
+  }
+  return query.nodes
+    .filter((item) => item.label === selectedEntity)
+    .map((item) => {
+      // Check if the attribute exists, return its value if it does, or an empty string otherwise
+      return item.attributes && attributeKey in item.attributes && item.attributes[attributeKey] != ''
+        ? item.attributes[attributeKey]
+        : 'NoData';
+    });
+};
+
 const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({ data, settings }, refExternal) => {
   const internalRef = useRef<HTMLDivElement>(null);
 
@@ -67,29 +92,33 @@ const Vis1D = forwardRef<Vis1DVisHandle, VisualizationPropTypes<Vis1DProps>>(({
     },
   }));
 
-  const getAttributeValues = (attributeKey: string | number | undefined) => {
-    if (!settings.selectedEntity || !attributeKey) {
-      return [];
-    }
-
-    return data.nodes
-      .filter((item) => item.label === settings.selectedEntity && item.attributes && attributeKey in item.attributes)
-      .map((item) => item.attributes[attributeKey]);
-  };
-
-  const xAxisData = useMemo(() => getAttributeValues(settings.xAxisLabel), [data, settings.selectedEntity, settings.xAxisLabel]);
-  const yAxisData = useMemo(() => getAttributeValues(settings.yAxisLabel), [data, settings.selectedEntity, settings.yAxisLabel]);
+  const xAxisData = useMemo(
+    () => getAttributeValues(data, settings.selectedEntity, settings.xAxisLabel),
+    [data, settings.selectedEntity, settings.xAxisLabel],
+  );
+  const yAxisData = useMemo(
+    () => getAttributeValues(data, settings.selectedEntity, settings.yAxisLabel),
+    [data, settings.selectedEntity, settings.yAxisLabel],
+  );
+  const zAxisData = useMemo(
+    () => getAttributeValues(data, settings.selectedEntity, settings.zAxisLabel),
+    [data, settings.selectedEntity, settings.zAxisLabel],
+  );
 
   return (
     <div className="h-full w-full flex items-center justify-center overflow-hidden" ref={internalRef}>
       <CustomChartPlotly
         xAxisData={xAxisData as string[] | number[]}
         yAxisData={yAxisData as string[] | number[]}
+        zAxisData={zAxisData as string[] | number[]}
         plotType={settings.plotType}
         title={settings.title}
         showAxis={settings.showAxis}
         xAxisLabel={settings.xAxisLabel}
         yAxisLabel={settings.yAxisLabel}
+        zAxisLabel={settings.zAxisLabel}
+        groupBy={settings.groupData}
+        stack={settings.stack}
       />
     </div>
   );
@@ -120,7 +149,15 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
         const newAttributeOptions = Object.keys(graphMetadata.nodes.types[settings.selectedEntity].attributes);
         if (settings.xAxisLabel === '') {
           updateSettings({ xAxisLabel: newAttributeOptions[0] });
+
+          // !TODO: instead of contain "datum" chekc type: if it is date
+          if (newAttributeOptions[0].includes('Datum')) {
+            updateSettings({ groupData: 'yearly' });
+          } else {
+            updateSettings({ groupData: undefined });
+          }
         }
+        newAttributeOptions.unshift(' ');
         setAttributeOptions(newAttributeOptions);
       } else {
       }
@@ -158,6 +195,9 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
             options={mutablePlotTypes}
             onChange={(value: string | number) => {
               updateSettings({ plotType: value as (typeof plotTypeOptions)[number] });
+              if (value === 'bar' || value === 'histogram' || value === 'pie') {
+                updateSettings({ yAxisLabel: '' });
+              }
             }}
           />
         </div>
@@ -168,7 +208,14 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
             value={settings.xAxisLabel}
             options={attributeOptions}
             onChange={(value) => {
-              updateSettings({ xAxisLabel: value as string });
+              const valueString = value as string;
+              updateSettings({ xAxisLabel: valueString });
+
+              if (!valueString.includes('Datum')) {
+                updateSettings({ groupData: undefined });
+              } else {
+                updateSettings({ groupData: 'monthly' });
+              }
             }}
           />
         </div>
@@ -185,6 +232,37 @@ const Vis1DSettings = ({ settings, graphMetadata, updateSettings }: Visualizatio
             />
           </div>
         )}
+        {(settings.plotType === 'bar' || settings.plotType === 'scatter' || settings.plotType === 'histogram') && (
+          <div className="mb-2">
+            <Input
+              type="dropdown"
+              label="Color:"
+              value={settings.zAxisLabel}
+              options={attributeOptions}
+              onChange={(value) => {
+                updateSettings({ zAxisLabel: value as string });
+              }}
+            />
+          </div>
+        )}
+        {settings.plotType === 'histogram' && (
+          <div className="mb-2">
+            <Input type="boolean" label="Normalize: " value={settings.stack} onChange={(val) => updateSettings({ stack: val })} />
+          </div>
+        )}
+        {settings.xAxisLabel?.includes('Datum') && (
+          <div className="mb-2">
+            <Input
+              type="dropdown"
+              label="Group Time:"
+              value={settings.groupData}
+              options={['', 'monthly', 'quarterly', 'yearly']}
+              onChange={(value) => {
+                updateSettings({ groupData: value as string });
+              }}
+            />
+          </div>
+        )}
         <div className="mb-2">
           <Input type="boolean" label="Show axis" value={settings.showAxis} onChange={(val) => updateSettings({ showAxis: val })} />
         </div>
diff --git a/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx b/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx
index 2b9a9219d..abe5253da 100644
--- a/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx
+++ b/libs/shared/lib/vis/visualizations/vis1D/components/CustomChartPlotly.tsx
@@ -2,6 +2,8 @@ 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';
+import { PlotType } from 'plotly.js';
+import { scaleOrdinal, scaleLinear, scaleQuantize } from 'd3';
 
 const getCSSVariableHSL = (varName: string) => {
   const rootStyles = getComputedStyle(document.documentElement);
@@ -9,121 +11,541 @@ const getCSSVariableHSL = (varName: string) => {
   return `hsl(${hslValue})`;
 };
 export const plotTypeOptions = ['bar', 'scatter', 'line', 'histogram', 'pie'] as const;
+export type SupportedPlotType = (typeof plotTypeOptions)[number];
 
 export interface CustomChartPlotlyProps {
   xAxisData: string[] | number[];
-  plotType: (typeof plotTypeOptions)[number];
+  yAxisData: string[] | number[];
+  zAxisData?: string[] | number[];
+  plotType: SupportedPlotType;
   title: string;
   showAxis: boolean;
-  yAxisData: string[] | number[];
   xAxisLabel?: string;
   yAxisLabel?: string;
+  zAxisLabel?: string;
+  groupBy?: string;
+  stack: boolean;
 }
 
-export const getPlotData = (
+const groupByTime = (xAxisData: string[], groupBy: string, additionalVariableData?: (string | number)[]) => {
+  // Function to parse the date-time string into a JavaScript Date object
+  const parseDate = (dateStr: string) => {
+    // Remove nanoseconds part and use just the standard "YYYY-MM-DD HH:MM:SS" part
+    const cleanedDateStr = dateStr.split('.')[0];
+    return new Date(cleanedDateStr);
+  };
+
+  // Grouping logic
+  const groupedData = xAxisData.reduce(
+    (acc, dateStr, index) => {
+      const date = parseDate(dateStr);
+      let groupKey: string;
+
+      if (groupBy === 'yearly') {
+        groupKey = date.getFullYear().toString(); // Group by year (e.g., "2012")
+      } else if (groupBy === 'quarterly') {
+        const month = date.getMonth() + 1; // Adjust month for zero-indexed months
+        const quarter = Math.floor((month - 1) / 3) + 1; // Calculate quarter (Q1-Q4)
+        groupKey = `${date.getFullYear()}-Q${quarter}`;
+      } else if (groupBy === 'monthly') {
+        // Group by month, e.g., "2012-07"
+        groupKey = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`;
+      } else {
+        // Default case: group by year (or some other grouping logic)
+        groupKey = date.getFullYear().toString();
+      }
+
+      // Initialize the group if it doesn't exist
+      if (!acc[groupKey]) {
+        acc[groupKey] = additionalVariableData
+          ? typeof additionalVariableData[0] === 'number'
+            ? 0 // Initialize sum for numbers
+            : [] // Initialize array for strings
+          : 0; // Initialize count for no additional data
+      }
+
+      // Aggregate additional variable if provided
+      if (additionalVariableData) {
+        if (typeof additionalVariableData[index] === 'number') {
+          acc[groupKey] = (acc[groupKey] as number) + (additionalVariableData[index] as number);
+        } else if (typeof additionalVariableData[index] === 'string') {
+          acc[groupKey] = [...(acc[groupKey] as string[]), additionalVariableData[index] as string];
+        }
+      } else {
+        // Increment the count if no additionalVariableData
+        acc[groupKey] = (acc[groupKey] as number) + 1;
+      }
+
+      return acc;
+    },
+    {} as Record<string, number | string[]>,
+  );
+
+  // Extract grouped data into arrays for Plotly
+  const xValuesGrouped = Object.keys(groupedData);
+  const yValuesGrouped = Object.values(groupedData);
+
+  return { xValuesGrouped, yValuesGrouped };
+};
+
+const computeStringTickValues = (xValues: any[], maxTicks: number, maxLabelLength: number): any[] => {
+  const truncatedValues = xValues.map((label) => (label.length > maxLabelLength ? `${label.slice(0, maxLabelLength)}…` : label));
+
+  return truncatedValues;
+};
+
+export const preparePlotData = (
   xAxisData: (string | number)[],
-  plotType: (typeof plotTypeOptions)[number],
+  plotType: SupportedPlotType,
   yAxisData?: (string | number)[],
-): Partial<Plotly.PlotData>[] => {
+  zAxisData?: (string | number)[],
+  xAxisLabel?: string,
+  yAxisLabel?: string,
+  zAxisLabel?: string,
+  showAxis = true,
+  groupBy?: string,
+  stack?: boolean,
+): { plotData: Partial<Plotly.PlotData>[]; layout: Partial<Plotly.Layout> } => {
+  const primaryColor = getCSSVariableHSL('--clr-sec--400');
+  const lengthLabelsX = 7; // !TODO computed number of elements based
+  const lengthLabelsY = 8; // !TODO computed number of elements based
   const mainColors = visualizationColors.GPCat.colors[14];
 
-  const primaryColor = getCSSVariableHSL('--clr-sec--400');
+  const sharedTickFont = {
+    family: 'monospace',
+    size: 12,
+    color: '#374151', // !TODO get GP value
+  };
+
   let xValues: (string | number)[] = [];
   let yValues: (string | number)[] = [];
 
-  if (plotType === 'scatter' || plotType === 'line') {
-    if (xAxisData.length != 0 && yAxisData && yAxisData.length != 0) {
+  let colorScale: any;
+  let colorDataZ: string[] = [];
+  let colorbar: any = {};
+
+  if (zAxisData && zAxisData.length > 0 && typeof zAxisData[0] === 'number') {
+    const mainColorsSeq = visualizationColors.GPSeq.colors[9];
+    const numericZAxisData = zAxisData.filter((item): item is number => typeof item === 'number');
+    const zMin = numericZAxisData.reduce((min, val) => (val < min ? val : min), zAxisData[0]);
+    const zMax = numericZAxisData.reduce((max, val) => (val > max ? val : max), zAxisData[0]);
+
+    // !TODO: option to have a linear or quantize scale
+    colorScale = scaleQuantize<string>().domain([zMin, zMax]).range(mainColorsSeq);
+
+    colorDataZ = zAxisData?.map((item) => colorScale(item) || primaryColor);
+
+    colorbar = {
+      title: 'Color Legend',
+      tickvals: [zMin, zMax],
+      ticktext: [`${zMin}`, `${zMax}`],
+    };
+  } else {
+    const uniqueZAxisData = Array.from(new Set(zAxisData));
+
+    if (zAxisData && uniqueZAxisData) {
+      colorScale = scaleOrdinal<string>().domain(uniqueZAxisData.map(String)).range(mainColors);
+
+      colorDataZ = zAxisData?.map((item) => colorScale(String(item)) || primaryColor);
+      const sortedDomain = uniqueZAxisData.sort();
+      colorbar = {
+        title: 'Color Legend',
+        tickvals: sortedDomain,
+        ticktext: sortedDomain.map((val) => String(val)),
+        tickmode: 'array',
+      };
+    }
+  }
+
+  if (!groupBy) {
+    if (xAxisData.length !== 0 && yAxisData && yAxisData.length !== 0) {
       xValues = xAxisData;
       yValues = yAxisData;
-    } else if (xAxisData.length != 0 && yAxisData && yAxisData.length == 0) {
+    } 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) {
+    } 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);
+    if (groupBy) {
+      if (yAxisData && yAxisData.length !== 0) {
+        const { xValuesGrouped, yValuesGrouped } = groupByTime(xAxisData as string[], groupBy, yAxisData);
+        xValues = xValuesGrouped;
+        yValues = yValuesGrouped.flat();
+      } else {
+        const { xValuesGrouped, yValuesGrouped } = groupByTime(xAxisData as string[], groupBy);
+        xValues = xValuesGrouped;
+        yValues = yValuesGrouped.flat();
+      }
+    } else {
+      xValues = xAxisData;
+      yValues = xAxisData.map((_, index) => index + 1);
+    }
   }
 
-  switch (plotType) {
-    case 'bar':
-      return [
-        {
-          type: 'bar',
-          x: xValues,
-          y: yValues,
-          marker: { color: primaryColor },
-        },
-      ];
-    case 'scatter':
-      return [
-        {
-          type: 'scatter',
-          x: xValues,
-          y: yValues,
-          mode: 'markers',
-          marker: { color: primaryColor, size: 12 },
-        },
-      ];
-    case 'line':
-      return [
-        {
-          type: 'scatter',
-          x: xValues,
-          y: yValues,
-          mode: 'lines',
-          line: { color: primaryColor },
-        },
-      ];
-    case 'histogram':
-      // !TODO: Apply for other data types?
-      if (typeof xAxisData[0] === 'string') {
-        const frequencyMap = xAxisData.reduce(
-          (acc, item) => {
-            acc[item] = (acc[item] || 0) + 1;
-            return acc;
-          },
-          {} as Record<string, number>,
-        );
+  let sortedLabels: string[] = [];
+  let sortedFrequencies = [];
+
+  let truncatedXLabels: string[] = [];
+  let truncatedYLabels: string[] = [];
+  let yAxisRange: number[] = [];
+
+  if (typeof xValues[0] === 'string') {
+    truncatedXLabels = computeStringTickValues(xValues, 2, lengthLabelsX);
+  }
+
+  if (typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line')) {
+    truncatedYLabels = computeStringTickValues(yValues, 2, lengthLabelsY);
+  }
+  const plotData = (() => {
+    switch (plotType) {
+      case 'bar':
+        if (typeof xAxisData[0] === 'string' && groupBy == undefined) {
+          const frequencyMap = xAxisData.reduce(
+            (acc, item) => {
+              acc[item] = (acc[item] || 0) + 1;
+              return acc;
+            },
+            {} as Record<string, number>,
+          );
 
-        const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
-        const sortedLabels = sortedEntries.map(([label]) => label);
-        const sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
+          const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
 
+          sortedLabels = sortedEntries.map(([label]) => String(label));
+          sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
+
+          // !TODO: y ranges: max value showed is rounded, eg 54 -> 50
+          // need to specify tickvales and ticktext
+
+          const maxYValue = Math.max(...sortedFrequencies);
+          yAxisRange = [0, maxYValue];
+
+          return [
+            {
+              type: 'bar' as PlotType,
+              x: xValues,
+              y: yValues,
+              marker: {
+                color: colorDataZ?.length != 0 ? colorDataZ : primaryColor,
+              },
+              customdata: sortedLabels,
+              hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
+            },
+          ];
+        } else {
+          return [
+            {
+              type: 'bar' as PlotType,
+              x: xValues,
+              y: yValues,
+              marker: { color: primaryColor },
+              customdata: xValues,
+              hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
+            },
+          ];
+        }
+
+      case 'scatter':
+        return [
+          {
+            type: 'scatter' as PlotType,
+            x: xValues,
+            y: yValues,
+            mode: 'markers' as 'markers',
+            marker: {
+              color: zAxisData && zAxisData.length > 0 ? colorDataZ : primaryColor,
+              size: 7,
+              stroke: 1,
+            },
+            customdata:
+              xValues.length === 0
+                ? yValues.map((y) => `Y: ${y}`)
+                : yValues.length === 0
+                  ? xValues.map((x) => `X: ${x}`)
+                  : xValues.map((x, index) => {
+                      const zValue = zAxisData && zAxisData.length > 0 ? zAxisData[index] : null;
+                      return zValue ? `X: ${x} | Y: ${yValues[index]} | Color: ${zValue}` : `X: ${x} | Y: ${yValues[index]}`;
+                    }),
+            hovertemplate: '<b>%{customdata}</b><extra></extra>',
+          },
+        ];
+      case 'line':
         return [
           {
-            type: 'bar',
-            x: sortedLabels,
-            y: sortedFrequencies,
-            marker: { color: primaryColor },
+            type: 'scatter' as PlotType,
+            x: xValues,
+            y: yValues,
+            mode: 'lines' as 'lines',
+            line: { color: primaryColor },
+            customdata: xValues.map((label) => (label === 'undefined' || label === 'null' || label === '' ? 'nonData' : '')),
+            hovertemplate: '<b>%{customdata}</b><extra></extra>',
           },
         ];
-      } else {
+      case 'histogram':
+        if (typeof xAxisData[0] === 'string') {
+          if (zAxisData && zAxisData?.length > 0) {
+            const frequencyMap = xAxisData.reduce(
+              (acc, item, index) => {
+                const color = zAxisData ? colorScale(zAxisData[index]) : primaryColor;
+
+                if (!acc[item]) {
+                  acc[item] = {
+                    count: 0,
+                    colors: [],
+                    zValues: [],
+                    zValueCounts: {},
+                  };
+                }
+
+                acc[item].count++;
+                acc[item].colors.push(color);
+                acc[item].zValues.push(zAxisData[index].toString());
+                // Group and count zValues
+                const zValue = zAxisData[index] || '(Empty)';
+                acc[item].zValueCounts[zValue] = (acc[item].zValueCounts[zValue] || 0) + 1;
+
+                return acc;
+              },
+              {} as Record<
+                string,
+                {
+                  count: number;
+                  colors: string[];
+                  zValues: string[];
+                  zValueCounts: Record<string, number>; // To store grouped counts
+                }
+              >,
+            );
+            const colorToLegendName = new Map();
+            const sortedCategories = Object.entries(frequencyMap).sort((a, b) => b[1].count - a[1].count);
+
+            const tracesByColor: Record<string, { x: string[]; y: number[] }> = {};
+
+            sortedCategories.forEach(([label, { colors, zValues }]) => {
+              colors.forEach((color, idx) => {
+                const zValue = zValues[idx];
+
+                if (!colorToLegendName.has(color)) {
+                  colorToLegendName.set(color, zValue);
+                }
+
+                if (!tracesByColor[color]) {
+                  tracesByColor[color] = { x: [], y: [] };
+                }
+                tracesByColor[color].x.push(label);
+                tracesByColor[color].y.push(1);
+              });
+            });
+
+            sortedLabels = sortedCategories.map((element) => element[0]);
+
+            const traces = Array.from(colorToLegendName.entries()).map(([color, legendName]) => {
+              const colorData = tracesByColor[color];
+              const categoryCountMap: Record<string, number> = {};
+
+              sortedLabels.forEach((label) => {
+                categoryCountMap[label] = frequencyMap[label].count;
+              });
+              const yValues = colorData.x.map((label, idx) => {
+                const totalCount = categoryCountMap[label];
+                const countForColor = colorData.y[idx];
+                return stack ? (countForColor / totalCount) * 100 : countForColor;
+              });
+
+              const customdata = colorData.x.map((label, idx) => {
+                const colorTranslation = colorToLegendName.get(color) === ' ' ? '(Empty)' : colorToLegendName.get(color);
+                const percentage = ((100 * frequencyMap[label].zValueCounts[colorTranslation]) / frequencyMap[label].count).toFixed(1);
+                return [label, !stack ? frequencyMap[label]?.zValueCounts[colorTranslation] || 0 : percentage, colorTranslation || ' '];
+              });
+              return {
+                x: colorData.x,
+                y: yValues,
+                type: 'bar' as PlotType,
+                name: legendName,
+                marker: { color: color },
+                customdata: customdata,
+                hovertemplate:
+                  '<b>X: %{customdata[0]}</b><br>' + '<b>Y: %{customdata[1]}</b><br>' + '<b>Color: %{customdata[2]}</b><extra></extra>',
+                ...(stack ? { stackgroup: 'one' } : {}),
+              };
+            });
+
+            return traces;
+          } else {
+            const frequencyMap = xAxisData.reduce(
+              (acc, item) => {
+                acc[item] = (acc[item] || 0) + 1;
+                return acc;
+              },
+              {} as Record<string, number>,
+            );
+
+            const sortedEntries = Object.entries(frequencyMap).sort((a, b) => b[1] - a[1]);
+
+            sortedLabels = sortedEntries.map(([label]) => String(label));
+            sortedFrequencies = sortedEntries.map(([, frequency]) => frequency);
+
+            return [
+              {
+                type: 'bar' as PlotType,
+                x: sortedLabels,
+                y: sortedFrequencies,
+                marker: { color: primaryColor },
+                customdata: sortedLabels,
+                hovertemplate: '<b>%{customdata}</b>: %{y}<extra></extra>',
+              },
+            ];
+          }
+        } else {
+          if (zAxisData && zAxisData?.length > 0) {
+            const binCount = 20; // Number of bins (you can make this configurable)
+            const numericXAxisData = xAxisData.map((val) => Number(val)).filter((val) => !isNaN(val));
+
+            const xMin = numericXAxisData.reduce((min, val) => Math.min(min, val), Infinity);
+            const xMax = numericXAxisData.reduce((max, val) => Math.max(max, val), -Infinity);
+
+            const binSize = (xMax - xMin) / binCount;
+
+            // Create bins
+            const bins = Array.from({ length: binCount }, (_, i) => ({
+              range: [xMin + i * binSize, xMin + (i + 1) * binSize],
+              count: 0,
+              zValueCounts: {} as Record<string, number>, // To track zAxisData counts per bin
+            }));
+
+            // Assign data points to bins
+            numericXAxisData.forEach((xValue, index) => {
+              const zValue = zAxisData ? zAxisData[index] || '(Empty)' : '(Empty)';
+              const binIndex = Math.floor((xValue - xMin) / binSize);
+              const bin = bins[Math.min(binIndex, bins.length - 1)]; // Ensure the last value falls into the final bin
+
+              bin.count++;
+              bin.zValueCounts[zValue] = (bin.zValueCounts[zValue] || 0) + 1;
+            });
+
+            const colorToLegendName = new Map();
+            const tracesByColor: Record<string, { x: string[]; y: number[] }> = {};
+
+            bins.forEach((bin, binIndex) => {
+              const binLabel = `[${bin.range[0].toFixed(1)}, ${bin.range[1].toFixed(1)})`;
+
+              Object.entries(bin.zValueCounts).forEach(([zValue, count]) => {
+                const color = zAxisData ? colorScale(zValue) : primaryColor;
+
+                if (!colorToLegendName.has(color)) {
+                  colorToLegendName.set(color, zValue);
+                }
+
+                if (!tracesByColor[color]) {
+                  tracesByColor[color] = { x: [], y: [] };
+                }
+
+                tracesByColor[color].x.push(binLabel);
+                tracesByColor[color].y.push(stack ? (count / bin.count) * 100 : count);
+              });
+            });
+
+            const traces = Array.from(colorToLegendName.entries()).map(([color, legendName]) => {
+              const colorData = tracesByColor[color];
+              const customdata = colorData.x.map((binLabel, idx) => {
+                const countForColor = colorData.y[idx];
+                const percentage = stack ? countForColor.toFixed(1) + '%' : countForColor.toFixed(0);
+                return [binLabel, countForColor, percentage, legendName];
+              });
+
+              return {
+                x: colorData.x,
+                y: colorData.y,
+                type: 'bar' as PlotType,
+                name: legendName,
+                marker: { color },
+                customdata,
+                autobinx: true,
+                hovertemplate:
+                  '<b>Bin: %{customdata[0]}</b><br>' +
+                  '<b>Count/Percentage: %{customdata[2]}</b><br>' +
+                  '<b>Group: %{customdata[3]}</b><extra></extra>',
+                ...(stack ? { stackgroup: 'one' } : {}),
+              };
+            });
+
+            return traces;
+          } else {
+            // No zAxisData, simple histogram logic
+            return [
+              {
+                type: 'histogram' as PlotType,
+                x: xAxisData,
+                marker: { color: primaryColor },
+                customdata: xAxisData,
+              },
+            ];
+          }
+        }
+      case 'pie':
         return [
           {
-            type: 'histogram',
-            x: xAxisData,
-            marker: { color: primaryColor },
+            type: 'pie' as PlotType,
+            labels: xValues.map(String),
+            values: xAxisData,
+            marker: { colors: mainColors },
           },
         ];
-      }
-    case 'pie':
-      return [
-        {
-          type: 'pie',
-          labels: xValues.map(String),
-          values: xAxisData,
-          marker: { colors: mainColors },
-        },
-      ];
-
-    default:
-      return [];
-  }
+      default:
+        return [];
+    }
+  })();
+
+  const layout: Partial<Plotly.Layout> = {
+    barmode: 'stack',
+    xaxis: {
+      title: {
+        text: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
+        standoff: 30,
+      },
+      tickfont: sharedTickFont,
+      showgrid: false,
+      visible: showAxis,
+      ...(typeof xAxisData[0] === 'string' || (plotType === 'histogram' && sortedLabels.length > 0)
+        ? { type: 'category', categoryarray: sortedLabels, categoryorder: 'array' }
+        : {}),
+      showline: true,
+      zeroline: false,
+      tickvals: typeof xValues[0] == 'string' ? xValues : undefined,
+      ticktext: typeof xValues[0] == 'string' ? truncatedXLabels : undefined,
+    },
+
+    yaxis: {
+      showgrid: false,
+      visible: showAxis,
+      showline: true,
+      zeroline: false,
+      tickfont: sharedTickFont,
+      title: {
+        text: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
+        standoff: 30,
+      },
+      tickvals: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? yValues : undefined,
+      ticktext: typeof yValues[0] === 'string' && (plotType === 'scatter' || plotType === 'line') ? truncatedYLabels : undefined,
+    },
+    font: {
+      family: 'Inter',
+      size: 12,
+      color: '#374151',
+    },
+    hoverlabel: {
+      bgcolor: 'rgba(255, 255, 255, 0.8)',
+      bordercolor: 'rgba(0, 0, 0, 0.2)',
+      font: {
+        family: 'monospace',
+        size: 14,
+        color: '#374151',
+      },
+    },
+  };
+  return { plotData, layout };
 };
 
 export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
@@ -134,6 +556,10 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
   yAxisData,
   xAxisLabel,
   yAxisLabel,
+  groupBy,
+  zAxisData,
+  zAxisLabel,
+  stack,
 }) => {
   const internalRef = useRef<HTMLDivElement>(null);
   const [divSize, setDivSize] = useState({ width: 0, height: 0 });
@@ -182,10 +608,72 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
     setHoveredPoint(null);
   };
 
+  const { plotData, layout } = preparePlotData(
+    xAxisData,
+    plotType,
+    yAxisData,
+    zAxisData,
+    xAxisLabel,
+    yAxisLabel,
+    zAxisLabel,
+    showAxis,
+    groupBy,
+    stack,
+  );
+  // !TODO: implement pattern fill for nonData
+  /*
+  useEffect(() => {
+    const svg = document.querySelector('svg');
+    if (svg) {
+      // Create or find the `defs` section
+      let defs = svg.querySelector('defs');
+      if (!defs) {
+        defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
+        svg.insertBefore(defs, svg.firstChild);
+      }
+
+      // Check if the pattern already exists
+      let pattern = defs.querySelector('#diagonalHatch');
+      if (!pattern) {
+        // Create the diagonal hatch pattern
+        pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
+        pattern.setAttribute('id', 'diagonalHatch');
+        pattern.setAttribute('width', '6');
+        pattern.setAttribute('height', '6');
+        pattern.setAttribute('patternTransform', 'rotate(45)');
+        pattern.setAttribute('patternUnits', 'userSpaceOnUse');
+
+        const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
+        rect.setAttribute('width', '2');
+        rect.setAttribute('height', '6');
+        rect.setAttribute('fill', '#cccccc');
+
+        pattern.appendChild(rect);
+        defs.appendChild(pattern);
+      }
+
+      //const bars = select('.points').selectAll('path').nodes();
+      const bars = document.querySelectorAll('.points path');
+      //console.log(bars);
+      if (plotType === 'histogram') {
+        bars.forEach((bar, index) => {
+          const customData = (plotData[0] as any).customdata[index];
+          //console.log(select(bar), customData, customData == 'nonData');
+          select(bar).style('fill', 'rgb(250, 0, 0)');
+
+          if (customData == 'nonData') {
+            //select(bar).style('fill', 'url(#diagonalHatch)');
+          }
+          //console.log(bar);
+        });
+      }
+    }
+  }, [plotData]);
+  */
   return (
     <div className="h-full w-full flex items-center justify-center overflow-hidden relative" ref={internalRef}>
       <Plot
-        data={getPlotData(xAxisData, plotType, yAxisData)}
+        data={plotData}
         config={{
           responsive: true,
           scrollZoom: false,
@@ -193,38 +681,11 @@ export const CustomChartPlotly: React.FC<CustomChartPlotlyProps> = ({
           displaylogo: false,
         }}
         layout={{
+          ...layout,
           width: divSize.width,
           height: divSize.height,
           title: title,
           dragmode: false,
-          font: {
-            family: 'Inter, sans-serif',
-            size: 12,
-            color: '#374151',
-          },
-          xaxis: {
-            title: showAxis ? (xAxisLabel ? xAxisLabel : '') : '',
-            showgrid: false,
-            visible: showAxis,
-            showline: true,
-            zeroline: false,
-          },
-          yaxis: {
-            title: showAxis ? (yAxisLabel ? yAxisLabel : '') : '',
-            showgrid: false,
-            visible: showAxis,
-            showline: true,
-            zeroline: false,
-          },
-          hoverlabel: {
-            bgcolor: 'rgba(255, 255, 255, 0.8)',
-            bordercolor: 'rgba(0, 0, 0, 0.2)',
-            font: {
-              family: 'Inter, sans-serif',
-              size: 14,
-              color: '#374151',
-            },
-          },
         }}
         onHover={handleHover}
         onUnhover={handleUnhover}
-- 
GitLab