import React, { useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from 'react'; import { Table, AugmentedNodeAttributes } from './components/Table'; import { VisualizationPropTypes, VISComponentType, VisualizationSettingsPropTypes } from '../../common'; import { Input } from '@graphpolaris/shared/lib/components/inputs'; import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { useSearchResultData } from '@graphpolaris/shared/lib/data-access'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; import { Accordion, AccordionBody, AccordionHead, AccordionItem } from '@graphpolaris/shared/lib/components/accordion'; import html2canvas from 'html2canvas'; export interface TableVisHandle { exportImageInternal: () => void; } export type TableProps = { id: string; name: string; showBarplot: boolean; itemsPerPage: number; displayAttributes: string[]; displayEntity: string; maxBarsCount: number; }; const settings: TableProps = { id: 'TableVis', name: 'TableVis', itemsPerPage: 10, showBarplot: true, displayAttributes: [], displayEntity: '', maxBarsCount: 10, }; export const TableVis = forwardRef<TableVisHandle, VisualizationPropTypes<TableProps>>( ({ data, schema, settings, updateSettings, graphMetadata }, refExternal) => { const searchResults = useSearchResultData(); const ref = useRef<HTMLDivElement>(null); useEffect(() => { if (graphMetadata != undefined && settings.displayEntity === '') { if (!graphMetadata.nodes.labels.includes(settings.displayEntity)) { updateSettings({ displayEntity: graphMetadata.nodes.labels[0], displayAttributes: Object.keys(graphMetadata.nodes.types[graphMetadata.nodes.labels[0]].attributes), }); } } }, [graphMetadata, data, settings]); const attributesArray = useMemo<AugmentedNodeAttributes[]>(() => { //const similiarityThreshold = 0.9; let displayAttributesSorted: string[]; displayAttributesSorted = [...settings.displayAttributes].sort((a, b) => a.localeCompare(b)); const dataNodes = (searchResults?.nodes?.length ?? 0) === 0 ? data.nodes : searchResults.nodes; return ( dataNodes .filter((node) => { // some dataset do not have label field let labelNode = ''; if (node.label !== undefined) { labelNode = node.label; } else { const idParts = node._id.split('/'); labelNode = idParts[0]; } return labelNode === settings.displayEntity; }) ///.filter((obj) => obj.similarity === undefined || obj.similarity >= similiarityThreshold) .map((node) => { // get attributes filtered and sorted const filteredAttributes = Object.fromEntries( Object.entries(node.attributes) .filter(([attr]) => settings.displayAttributes.includes(attr)) .sort(([attrA], [attrB]) => settings.displayAttributes.indexOf(attrA) - settings.displayAttributes.indexOf(attrB)), ); // doubled types structure to handle discrepancies in schema object in sb and dev env. let types = schema.nodes.find((n: any) => { let labelNode = node.label; return labelNode === n.key; })?.attributes?.attributes ?? schema.nodes.find((n: any) => { let labelNode = node.label; return labelNode === n.name; })?.attributes; if (types) { return { attribute: filteredAttributes, type: Object.fromEntries(types.map((t: any) => [t.name, t.type])), }; } else { return { attribute: filteredAttributes, type: {}, }; } }) ); }, [data.nodes, settings.displayEntity, settings.displayAttributes, searchResults]); const exportImageInternal = () => { if (ref.current) { // Check if divRef.current is not null html2canvas(ref.current).then((canvas) => { const pngData = canvas.toDataURL('image/png'); const a = document.createElement('a'); a.href = pngData; a.download = 'tablevis.png'; a.click(); }); } else { console.error('The referenced div is null.'); } }; useImperativeHandle(refExternal, () => ({ exportImageInternal, })); return ( <div className="h-full w-full" ref={ref}> {attributesArray.length > 0 && ( <Table data={attributesArray} itemsPerPage={settings.itemsPerPage} showBarPlot={settings.showBarplot} showAttributes={settings.displayAttributes} selectedEntity={settings.displayEntity} maxBarsCount={settings.maxBarsCount} /> )} </div> ); }, ); const TableSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<TableProps>) => { useEffect(() => { if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.displayEntity === '') { updateSettings({ displayEntity: graphMetadata.nodes.labels[0] }); } }, [graphMetadata]); const selectedNodeAttributes = useMemo(() => { if (settings.displayEntity) { const nodeType = graphMetadata.nodes.types[settings.displayEntity]; if (nodeType && nodeType.attributes) { return Object.keys(nodeType.attributes).sort((a, b) => a.localeCompare(b)); } } return []; }, [settings.displayEntity, graphMetadata]); useEffect(() => { if (graphMetadata && graphMetadata.nodes && graphMetadata.nodes.labels.length > 0 && settings.displayAttributes.length === 0) { updateSettings({ displayAttributes: selectedNodeAttributes }); } }, [selectedNodeAttributes, graphMetadata]); return ( <SettingsContainer> <div className="my-2"> <Input className="w-full text-justify justify-center" type="dropdown" value={settings.displayEntity} options={graphMetadata.nodes.labels} onChange={(val) => updateSettings({ displayEntity: val as string })} overrideRender={ <EntityPill title={ <div className="flex flex-row justify-between items-center cursor-pointer"> <span>{settings.displayEntity || ''}</span> <Button variantType="secondary" variant="ghost" size="2xs" iconComponent="icon-[ic--baseline-arrow-drop-down]" /> </div> } /> } ></Input> <div className="my-2"> <Input type="boolean" label="Show barplot" value={settings.showBarplot} onChange={(val) => updateSettings({ showBarplot: val })} /> </div> <div className="my-2"> <Input type="dropdown" label="Items per page" value={settings.itemsPerPage} onChange={(val) => updateSettings({ itemsPerPage: val as number })} options={[10, 25, 50, 100]} /> </div> <div className="my-2"> <Input type="number" label="Max Bars in Bar Plots" value={settings.maxBarsCount} onChange={(val) => updateSettings({ maxBarsCount: val })} /> </div> <Accordion> <AccordionItem> <AccordionHead> <span className="text-sm">Attributes to display:</span> </AccordionHead> <AccordionBody> <Input type="checkbox" value={settings.displayAttributes} options={selectedNodeAttributes} onChange={(val: string[] | string) => { const updatedVal = Array.isArray(val) ? val : [val]; updateSettings({ displayAttributes: updatedVal }); }} /> </AccordionBody> </AccordionItem> </Accordion> </div> </SettingsContainer> ); }; const tableRef = React.createRef<{ exportImageInternal: () => void }>(); export const TableComponent: VISComponentType<TableProps> = { displayName: 'TableVis', description: 'Node Attribute Statistics and Details', component: React.forwardRef((props: VisualizationPropTypes<TableProps>, ref) => <TableVis {...props} ref={tableRef} />), settingsComponent: TableSettings, settings: settings, exportImage: () => { if (tableRef.current) { tableRef.current.exportImageInternal(); } else { console.error('Map reference is not set.'); } }, }; export default TableComponent;