From ab3459ec10c364c3f4c2945b01b602ffb26803e9 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Wed, 8 May 2019 15:55:54 -0400 Subject: [PATCH 01/10] add initial node descriptions --- src/app.js | 1 - src/path-graph.css | 12 ++- src/path-graph.js | 229 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 181 insertions(+), 61 deletions(-) diff --git a/src/app.js b/src/app.js index 8124bfd..7403643 100644 --- a/src/app.js +++ b/src/app.js @@ -154,7 +154,6 @@ class App extends Component { if (!this.props.sourceNode.id && !this.props.targetNode.id) return; const title = - 'hetmech · ' + (this.props.sourceNode.name || '___') + ' → ' + (this.props.targetNode.name || '___'); diff --git a/src/path-graph.css b/src/path-graph.css index d642b8f..dbe103a 100644 --- a/src/path-graph.css +++ b/src/path-graph.css @@ -6,12 +6,18 @@ border: solid #e0e0e0 1px; position: absolute; } -#graph * { - user-select: none; -} .graph_dimensions { margin: 0 10px; } .graph_expand_collapse_button { margin: 0 !important; } +#graph_overlay { + pointer-events: none; +} +#graph_info { + position: absolute; + left: 0; + bottom: 0; + font-size: 8px; +} \ No newline at end of file diff --git a/src/path-graph.js b/src/path-graph.js index bfab5da..6c7c6f4 100644 --- a/src/path-graph.js +++ b/src/path-graph.js @@ -36,7 +36,7 @@ const edgeSpreadDistance = 20; const edgeSpreadAngle = (45 / 360) * 2 * Math.PI; const inkColor = '#424242'; const backgroundColor = '#fafafa'; -const highlightColor = '#02b3e4'; +const highlightColor = '#B3E8F7'; // path graph section component export class PathGraph extends Component { @@ -249,12 +249,15 @@ export class Graph extends Component { this.state = {}; this.state.data = this.assembleGraph(null); + this.state.selectedElement = null; this.fitView = this.fitView.bind(this); this.resetView = this.resetView.bind(this); this.onSimulationTick = this.onSimulationTick.bind(this); + this.onNodeEdgeClick = this.onNodeEdgeClick.bind(this); this.onNodeDragStart = this.onNodeDragStart.bind(this); this.onNodeDragEnd = this.onNodeDragEnd.bind(this); + this.onViewClick = this.onViewClick.bind(this); } // when component mounts @@ -275,7 +278,7 @@ export class Graph extends Component { // positions/velocities/etc for (const newNode of this.state.data.nodes) { for (const oldNode of prevState.data.nodes) { - if (newNode.id === oldNode.id) { + if (newNode.neo4j_id === oldNode.neo4j_id) { newNode.x = oldNode.x; newNode.y = oldNode.y; newNode.fx = oldNode.fx; @@ -320,7 +323,7 @@ export class Graph extends Component { d3 .forceLink() .distance(nodeDistance) - .id((d) => d.id) + .id((d) => d.neo4j_id) ) .force( 'collide', @@ -340,6 +343,9 @@ export class Graph extends Component { .on('zoom', this.onViewZoom); svg.call(viewZoomHandler); + // handle clicks on background + svg.on('click', this.onViewClick); + // create handler for dragging nodes const nodeDragHandler = d3 .drag() @@ -388,8 +394,8 @@ export class Graph extends Component { const angle = Math.atan2(y2 - y1, x2 - x1); // get radius of source/target nodes - const sourceRadius = nodeRadius - 1; - let targetRadius = nodeRadius - 1; + const sourceRadius = nodeRadius - 0.25; + let targetRadius = nodeRadius - 0.25; // increase target node radius to bring tip of arrowhead out of circle if (d.directed) targetRadius += edgeArrowSize / 4; @@ -448,8 +454,8 @@ export class Graph extends Component { let angle = Math.atan2(y2 - y1, x2 - x1); // get radius of source/target nodes - const sourceRadius = nodeRadius - 1; - let targetRadius = nodeRadius - 1; + const sourceRadius = nodeRadius - 0.25; + let targetRadius = nodeRadius - 0.25; // increase target node radius to bring tip of arrowhead out of circle if (d.directed) targetRadius += edgeArrowSize / 4; @@ -585,6 +591,30 @@ export class Graph extends Component { }); } + // when node or edge clicked by user + onNodeEdgeClick(d) { + d3.event.stopPropagation(); + + if (!d.selected) { + this.deselectAll(); + d.selected = true; + } else + this.deselectAll(); + + this.updateNodeCircles(); + this.updateEdgeLines(); + + this.setState({ selectedElement: d }); + } + + // deselect all elements + deselectAll() { + for (const node of this.state.data.nodes) + node.selected = undefined; + for (const edge of this.state.data.edges) + edge.selected = undefined; + } + // when node dragged by user onNodeDragStart() { this.state.simulation.alphaTarget(1).restart(); @@ -601,6 +631,14 @@ export class Graph extends Component { this.state.simulation.alphaTarget(0).restart(); } + // when view/background is clicked by user + onViewClick() { + this.deselectAll(); + this.updateNodeCircles(); + this.updateEdgeLines(); + this.setState({ selectedElement: null }); + } + // when view panned or zoomed by user onViewZoom() { d3.select('#graph_view').attr('transform', d3.event.transform); @@ -631,8 +669,7 @@ export class Graph extends Component { .attr('fill', 'none') .attr('stroke', highlightColor) .attr('stroke-width', edgeArrowSize) - .attr('stroke-linecap', 'square') - .attr('opacity', 0.35); + .attr('stroke-linecap', 'square'); edgeLineHighlights.exit().remove(); } @@ -647,6 +684,7 @@ export class Graph extends Component { edgeLines .enter() .append('path') + .on('click', this.onNodeEdgeClick) .merge(edgeLines) .attr('class', 'graph_edge_line') .attr('marker-end', (d) => { @@ -657,7 +695,14 @@ export class Graph extends Component { }) .attr('fill', 'none') .attr('stroke', inkColor) - .attr('stroke-width', edgeThickness); + .attr('stroke-width', edgeThickness) + .style('stroke-dasharray', (d) => { + if (d.selected) + return edgeThickness * 2 + ' ' + edgeThickness; + else + return 'none'; + }) + .style('cursor', 'pointer'); edgeLines.exit().remove(); } @@ -672,13 +717,15 @@ export class Graph extends Component { edgeLabels .enter() .append('text') + .on('click', this.onNodeEdgeClick) .merge(edgeLabels) .attr('class', 'graph_edge_label') .attr('font-size', edgeFontSize) .attr('font-weight', 500) .attr('text-anchor', 'middle') - .attr('pointer-events', 'none') + .attr('user-select', 'none') .attr('fill', inkColor) + .style('cursor', 'pointer') .text((d) => d.kind); edgeLabels.exit().remove(); @@ -701,8 +748,7 @@ export class Graph extends Component { .attr('r', nodeRadius) .attr('fill', 'none') .attr('stroke', highlightColor) - .attr('stroke-width', edgeArrowSize) - .attr('opacity', 0.35); + .attr('stroke-width', edgeArrowSize); nodeCircleHighlights.exit().remove(); } @@ -717,12 +763,21 @@ export class Graph extends Component { nodeCircles .enter() .append('circle') + .call(this.state.nodeDragHandler) + .on('click', this.onNodeEdgeClick) .merge(nodeCircles) .attr('class', 'graph_node_circle') .attr('r', nodeRadius) .attr('fill', (d) => this.getNodeFillColor(d.metanode)) - .style('cursor', 'pointer') - .call(this.state.nodeDragHandler); + .attr('stroke', (d) => { + if (d.selected) + return inkColor; + else + return 'none'; + }) + .attr('stroke-width', edgeThickness) + .style('stroke-dasharray', edgeThickness * 2 + ' ' + edgeThickness) + .style('cursor', 'pointer'); nodeCircles.exit().remove(); } @@ -763,10 +818,10 @@ export class Graph extends Component { .style('color', (d) => this.getNodeTextColor(d.metanode)) .style('word-break', 'break-word') .html((d) => { - if (d.name.length > nodeCharLimit) - return d.name.substr(0, nodeCharLimit - 3) + '...'; + if (d.data.name.length > nodeCharLimit) + return d.data.name.substr(0, nodeCharLimit - 3) + '...'; else - return d.name; + return d.data.name; }); nodeLabels.exit().remove(); @@ -789,13 +844,13 @@ export class Graph extends Component { const data = this.state.data; data.nodes.forEach((node) => { - if (node.id === data.source) { + if (node.neo4j_id === data.source_neo4j_id) { if (!node.x && !node.fx) node.fx = -nodeDistance * 2; if (!node.y && !node.fy) node.fy = 0; } - if (node.id === data.target) { + if (node.neo4j_id === data.target_neo4j_id) { if (!node.x && !node.fx) node.fx = nodeDistance * 2; if (!node.y && !node.fy) @@ -825,7 +880,12 @@ export class Graph extends Component { // construct graph object with relevant properties for each node/edge assembleGraph(pathQueries) { // empty graph object - const graph = { source: null, target: null, nodes: [], edges: [] }; + const graph = { + source_neo4j_id: null, + target_neo4j_id: null, + nodes: [], + edges: [] + }; // if null explicitly provided as argument, return empty graph object if (pathQueries === null) @@ -841,8 +901,8 @@ export class Graph extends Component { // get source/target nodes from first path in pathQueries const firstPath = pathQueries[0].paths[0]; - graph.source = firstPath.node_ids[0]; - graph.target = firstPath.node_ids[firstPath.node_ids.length - 1]; + graph.source_neo4j_id = firstPath.node_ids[0]; + graph.target_neo4j_id = firstPath.node_ids[firstPath.node_ids.length - 1]; // loop through all paths in pathQueries for (const pathQuery of pathQueries) { @@ -853,13 +913,16 @@ export class Graph extends Component { // loop through nodes in path for (const nodeId of path.node_ids) { - const existingNode = graph.nodes.find((node) => node.id === nodeId); + const node = pathQuery.nodes[nodeId]; + const existingNode = graph.nodes.find( + (existing) => existing.neo4j_id === node.neo4j_id + ); if (!existingNode) { // if node hasn't been added to graph yet, add it graph.nodes.push({ - id: nodeId, - metanode: pathQuery.nodes[nodeId].metanode, - name: pathQuery.nodes[nodeId].data.name, + // copy all properties of node + ...node, + // add highlight property highlighted: path.highlighted }); } else if (path.highlighted) @@ -869,20 +932,24 @@ export class Graph extends Component { // loop through edges in path for (const relId of path.rel_ids) { + const edge = pathQuery.relationships[relId]; const existingEdge = graph.edges.find( - (edge) => - edge.source === pathQuery.relationships[relId].source_neo4j_id && - edge.target === pathQuery.relationships[relId].target_neo4j_id && - edge.kind === pathQuery.relationships[relId].kind && - edge.directed === pathQuery.relationships[relId].directed + (existing) => + existing.source_neo4j_id === edge.source_neo4j_id && + existing.target_neo4j_id === edge.target_neo4j_id && + existing.kind === edge.kind && + existing.directed === edge.directed ); if (!existingEdge) { // if edge hasn't been added to graph yet, add it graph.edges.push({ - source: pathQuery.relationships[relId].source_neo4j_id, - target: pathQuery.relationships[relId].target_neo4j_id, - kind: pathQuery.relationships[relId].kind, - directed: pathQuery.relationships[relId].directed, + // copy all properties of edge + ...edge, + // set duplicate properties "source" and "target" because d3 + // needs them (with those names) to create links between nodes + source: edge.source_neo4j_id, + target: edge.target_neo4j_id, + // add highlight property highlighted: path.highlighted }); } else if (path.highlighted) @@ -904,8 +971,10 @@ export class Graph extends Component { for (const edgeBin of edgeBins) { const match = edgeBin.find( (edgeB) => - (edgeA.source === edgeB.source && edgeA.target === edgeB.target) || - (edgeA.source === edgeB.target && edgeA.target === edgeB.source) + (edgeA.source_neo4j_id === edgeB.source_neo4j_id && + edgeA.target_neo4j_id === edgeB.target_neo4j_id) || + (edgeA.source_neo4j_id === edgeB.target_neo4j_id && + edgeA.target_neo4j_id === edgeB.source_neo4j_id) ); // if matching bin found, add edge to it if (match) { @@ -924,7 +993,7 @@ export class Graph extends Component { // for each edge in bin, assign coincident "offset", a value between // -1 and 1 used for drawing, where 0 is straight line, negative is curve // on one side, and positive is curve on other side - const firstSource = edgeBin[0].source; + const firstSource = edgeBin[0].source_neo4j_id; for (let index = 0; index < edgeBin.length; index++) { // default offset to 0 let offset = 0; @@ -932,34 +1001,20 @@ export class Graph extends Component { offset = -0.5 + index / (edgeBin.length - 1); // if edge source/target order in reverse order as rest of bin, // invert offset - if (edgeBin[index].source !== firstSource) + if (edgeBin[index].source_neo4j_id !== firstSource) offset *= -1; edgeBin[index].coincidentOffset = offset; } } - // sort by key === true last - function compareBoolean(a, b, key) { - if (a[key] === true && b[key] !== true) - return 1; - else if (a[key] !== true && b[key] === true) - return -1; - else - return 0; - } - - // sort lists by highlighted last to ensure higher z-index - graph.nodes.sort((a, b) => compareBoolean(a, b, 'highlighted')); - graph.edges.sort((a, b) => compareBoolean(a, b, 'highlighted')); - // put source and target node at end of list to ensure highest z-index const sourceNodeIndex = graph.nodes.findIndex( - (node) => node.id === graph.source + (node) => node.neo4j_id === graph.source_neo4j_id ); if (sourceNodeIndex !== -1) graph.nodes.push(graph.nodes.splice(sourceNodeIndex, 1)[0]); const targetNodeIndex = graph.nodes.findIndex( - (node) => node.id === graph.target + (node) => node.neo4j_id === graph.target_neo4j_id ); if (targetNodeIndex !== -1) graph.nodes.push(graph.nodes.splice(targetNodeIndex, 1)[0]); @@ -979,6 +1034,55 @@ export class Graph extends Component { left = minLeft; } + // title text + const title = + (this.props.sourceNode.name || '___') + + ' → ' + + (this.props.targetNode.name || '___'); + + // description text + const description = [ + 'Graph visualization of the connectivity between ', + this.props.sourceNode.name || '___', + ' (', + this.props.sourceNode.metanode || '___', + ') and ', + this.props.targetNode.name || '___', + ' (', + this.props.targetNode.metanode || '___', + '). ', + '\n\n', + 'Created at ', + window.location.href, + '\n\n', + 'This subgraph of Hetionet v1.0 was created from paths between the ', + 'specified source/target nodes that occurred more than expected ', + 'by chance. ', + 'See https://het.io for more information. ' + ].join(''); + + // selected element info + let info = ''; + const el = this.state.selectedElement; + if (el) { + console.log(el); + let lines = []; + lines.push(el.data.name || ''); + lines.push(el.node_label || ''); + lines.push(el.data.description || ''); + lines.push(el.data.source || ''); + lines.push(el.data.identifier || ''); + lines.push(el.data.url || ''); + lines.push(el.neo4j_id || ''); + lines = lines.filter((line) => line !== ''); + info = lines.map((line, index) => ( + + {line} +
+
+ )); + } + return (
+ {title} + {description}
); @@ -1033,6 +1146,8 @@ export class Graph extends Component { // connect component to global state Graph = connect( (state) => ({ + sourceNode: state.sourceNode, + targetNode: state.targetNode, pathQueries: state.pathQueries, hetioStyles: state.hetioStyles }), From 7c9ce5f0601cdf019bd54620bc3b809ead4a822b Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Wed, 8 May 2019 17:22:53 -0400 Subject: [PATCH 02/10] update info --- src/path-graph.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/path-graph.js b/src/path-graph.js index 6c7c6f4..a1e0582 100644 --- a/src/path-graph.js +++ b/src/path-graph.js @@ -1065,14 +1065,22 @@ export class Graph extends Component { let info = ''; const el = this.state.selectedElement; if (el) { - console.log(el); let lines = []; lines.push(el.data.name || ''); lines.push(el.node_label || ''); lines.push(el.data.description || ''); lines.push(el.data.source || ''); + lines.push( + el.data.sources && el.data.sources.length > 0 + ? el.data.sources.join(', ') + : '' + ); lines.push(el.data.identifier || ''); lines.push(el.data.url || ''); + lines.push(el.kind || ''); + lines.push( + el.directed === undefined ? '' : el.directed ? 'directed' : 'undirected' + ); lines.push(el.neo4j_id || ''); lines = lines.filter((line) => line !== ''); info = lines.map((line, index) => ( From 32f77074c4225b71b56cf6d4cb5845599c1f652c Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 13:07:54 -0400 Subject: [PATCH 03/10] move table outside of graph, improve data display --- src/node-results.js | 33 +++--- src/path-graph.css | 14 ++- src/path-graph.js | 247 ++++++++++++++++++++++++++++++++++++-------- src/styles.css | 2 +- src/util.js | 9 ++ 5 files changed, 237 insertions(+), 68 deletions(-) diff --git a/src/node-results.js b/src/node-results.js index e36c740..84cf0d1 100644 --- a/src/node-results.js +++ b/src/node-results.js @@ -9,6 +9,7 @@ import { Tooltip } from './tooltip.js'; import { TextButton } from './buttons.js'; import { DynamicField } from './dynamic-field.js'; import { CollapsibleSection } from './collapsible-section.js'; +import { shortenUrl } from './util.js'; // node results section component // details about source/target nodes @@ -85,6 +86,17 @@ class TableFull extends Component { fields = fields.concat(extraFields); } + // helper text when user hovers over given field + let tooltipText = {}; + if (this.props.hetioDefinitions.properties) { + tooltipText = { + ...tooltipText, + ...this.props.hetioDefinitions.properties.common, + ...this.props.hetioDefinitions.properties.nodes + }; + } + tooltipText = { ...tooltipText, ...this.props.hetmechDefinitions }; + // determine contents of first and second column for each row entry return fields.map((field, index) => { // set first col to field name @@ -95,6 +107,7 @@ class TableFull extends Component { secondCol = this.props.node.data[field]; if (secondCol === undefined) secondCol = ''; + secondCol = String(secondCol); // handle special field cases if (field === 'metanode') { @@ -117,17 +130,6 @@ class TableFull extends Component { ); } - // helper text when user hovers over given field - let tooltipText = {}; - if (this.props.hetioDefinitions.properties) { - tooltipText = { - ...tooltipText, - ...this.props.hetioDefinitions.properties.common, - ...this.props.hetioDefinitions.properties.nodes - }; - } - tooltipText = { ...tooltipText, ...this.props.hetmechDefinitions }; - // return row entry return ( @@ -186,12 +188,3 @@ class TableEmpty extends Component { ); } } - -// remove unnecessary preceding 'www.' and etc from url -function shortenUrl(url) { - const remove = ['http://', 'https://', 'www.']; - for (const str of remove) - url = url.replace(str, ''); - - return url; -} diff --git a/src/path-graph.css b/src/path-graph.css index dbe103a..3e3e070 100644 --- a/src/path-graph.css +++ b/src/path-graph.css @@ -12,7 +12,7 @@ .graph_expand_collapse_button { margin: 0 !important; } -#graph_overlay { +#graph_overlay { pointer-events: none; } #graph_info { @@ -20,4 +20,14 @@ left: 0; bottom: 0; font-size: 8px; -} \ No newline at end of file +} +.graph_info_header { + margin-top: 10px; + font-weight: 500; +} +#graph_info_table { + margin-top: 10px; +} +#graph_info_table td { + height: 20px; +} diff --git a/src/path-graph.js b/src/path-graph.js index a1e0582..4f8ac19 100644 --- a/src/path-graph.js +++ b/src/path-graph.js @@ -15,6 +15,8 @@ import { CollapsibleSection } from './collapsible-section.js'; import { NumberBox } from './number-box.js'; import { TextButton } from './buttons.js'; import { downloadSvg } from './util.js'; +import { Tooltip } from './tooltip.js'; +import { sortCustom } from './util.js'; import './path-graph.css'; // graph settings @@ -49,6 +51,7 @@ export class PathGraph extends Component { this.state.height = maxHeight; this.state.nodeCount = 0; this.state.edgeCount = 0; + this.state.selectedElement = null; this.graph = React.createRef(); @@ -58,6 +61,7 @@ export class PathGraph extends Component { this.setWidth = this.setWidth.bind(this); this.setHeight = this.setHeight.bind(this); this.setGraphCounts = this.setGraphCounts.bind(this); + this.setSelectedElement = this.setSelectedElement.bind(this); } // when component mounts @@ -140,8 +144,20 @@ export class PathGraph extends Component { }); } + // sets the selected node/edge + setSelectedElement(element) { + this.setState({ selectedElement: element }); + } + // display component render() { + let info = ''; + if (this.state.selectedElement) { + if (this.state.selectedElement.elementType === 'node') + info = ; + if (this.state.selectedElement.elementType === 'edge') + info = ; + } return (
{this.state.nodeCount} nodes, {this.state.edgeCount} edges @@ -230,11 +246,13 @@ export class PathGraph extends Component { + {info}
); @@ -249,7 +267,6 @@ export class Graph extends Component { this.state = {}; this.state.data = this.assembleGraph(null); - this.state.selectedElement = null; this.fitView = this.fitView.bind(this); this.resetView = this.resetView.bind(this); @@ -604,7 +621,7 @@ export class Graph extends Component { this.updateNodeCircles(); this.updateEdgeLines(); - this.setState({ selectedElement: d }); + this.props.setSelectedElement(d); } // deselect all elements @@ -636,7 +653,7 @@ export class Graph extends Component { this.deselectAll(); this.updateNodeCircles(); this.updateEdgeLines(); - this.setState({ selectedElement: null }); + this.props.setSelectedElement(null); } // when view panned or zoomed by user @@ -923,7 +940,9 @@ export class Graph extends Component { // copy all properties of node ...node, // add highlight property - highlighted: path.highlighted + highlighted: path.highlighted, + // mark as node + elementType: 'node' }); } else if (path.highlighted) // if node already in graph, still update highlight status @@ -950,7 +969,9 @@ export class Graph extends Component { source: edge.source_neo4j_id, target: edge.target_neo4j_id, // add highlight property - highlighted: path.highlighted + highlighted: path.highlighted, + // mark as edge + elementType: 'edge' }); } else if (path.highlighted) // if edge already in graph, still update highlight status @@ -1061,36 +1082,6 @@ export class Graph extends Component { 'See https://het.io for more information. ' ].join(''); - // selected element info - let info = ''; - const el = this.state.selectedElement; - if (el) { - let lines = []; - lines.push(el.data.name || ''); - lines.push(el.node_label || ''); - lines.push(el.data.description || ''); - lines.push(el.data.source || ''); - lines.push( - el.data.sources && el.data.sources.length > 0 - ? el.data.sources.join(', ') - : '' - ); - lines.push(el.data.identifier || ''); - lines.push(el.data.url || ''); - lines.push(el.kind || ''); - lines.push( - el.directed === undefined ? '' : el.directed ? 'directed' : 'undirected' - ); - lines.push(el.neo4j_id || ''); - lines = lines.filter((line) => line !== ''); - info = lines.map((line, index) => ( - - {line} -
-
- )); - } - return (
- -
{info}
-
); @@ -1163,3 +1147,176 @@ Graph = connect( null, { forwardRef: true } )(Graph); + +// selected node info component +class SelectedNodeInfo extends Component { + // display row entries + rows() { + // helper text when user hovers over given field + let tooltipText = {}; + if (this.props.hetioDefinitions.properties) { + tooltipText = { + ...tooltipText, + ...this.props.hetioDefinitions.properties.common, + ...this.props.hetioDefinitions.properties.nodes + }; + } + tooltipText = { ...tooltipText, ...this.props.hetmechDefinitions }; + if (tooltipText['id']) + tooltipText['neo4j_id'] = tooltipText['id']; + + // get primary fields from node + let primaryFields = ['metanode', 'neo4j_id']; + // get first/second column text (key/value) for each field + primaryFields = primaryFields.map((field) => ({ + firstCol: field, + secondCol: this.props.node[field], + tooltipText: tooltipText[field] + })); + + // get 'extra fields' from node 'data' field + let extraFields = Object.keys(this.props.node.data); + // sort extra fields alphabetically + extraFields = extraFields.sort(); + // get first/second column text (key/value) for each field + extraFields = extraFields.map((field) => ({ + firstCol: field, + secondCol: this.props.node.data[field], + tooltipText: tooltipText[field] + })); + + // combine primary and extra fields + let fields = primaryFields.concat(extraFields); + + // display fields in custom order + const order = [ + 'name', + 'metanode', + 'description', + 'identifier', + 'source', + 'url', + 'neo4j_id' + ]; + fields = sortCustom(fields, order, 'firstCol'); + + // make columns from fields + const cols = fields.map((field, index) => { + return ( + + + {field.firstCol} + + {field.secondCol} + + ); + }); + + // make rows in groups of two + const rows = new Array(Math.ceil(cols.length / 2)) + .fill() + .map(() => cols.splice(0, 2)) + .map((col, index) => {col}); + + return rows; + } + + // display component + render() { + return ( + <> +
Selected node:
+ + {this.rows()} +
+ + ); + } +} +// connect component to global state +SelectedNodeInfo = connect((state) => ({ + hetioDefinitions: state.hetioDefinitions, + hetmechDefinitions: state.hetmechDefinitions +}))(SelectedNodeInfo); + +// selected edge info component +class SelectedEdgeInfo extends Component { + // display row entries + rows() { + // helper text when user hovers over given field + let tooltipText = {}; + if (this.props.hetioDefinitions.properties) { + tooltipText = { + ...tooltipText, + ...this.props.hetioDefinitions.properties.common, + ...this.props.hetioDefinitions.properties.edges + }; + } + tooltipText = { ...tooltipText, ...this.props.hetmechDefinitions }; + if (tooltipText['id']) + tooltipText['neo4j_id'] = tooltipText['id']; + + // get primary fields from node + let primaryFields = ['kind', 'directed', 'neo4j_id']; + // get first/second column text (key/value) for each field + primaryFields = primaryFields.map((field) => ({ + firstCol: field, + secondCol: String(this.props.edge[field]), + tooltipText: tooltipText[field] + })); + + // get 'extra fields' from node 'data' field + let extraFields = Object.keys(this.props.edge.data); + // sort extra fields alphabetically + extraFields = extraFields.sort(); + // get first/second column text (key/value) for each field + extraFields = extraFields.map((field) => ({ + firstCol: field, + secondCol: String(this.props.edge.data[field]), + tooltipText: tooltipText[field] + })); + + // combine primary and extra fields + let fields = primaryFields.concat(extraFields); + + // display fields in custom order + const order = ['kind', 'neo4j_id', 'source']; + fields = sortCustom(fields, order, 'firstCol'); + + // make columns from fields + const cols = fields.map((field, index) => { + return ( + + + {field.firstCol} + + {field.secondCol} + + ); + }); + // make rows in groups of two + const rows = new Array(Math.ceil(cols.length / 2)) + .fill() + .map(() => cols.splice(0, 2)) + .map((col, index) => {col}); + + return rows; + } + + // display component + render() { + return ( + <> +
Selected edge:
+ + {this.rows()} +
+ + ); + } +} +// connect component to global state +SelectedEdgeInfo = connect((state) => ({ + hetioDefinitions: state.hetioDefinitions, + hetmechDefinitions: state.hetmechDefinitions +}))(SelectedEdgeInfo); diff --git a/src/styles.css b/src/styles.css index de0e9d4..47d7fc0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -163,7 +163,7 @@ td > * { width: 30px; } .col_s { - width: 75px; + width: 80px; } .col_m { width: 100px; diff --git a/src/util.js b/src/util.js index 242fbcd..4f10160 100644 --- a/src/util.js +++ b/src/util.js @@ -165,3 +165,12 @@ export function sortCustom(array, order, key) { return b - a; }); } + +// remove unnecessary preceding 'www.' and etc from url +export function shortenUrl(url) { + const remove = ['http://', 'https://', 'www.']; + for (const str of remove) + url = url.replace(str, ''); + + return url; +} From 9aa14d4fbbba2e396bf6d7540e83b735bceb4698 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 13:36:46 -0400 Subject: [PATCH 04/10] refactor node results table in same manner --- src/node-results.js | 96 ++++++++++++++++++++++++++++----------------- src/path-graph.js | 12 ++---- 2 files changed, 65 insertions(+), 43 deletions(-) diff --git a/src/node-results.js b/src/node-results.js index 84cf0d1..9d74aec 100644 --- a/src/node-results.js +++ b/src/node-results.js @@ -10,6 +10,7 @@ import { TextButton } from './buttons.js'; import { DynamicField } from './dynamic-field.js'; import { CollapsibleSection } from './collapsible-section.js'; import { shortenUrl } from './util.js'; +import { sortCustom } from './util.js'; // node results section component // details about source/target nodes @@ -69,23 +70,6 @@ class TableFull extends Component { // display row entries rows() { - // explicitly specify and order primary fields - let fields = ['name', 'metanode', 'identifier', 'source']; - - if (this.state.showMore) { - // get 'extra fields' from node 'data' field - let extraFields = Object.keys(this.props.node.data); - // remove unnecessary fields - extraFields.splice(extraFields.indexOf('source'), 1); - extraFields.splice(extraFields.indexOf('url'), 1); - // sort extra fields alphabetically - extraFields = extraFields.sort(); - // add 'id' to beginning of extra fields - extraFields.unshift('id'); - // append 'extraFields' to primary 'fields' - fields = fields.concat(extraFields); - } - // helper text when user hovers over given field let tooltipText = {}; if (this.props.hetioDefinitions.properties) { @@ -97,22 +81,15 @@ class TableFull extends Component { } tooltipText = { ...tooltipText, ...this.props.hetmechDefinitions }; - // determine contents of first and second column for each row entry - return fields.map((field, index) => { - // set first col to field name - const firstCol = field; - // default second col to field value in node - let secondCol = this.props.node[field]; - if (secondCol === undefined) - secondCol = this.props.node.data[field]; - if (secondCol === undefined) - secondCol = ''; - secondCol = String(secondCol); - + // get primary fields from top level of node + let primaryFields = ['name', 'metanode', 'source', 'identifier', 'id']; + // get first/second column text (key/value) for each field + primaryFields = primaryFields.map((field) => { // handle special field cases + let specialSecondCol; if (field === 'metanode') { // make text with metanode chip - secondCol = ( + specialSecondCol = ( <> {this.props.node[field]} @@ -123,21 +100,70 @@ class TableFull extends Component { const linkUrl = this.props.node.url || this.props.node.data.url || ''; let linkText = this.props.node.data.source || linkUrl; linkText = shortenUrl(linkText); - secondCol = ( + specialSecondCol = ( {linkText} ); } + // get first/second column text (key/value) for each field + return { + firstCol: field, + secondCol: specialSecondCol || String(this.props.node[field]), + tooltipText: tooltipText[field] + }; + }); + // remove id and identifier if table not expanded + if (!this.state.showMore) { + primaryFields.splice( + primaryFields.findIndex((field) => field.firstCol === 'id'), + 1 + ); + primaryFields.splice( + primaryFields.findIndex((field) => field.firstCol === 'identifier'), + 1 + ); + } + + // get 'extra fields' from node 'data' field + let extraFields = []; + if (this.state.showMore) { + extraFields = Object.keys(this.props.node.data); + // remove source and url, since they are combined and added to + // primary fields above + extraFields.splice(extraFields.indexOf('source'), 1); + extraFields.splice(extraFields.indexOf('url'), 1); + // get first/second column text (key/value) for each field + extraFields = extraFields.map((field) => ({ + firstCol: field, + secondCol: String(this.props.node.data[field]), + tooltipText: tooltipText[field] + })); + } + + // combine primary and extra fields + let fields = primaryFields.concat(extraFields); - // return row entry + // display fields in custom order + const order = [ + 'name', + 'metanode', + 'source', + 'description', + 'identifier', + 'id' + ]; + fields = sortCustom(fields, order, 'firstCol'); + + // make rows from fields + return fields.map((field, index) => { return ( - - {firstCol} + + {field.firstCol} - + ); diff --git a/src/path-graph.js b/src/path-graph.js index 4f8ac19..ac6dc7d 100644 --- a/src/path-graph.js +++ b/src/path-graph.js @@ -1165,7 +1165,7 @@ class SelectedNodeInfo extends Component { if (tooltipText['id']) tooltipText['neo4j_id'] = tooltipText['id']; - // get primary fields from node + // get primary fields from top level of node let primaryFields = ['metanode', 'neo4j_id']; // get first/second column text (key/value) for each field primaryFields = primaryFields.map((field) => ({ @@ -1176,8 +1176,6 @@ class SelectedNodeInfo extends Component { // get 'extra fields' from node 'data' field let extraFields = Object.keys(this.props.node.data); - // sort extra fields alphabetically - extraFields = extraFields.sort(); // get first/second column text (key/value) for each field extraFields = extraFields.map((field) => ({ firstCol: field, @@ -1192,10 +1190,10 @@ class SelectedNodeInfo extends Component { const order = [ 'name', 'metanode', - 'description', - 'identifier', 'source', 'url', + 'description', + 'identifier', 'neo4j_id' ]; fields = sortCustom(fields, order, 'firstCol'); @@ -1256,7 +1254,7 @@ class SelectedEdgeInfo extends Component { if (tooltipText['id']) tooltipText['neo4j_id'] = tooltipText['id']; - // get primary fields from node + // get primary fields from top level of node let primaryFields = ['kind', 'directed', 'neo4j_id']; // get first/second column text (key/value) for each field primaryFields = primaryFields.map((field) => ({ @@ -1267,8 +1265,6 @@ class SelectedEdgeInfo extends Component { // get 'extra fields' from node 'data' field let extraFields = Object.keys(this.props.edge.data); - // sort extra fields alphabetically - extraFields = extraFields.sort(); // get first/second column text (key/value) for each field extraFields = extraFields.map((field) => ({ firstCol: field, From 14a9885431987f206dcd58c650d978497eaebf78 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 13:39:03 -0400 Subject: [PATCH 05/10] tweak graph selected node info css --- src/path-graph.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/path-graph.css b/src/path-graph.css index 3e3e070..426ee56 100644 --- a/src/path-graph.css +++ b/src/path-graph.css @@ -26,7 +26,7 @@ font-weight: 500; } #graph_info_table { - margin-top: 10px; + margin-top: 5px; } #graph_info_table td { height: 20px; From 42cc936a29dc4598267b7c1ff558a5769ff6d8f6 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 13:59:05 -0400 Subject: [PATCH 06/10] tweak css of selected info header --- src/path-graph.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/path-graph.js b/src/path-graph.js index ac6dc7d..674602c 100644 --- a/src/path-graph.js +++ b/src/path-graph.js @@ -1223,7 +1223,7 @@ class SelectedNodeInfo extends Component { render() { return ( <> -
Selected node:
+
Selected Node
{this.rows()}
@@ -1303,7 +1303,7 @@ class SelectedEdgeInfo extends Component { render() { return ( <> -
Selected edge:
+
Selected Edge
{this.rows()}
From 8538721590dddbedcf7514213e21ba475d28114a Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 16:23:00 -0400 Subject: [PATCH 07/10] refactor selectedinfo component --- src/path-graph.js | 160 ++++++++++++++-------------------------------- 1 file changed, 49 insertions(+), 111 deletions(-) diff --git a/src/path-graph.js b/src/path-graph.js index 674602c..d575b48 100644 --- a/src/path-graph.js +++ b/src/path-graph.js @@ -153,10 +153,38 @@ export class PathGraph extends Component { render() { let info = ''; if (this.state.selectedElement) { - if (this.state.selectedElement.elementType === 'node') - info = ; - if (this.state.selectedElement.elementType === 'edge') - info = ; + if (this.state.selectedElement.elementType === 'node') { + info = ( + <> +
Selected Node
+ + + ); + } + if (this.state.selectedElement.elementType === 'edge') { + info = ( + <> +
Selected Edge
+ + + ); + } } return (
@@ -1148,97 +1176,8 @@ Graph = connect( { forwardRef: true } )(Graph); -// selected node info component -class SelectedNodeInfo extends Component { - // display row entries - rows() { - // helper text when user hovers over given field - let tooltipText = {}; - if (this.props.hetioDefinitions.properties) { - tooltipText = { - ...tooltipText, - ...this.props.hetioDefinitions.properties.common, - ...this.props.hetioDefinitions.properties.nodes - }; - } - tooltipText = { ...tooltipText, ...this.props.hetmechDefinitions }; - if (tooltipText['id']) - tooltipText['neo4j_id'] = tooltipText['id']; - - // get primary fields from top level of node - let primaryFields = ['metanode', 'neo4j_id']; - // get first/second column text (key/value) for each field - primaryFields = primaryFields.map((field) => ({ - firstCol: field, - secondCol: this.props.node[field], - tooltipText: tooltipText[field] - })); - - // get 'extra fields' from node 'data' field - let extraFields = Object.keys(this.props.node.data); - // get first/second column text (key/value) for each field - extraFields = extraFields.map((field) => ({ - firstCol: field, - secondCol: this.props.node.data[field], - tooltipText: tooltipText[field] - })); - - // combine primary and extra fields - let fields = primaryFields.concat(extraFields); - - // display fields in custom order - const order = [ - 'name', - 'metanode', - 'source', - 'url', - 'description', - 'identifier', - 'neo4j_id' - ]; - fields = sortCustom(fields, order, 'firstCol'); - - // make columns from fields - const cols = fields.map((field, index) => { - return ( - - - {field.firstCol} - - {field.secondCol} - - ); - }); - - // make rows in groups of two - const rows = new Array(Math.ceil(cols.length / 2)) - .fill() - .map(() => cols.splice(0, 2)) - .map((col, index) => {col}); - - return rows; - } - - // display component - render() { - return ( - <> -
Selected Node
- - {this.rows()} -
- - ); - } -} -// connect component to global state -SelectedNodeInfo = connect((state) => ({ - hetioDefinitions: state.hetioDefinitions, - hetmechDefinitions: state.hetmechDefinitions -}))(SelectedNodeInfo); - -// selected edge info component -class SelectedEdgeInfo extends Component { +// selected node/edge info component +class SelectedInfo extends Component { // display row entries rows() { // helper text when user hovers over given field @@ -1254,21 +1193,23 @@ class SelectedEdgeInfo extends Component { if (tooltipText['id']) tooltipText['neo4j_id'] = tooltipText['id']; - // get primary fields from top level of node - let primaryFields = ['kind', 'directed', 'neo4j_id']; + const element = this.props.node || this.props.edge; + + // get primary fields from top level of node/edge + let primaryFields = this.props.primaryFields; // get first/second column text (key/value) for each field primaryFields = primaryFields.map((field) => ({ firstCol: field, - secondCol: String(this.props.edge[field]), + secondCol: String(element[field]), tooltipText: tooltipText[field] })); - // get 'extra fields' from node 'data' field - let extraFields = Object.keys(this.props.edge.data); + // get 'extra fields' from node/edge 'data' field + let extraFields = Object.keys(element.data); // get first/second column text (key/value) for each field extraFields = extraFields.map((field) => ({ firstCol: field, - secondCol: String(this.props.edge.data[field]), + secondCol: String(element.data[field]), tooltipText: tooltipText[field] })); @@ -1276,8 +1217,7 @@ class SelectedEdgeInfo extends Component { let fields = primaryFields.concat(extraFields); // display fields in custom order - const order = ['kind', 'neo4j_id', 'source']; - fields = sortCustom(fields, order, 'firstCol'); + fields = sortCustom(fields, this.props.order, 'firstCol'); // make columns from fields const cols = fields.map((field, index) => { @@ -1290,6 +1230,7 @@ class SelectedEdgeInfo extends Component { ); }); + // make rows in groups of two const rows = new Array(Math.ceil(cols.length / 2)) .fill() @@ -1302,17 +1243,14 @@ class SelectedEdgeInfo extends Component { // display component render() { return ( - <> -
Selected Edge
- - {this.rows()} -
- + + {this.rows()} +
); } } // connect component to global state -SelectedEdgeInfo = connect((state) => ({ +SelectedInfo = connect((state) => ({ hetioDefinitions: state.hetioDefinitions, hetmechDefinitions: state.hetmechDefinitions -}))(SelectedEdgeInfo); +}))(SelectedInfo); From 740accdcd369b315745bcd527d2f87fce56a3f04 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 16:38:02 -0400 Subject: [PATCH 08/10] change shortenurl to regex --- src/util.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util.js b/src/util.js index 4f10160..2defa1d 100644 --- a/src/util.js +++ b/src/util.js @@ -168,9 +168,9 @@ export function sortCustom(array, order, key) { // remove unnecessary preceding 'www.' and etc from url export function shortenUrl(url) { - const remove = ['http://', 'https://', 'www.']; - for (const str of remove) - url = url.replace(str, ''); + const regexes = ['^http:\/\/', '^https:\/\/', '\/\/www\.']; + for (const regex of regexes) + url = url.replace(new RegExp(regex), ''); return url; } From 5f169c53f43801f841d98940a680d5cb20194639 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 16:43:56 -0400 Subject: [PATCH 09/10] fix lint error --- src/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index 2defa1d..f490ee3 100644 --- a/src/util.js +++ b/src/util.js @@ -168,7 +168,7 @@ export function sortCustom(array, order, key) { // remove unnecessary preceding 'www.' and etc from url export function shortenUrl(url) { - const regexes = ['^http:\/\/', '^https:\/\/', '\/\/www\.']; + const regexes = ['^http://', '^https://', '//www.']; for (const regex of regexes) url = url.replace(new RegExp(regex), ''); From ff550f9ac30921469554112ad53d5f6c9d7fc495 Mon Sep 17 00:00:00 2001 From: Vincent Rubinetti Date: Thu, 9 May 2019 16:51:47 -0400 Subject: [PATCH 10/10] change shortenurl regex --- src/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.js b/src/util.js index f490ee3..9f92233 100644 --- a/src/util.js +++ b/src/util.js @@ -168,7 +168,7 @@ export function sortCustom(array, order, key) { // remove unnecessary preceding 'www.' and etc from url export function shortenUrl(url) { - const regexes = ['^http://', '^https://', '//www.']; + const regexes = ['^http://', '^https://', '^www.']; for (const regex of regexes) url = url.replace(new RegExp(regex), '');