diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx index 16e53908606e83c9977aa6f4eab517132cc051e7..b84ba8f6b71e7fb2418bf571823d2232532bcebd 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 b3c56b11d9584eff7919b3b5fd2b7f2c17293fdb..0000000000000000000000000000000000000000 --- 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 974612decd147b5d645367794c5feaff42fc36c8..ca8bf58d64a4ca28cc235c33f90b84379965fd98 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 0000000000000000000000000000000000000000..d43d86256b52b9b6bd7a151e5f4468c99dbfd05c --- /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