tablevis.tsx 8.87 KiB
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 });
}}
/>
test
</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;