diff --git a/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx b/libs/shared/lib/vis/visualizations/vis1D/Vis1D.tsx index 739c992f50c1d97d51ddf030f4034b484a54dd40..846ab862ba48dc0feab31dc7211e5bcff37c9610 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 796bde78d6d7ab0922d2681ca70c6aa882f6d292..04229b1d0a0d85f8e00b41142911621aed9da77e 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> ); };