From 1319dce0dba5d6e7763865bc2f13c80ffd1740c6 Mon Sep 17 00:00:00 2001 From: Dennis Collaris <d.a.c.collaris@uu.nl> Date: Mon, 3 Feb 2025 10:43:12 +0000 Subject: [PATCH] feat(mapvis): implement directed arrows --- .../layers/nodelink-layer/NodeLinkLayer.tsx | 94 +++++++++++++++++-- .../layers/nodelink-layer/NodeLinkOptions.tsx | 10 ++ 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx index 2df47ed97..6ce5e9681 100644 --- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx +++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx @@ -143,10 +143,88 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { graphMetadata.edges.labels.forEach((label: string) => { const layerId = `${label}-edges-line`; - const edgeData = data.edges.filter(edge => { - const from = nodeLocations[edge.from]; - const to = nodeLocations[edge.to]; - return from && to && !hiddenNodes.has(edge.from) && !hiddenNodes.has(edge.to); + const showArrows = layerSettings?.showArrows; + + const parseEdge = (edge: EdgeQueryResult) => { + const [sx, sy] = nodeLocations[edge.from]; + const [tx, ty] = nodeLocations[edge.to]; + + const [wsx, wsy] = this.context.viewport.projectPosition([sx, sy, 1]); + // eslint-disable-next-line prefer-const + let [wtx, wty, wtz] = this.context.viewport.projectPosition([tx, ty, 1]); + + if (showArrows) { + // Perpendicular normalized vector + let ax = wtx - wsx; + let ay = wty - wsy; + const amag = Math.sqrt(ax * ax + ay * ay); + if (amag != 0) { + ax /= amag; + ay /= amag; + } + + const nodeRadius = wtz * 40; + + if (wtx - wsx != 0) wtx -= (nodeRadius / amag) * (wtx - wsx); + if (wty - wsy != 0) wty -= (nodeRadius / amag) * (wty - wsy); + + // Draw arrow heads + const arrowSize = nodeRadius / 7; + const arrowRatio = 1.75; + + let px = wty - wsy; + let py = -(wtx - wsx); + const pmag = Math.sqrt(px * px + py * py); + if (pmag != 0) { + px /= pmag; + py /= pmag; + } + + const arrow1_x = px - ax * arrowRatio; + const arrow1_y = py - ay * arrowRatio; + const arrow2_x = -px - ax * arrowRatio; + const arrow2_y = -py - ay * arrowRatio; + + return [ + // Arrow head line 1 + { + label: edge.label, + from: this.context.viewport.unprojectPosition([wtx, wty]), + to: this.context.viewport.unprojectPosition([wtx + arrow1_x * arrowSize, wty + arrow1_y * arrowSize]), + attributes: edge.attributes, + }, + // Edge + { + _id: edge._id, + label: edge.label, + attributes: edge.attributes, + from: [sx, sy], + fromID: edge.from, + to: this.context.viewport.unprojectPosition([wtx, wty]), + toID: edge.to, + }, + // Arrow head line 2 + { + label: edge.label, + from: this.context.viewport.unprojectPosition([wtx, wty]), + to: this.context.viewport.unprojectPosition([wtx + arrow2_x * arrowSize, wty + arrow2_y * arrowSize]), + attributes: edge.attributes, + }, + ]; + } else { + return { + _id: edge._id, + label: edge.label, + attributes: edge.attributes, + from: [sx, sy], + fromID: edge.from, + to: [tx, ty], + toID: edge.to, + }; + } + }; + const edgeData = data.edges.flatMap(parseEdge).filter(edge => { + return edge != null && edge.from && edge.to && !hiddenNodes.has(edge.fromID) && !hiddenNodes.has(edge.toID); }); this._layers[layerId] = new LineLayer({ @@ -155,8 +233,8 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { visible: !layerSettings?.edges[label]?.hidden, pickable: true, getWidth: layerSettings?.edges[label]?.width, - getSourcePosition: d => getNodeLocation(d.from), - getTargetPosition: d => getNodeLocation(d.to), + getSourcePosition: d => d.from, + getTargetPosition: d => d.to, getColor: (d: EdgeQueryResult) => this.getEdgeColor(d), extensions: [brushingExtension], brushingEnabled: layerSettings?.enableBrushing, @@ -169,8 +247,8 @@ export class NodeLinkLayer extends CompositeLayer<CompositeLayerType> { data: ml.linkPrediction.result, pickable: false, getWidth: 1, - getSourcePosition: d => getNodeLocation(d.from), - getTargetPosition: d => getNodeLocation(d.to), + getSourcePosition: d => d.from, + getTargetPosition: d => d.to, getColor: d => [0, 0, 0], }); } diff --git a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx index 85a282189..d54313a49 100644 --- a/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx +++ b/src/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkOptions.tsx @@ -30,6 +30,7 @@ const defaultEdgeSettings = () => ({ colorByAttribute: false, colorAttribute: undefined, colorAttributeType: undefined, + showArrows: false, }); export function NodeLinkOptions({ @@ -275,6 +276,15 @@ export function NodeLinkOptions({ }} /> + <Input + label="Show arrows" + type="boolean" + value={settings?.showArrows} + onChange={val => { + updateLayerSettings({ showArrows: val as boolean }); + }} + /> + <Accordion> <AccordionItem> <AccordionHead> -- GitLab