diff --git a/src/App.js b/src/App.js index 4af5cd2..724c959 100644 --- a/src/App.js +++ b/src/App.js @@ -18,7 +18,6 @@ import { NodeViewWidget } from './app/widgets'; import { addWidget } from '@metacell/geppetto-meta-client/common/layout/actions'; import { WidgetStatus } from "@metacell/geppetto-meta-client/common/layout/model"; import DatasetsListSplinter from "./components/DatasetsListViewer/DatasetsListSplinter"; - import config from './config/app.json'; const App = () => { @@ -132,6 +131,7 @@ const App = () => { const splinter = new DatasetsListSplinter(undefined, file.data); let graph = await splinter.getGraph(); let datasets = graph.nodes.filter((node) => node?.attributes?.hasDoi); + let version = graph.nodes.find( node => node?.attributes?.versionInfo)?.attributes?.versionInfo const match = datasets.find( node => node.attributes?.hasDoi?.[0]?.includes(doi)); if ( match ) { const datasetID = match.name; @@ -140,6 +140,20 @@ const App = () => { setLoading(false); setInitialised(false); } + + let datasetStorage = {}; + if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { + let parsedDatasets = [] + datasets.forEach( node => { + parsedDatasets.push({ name : node.name , doi : node.attributes?.hasDoi?.[0], label : node.attributes ? node.attributes?.label?.[0]?.toLowerCase() : null}); + }); + datasetStorage = { + version : version[0], + datasets : parsedDatasets + } + + localStorage.setItem(config.datasetsStorage, JSON.stringify(datasetStorage)); + } }; useEffect(() => { @@ -149,9 +163,21 @@ const App = () => { if (doi && doi !== "" ) { if ( doiMatch ){ - const fileHandler = new FileHandler(); - const summaryURL = config.repository_url + config.available_datasets; - fileHandler.get_remote_file(summaryURL, loadDatsetFromDOI); + if ( localStorage.getItem(config.datasetsStorage) ) { + let storedDatasetsInfo = JSON.parse(localStorage.getItem(config.datasetsStorage)); + const match = storedDatasetsInfo.datasets.find( node => node?.doi.includes(doi)); + if ( match ) { + const datasetID = match.name; + loadFiles(datasetID); + } else { + setLoading(false); + setInitialised(false); + } + } else { + const fileHandler = new FileHandler(); + const summaryURL = config.repository_url + config.available_datasets; + fileHandler.get_remote_file(summaryURL, loadDatsetFromDOI); + } } } }, []); diff --git a/src/components/DatasetsListViewer/DatasetsListDialog.js b/src/components/DatasetsListViewer/DatasetsListDialog.js index b63ef04..7cc464e 100644 --- a/src/components/DatasetsListViewer/DatasetsListDialog.js +++ b/src/components/DatasetsListViewer/DatasetsListDialog.js @@ -106,8 +106,24 @@ const DatasetsListDialog = (props) => { let datasets = graph.nodes.filter((node) => node?.attributes?.hasUriApi); datasets.forEach( node => node.attributes ? node.attributes.lowerCaseLabel = node.attributes?.label?.[0]?.toLowerCase() : null ); datasets = datasets.filter( node => node?.attributes?.statusOnPlatform?.[0]?.includes(PUBLISHED) ); - dispatch(setDatasetsList(datasets)); - setFilteredDatasets(datasets); + + + let version = graph.nodes.find( node => node?.attributes?.versionInfo)?.attributes?.versionInfo + let datasetStorage = {}; + if ( version !== undefined && localStorage.getItem(config.datasetsStorage)?.version !== version[0] ) { + let parsedDatasets = [] + datasets.forEach( node => { + parsedDatasets.push({ name : node.name , doi : node.attributes?.hasDoi?.[0], label : node.attributes ? node.attributes.lowerCaseLabel : null}); + }); + datasetStorage = { + version : version[0], + datasets : parsedDatasets + } + + localStorage.setItem(config.datasetsStorage, JSON.stringify(datasetStorage)); + dispatch(setDatasetsList(datasetStorage.datasets)); + setFilteredDatasets(datasetStorage.datasets); + } }; const summaryURL = config.repository_url + config.available_datasets; fileHandler.get_remote_file(summaryURL, callback); @@ -116,7 +132,7 @@ const DatasetsListDialog = (props) => { const handleChange = (event) => { const lowerCaseSearch = event.target.value.toLowerCase(); let filtered = datasets.filter((dataset) => - dataset.attributes.lowerCaseLabel.includes(lowerCaseSearch) || dataset.name.includes(lowerCaseSearch) + dataset.label?.includes(lowerCaseSearch) || dataset.name?.includes(lowerCaseSearch) ); setSearchField(lowerCaseSearch); setFilteredDatasets(filtered); @@ -139,7 +155,15 @@ const DatasetsListDialog = (props) => { } useEffect(() => { - open && datasets.length === 0 && loadDatasets(); + if ( open && datasets.length === 0 ) { + if ( localStorage.getItem(config.datasetsStorage) ) { + let storedDatasetsInfo = JSON.parse(localStorage.getItem(config.datasetsStorage)); + dispatch(setDatasetsList(storedDatasetsInfo.datasets)); + setFilteredDatasets(storedDatasetsInfo.datasets); + } else { + loadDatasets(); + } + } }); return ( @@ -202,7 +226,7 @@ const DatasetsListDialog = (props) => { className="dataset_list_text" dangerouslySetInnerHTML={{ __html: - getFormattedListTex(dataset.attributes?.label[0]) + getFormattedListTex(dataset.label) }} /> } diff --git a/src/components/DatasetsListViewer/DatasetsListSplinter.js b/src/components/DatasetsListViewer/DatasetsListSplinter.js index 553ae3d..c3284f0 100644 --- a/src/components/DatasetsListViewer/DatasetsListSplinter.js +++ b/src/components/DatasetsListViewer/DatasetsListSplinter.js @@ -254,7 +254,7 @@ class Splinter { dataset_node.proxies = dataset_node.proxies.concat(ontology_node.proxies); dataset_node.level = 1; this.nodes.set(dataset_node.id, dataset_node); - this.nodes.delete(ontology_node.id); + // this.nodes.delete(ontology_node.id); // fix links that were pointing to the ontology let temp_edges = this.edges.map(link => { if (link.source === ontology_node.id) { diff --git a/src/components/GraphViewer/GraphViewer.js b/src/components/GraphViewer/GraphViewer.js index a347bf9..f07b6a2 100644 --- a/src/components/GraphViewer/GraphViewer.js +++ b/src/components/GraphViewer/GraphViewer.js @@ -45,9 +45,11 @@ const GraphViewer = (props) => { const [loading, setLoading] = React.useState(false); const [data, setData] = React.useState({ nodes : [], links : []}); const nodeSelected = useSelector(state => state.sdsState.instance_selected.graph_node); + const nodeClickSource = useSelector(state => state.sdsState.instance_selected.source); const groupSelected = useSelector(state => state.sdsState.group_selected.graph_node); const [collapsed, setCollapsed] = React.useState(true); const [previouslySelectedNodes, setPreviouslySelectedNodes] = useState(new Set()); + let triggerCenter = false; const handleLayoutClick = (event) => { setLayoutAnchorEl(event.currentTarget); @@ -70,7 +72,11 @@ const GraphViewer = (props) => { if ( node.type === rdfTypes.Subject.key || node.type === rdfTypes.Sample.key || node.type === rdfTypes.Collection.key ) { node.collapsed = !node.collapsed; collapseSubLevels(node, node.collapsed, { links : 0 }); - const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + + collapseSubLevels(node, true, { links : 0 }); + updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); setData(updatedData); } handleNodeHover(node); @@ -115,7 +121,7 @@ const GraphViewer = (props) => { setCollapsed(!collapsed) setTimeout( () => { resetCamera(); - },100) + },200) } /** @@ -168,6 +174,10 @@ const GraphViewer = (props) => { const onEngineStop = () => { setForce(); + if ( triggerCenter ) { + graphRef?.current?.ggv?.current.centerAt(selectedNode.x, selectedNode.y, ONE_SECOND); + triggerCenter = false; + } } useEffect(() => { @@ -209,7 +219,7 @@ const GraphViewer = (props) => { }); useEffect(() => { - if ( groupSelected ) { + if ( groupSelected && groupSelected?.dataset_id?.includes(props.graph_id)) { setSelectedNode(groupSelected); handleNodeHover(groupSelected); graphRef?.current?.ggv?.current.centerAt(groupSelected.x, groupSelected.y, ONE_SECOND); @@ -224,26 +234,46 @@ const GraphViewer = (props) => { }, [selectedNode]); useEffect(() => { - if ( nodeSelected ) { + if ( nodeSelected && ( nodeSelected?.tree_reference?.dataset_id?.includes(props.graph_id) || + nodeSelected?.dataset_id?.includes(props.graph_id) )) { if ( nodeSelected?.id !== selectedNode?.id ){ let node = nodeSelected; - let collapsed = nodeSelected.collapsed - while ( node?.parent && !collapsed ) { - node = node.parent; - collapsed = node.collapsed + let collapsed = node.collapsed + let parent = node.parent; + let prevNode = node; + while ( parent && parent?.collapsed ) { + prevNode = parent; + parent = parent.parent; } - if ( collapsed ) { - node.collapsed = !node.collapsed; - collapseSubLevels(node, node.collapsed, { links : 0 }); - const updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); - setData(updatedData); + + if ( prevNode && nodeSelected.collapsed && nodeClickSource === "TREE") { + if ( prevNode.type == rdfTypes.Subject.key || prevNode.type == rdfTypes.Sample.key || + prevNode.type == rdfTypes.Collection.key ) { + prevNode.collapsed = false; + collapseSubLevels(prevNode, false, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + } + if ( node.parent?.type == rdfTypes.Subject.key || node.parent?.type == rdfTypes.Sample.key || + node.parent?.type == rdfTypes.Collection.key ) { + collapseSubLevels(node.parent, true, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + } else { + collapseSubLevels(node, true, { links : 0 }); + let updatedData = getPrunedTree(props.graph_id, selectedLayout.layout); + setData(updatedData); + } } setSelectedNode(nodeSelected); handleNodeHover(nodeSelected); - graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, 10); + graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } else { handleNodeHover(nodeSelected); + graphRef?.current?.ggv?.current.centerAt(nodeSelected.x, nodeSelected.y, ONE_SECOND); } + const divElement = document.getElementById(nodeSelected.id + detailsLabel); + divElement?.scrollIntoView({ behavior: 'smooth' }); } },[nodeSelected]) diff --git a/src/components/NodeDetailView/Details/DatasetDetails.js b/src/components/NodeDetailView/Details/DatasetDetails.js index c2bfe4a..5e9d0a0 100644 --- a/src/components/NodeDetailView/Details/DatasetDetails.js +++ b/src/components/NodeDetailView/Details/DatasetDetails.js @@ -64,7 +64,9 @@ const DatasetDetails = (props) => { - + + { property.link?.asText ? {value} : } + ) } diff --git a/src/components/Sidebar/TreeView/InstancesTreeView.js b/src/components/Sidebar/TreeView/InstancesTreeView.js index 5738f0f..65da03d 100644 --- a/src/components/Sidebar/TreeView/InstancesTreeView.js +++ b/src/components/Sidebar/TreeView/InstancesTreeView.js @@ -31,7 +31,7 @@ const InstancesTreeView = (props) => { } else { dispatch(selectInstance({ dataset_id: dataset_id, - graph_node: node?.graph_reference?.id, + graph_node: node?.graph_reference?.id || node?.id, tree_node: node?.id, source: TREE_SOURCE })); diff --git a/src/config/app.json b/src/config/app.json index 761251d..13eab97 100644 --- a/src/config/app.json +++ b/src/config/app.json @@ -17,5 +17,6 @@ "datasetsButtonText" : "Import a new dataset", "datasetsDialogSearchText" : "Search datasets by label or id", "datasetsButtonSubtitleText" : "Select a dataset to load" - } + }, + "datasetsStorage" : "publishedDatasets" } \ No newline at end of file diff --git a/src/redux/initialState.js b/src/redux/initialState.js index ce740f1..ccf8d90 100644 --- a/src/redux/initialState.js +++ b/src/redux/initialState.js @@ -132,9 +132,6 @@ export default function sdsClientReducer(state = {}, action) { if (a.visible === b.visible) { // Preserve the original order for items with the same visibility return updatedMetadataModel[groupTitle].indexOf(a) - updatedMetadataModel[groupTitle].indexOf(b); - } else { - // Move visible items to the top - return a.visible ? -1 : 1; } }); } diff --git a/src/utils/GraphViewerHelper.js b/src/utils/GraphViewerHelper.js index f7109d6..5dad9cf 100644 --- a/src/utils/GraphViewerHelper.js +++ b/src/utils/GraphViewerHelper.js @@ -1,6 +1,7 @@ import React, {useCallback} from 'react'; import { rdfTypes } from './graphModel'; import { current } from '@reduxjs/toolkit'; +import * as d3 from 'd3'; export const NODE_FONT = '500 5px Inter, sans-serif'; export const ONE_SECOND = 1000; @@ -156,7 +157,7 @@ export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, prev ctx.fillText(...textProps); if ( node.childLinks?.length && node.collapsed ) { let children = { links : 0 }; - collapseSubLevels(node, undefined, children) + collapseSubLevels(node, true, children) const collapsedNodes = [children.links, node.x, textHoverPosition[1]]; ctx.fillStyle = GRAPH_COLORS.collapsedFolder; ctx.textAlign = 'center'; @@ -168,207 +169,28 @@ export const paintNode = (node, ctx, hoverNode, selectedNode, nodeSelected, prev export const collapseSubLevels = (node, collapsed, children) => { node?.childLinks?.forEach( n => { - if ( collapsed !== undefined ) n.target.collapsed = collapsed; - collapseSubLevels(n.target, collapsed, children); - children.links = children.links + 1; + if ( collapsed !== undefined ) { + n.target.collapsed = collapsed; + collapseSubLevels(n.target, collapsed, children); + children.links = children.links + 1; + } }); } -export const determineNodePosition = (positionsMap, levels, n, targetCoord) => { - let nearestParentNeighbor = null; - let nearestParentNeighborIndex = 0; - let leftParentMatch = false; - let firstParentCollection = null; - let firstParentCollectionIndex = 0; - sortArray(levels[n.level - 1]) - levels[n.level-1]?.forEach( ( node, index ) => { - if ( !leftParentMatch && (node.type == "Collection" || node.type == "Sample") && node.id !== n.parent.id && !node.collapsed ) { - firstParentCollection = node; - firstParentCollectionIndex = index; - } - - if ( !leftParentMatch && node.id === n.parent?.id) { - leftParentMatch = true; - nearestParentNeighbor = node; - nearestParentNeighborIndex = index; - } - }) - - let nodesInBetween = Math.floor((( firstParentCollection?.neighbors?.length - 1) + ( nearestParentNeighbor?.neighbors?.length - 1)) /2 ) - 1; - if ( firstParentCollection === nearestParentNeighbor ) { - nodesInBetween = Math.floor((( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); - } else if ( firstParentCollection == null ) { - nodesInBetween = Math.floor((( nearestParentNeighbor?.neighbors?.length - 1)) /2 ); - } - - let spacesNeeded = nearestParentNeighborIndex - firstParentCollectionIndex; - - let nearestNeighbor = null; - let nearestNeighborIndex = 0; - let leftMatch = false; - let leftMatchIndex = 0; - let firstCollection = null; - levels[n.level]?.forEach( ( node, index ) => { - if ( leftMatch && nearestNeighbor == null && ( node.type == "Collection" || node.type == "Sample" )&& !node.collapsed) { - nearestNeighbor = node; - nearestNeighborIndex = index; - } - - if ( !leftMatch ) { - firstCollection = n; - } - - if ( !leftMatch && node.id === n.id ) { - leftMatch = true; - leftMatchIndex = index - 1; - } - }) - - let position = positionsMap[n.level]; - if ( !isNaN(nodesInBetween) && spacesNeeded - 1 > nodesInBetween) { - let neighbors = n?.parent?.neighbors; - let matchNeighbor = neighbors.findIndex( (node) => node.id === n.id ) ;; - if ( matchNeighbor === 1 && n.level === Object.keys(levels)?.length ) { - position = position + (Math.abs( spacesNeeded - nodesInBetween) * nodeSpace) - } else { - position = position + nodeSpace - } - } else if ( nearestNeighborIndex > 0 ) { - if ( nearestNeighbor != null && !n.collapsed ){ - let middleNode = nearestNeighbor.neighbors?.[Math.floor(nearestNeighbor.neighbors?.length / 2)]?.[targetCoord]; - if ( middleNode ) { - position = middleNode - } - position = position - ( (nearestNeighborIndex - leftMatchIndex - 1) * nodeSpace) - } else if ( n.collapsed ) { - position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] - ( -1 * nodeSpace) : position - ( -1 * nodeSpace) - } else if ( nearestNeighborIndex - leftMatchIndex > 0 ) { - position = position - ( nodeSpace) - } else { - position = (position + nodeSpace) - } - } else if ( n.collapsed) { - position = nearestNeighborIndex?.[targetCoord] ? nearestNeighborIndex?.[targetCoord] - ( -1 * nodeSpace): position - ( -1 * nodeSpace) - } else { - position = position + nodeSpace - } - positionsMap[n.level] = position; - return position -} -const sortArray = (arrayToSort) => { - arrayToSort?.sort( (a, b) => { - if ( a?.attributes?.relativePath && b?.attributes?.relativePath) { - let aParent = a; - let aPath= ""; - while ( aParent?.type != "Subject" ){ - aParent = aParent?.parent; - if ( aParent?.attributes?.relativePath ){ - aPath = aPath + "/" + aParent?.attributes?.relativePath - } - } - let bParent = b; - let bPath = "" - while ( bParent?.type != "Subject"){ - bParent = bParent?.parent; - if ( bParent?.attributes?.relativePath ){ - bPath = bPath + "/" +bParent?.attributes?.relativePath - } - } - aPath = (aParent?.id ) + "/" + aPath - bPath = (bParent?.id ) + "/" + bPath - return aPath.localeCompare(bPath); - } else { - return a?.id.localeCompare(b.id); - } - }); +const hierarchy = (data) =>{ + return d3.hierarchy(data); } -/** - * Algorithm used to position nodes in a Tree. Position depends on the layout, - * either Tree or Vertical Layout views. - * @param {*} levels - How many levels we need for this tree. This depends on the dataset subjects/samples/folders/files. - * @param {*} layout - * @param {*} furthestLeft - */ -export const algorithm = (levels, layout, furthestLeft) => { - let positionsMap = {}; - let levelsMapKeys = Object.keys(levels); - - levelsMapKeys.forEach( level => { - furthestLeft = 0 - (Math.ceil(levels[level].length)/2 * nodeSpace ); - positionsMap[level] = furthestLeft + nodeSpace; - sortArray(levels[level]); - }); - - // Start assigning the graph from the bottom up - let neighbors = 0; - levelsMapKeys.reverse().forEach( level => { - let collapsedInLevel = levels[level].filter( n => n.collapsed); - let notcollapsedInLevel = levels[level].filter( n => !n.collapsed); - levels[level].forEach ( (n, index) => { - neighbors = n?.neighbors?.filter(neighbor => { return neighbor.level > n.level }); - if ( !n.collapsed ) { - if ( neighbors?.length > 0 ) { - let max = Number.MIN_SAFE_INTEGER, min = Number.MAX_SAFE_INTEGER; - neighbors.forEach( neighbor => { - if ( layout === TOP_DOWN.layout ) { - if ( neighbor.xPos > max ) { max = neighbor.xPos }; - if ( neighbor.xPos <= min ) { min = neighbor.xPos }; - } else if ( layout === LEFT_RIGHT.layout ) { - if ( neighbor.yPos > max ) { max = neighbor.yPos }; - if ( neighbor.yPos <= min ) { min = neighbor.yPos }; - } - }); - if ( layout === TOP_DOWN.layout ) { - n.xPos = min === max ? min : min + ((max - min) * .5); - } else if ( layout === LEFT_RIGHT.layout ) { - n.yPos = min === max ? min : min + ((max - min) * .5); - } - if ( notcollapsedInLevel?.length > 0 && collapsedInLevel.length > 0) { - if ( n.type === "Subject" || n.parent?.type === "Subject" || n.parent?.parent?.type === "Subject" ) { - updateConflictedNodes(levels[level], n, positionsMap, level, index, layout); - } - } - - if ( layout === TOP_DOWN.layout ) { - n.fx = n.xPos; - n.fy = 50 * n.level; - positionsMap[n.level] = n.xPos + nodeSpace; - } else if ( layout === LEFT_RIGHT.layout ) { - n.fy = n.yPos; - n.fx = 50 * n.level; - positionsMap[n.level] = n.yPos + nodeSpace; - } - } else { - if ( layout === TOP_DOWN.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "xPos"); - n.xPos = position; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( layout === LEFT_RIGHT.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "yPos"); - n.yPos = position; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } - }else { - if ( layout === TOP_DOWN.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "xPos"); - n.xPos = position; - n.fx = n.xPos; - n.fy = 50 * n.level; - } else if ( layout === LEFT_RIGHT.layout ) { - let position = determineNodePosition(positionsMap, levels, n, "yPos"); - n.yPos = position; - n.fy = n.yPos; - n.fx = 50 * n.level; - } - } - }) +const dendrogram = (data) => { + const dendrogramGenerator = d3.cluster().nodeSize([1, 100]) + .separation(function(a,b){ + return 1 + d3.sum([a,b].map(function(d){ + return 15 + })) }); - } + return dendrogramGenerator(hierarchy(data)); +} /** * Create Graph ID @@ -412,59 +234,72 @@ export const getPrunedTree = (graph_id, layout) => { levels[n.level] = [n]; } }) - - // Calculate level with max amount of nodes - let maxLevel = Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b); - let maxLevelNodes = levels[maxLevel]; - // The furthestLeft a node can be - let furthestLeft = 0 - (Math.ceil(maxLevelNodes.length)/2 * nodeSpace ); + + // Calculate level with max amount of nodes + let maxLevel = parseInt(Object.keys(levels).reduce((a, b) => levels[a].length > levels[b].length ? a : b)); - algorithm(levels, layout, furthestLeft); + + let root = levelsMap["1"]?.[0]; - const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; - return graph; - }; + let data = { + type : "node", + name : root?.id, + value : levelsMap["1"]?.[0]?.level - 1, + children : [] + }; - /** - * Update Nodes x and y position, used for vertical and tree view layouts. - * @param {*} nodes - The nodes we have for the dataset - * @param {*} conflictNode - Conflicting Node that needs re positioning - * @param {*} positionsMap - Object keeping track of positions of nodes - * @param {*} level - level of tree - * @param {*} index - Index of conflict node in this tree level - * @param {*} layout - The layout we are using to display these nodes - */ - const updateConflictedNodes = (nodes, conflictNode, positionsMap, level, index, layout) => { - let matchIndex = index; - for ( let i = 0; i < index ; i++ ) { - let conflict = nodes.find ( n => !n.collapsed && n?.parent?.id === nodes[i]?.parent?.id) - if ( conflict === undefined ){ - conflict = nodes.find ( n => !n.collapsed ) - if ( conflict === undefined ){ - conflict = conflictNode; - } + function traverse(node, data) { + if (node === null) { + return; } - matchIndex = nodes.findIndex( n => n.id === conflict.id ); + node.neighbors?.forEach( n => { + if ( visibleNodes?.find( node => node.id === n.id ) ) { + if ( n.neighbors?.length > 1 ) { + if ( n?.level > node.level ) { + let node = { + type : "node", + name : n.id, + value : n?.level - 1, + children : [] + } + data.children.push(node); + traverse(n, node) + } + } else { + data.children.push({type : "leaf", + name : n.id, + value : n?.level - 1}) + } + } + }); + } + + traverse(root,data) + + // Use D3 cluster to give position to nodes + const allNodes = dendrogram(data).descendants(); + let mapNodes = {}; + allNodes.forEach( n => mapNodes[n.data?.name] = n ); + + // Assign position of nodes + visibleNodes.forEach( n => { if ( layout === TOP_DOWN.layout ) { - let furthestLeft = conflict?.xPos; - if ( nodes?.[i]?.collapsed ) { - furthestLeft = conflict.xPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].xPos =furthestLeft; - positionsMap[level] = furthestLeft + nodeSpace; + if ( mapNodes[n.id] ) { + n.xPos = mapNodes[n.id].x + n.fx = n.xPos; + n.fy = 50 * n.level; } - nodes[i].fx = nodes[i].xPos; - nodes[i].fy = 50 * nodes[i].level; - } else if ( layout === LEFT_RIGHT.layout ) { - let furthestLeft = conflict?.yPos; - if ( nodes[i].collapsed ) { - furthestLeft = conflict.yPos - ((((matchIndex - i )/2)) * nodeSpace ); - nodes[i].yPos =furthestLeft; + } + if ( layout === LEFT_RIGHT.layout ) { + if ( mapNodes[n.id] ) { + n.yPos = mapNodes[n.id].x + n.fy = n.yPos; + n.fx = 50 * n.level; } - positionsMap[level] = furthestLeft + nodeSpace; - nodes[i].fy = nodes[i].yPos; - positionsMap[level] = nodes[i].fy + nodeSpace; - nodes[i].fx = 50 * nodes[i].level; } - } - } \ No newline at end of file + }) + + const graph = { nodes : visibleNodes, links : visibleLinks, levelsMap : levelsMap, hierarchyVariant : maxLevel * 20 }; + return graph; + }; diff --git a/src/utils/Splinter.js b/src/utils/Splinter.js index f68ca03..625e996 100644 --- a/src/utils/Splinter.js +++ b/src/utils/Splinter.js @@ -14,6 +14,7 @@ import { protocols_key, contributors_key, SUBJECTS_LEVEL, PROTOCOLS_LEVEL, CRONTRIBUTORS_LEVEL } from '../constants'; +import * as d3 from 'd3'; const N3 = require('n3'); const ttl2jsonld = require('@frogcat/ttl2jsonld').parse; @@ -231,7 +232,11 @@ class Splinter { if ( !existingLing ) { const a = this.nodes.get( link.source ); const b = this.nodes.get( link.target ); - if ( a && b && ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ) &&!((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1))) { + const awardEmpty = ( a?.type !== rdfTypes.Award.key && b?.type !== rdfTypes.Award.key ); + const collectionEmpty = ((a?.type === rdfTypes.Collection.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Collection.key && b.children_counter < 1)); + const sampleEmpty = ((a?.type === rdfTypes.Sample.key && a.children_counter < 1 ) || ( b?.type === rdfTypes.Sample.key && b.children_counter < 1)) + const sameLevels = a?.level === b?.level; + if ( a && b && awardEmpty && !collectionEmpty && !sampleEmpty && !sameLevels) { !a.neighbors && (a.neighbors = []); !b.neighbors && (b.neighbors = []); if ( !a.neighbors.find( n => n.id === b.id )){ @@ -256,10 +261,18 @@ class Splinter { } } }); - console.log("This levels map ", this.levelsMap) + + let newCleanLinks = cleanLinks.filter(link => { + + const collectionEmpty = ((link?.target?.type === rdfTypes.Collection.key && link?.target?.neighbors?.length <= 1 ) || ( link?.source?.type === rdfTypes.Collection.key && link?.source?.neighbors?.length <= 1)); + if ( collectionEmpty ) { + return false; + } + return true; + }); return { nodes: filteredNodes, - links: cleanLinks, + links: newCleanLinks, levelsMap : this.levelsMap }; } @@ -679,11 +692,11 @@ class Splinter { target_node.level = protocols.level + 1; target_node.parent = protocols; this.nodes.set(target_node.id, target_node); - } else if (link.source === id && target_node.type === rdfTypes.Sample.key) { - // link.source = target_node.attributes.derivedFrom[0]; - // target_node.level = subjects.level + 2; - // target_node.parent = this.nodes.get(target_node.attributes.derivedFrom[0]); - // this.nodes.set(target_node.id, target_node); + } else if (link.source === id && target_node.type === rdfTypes.Sample.key ) { + link.source = target_node.attributes.derivedFrom[0]; + target_node.level = subjects.level + 2; + target_node.parent = this.nodes.get(target_node.attributes.derivedFrom[0]); + this.nodes.set(target_node.id, target_node); } let source_node = this.nodes.get(link.source); if ( source_node?.childLinks ) { @@ -716,6 +729,27 @@ class Splinter { } } } + + if (node.type === rdfTypes.Sample.key) { + if (node.attributes.derivedFrom !== undefined) { + let source = this.nodes.get(node.attributes.derivedFrom[0]); + if ( source !== undefined ) { + source.children_counter++ + array[index].level = source.level + 1; + this.forced_edges.push({ + source: node.attributes.derivedFrom[0], + target: node.id + }); + } + } + + if (node.attributes?.hasFolderAboutIt !== undefined) { + node.attributes.hasFolderAboutIt = + [Array.from(this.nodes)[0][1].attributes.hasUriPublished[0] + + "?datasetDetailsTab=files&path=files/" + + node.tree_reference?.dataset_relative_path]; + } + } if (node.type === rdfTypes.Subject.key) { if (node.attributes?.animalSubjectIsOfStrain !== undefined) { @@ -863,59 +897,59 @@ class Splinter { } - mergeData() { + mergeData() { this.nodes.forEach((value, key) => { if (value.attributes !== undefined && value.attributes.hasFolderAboutIt !== undefined) { value.attributes.hasFolderAboutIt.forEach(folder => { let jsonNode = this.tree_map.get(folder); const splitName = jsonNode.dataset_relative_path.split('/'); let newName = jsonNode.basename; - if ( value.type == "Subject" && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { + if ( value.type === rdfTypes.Subject.key && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { newName = splitName[0] } - let parentNode = value; - if ( value.type == "Sample" && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { - newName = splitName[0] + "/" + newName - parentNode = this.nodes.get(value.attributes.derivedFrom[0]) - if (parentNode?.attributes !== undefined && parentNode?.attributes?.hasFolderAboutIt !== undefined) { - parentNode?.attributes?.hasFolderAboutIt.forEach(folder => { - let jNode = this.tree_map.get(folder); - this.tree_parents_map2.delete(jNode.remote_id); - }) - } + if ( value.type === rdfTypes.Sample.key && value.attributes?.localId?.[0] == splitName[splitName.length - 1] ) { + newName = splitName[0] + "/" + newName } + let parentNode = value; let newNode = this.buildFolder(jsonNode, newName, parentNode); - let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { - child.parent_id = newNode.uri_api - return child; - }); + if ( value.type === rdfTypes.Sample.key) { + newNode.remote_id = jsonNode.basename + '_' + newName; + newNode.uri_api = newNode.remote_id + // this.tree_parents_map2.delete(jsonNode.remote_id); + } - if (!this.filterNode(newNode) && (this.nodes.get(newNode.remote_id)) === undefined) { - this.linkToNode(newNode, parentNode); - } - if (this.tree_parents_map2.get(newNode.uri_api) === undefined) { - this.tree_parents_map2.set(newNode.uri_api, folderChildren); - this.tree_parents_map2.delete(newNode.parent_id); - folderChildren?.forEach(child => { - if (!this.filterNode(child) ) { - this.linkToNode(child, this.nodes.get(newNode.remote_id)); - } - }); - } else { - let tempChildren = folderChildren === undefined ? [...this.tree_parents_map2.get(newNode.uri_api)] : [...this.tree_parents_map2.get(newNode.uri_api), ...folderChildren];; - this.tree_parents_map2.set(newNode.uri_api, tempChildren); - this.tree_parents_map2.delete(newNode.parent_id); - tempChildren?.forEach(child => { - if (!this.filterNode(child) ) { - this.linkToNode(child, this.nodes.get(newNode.remote_id)); - } - }); - } - //} + let folderChildren = this.tree_parents_map2.get(newNode.parent_id)?.map(child => { + child.parent_id = newNode.uri_api + child.collapsed = true; + return child; + }); + + if (!this.filterNode(newNode) && (this.nodes.get(newNode.remote_id)) === undefined) { + this.linkToNode(newNode, parentNode); + } + + if (this.tree_parents_map2.get(newNode.uri_api) === undefined) { + this.tree_parents_map2.set(newNode.uri_api, folderChildren); + this.tree_parents_map2.delete(newNode.parent_id); + folderChildren?.forEach(child => { + if (!this.filterNode(child) ) { + this.linkToNode(child, this.nodes.get(newNode.remote_id)); + } + }); + } else { + let tempChildren = folderChildren === undefined ? [...this.tree_parents_map2.get(newNode.uri_api)] : [...this.tree_parents_map2.get(newNode.uri_api), ...folderChildren];; + this.tree_parents_map2.set(newNode.uri_api, tempChildren); + this.tree_parents_map2.delete(newNode.parent_id); + tempChildren?.forEach(child => { + if (!this.filterNode(child) ) { + this.linkToNode(child, this.nodes.get(newNode.remote_id)); + } + }); + } }) } }); @@ -937,17 +971,16 @@ class Splinter { level = this.nodes.get(parent.attributes.derivedFrom[0])?.level + 1; } } + const new_node = this.buildNodeFromJson(node, level); if ( parent ) { parent.children_counter++; - const new_node = this.buildNodeFromJson(node, level); new_node.parent = parent; - + new_node.id = parent.id + new_node.id; this.forced_edges.push({ source: parent?.id, target: new_node?.id }); new_node.childLinks = []; - new_node.collapsed = new_node.type === typesModel.NamedIndividual.subject.type if ( !this.nodes.get(new_node.id) ) { this.nodes.set(new_node.id, this.factory.createNode(new_node)); var children = this.tree_parents_map2.get(node.remote_id); @@ -1091,4 +1124,4 @@ class Splinter { } } -export default Splinter; +export default Splinter; \ No newline at end of file diff --git a/src/utils/graphModel.js b/src/utils/graphModel.js index b25d42e..7a1d2b8 100644 --- a/src/utils/graphModel.js +++ b/src/utils/graphModel.js @@ -22,6 +22,12 @@ export const rdfTypes = { "key": "hasUriHuman", "property": "hasUriHuman", "label": "To be filled" + }, + { + "type" : "owl", + "key" : "versionInfo", + "property" : "versionInfo", + "label" : "Version" } ] }, @@ -122,7 +128,8 @@ export const rdfTypes = { "label": "Title", "visible" : true, "link" : { - "property" : "hasUriPublished" + "property" : "hasUriPublished", + "asText" : true } }, { @@ -202,7 +209,8 @@ export const rdfTypes = { "label": "DOI", "visible" : true, "link" : { - "property" : "hasUriPublished" + "property" : "hasUriPublished", + "asText" : true } }, { @@ -714,7 +722,7 @@ export const rdfTypes = { "type": "TEMP", "key": "hasFolderAboutIt", "property": "hasFolderAboutIt", - "label": "Related Folder", + "label": "Find in SPARC Portal", "visible" : true }, { @@ -722,7 +730,7 @@ export const rdfTypes = { "key": "wasDerivedFromSubject", "property": "derivedFrom", "label": "Derived from Subject", - "visible" : true + "visible" : false }, { "type": "TEMP", @@ -743,7 +751,7 @@ export const rdfTypes = { "key": "hasDerivedInformationAsParticipant", "property": "hasDerivedInformationAsParticipant", "label": "Derived Information as Participant", - "visible" : true + "visible" : false }, { "type": "TEMP", diff --git a/yarn.lock b/yarn.lock index 1081921..c37197d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5321,6 +5321,13 @@ css-blank-pseudo@^0.1.4: dependencies: postcss "^7.0.5" +css-box-model@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" + integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== + dependencies: + tiny-invariant "^1.0.6" + css-color-keywords@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/css-color-keywords/-/css-color-keywords-1.0.0.tgz#fea2616dc676b2962686b3af8dbdbe180b244e05" @@ -11733,7 +11740,7 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== -memoize-one@^5.0.0: +memoize-one@^5.0.0, memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -14423,6 +14430,11 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== +raf-schd@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" + integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== + raf@^3.2.0, raf@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39" @@ -14494,6 +14506,19 @@ react-app-polyfill@^2.0.0: regenerator-runtime "^0.13.7" whatwg-fetch "^3.4.1" +react-beautiful-dnd@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-color@^2.17.3: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" @@ -14792,6 +14817,18 @@ react-redux@^5.0.6: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" +react-redux@^7.2.0: + version "7.2.9" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" + integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/react-redux" "^7.1.20" + hoist-non-react-statics "^3.3.2" + loose-envify "^1.4.0" + prop-types "^15.7.2" + react-is "^17.0.2" + react-redux@^7.2.4: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de" @@ -17302,6 +17339,11 @@ tiny-invariant@^1.0.2: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== +tiny-invariant@^1.0.6: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tiny-warning@^1.0.0, tiny-warning@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" @@ -17929,6 +17971,11 @@ url@^0.11.0, url@~0.11.0: punycode "1.3.2" querystring "0.2.0" +use-memo-one@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" + integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"