diff --git a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx index 9b88cc33f8c76d7b1b72d897e97af5984833ee76..011beea2504b09fc208f7abff19b9864bf14f247 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState, useMemo } from 'react'; +import React, { useEffect, useRef, useState, useMemo, forwardRef, useImperativeHandle } from 'react'; import { PaohvisDataPaginated, RowInformation, LinesHyperEdges } from './types'; import { parseQueryResult } from './utils/dataProcessing'; @@ -15,6 +15,11 @@ import { Button } from '@graphpolaris/shared/lib/components/buttons'; import { EntityPill } from '@graphpolaris/shared/lib/components/pills/Pill'; import { cloneDeep } from 'lodash-es'; import { useImmer } from 'use-immer'; +import html2canvas from 'html2canvas'; + +export interface PaohVisHandle { + exportImageInternal: () => void; +} export type PaohVisProps = { rowHeight: number; @@ -43,320 +48,322 @@ const settings: PaohVisProps = { mergeData: false, }; -export const PaohVis = ({ data, graphMetadata, schema, settings, updateSettings }: VisualizationPropTypes<PaohVisProps>) => { - // general - const [loading, setLoading] = useState(true); - - // row states - const [informationRow, setInformationRow] = useState<RowInformation>([]); // rows that will be rendered, sorted and sliced by pagination - const [informationRowAllData, setInformationRowAllData] = useState<RowInformation>([]); // rows that will be rendered, sorted but not sliced used by pagination - const [informationRowOriginal, setInformationRowOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination - - const [sortingOrderRow, setSortingOrderRow] = useState<'asc' | 'desc' | 'original'>('original'); - const [originalPermutationIndicesRow, setOriginalPermutationIndicesRow] = useState<number[]>([]); - const [permutationIndicesRow, setPermutationIndicesRow] = useState<number[]>([]); - const [previousHeaderRow, setPreviousHeaderRow] = useState<string>('none'); - - // rows visible, even without rows selected - const [numRowsVisible, setNumRowsVisible] = useState<number>(0); - const [numColsVisible, setNumColsVisible] = useState<number>(0); - - // columns states - const [informationColumn, setInformationColumn] = useState<RowInformation>([]); - const [informationColumnAllData, setInformationColumnAllData] = useState<RowInformation>([]); - const [informationColumnOriginal, setInformationColumnOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination - const [sortingOrderColumn, setSortingOrderColumn] = useState<'asc' | 'desc' | 'original'>('original'); - - const [originalPermutationIndicesColumn, setOriginalPermutationIndicesColumn] = useState<number[]>([]); - const [permutationIndicesColumn, setPermutationIndicesColumn] = useState<number[]>([]); - const [previousHeaderColumn, setPreviousHeaderColumn] = useState<string>('none'); - - // lines hyperEdge - const [lineHyperEdges, setLineHyperEdges] = useState<LinesHyperEdges[]>([]); - - // - const [indicesRowsForColumnSort, setIndicesRowsForColumnSort] = useState<number[]>([]); - const [indicesColumnForRowSort, setIndicesColumnForRowSort] = useState<number[]>([]); - - // render states - const svgRef = useRef<SVGSVGElement>(null); - - // states track order headers attributes - const prevDisplayAttributesColumns = useRef<string[]>(); - - // information hyperedgesBlock - // dataModel renders bounded by pagination - const [dataModel, setDataModel] = useImmer<PaohvisDataPaginated>({ - pageData: { - rowLabels: [], - hyperEdgeRanges: [], - rowDegrees: {}, - nodes: [], - edges: [], - }, - data: { - rowLabels: [], - hyperEdgeRanges: [], - rowDegrees: {}, - nodes: [], - edges: [], - }, - originalData: { - rowLabels: [], - hyperEdgeRanges: [], - rowDegrees: {}, - nodes: [], - edges: [], - }, - }); - - const [widthTotalRowInformation, setWidthTotalRowInformation] = useState<number>(0); - const [widthTotalColumnInformation, setWidthTotalColumnInformation] = useState<number>(0); - - const classTopTextColumns = 'font-mono text-secondary-800 mx-1 overflow-hidden whitespace-nowrap text-ellipsis'; - - const configStyle: { [key: string]: string } = { - colorText: 'hsl(var(--clr-sec--800))', - colorTextUnselect: 'hsl(var(--clr-sec--400))', - colorLinesHyperEdge: 'hsl(var(--clr-black))', - colorLinesGrid: 'hsl(var(--clr-sec--300))', - colorLinesGridByClass: 'fill-secondary-300', - }; +const PaohVis = forwardRef<PaohVisHandle, VisualizationPropTypes<PaohVisProps>>( + ({ data, graphMetadata, schema, settings, updateSettings }, refExternal) => { + // general + const [loading, setLoading] = useState(true); - let configPaohvis = useMemo( - () => ({ - rowHeight: 30, - hyperEdgeRanges: 30, - rowsMaxPerPage: settings.numRowsDisplay, - columnsMaxPerPage: settings.numColumnsDisplay, - maxSizeTextColumns: 120, - maxSizeTextRows: 120, - maxSizeTextID: 70, - sizeIcons: 16, - }), - [settings], - ); + // row states + const [informationRow, setInformationRow] = useState<RowInformation>([]); // rows that will be rendered, sorted and sliced by pagination + const [informationRowAllData, setInformationRowAllData] = useState<RowInformation>([]); // rows that will be rendered, sorted but not sliced used by pagination + const [informationRowOriginal, setInformationRowOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination - // - // Methods - // - - const onMouseEnterRowLabels = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { - const targetClassList = (event.currentTarget as SVGGElement).classList; - // all elements - unselect - selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect); - - selectAll('.' + targetClassList[1]) - .selectAll('span') - .style('color', configStyle.colorText); - - // all hyperedges - unselect - const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); - hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3'); - hyperEdgeBlock.selectAll('line').attr('opacity', '.3'); - selectAll('.text-columns').selectAll('span').style('color', configStyle.colorTextUnselect); - - // get row selected - const rowSelection: number = parseInt(targetClassList[1].substring('row-'.length), 10); - - // get circles on the same row - selectAll('.circle-' + (rowSelection + (currentPageRows?.startIndexRow ?? 0))).each(function (d, i) { - // get all hyperedges which are connected those circles - const hyperEdge = (select(this).node() as Element)?.parentNode?.parentNode; - if (hyperEdge instanceof Element) { - const classList = Array.from(hyperEdge.classList); - // text columns - selectAll('.col-' + classList[1].substring('hyperEdge-col-'.length)) - .selectAll('span') - .style('color', configStyle.colorText); - - // hypererdge - select('.' + classList[1]) - .selectAll('circle') - .attr('fill', 'hsl(var(--clr-acc))') - .attr('stroke-opacity', '1'); - - select('.' + classList[1]) - .selectAll('line') - .attr('opacity', '1'); - - // text rows - selectAll('.' + classList[1]) - .select('.hyperEdgeBlockCircles') - .selectAll('circle') - .each(function () { - const circleInside: number = parseInt(select(this).attr('class').substring('circle-'.length), 10); - selectAll('.row-' + (circleInside - (currentPageRows?.startIndexRow ?? 0))) - .selectAll('span') - .style('color', configStyle.colorText); - }); - } - }); - }; + const [sortingOrderRow, setSortingOrderRow] = useState<'asc' | 'desc' | 'original'>('original'); + const [originalPermutationIndicesRow, setOriginalPermutationIndicesRow] = useState<number[]>([]); + const [permutationIndicesRow, setPermutationIndicesRow] = useState<number[]>([]); + const [previousHeaderRow, setPreviousHeaderRow] = useState<string>('none'); - const onMouseLeaveRowLabels = () => { - selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText); - const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); - hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1'); - hyperEdgeBlock.selectAll('circle').attr('fill', 'white'); - hyperEdgeBlock.selectAll('line').attr('opacity', '1'); - selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText); - }; + // rows visible, even without rows selected + const [numRowsVisible, setNumRowsVisible] = useState<number>(0); + const [numColsVisible, setNumColsVisible] = useState<number>(0); - const onMouseEnterHyperEdge = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { - const targetClassList = (event.currentTarget as SVGGElement).classList; - // all elements - const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); - // all elements: hyperedges - hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3'); - hyperEdgeBlock.selectAll('line').attr('opacity', '.3'); - // all elements: column text and row text - selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorTextUnselect); - selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect); - - // selected elements - const hyperEdgeSelected = select('.' + targetClassList[1]); - hyperEdgeSelected.selectAll('circle').attr('fill', 'hsl(var(--clr-acc))'); - hyperEdgeSelected.selectAll('circle').attr('stroke-opacity', '1'); - hyperEdgeSelected.selectAll('line').attr('opacity', '1'); - - // selected elements col text - const columnSelection = targetClassList[1].substring('hyperEdge-'.length); - - selectAll('.' + columnSelection) - .selectAll('span') - .style('color', configStyle.colorText); - - // selected elements nodes text - hyperEdgeSelected.selectAll('circle').each(function (d, i) { - const className = select(this).attr('class'); - - const index = className.split('circle-')[1]; - if (currentPageRows) { - const indexNumber = parseInt(index); - const rowSelector = `.row-${indexNumber - currentPageRows.startIndexRow}`; - select(rowSelector).selectAll('span').style('color', configStyle.colorText); - } + // columns states + const [informationColumn, setInformationColumn] = useState<RowInformation>([]); + const [informationColumnAllData, setInformationColumnAllData] = useState<RowInformation>([]); + const [informationColumnOriginal, setInformationColumnOriginal] = useState<RowInformation>([]); // rows original, no sorted no pagination + const [sortingOrderColumn, setSortingOrderColumn] = useState<'asc' | 'desc' | 'original'>('original'); + + const [originalPermutationIndicesColumn, setOriginalPermutationIndicesColumn] = useState<number[]>([]); + const [permutationIndicesColumn, setPermutationIndicesColumn] = useState<number[]>([]); + const [previousHeaderColumn, setPreviousHeaderColumn] = useState<string>('none'); + + // lines hyperEdge + const [lineHyperEdges, setLineHyperEdges] = useState<LinesHyperEdges[]>([]); + + // + const [indicesRowsForColumnSort, setIndicesRowsForColumnSort] = useState<number[]>([]); + const [indicesColumnForRowSort, setIndicesColumnForRowSort] = useState<number[]>([]); + + // render states + const svgRef = useRef<SVGSVGElement>(null); + const divRef = useRef<HTMLDivElement>(null); + + // states track order headers attributes + const prevDisplayAttributesColumns = useRef<string[]>(); + + // information hyperedgesBlock + // dataModel renders bounded by pagination + const [dataModel, setDataModel] = useImmer<PaohvisDataPaginated>({ + pageData: { + rowLabels: [], + hyperEdgeRanges: [], + rowDegrees: {}, + nodes: [], + edges: [], + }, + data: { + rowLabels: [], + hyperEdgeRanges: [], + rowDegrees: {}, + nodes: [], + edges: [], + }, + originalData: { + rowLabels: [], + hyperEdgeRanges: [], + rowDegrees: {}, + nodes: [], + edges: [], + }, }); - }; - const onMouseLeaveHyperEdge = () => { - // all elements - selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText); - selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText); + const [widthTotalRowInformation, setWidthTotalRowInformation] = useState<number>(0); + const [widthTotalColumnInformation, setWidthTotalColumnInformation] = useState<number>(0); - const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); - hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1'); - hyperEdgeBlock.selectAll('circle').attr('fill', 'white'); - hyperEdgeBlock.selectAll('line').attr('opacity', '1'); - }; + const classTopTextColumns = 'font-mono text-secondary-800 mx-1 overflow-hidden whitespace-nowrap text-ellipsis'; - const handleClickHeaderSorting = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { - // get target header - let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersRows-', ''); - targeHeader = targeHeader == '#' ? '# Connections' : targeHeader; + const configStyle: { [key: string]: string } = { + colorText: 'hsl(var(--clr-sec--800))', + colorTextUnselect: 'hsl(var(--clr-sec--400))', + colorLinesHyperEdge: 'hsl(var(--clr-black))', + colorLinesGrid: 'hsl(var(--clr-sec--300))', + colorLinesGridByClass: 'fill-secondary-300', + }; - // set sorting orders. Tracks header change, new header changes to asc - let newSortingOrder: 'asc' | 'desc' | 'original'; + let configPaohvis = useMemo( + () => ({ + rowHeight: 30, + hyperEdgeRanges: 30, + rowsMaxPerPage: settings.numRowsDisplay, + columnsMaxPerPage: settings.numColumnsDisplay, + maxSizeTextColumns: 120, + maxSizeTextRows: 120, + maxSizeTextID: 70, + sizeIcons: 16, + }), + [settings], + ); - if (targeHeader !== previousHeaderRow) { - newSortingOrder = 'desc'; - } else { - switch (sortingOrderRow) { + // + // Methods + // + + const onMouseEnterRowLabels = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { + const targetClassList = (event.currentTarget as SVGGElement).classList; + // all elements - unselect + selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect); + + selectAll('.' + targetClassList[1]) + .selectAll('span') + .style('color', configStyle.colorText); + + // all hyperedges - unselect + const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); + hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3'); + hyperEdgeBlock.selectAll('line').attr('opacity', '.3'); + selectAll('.text-columns').selectAll('span').style('color', configStyle.colorTextUnselect); + + // get row selected + const rowSelection: number = parseInt(targetClassList[1].substring('row-'.length), 10); + + // get circles on the same row + selectAll('.circle-' + (rowSelection + (currentPageRows?.startIndexRow ?? 0))).each(function (d, i) { + // get all hyperedges which are connected those circles + const hyperEdge = (select(this).node() as Element)?.parentNode?.parentNode; + if (hyperEdge instanceof Element) { + const classList = Array.from(hyperEdge.classList); + // text columns + selectAll('.col-' + classList[1].substring('hyperEdge-col-'.length)) + .selectAll('span') + .style('color', configStyle.colorText); + + // hypererdge + select('.' + classList[1]) + .selectAll('circle') + .attr('fill', 'hsl(var(--clr-acc))') + .attr('stroke-opacity', '1'); + + select('.' + classList[1]) + .selectAll('line') + .attr('opacity', '1'); + + // text rows + selectAll('.' + classList[1]) + .select('.hyperEdgeBlockCircles') + .selectAll('circle') + .each(function () { + const circleInside: number = parseInt(select(this).attr('class').substring('circle-'.length), 10); + selectAll('.row-' + (circleInside - (currentPageRows?.startIndexRow ?? 0))) + .selectAll('span') + .style('color', configStyle.colorText); + }); + } + }); + }; + + const onMouseLeaveRowLabels = () => { + selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText); + const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); + hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1'); + hyperEdgeBlock.selectAll('circle').attr('fill', 'white'); + hyperEdgeBlock.selectAll('line').attr('opacity', '1'); + selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText); + }; + + const onMouseEnterHyperEdge = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { + const targetClassList = (event.currentTarget as SVGGElement).classList; + // all elements + const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); + // all elements: hyperedges + hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '.3'); + hyperEdgeBlock.selectAll('line').attr('opacity', '.3'); + // all elements: column text and row text + selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorTextUnselect); + selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorTextUnselect); + + // selected elements + const hyperEdgeSelected = select('.' + targetClassList[1]); + hyperEdgeSelected.selectAll('circle').attr('fill', 'hsl(var(--clr-acc))'); + hyperEdgeSelected.selectAll('circle').attr('stroke-opacity', '1'); + hyperEdgeSelected.selectAll('line').attr('opacity', '1'); + + // selected elements col text + const columnSelection = targetClassList[1].substring('hyperEdge-'.length); + + selectAll('.' + columnSelection) + .selectAll('span') + .style('color', configStyle.colorText); + + // selected elements nodes text + hyperEdgeSelected.selectAll('circle').each(function (d, i) { + const className = select(this).attr('class'); + + const index = className.split('circle-')[1]; + if (currentPageRows) { + const indexNumber = parseInt(index); + const rowSelector = `.row-${indexNumber - currentPageRows.startIndexRow}`; + select(rowSelector).selectAll('span').style('color', configStyle.colorText); + } + }); + }; + + const onMouseLeaveHyperEdge = () => { + // all elements + selectAll('.colsLabel').selectAll('span').style('color', configStyle.colorText); + selectAll('.rowsLabel').selectAll('span').style('color', configStyle.colorText); + + const hyperEdgeBlock = selectAll('.hyperEdgeBlock'); + hyperEdgeBlock.selectAll('circle').attr('stroke-opacity', '1'); + hyperEdgeBlock.selectAll('circle').attr('fill', 'white'); + hyperEdgeBlock.selectAll('line').attr('opacity', '1'); + }; + + const handleClickHeaderSorting = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { + // get target header + let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersRows-', ''); + targeHeader = targeHeader == '#' ? '# Connections' : targeHeader; + + // set sorting orders. Tracks header change, new header changes to asc + let newSortingOrder: 'asc' | 'desc' | 'original'; + + if (targeHeader !== previousHeaderRow) { + newSortingOrder = 'desc'; + } else { + switch (sortingOrderRow) { + case 'asc': + newSortingOrder = 'original'; + break; + case 'desc': + newSortingOrder = 'asc'; + break; + case 'original': + newSortingOrder = 'desc'; + break; + } + } + if (newSortingOrder == 'original') { + // reset previous state + setPreviousHeaderRow('none'); + } else { + setPreviousHeaderRow(targeHeader); + } + + setSortingOrderRow(newSortingOrder); + + // get permutations indices + let sortedIndices: number[]; + switch (newSortingOrder) { case 'asc': - newSortingOrder = 'original'; + sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'asc'); break; case 'desc': - newSortingOrder = 'asc'; + sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'desc'); break; case 'original': - newSortingOrder = 'desc'; + sortedIndices = originalPermutationIndicesRow; break; - } - } - if (newSortingOrder == 'original') { - // reset previous state - setPreviousHeaderRow('none'); - } else { - setPreviousHeaderRow(targeHeader); - } - - setSortingOrderRow(newSortingOrder); - - // get permutations indices - let sortedIndices: number[]; - switch (newSortingOrder) { - case 'asc': - sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'asc'); - break; - case 'desc': - sortedIndices = sortIndices(informationRowOriginal, targeHeader, 'desc'); - break; - case 'original': - sortedIndices = originalPermutationIndicesRow; - break; - - default: - sortedIndices = []; - break; - } - - setIndicesRowsForColumnSort(sortedIndices); - // sort according permutations - const sortedRowInformation = sortRowInformation(informationRowOriginal, sortedIndices); - const sortedRowInformationSliced = sortedRowInformation.map((row) => ({ - ...row, - data: row.data.slice(currentPageRows?.startIndexRow, currentPageRows?.endIndexRow), - })); - - // update rows - const sortedRowInformationSlicedFiltered = sortedRowInformationSliced.filter((row) => settings.attributeRowShow.includes(row.header)); - - setInformationRow(sortedRowInformationSlicedFiltered); - setInformationRowAllData(sortedRowInformation); - // hyperEdge - sort according permutations indices - const dataModelOriginalTemporal = cloneDeep(dataModel.originalData); - - for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) { - for ( - let indexRowsIndices = 0; - indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length; - indexRowsIndices++ - ) { - dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = sortedIndices.indexOf( - dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices], - ); + default: + sortedIndices = []; + break; } - // sort indices - correct render - dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[ - indexColOrder - ].hyperEdges.indices.sort((a: number, b: number) => a - b); - } + setIndicesRowsForColumnSort(sortedIndices); + // sort according permutations + const sortedRowInformation = sortRowInformation(informationRowOriginal, sortedIndices); + const sortedRowInformationSliced = sortedRowInformation.map((row) => ({ + ...row, + data: row.data.slice(currentPageRows?.startIndexRow, currentPageRows?.endIndexRow), + })); + + // update rows + const sortedRowInformationSlicedFiltered = sortedRowInformationSliced.filter((row) => settings.attributeRowShow.includes(row.header)); + + setInformationRow(sortedRowInformationSlicedFiltered); + setInformationRowAllData(sortedRowInformation); + + // hyperEdge - sort according permutations indices + const dataModelOriginalTemporal = cloneDeep(dataModel.originalData); + + for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) { + for ( + let indexRowsIndices = 0; + indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length; + indexRowsIndices++ + ) { + dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = sortedIndices.indexOf( + dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices], + ); + } - const dataModelOriginalTemporalSorted = indicesColumnForRowSort - .map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index]) - .filter((d) => !!d); - const sortedArrayDataModelFiltered = dataModelOriginalTemporalSorted.slice( - currentPageColumns?.startIndexColumn, - currentPageColumns?.endIndexColumn, - ); + // sort indices - correct render + dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[ + indexColOrder + ].hyperEdges.indices.sort((a: number, b: number) => a - b); + } - setDataModel((draft) => { - draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered; - draft.data.hyperEdgeRanges = dataModelOriginalTemporalSorted; - }); + const dataModelOriginalTemporalSorted = indicesColumnForRowSort + .map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index]) + .filter((d) => !!d); + const sortedArrayDataModelFiltered = dataModelOriginalTemporalSorted.slice( + currentPageColumns?.startIndexColumn, + currentPageColumns?.endIndexColumn, + ); - // Update lines - if (currentPageRows) { - const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => { - return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices); + setDataModel((draft) => { + draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered; + draft.data.hyperEdgeRanges = dataModelOriginalTemporalSorted; }); - setLineHyperEdges(newLinePositions); - } - /* + // Update lines + if (currentPageRows) { + const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => { + return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices); + }); + setLineHyperEdges(newLinePositions); + } + + /* setDataModel({ rowLabels: dataModelOriginalTemporal.rowLabels.slice(currentPageRows?.startIndexRow, currentPageRows?.endIndexRow), hyperEdgeRanges: dataModelOriginalTemporalSorted.hyperEdgeRanges.slice( @@ -370,533 +377,569 @@ export const PaohVis = ({ data, graphMetadata, schema, settings, updateSettings hyperEdgeRanges: dataModelOriginalTemporal.hyperEdgeRanges, }); */ - }; + }; - const handleClickHeaderSortingColumns = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { - // get target header - let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersCols-', ''); + const handleClickHeaderSortingColumns = (event: React.MouseEvent<SVGGElement, MouseEvent>) => { + // get target header + let targeHeader = (event.currentTarget as SVGGElement).classList[0].replace('headersCols-', ''); + + targeHeader = targeHeader == '#' ? '# Connections' : targeHeader; + // set sorting orders. Tracks header change, new header changes to asc + let newSortingOrder: 'asc' | 'desc' | 'original'; + + if (targeHeader !== previousHeaderColumn) { + newSortingOrder = 'desc'; + } else { + switch (sortingOrderColumn) { + case 'asc': + newSortingOrder = 'original'; + break; + case 'desc': + newSortingOrder = 'asc'; + break; + case 'original': + newSortingOrder = 'desc'; + break; + } + } + if (newSortingOrder == 'original') { + // reset previos state + setPreviousHeaderColumn('none'); + } else { + setPreviousHeaderColumn(targeHeader); + } - targeHeader = targeHeader == '#' ? '# Connections' : targeHeader; - // set sorting orders. Tracks header change, new header changes to asc - let newSortingOrder: 'asc' | 'desc' | 'original'; + setSortingOrderColumn(newSortingOrder); - if (targeHeader !== previousHeaderColumn) { - newSortingOrder = 'desc'; - } else { - switch (sortingOrderColumn) { + // get permutations indices + let sortedIndices: number[]; + switch (newSortingOrder) { case 'asc': - newSortingOrder = 'original'; + sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'asc'); break; case 'desc': - newSortingOrder = 'asc'; + sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'desc'); break; case 'original': - newSortingOrder = 'desc'; + sortedIndices = originalPermutationIndicesColumn; + break; + + default: + sortedIndices = []; break; } - } - if (newSortingOrder == 'original') { - // reset previos state - setPreviousHeaderColumn('none'); - } else { - setPreviousHeaderColumn(targeHeader); - } - setSortingOrderColumn(newSortingOrder); - - // get permutations indices - let sortedIndices: number[]; - switch (newSortingOrder) { - case 'asc': - sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'asc'); - break; - case 'desc': - sortedIndices = sortIndices(informationColumnOriginal, targeHeader, 'desc'); - break; - case 'original': - sortedIndices = originalPermutationIndicesColumn; - break; - - default: - sortedIndices = []; - break; - } + // sort according permutations + const sortedColumnInformation = sortRowInformation(informationColumnOriginal, sortedIndices); - // sort according permutations - const sortedColumnInformation = sortRowInformation(informationColumnOriginal, sortedIndices); + // slice according pagination + const sortedColumnInformationSliced = sortedColumnInformation.map((row) => ({ + ...row, + data: row.data.slice(currentPageColumns?.startIndexColumn, currentPageColumns?.endIndexColumn), + })); - // slice according pagination - const sortedColumnInformationSliced = sortedColumnInformation.map((row) => ({ - ...row, - data: row.data.slice(currentPageColumns?.startIndexColumn, currentPageColumns?.endIndexColumn), - })); + // update rows + const sortedColumnInformationSlicedFiltered = sortedColumnInformationSliced.filter((row) => + settings.attributeColumnShow.includes(row.header), + ); - // update rows - const sortedColumnInformationSlicedFiltered = sortedColumnInformationSliced.filter((row) => - settings.attributeColumnShow.includes(row.header), - ); + setInformationColumn(sortedColumnInformationSlicedFiltered); + setInformationColumnAllData(sortedColumnInformation); + // hyperEdge - sort according permutations indices + const dataModelOriginalTemporal = cloneDeep(dataModel.originalData); + for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) { + for ( + let indexRowsIndices = 0; + indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length; + indexRowsIndices++ + ) { + dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = indicesRowsForColumnSort.indexOf( + dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices], + ); + } - setInformationColumn(sortedColumnInformationSlicedFiltered); - setInformationColumnAllData(sortedColumnInformation); - // hyperEdge - sort according permutations indices - const dataModelOriginalTemporal = cloneDeep(dataModel.originalData); - for (let indexColOrder = 0; indexColOrder < dataModel.originalData.hyperEdgeRanges.length; indexColOrder++) { - for ( - let indexRowsIndices = 0; - indexRowsIndices < dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices.length; - indexRowsIndices++ - ) { - dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices] = indicesRowsForColumnSort.indexOf( - dataModel.originalData.hyperEdgeRanges[indexColOrder].hyperEdges.indices[indexRowsIndices], - ); + // sort indices - correct render + dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[ + indexColOrder + ].hyperEdges.indices.sort((a: number, b: number) => a - b); } - // sort indices - correct render - dataModelOriginalTemporal.hyperEdgeRanges[indexColOrder].hyperEdges.indices = dataModelOriginalTemporal.hyperEdgeRanges[ - indexColOrder - ].hyperEdges.indices.sort((a: number, b: number) => a - b); - } - - setIndicesColumnForRowSort(sortedIndices); - const sortedArrayDataModel = sortedIndices.map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index]).filter((d) => !!d); - - const sortedArrayDataModelFiltered = sortedArrayDataModel.slice( - currentPageColumns?.startIndexColumn, - currentPageColumns?.endIndexColumn, - ); + setIndicesColumnForRowSort(sortedIndices); + const sortedArrayDataModel = sortedIndices.map((index) => dataModelOriginalTemporal.hyperEdgeRanges[index]).filter((d) => !!d); - setDataModel((draft) => { - draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered; - draft.data.hyperEdgeRanges = sortedArrayDataModel; - }); + const sortedArrayDataModelFiltered = sortedArrayDataModel.slice( + currentPageColumns?.startIndexColumn, + currentPageColumns?.endIndexColumn, + ); - // Update lines hyperedges - if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) { - const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => { - return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices); + setDataModel((draft) => { + draft.pageData.hyperEdgeRanges = sortedArrayDataModelFiltered; + draft.data.hyperEdgeRanges = sortedArrayDataModel; }); - setLineHyperEdges(newLinePositions); - } - }; - - useEffect(() => { - if (!svgRef.current) return; - const resizeObserver = new ResizeObserver(() => {}); - resizeObserver.observe(svgRef.current); - return () => resizeObserver.disconnect(); // clean up - }, []); - useEffect(() => { - if ( - graphMetadata && - settings.columnNode !== '' && - settings.rowNode !== '' && - graphMetadata.nodes.types[settings.rowNode] && - graphMetadata.nodes.types[settings.columnNode] - ) { - const firstColumnLabels = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes).slice(0, 2); - const firstRowLabels = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes).slice(0, 2); + // Update lines hyperedges + if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) { + const newLinePositions = sortedArrayDataModelFiltered.map((hyperEdgeRange) => { + return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices); + }); + setLineHyperEdges(newLinePositions); + } + }; - if (firstColumnLabels && firstRowLabels) { - if (settings.attributeColumnShow.includes('_id')) { - updateSettings({ attributeColumnShow: [...firstColumnLabels, '# Connections'] }); - } - if (settings.attributeRowShow.includes('_id')) { - updateSettings({ attributeRowShow: [...firstRowLabels, '# Connections'] }); + useEffect(() => { + if (!svgRef.current) return; + const resizeObserver = new ResizeObserver(() => {}); + resizeObserver.observe(svgRef.current); + return () => resizeObserver.disconnect(); // clean up + }, []); + + useEffect(() => { + if ( + graphMetadata && + settings.columnNode !== '' && + settings.rowNode !== '' && + graphMetadata.nodes.types[settings.rowNode] && + graphMetadata.nodes.types[settings.columnNode] + ) { + const firstColumnLabels = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes).slice(0, 2); + const firstRowLabels = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes).slice(0, 2); + + if (firstColumnLabels && firstRowLabels) { + if (settings.attributeColumnShow.includes('_id')) { + updateSettings({ attributeColumnShow: [...firstColumnLabels, '# Connections'] }); + } + if (settings.attributeRowShow.includes('_id')) { + updateSettings({ attributeRowShow: [...firstRowLabels, '# Connections'] }); + } + setTimeout(() => setLoading(false), 100); // wait for the settings to update first } - setTimeout(() => setLoading(false), 100); // wait for the settings to update first } - } - }, [graphMetadata, settings]); + }, [graphMetadata, settings]); + + useEffect(() => { + if (loading || settings.rowNode === '' || settings.columnNode === '') return; + // set new data + // for dev env + + let labelEdge = ''; + let edgeSchema; + + let toNode = ''; + + let columnNodeAttributes; + let rowNodeAttributes; + + if (graphMetadata != undefined) { + labelEdge = graphMetadata.edges.labels[0]; + edgeSchema = schema.edges.find((obj) => String(obj.key).includes(labelEdge)); + toNode = edgeSchema?.target as string; + columnNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes); + rowNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes); + } else { + if ('to' in schema.edges[0]) toNode = schema.edges[0].to as string; + columnNodeAttributes = schema.nodes + .find((node: any) => { + return node.name === settings.columnNode; + }) + ?.attributes?.map((attributesStructure: any) => attributesStructure.name); + + rowNodeAttributes = schema.nodes + .find((node: any) => { + return node.name === settings.rowNode; + }) + ?.attributes?.map((attributesStructure: any) => attributesStructure.name); + } + const newData = parseQueryResult(data, settings as PaohVisProps, toNode, settings.mergeData); + // original data without slicing + setNumRowsVisible(Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)); + setNumColsVisible(Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length)); + + const rowNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.rowNode)); + const columnNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.columnNode)); + // to keep order of new attributes + prevDisplayAttributesColumns.current = [...columnNodeAttributes]; + + setDataModel({ + pageData: { + rowLabels: newData.rowLabels.slice(0, configPaohvis.rowsMaxPerPage), + hyperEdgeRanges: newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage), + rowDegrees: newData.rowDegrees, + nodes: newData.nodes, + edges: newData.edges, + }, + data: newData, + originalData: newData, + }); - useEffect(() => { - if (loading || settings.rowNode === '' || settings.columnNode === '') return; - // set new data - // for dev env + // Update lines hyperedges + const newLinePositions = newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage).map((hyperEdgeRange) => { + return intersectionElements( + [0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)], + hyperEdgeRange.hyperEdges.indices, + ); + }); + setLineHyperEdges(newLinePositions); - let labelEdge = ''; - let edgeSchema; + const originalIndicesColumns = Array.from({ length: newData.hyperEdgeRanges.length + 1 }, (_, index) => index); - let toNode = ''; + setIndicesColumnForRowSort(originalIndicesColumns); + setCurrentPageColumns({ + startIndexColumn: 0, + endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length), + }); - let columnNodeAttributes; - let rowNodeAttributes; + const hyperEdgeRangesKeys = [...newData.hyperEdgeRanges.keys()]; + setPermutationIndicesColumn(hyperEdgeRangesKeys); + setOriginalPermutationIndicesColumn(hyperEdgeRangesKeys); + setCurrentPageRows({ + startIndexRow: 0, + endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length), + }); - if (graphMetadata != undefined) { - labelEdge = graphMetadata.edges.labels[0]; - edgeSchema = schema.edges.find((obj) => String(obj.key).includes(labelEdge)); - toNode = edgeSchema?.target as string; - columnNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.columnNode].attributes); - rowNodeAttributes = Object.keys(graphMetadata.nodes.types[settings.rowNode].attributes); - } else { - if ('to' in schema.edges[0]) toNode = schema.edges[0].to as string; - columnNodeAttributes = schema.nodes - .find((node: any) => { - return node.name === settings.columnNode; - }) - ?.attributes?.map((attributesStructure: any) => attributesStructure.name); - - rowNodeAttributes = schema.nodes - .find((node: any) => { - return node.name === settings.rowNode; - }) - ?.attributes?.map((attributesStructure: any) => attributesStructure.name); - } - const newData = parseQueryResult(data, settings as PaohVisProps, toNode, settings.mergeData); - // original data without slicing - setNumRowsVisible(Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)); - setNumColsVisible(Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length)); + // columnInformation + const informationColumnTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries( + graphMetadata.nodes.types[settings.columnNode].attributes, + ).map(([k, v]) => { + const mappedData = columnNodes.map((node) => node.attributes[k]); + return { + header: k, + data: mappedData, + width: configPaohvis.maxSizeTextRows, + }; + }); - const rowNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.rowNode)); - const columnNodes = newData.nodes.filter((obj) => obj[graphMetadata === undefined ? '_id' : 'label'].includes(settings.columnNode)); - // to keep order of new attributes - prevDisplayAttributesColumns.current = [...columnNodeAttributes]; + const columnsIdDegree: { [_id: string]: number } = newData.hyperEdgeRanges.reduce((acc: { [_id: string]: number }, node) => { + acc[node._id] = node.degree; + return acc; + }, {}); - setDataModel({ - pageData: { - rowLabels: newData.rowLabels.slice(0, configPaohvis.rowsMaxPerPage), - hyperEdgeRanges: newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage), - rowDegrees: newData.rowDegrees, - nodes: newData.nodes, - edges: newData.edges, - }, - data: newData, - originalData: newData, - }); + informationColumnTemporalOriginal.push({ + header: '# Connections', + data: Object.values(columnsIdDegree), + width: configPaohvis.maxSizeTextColumns, + }); - // Update lines hyperedges - const newLinePositions = newData.hyperEdgeRanges.slice(0, configPaohvis.columnsMaxPerPage).map((hyperEdgeRange) => { - return intersectionElements([0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)], hyperEdgeRange.hyperEdges.indices); - }); - setLineHyperEdges(newLinePositions); + informationColumnTemporalOriginal.push({ + header: '_id', + data: columnNodes.map((node) => node._id), + width: configPaohvis.maxSizeTextID, + }); - const originalIndicesColumns = Array.from({ length: newData.hyperEdgeRanges.length + 1 }, (_, index) => index); + const informationColumnTemporal: { header: string; data: any[]; width: number }[] = informationColumnTemporalOriginal.map((d) => ({ + ...d, + data: d.data.slice(0, Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length)), + })); - setIndicesColumnForRowSort(originalIndicesColumns); - setCurrentPageColumns({ - startIndexColumn: 0, - endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length), - }); + setPermutationIndicesRow(originalPermutationIndicesRow); - const hyperEdgeRangesKeys = [...newData.hyperEdgeRanges.keys()]; - setPermutationIndicesColumn(hyperEdgeRangesKeys); - setOriginalPermutationIndicesColumn(hyperEdgeRangesKeys); - setCurrentPageRows({ - startIndexRow: 0, - endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length), - }); + const columnsAttrOrder = prevDisplayAttributesColumns.current; - // columnInformation - const informationColumnTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries( - graphMetadata.nodes.types[settings.columnNode].attributes, - ).map(([k, v]) => { - const mappedData = columnNodes.map((node) => node.attributes[k]); - return { - header: k, - data: mappedData, - width: configPaohvis.maxSizeTextRows, - }; - }); + if (columnsAttrOrder) { + // sort them according order of showing them + informationColumnTemporal.sort((a, b) => { + const indexA = columnsAttrOrder.indexOf(a.header); + const indexB = columnsAttrOrder.indexOf(b.header); + return indexA - indexB; + }); - const columnsIdDegree: { [_id: string]: number } = newData.hyperEdgeRanges.reduce((acc: { [_id: string]: number }, node) => { - acc[node._id] = node.degree; - return acc; - }, {}); + informationColumnTemporalOriginal.sort((a, b) => { + const indexA = columnsAttrOrder.indexOf(a.header); + const indexB = columnsAttrOrder.indexOf(b.header); + return indexA - indexB; + }); - informationColumnTemporalOriginal.push({ - header: '# Connections', - data: Object.values(columnsIdDegree), - width: configPaohvis.maxSizeTextColumns, - }); + // select necessary variables to show + const filteredInformationColumnTemporal = informationColumnTemporal.filter((row) => + settings.attributeColumnShow.includes(row.header), + ); - informationColumnTemporalOriginal.push({ - header: '_id', - data: columnNodes.map((node) => node._id), - width: configPaohvis.maxSizeTextID, - }); + // set data + setInformationColumn(filteredInformationColumnTemporal); + setInformationColumnOriginal(informationColumnTemporalOriginal); + setInformationColumnAllData(informationColumnTemporalOriginal); + const totalWidthColumnInformation = filteredInformationColumnTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight; + setWidthTotalColumnInformation(totalWidthColumnInformation); + } else { + console.error(`Nodes for entity are undefined or empty.`); + } - const informationColumnTemporal: { header: string; data: any[]; width: number }[] = informationColumnTemporalOriginal.map((d) => ({ - ...d, - data: d.data.slice(0, Math.min(configPaohvis.columnsMaxPerPage, newData.hyperEdgeRanges.length)), - })); + // rowInformation - entityVertical + // build + + const informationRowTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries( + graphMetadata.nodes.types[settings.rowNode].attributes, + ).map(([k, v]) => { + const mappedData = rowNodes.map((node) => node.attributes[k]); - setPermutationIndicesRow(originalPermutationIndicesRow); + return { + header: k, + data: mappedData, + width: configPaohvis.maxSizeTextRows, + }; + }); - const columnsAttrOrder = prevDisplayAttributesColumns.current; + const idsRows = rowNodes.map((obj) => obj._id); - if (columnsAttrOrder) { - // sort them according order of showing them - informationColumnTemporal.sort((a, b) => { - const indexA = columnsAttrOrder.indexOf(a.header); - const indexB = columnsAttrOrder.indexOf(b.header); - return indexA - indexB; + informationRowTemporalOriginal.push({ + header: '# Connections', + data: idsRows.map((id) => newData.rowDegrees[id]), + width: configPaohvis.maxSizeTextRows, }); - informationColumnTemporalOriginal.sort((a, b) => { - const indexA = columnsAttrOrder.indexOf(a.header); - const indexB = columnsAttrOrder.indexOf(b.header); - return indexA - indexB; + informationRowTemporalOriginal.push({ + header: '_id', + data: idsRows, + width: configPaohvis.maxSizeTextID, //configPaohvis.maxSizeTextRows, }); - // select necessary variables to show - const filteredInformationColumnTemporal = informationColumnTemporal.filter((row) => - settings.attributeColumnShow.includes(row.header), - ); + const informationRowTemporal: { header: string; data: any[]; width: number }[] = informationRowTemporalOriginal.map((d) => ({ + ...d, + data: d.data.slice(0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)), + })); - // set data - setInformationColumn(filteredInformationColumnTemporal); - setInformationColumnOriginal(informationColumnTemporalOriginal); - setInformationColumnAllData(informationColumnTemporalOriginal); - const totalWidthColumnInformation = filteredInformationColumnTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight; - setWidthTotalColumnInformation(totalWidthColumnInformation); - } else { - console.error(`Nodes for entity are undefined or empty.`); - } + const objectWithIdHeader = informationRowTemporalOriginal.find((obj) => obj.header === '_id'); + let originalIndices: number[] = []; - // rowInformation - entityVertical - // build + if (objectWithIdHeader) { + originalIndices = [...objectWithIdHeader.data.keys()]; + } else { + console.error("Row Object with header 'id' not found"); + } + setOriginalPermutationIndicesRow(originalIndices); + setIndicesRowsForColumnSort(originalIndices); - const informationRowTemporalOriginal: { header: string; data: any[]; width: number }[] = Object.entries( - graphMetadata.nodes.types[settings.rowNode].attributes, - ).map(([k, v]) => { - const mappedData = rowNodes.map((node) => node.attributes[k]); + // select necessary variables to show + const filteredInformationRowTemporal = informationRowTemporal.filter((row) => settings.attributeRowShow.includes(row.header)); - return { - header: k, - data: mappedData, - width: configPaohvis.maxSizeTextRows, - }; + // set data + setInformationRow(filteredInformationRowTemporal); + setInformationRowOriginal(informationRowTemporalOriginal); + setInformationRowAllData(informationRowTemporalOriginal); + const totalWidthRowInformation = filteredInformationRowTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight; + + setWidthTotalRowInformation(totalWidthRowInformation); + }, [ + settings.rowNode, + settings.columnNode, + configPaohvis, + settings.attributeRowShow, + settings.attributeColumnShow, + settings.mergeData, + loading, + ]); + + const [currentPageColumns, setCurrentPageColumns] = useState<{ + startIndexColumn: number; + endIndexColumn: number; + } | null>({ + startIndexColumn: 0, + endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length), }); - const idsRows = rowNodes.map((obj) => obj._id); - - informationRowTemporalOriginal.push({ - header: '# Connections', - data: idsRows.map((id) => newData.rowDegrees[id]), - width: configPaohvis.maxSizeTextRows, + const [currentPageRows, setCurrentPageRows] = useState<{ + startIndexRow: number; + endIndexRow: number; + } | null>({ + startIndexRow: 0, + endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length), }); - informationRowTemporalOriginal.push({ - header: '_id', - data: idsRows, - width: configPaohvis.maxSizeTextID, //configPaohvis.maxSizeTextRows, - }); + const computedSizesSvg = useMemo(() => { + let tableWidth = 0; + let tableWidthWithExtraColumnLabelWidth = 0; - const informationRowTemporal: { header: string; data: any[]; width: number }[] = informationRowTemporalOriginal.map((d) => ({ - ...d, - data: d.data.slice(0, Math.min(configPaohvis.rowsMaxPerPage, newData.rowLabels.length)), - })); + dataModel.pageData.hyperEdgeRanges.forEach((hyperEdgeRange) => { + const columnWidth = 1 * settings.rowHeight; + tableWidth += columnWidth; - const objectWithIdHeader = informationRowTemporalOriginal.find((obj) => obj.header === '_id'); - let originalIndices: number[] = []; + if (tableWidth > tableWidthWithExtraColumnLabelWidth) tableWidthWithExtraColumnLabelWidth = tableWidth; + }); - if (objectWithIdHeader) { - originalIndices = [...objectWithIdHeader.data.keys()]; - } else { - console.error("Row Object with header 'id' not found"); - } - setOriginalPermutationIndicesRow(originalIndices); - setIndicesRowsForColumnSort(originalIndices); - - // select necessary variables to show - const filteredInformationRowTemporal = informationRowTemporal.filter((row) => settings.attributeRowShow.includes(row.header)); - - // set data - setInformationRow(filteredInformationRowTemporal); - setInformationRowOriginal(informationRowTemporalOriginal); - setInformationRowAllData(informationRowTemporalOriginal); - const totalWidthRowInformation = filteredInformationRowTemporal.reduce((acc, row) => acc + row.width, 0) + settings.rowHeight; - - setWidthTotalRowInformation(totalWidthRowInformation); - }, [ - settings.rowNode, - settings.columnNode, - configPaohvis, - settings.attributeRowShow, - settings.attributeColumnShow, - settings.mergeData, - loading, - ]); - - const [currentPageColumns, setCurrentPageColumns] = useState<{ - startIndexColumn: number; - endIndexColumn: number; - } | null>({ - startIndexColumn: 0, - endIndexColumn: Math.min(configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length), - }); - - const [currentPageRows, setCurrentPageRows] = useState<{ - startIndexRow: number; - endIndexRow: number; - } | null>({ - startIndexRow: 0, - endIndexRow: Math.min(configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length), - }); - - const computedSizesSvg = useMemo(() => { - let tableWidth = 0; - let tableWidthWithExtraColumnLabelWidth = 0; - - dataModel.pageData.hyperEdgeRanges.forEach((hyperEdgeRange) => { - const columnWidth = 1 * settings.rowHeight; - tableWidth += columnWidth; - - if (tableWidth > tableWidthWithExtraColumnLabelWidth) tableWidthWithExtraColumnLabelWidth = tableWidth; - }); + let finalTableWidth = tableWidthWithExtraColumnLabelWidth; + finalTableWidth += widthTotalRowInformation; + finalTableWidth += finalTableWidth * 0.01; - let finalTableWidth = tableWidthWithExtraColumnLabelWidth; - finalTableWidth += widthTotalRowInformation; - finalTableWidth += finalTableWidth * 0.01; + return { + tableWidthWithExtraColumnLabelWidth: finalTableWidth, + colWidth: widthTotalColumnInformation, + }; + }, [ + dataModel, + settings.rowHeight, + widthTotalColumnInformation, + widthTotalRowInformation, + settings.attributeColumnShow, + settings.attributeRowShow, + ]); + + const onWheel = (event: React.WheelEvent<SVGSVGElement>) => { + if (event.deltaY !== 0) { + if (event.shiftKey) onPageChangeColumns(event.deltaY > 0 ? settings.colJumpAmount : -settings.colJumpAmount); + else onPageChangeRows(event.deltaY > 0 ? settings.rowJumpAmount : -settings.rowJumpAmount); + } - return { - tableWidthWithExtraColumnLabelWidth: finalTableWidth, - colWidth: widthTotalColumnInformation, + if (event.deltaX !== 0) { + onPageChangeColumns(event.deltaX > 0 ? settings.colJumpAmount : -settings.colJumpAmount); + } }; - }, [ - dataModel, - settings.rowHeight, - widthTotalColumnInformation, - widthTotalRowInformation, - settings.attributeColumnShow, - settings.attributeRowShow, - ]); - - const onWheel = (event: React.WheelEvent<SVGSVGElement>) => { - if (event.deltaY !== 0) { - if (event.shiftKey) onPageChangeColumns(event.deltaY > 0 ? settings.colJumpAmount : -settings.colJumpAmount); - else onPageChangeRows(event.deltaY > 0 ? settings.rowJumpAmount : -settings.rowJumpAmount); - } - if (event.deltaX !== 0) { - onPageChangeColumns(event.deltaX > 0 ? settings.colJumpAmount : -settings.colJumpAmount); - } - }; + const onPageChangeColumns = (delta: number) => { + const startIndexColumn = (currentPageColumns?.startIndexColumn || 0) + delta; + if (startIndexColumn < 0 || startIndexColumn + 10 > dataModel.data.hyperEdgeRanges.length) return; + const endIndexColumn = Math.min(startIndexColumn + configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length); + setNumColsVisible(endIndexColumn - startIndexColumn); - const onPageChangeColumns = (delta: number) => { - const startIndexColumn = (currentPageColumns?.startIndexColumn || 0) + delta; - if (startIndexColumn < 0 || startIndexColumn + 10 > dataModel.data.hyperEdgeRanges.length) return; - const endIndexColumn = Math.min(startIndexColumn + configPaohvis.columnsMaxPerPage, dataModel.data.hyperEdgeRanges.length); - setNumColsVisible(endIndexColumn - startIndexColumn); + const slicedHyperEdgeRanges = dataModel.data.hyperEdgeRanges.slice(startIndexColumn, endIndexColumn); - const slicedHyperEdgeRanges = dataModel.data.hyperEdgeRanges.slice(startIndexColumn, endIndexColumn); + setDataModel((draft) => { + draft.pageData.hyperEdgeRanges = slicedHyperEdgeRanges; + }); - setDataModel((draft) => { - draft.pageData.hyperEdgeRanges = slicedHyperEdgeRanges; - }); + // Update lines hyperedges + if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) { + const newLinePositions = slicedHyperEdgeRanges.map((hyperEdgeRange) => { + return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices); + }); + setLineHyperEdges(newLinePositions); + } - // Update lines hyperedges - if (currentPageRows?.startIndexRow !== undefined && currentPageRows?.endIndexRow !== undefined) { - const newLinePositions = slicedHyperEdgeRanges.map((hyperEdgeRange) => { - return intersectionElements([currentPageRows?.startIndexRow, currentPageRows?.endIndexRow], hyperEdgeRange.hyperEdges.indices); - }); - setLineHyperEdges(newLinePositions); - } + // columns - // columns + const dataColumnsSorted = cloneDeep(informationColumnAllData); - const dataColumnsSorted = cloneDeep(informationColumnAllData); + const dataColumnsSortedSliced = dataColumnsSorted.map((table) => ({ + ...table, + data: table.data.slice(startIndexColumn, endIndexColumn), + })); - const dataColumnsSortedSliced = dataColumnsSorted.map((table) => ({ - ...table, - data: table.data.slice(startIndexColumn, endIndexColumn), - })); + const filteredInformationColumnsTemporal = dataColumnsSortedSliced.filter((row) => settings.attributeColumnShow.includes(row.header)); - const filteredInformationColumnsTemporal = dataColumnsSortedSliced.filter((row) => settings.attributeColumnShow.includes(row.header)); + setInformationColumn(filteredInformationColumnsTemporal); - setInformationColumn(filteredInformationColumnsTemporal); + setCurrentPageColumns({ + startIndexColumn: startIndexColumn, + endIndexColumn: endIndexColumn, + }); + }; - setCurrentPageColumns({ - startIndexColumn: startIndexColumn, - endIndexColumn: endIndexColumn, - }); - }; + const onPageChangeRows = (delta: number) => { + const startIndexRow = (currentPageRows?.startIndexRow || 0) + delta; + if (startIndexRow < 0 || startIndexRow + 10 > dataModel.data.rowLabels.length) return; + const endIndexRow = Math.min(startIndexRow + configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length); - const onPageChangeRows = (delta: number) => { - const startIndexRow = (currentPageRows?.startIndexRow || 0) + delta; - if (startIndexRow < 0 || startIndexRow + 10 > dataModel.data.rowLabels.length) return; - const endIndexRow = Math.min(startIndexRow + configPaohvis.rowsMaxPerPage, dataModel.data.rowLabels.length); + setNumRowsVisible(endIndexRow - startIndexRow); - setNumRowsVisible(endIndexRow - startIndexRow); + // slice data rows + const dataRowsSorted = cloneDeep(informationRowAllData); - // slice data rows - const dataRowsSorted = cloneDeep(informationRowAllData); + const dataRowsSortedSliced = dataRowsSorted.map((table) => ({ + ...table, + data: table.data.slice(startIndexRow, endIndexRow), + })); - const dataRowsSortedSliced = dataRowsSorted.map((table) => ({ - ...table, - data: table.data.slice(startIndexRow, endIndexRow), - })); + const filteredInformationRowTemporal = dataRowsSortedSliced.filter((row) => settings.attributeRowShow.includes(row.header)); - const filteredInformationRowTemporal = dataRowsSortedSliced.filter((row) => settings.attributeRowShow.includes(row.header)); + setInformationRow(filteredInformationRowTemporal); - setInformationRow(filteredInformationRowTemporal); + // set pagination rows + setCurrentPageRows({ + startIndexRow: startIndexRow, + endIndexRow: endIndexRow, + }); - // set pagination rows - setCurrentPageRows({ - startIndexRow: startIndexRow, - endIndexRow: endIndexRow, - }); + // Update lines hyperedges + const newLinePositions = dataModel.pageData.hyperEdgeRanges.map((hyperEdgeRange) => { + return intersectionElements([startIndexRow, endIndexRow], hyperEdgeRange.hyperEdges.indices); + }); + setLineHyperEdges(newLinePositions); + }; - // Update lines hyperedges - const newLinePositions = dataModel.pageData.hyperEdgeRanges.map((hyperEdgeRange) => { - return intersectionElements([startIndexRow, endIndexRow], hyperEdgeRange.hyperEdges.indices); - }); - setLineHyperEdges(newLinePositions); - }; + const exportImageInternal = () => { + if (divRef.current) { + const options = { + backgroundColor: '#FFFFFF', + foreignObjectRendering: false, + removeContainer: false, + }; + html2canvas(divRef.current, options).then((canvas) => { + const pngData = canvas.toDataURL('image/png'); + const a = document.createElement('a'); + a.href = pngData; + a.download = 'paohvis.png'; + a.click(); + }); + } else { + console.error('The referenced div is null.'); + } + }; - return ( - <svg - className="m-1 overflow-hidden" - ref={svgRef} - style={{ - width: '100%', - height: '99%', - }} - onWheel={onWheel} - > - <defs> - <pattern id="diagonal-lines" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)"> - <rect width="6" height="8" fill="transparent"></rect> - <rect x="6" width="2" height="8" fill="#eaeaea"></rect> - </pattern> - </defs> - - <RowLabels - dataRows={informationRow} - rowHeight={settings.rowHeight} - yOffset={computedSizesSvg.colWidth} - rowLabelColumnWidth={widthTotalRowInformation} - classTopTextColumns={classTopTextColumns} - onMouseEnterRowLabels={onMouseEnterRowLabels} - onMouseLeaveRowLabels={onMouseLeaveRowLabels} - handleClickHeaderSorting={handleClickHeaderSorting} - sortState={sortingOrderRow} - headerState={previousHeaderRow} - configStyle={configStyle} - numColumns={numColsVisible} - /> - - <HyperEdgeRangesBlock - dataModel={dataModel} - dataLinesHyperEdges={lineHyperEdges} - rowHeight={settings.rowHeight} - yOffset={computedSizesSvg.colWidth} - rowLabelColumnWidth={widthTotalRowInformation} - classTopTextColumns={classTopTextColumns} - currentPageRows={currentPageRows} - widthColumns={widthTotalColumnInformation} - columnHeaderInformation={informationColumn} - configStyle={configStyle} - onMouseEnterHyperEdge={onMouseEnterHyperEdge} - onMouseLeaveHyperEdge={onMouseLeaveHyperEdge} - numRows={numRowsVisible} - sortState={sortingOrderColumn} - handleClickHeaderSorting={handleClickHeaderSortingColumns} - headerState={previousHeaderColumn} - /> - </svg> - ); -}; + useImperativeHandle(refExternal, () => ({ + exportImageInternal, + })); + + return ( + <div + className="overflow-hidden" + ref={divRef} + style={{ + width: '100%', + height: '99%', + }} + > + <svg + className="m-1 overflow-hidden" + ref={svgRef} + style={{ + width: '100%', + height: '99%', + }} + onWheel={onWheel} + > + <defs> + <pattern id="diagonal-lines" patternUnits="userSpaceOnUse" width="8" height="8" patternTransform="rotate(45)"> + <rect width="6" height="8" fill="transparent"></rect> + <rect x="6" width="2" height="8" fill="#eaeaea"></rect> + </pattern> + </defs> + + <RowLabels + dataRows={informationRow} + rowHeight={settings.rowHeight} + yOffset={computedSizesSvg.colWidth} + rowLabelColumnWidth={widthTotalRowInformation} + classTopTextColumns={classTopTextColumns} + onMouseEnterRowLabels={onMouseEnterRowLabels} + onMouseLeaveRowLabels={onMouseLeaveRowLabels} + handleClickHeaderSorting={handleClickHeaderSorting} + sortState={sortingOrderRow} + headerState={previousHeaderRow} + configStyle={configStyle} + numColumns={numColsVisible} + /> + + <HyperEdgeRangesBlock + dataModel={dataModel} + dataLinesHyperEdges={lineHyperEdges} + rowHeight={settings.rowHeight} + yOffset={computedSizesSvg.colWidth} + rowLabelColumnWidth={widthTotalRowInformation} + classTopTextColumns={classTopTextColumns} + currentPageRows={currentPageRows} + widthColumns={widthTotalColumnInformation} + columnHeaderInformation={informationColumn} + configStyle={configStyle} + onMouseEnterHyperEdge={onMouseEnterHyperEdge} + onMouseLeaveHyperEdge={onMouseLeaveHyperEdge} + numRows={numRowsVisible} + sortState={sortingOrderColumn} + handleClickHeaderSorting={handleClickHeaderSortingColumns} + headerState={previousHeaderColumn} + /> + </svg> + </div> + ); + }, +); const PaohSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<PaohVisProps>) => { const [areCollapsedAttrRows, setAreCollapsedAttrRows] = useState<boolean>(true); @@ -1103,14 +1146,20 @@ const PaohSettings = ({ settings, graphMetadata, updateSettings }: Visualization ); }; +const paohvisRef = React.createRef<{ exportImageInternal: () => void }>(); + export const PaohVisComponent: VISComponentType<PaohVisProps> = { displayName: 'PaohVis', description: 'Paths and Connection', - component: PaohVis, + component: React.forwardRef((props: VisualizationPropTypes<PaohVisProps>, ref) => <PaohVis {...props} ref={paohvisRef} />), settingsComponent: PaohSettings, settings: settings, exportImage: () => { - alert('Not yet supported'); + if (paohvisRef.current) { + paohvisRef.current.exportImageInternal(); + } else { + console.error('Paohvis reference is not set.'); + } }, };