|
4 | 4 | /* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
5 | 5 | /* eslint-disable @typescript-eslint/ban-types */
|
6 | 6 |
|
7 |
| -import React, { useState, useEffect } from 'react'; |
| 7 | +import React, { useEffect, useCallback, useState } from 'react'; |
8 | 8 | import * as d3 from 'd3';
|
9 | 9 |
|
10 | 10 | const Map = (props) => {
|
11 |
| - const { snapshots } = props; |
12 |
| - const lastSnap = snapshots.length - 1; |
| 11 | + //import props |
| 12 | + const { viewIndex, snapshots ,x ,y, k, setZoomState} = props; |
| 13 | + let lastSnap: number | null = null; |
| 14 | + if (viewIndex < 0) lastSnap = snapshots.length - 1; |
| 15 | + else lastSnap = viewIndex; |
13 | 16 |
|
14 |
| - // set the heights and width of the tree to be passed into treeMap function |
| 17 | + //external constants |
15 | 18 | const width: number = 900;
|
16 | 19 | const height: number = 600;
|
| 20 | + let data = snapshots[lastSnap]; |
17 | 21 |
|
18 |
| - // this state allows the canvas to stay at the zoom level on multiple re-renders |
19 |
| - const [{ x, y, k }, setZoomState]: any = useState({ x: 0, y: 0, k: 0 }); |
20 | 22 | useEffect(() => {
|
| 23 | + document.getElementById('canvas').innerHTML = '_'; |
21 | 24 | setZoomState(d3.zoomTransform(d3.select('#canvas').node()));
|
22 |
| - }, [snapshots[lastSnap]]); |
| 25 | + return makeChart(data); |
| 26 | + }, [data]); |
23 | 27 |
|
24 |
| - // Create D3 Tree Diagram |
25 |
| - useEffect(() => { |
26 |
| - document.getElementById('canvas').innerHTML = ''; |
| 28 | + const makeChart = useCallback( |
| 29 | + (data) => { |
| 30 | + // Establish Constants |
| 31 | + const margin = { top: 10, right: 120, bottom: 10, left: 120 }; |
| 32 | + const dy = 120; |
| 33 | + const dx = 100; |
| 34 | + const tree = d3.tree().nodeSize([dx, dy]); |
| 35 | + const diagonal = d3 |
| 36 | + .linkHorizontal() |
| 37 | + .x((d) => d.y) |
| 38 | + .y((d) => d.x); |
| 39 | + const root = d3.hierarchy(data); |
27 | 40 |
|
28 |
| - // creating the main svg container for d3 elements |
29 |
| - const svgContainer: any = d3 |
30 |
| - .select('#canvas') |
31 |
| - .attr('width', width) |
32 |
| - .attr('height', height); |
| 41 | + // Determine descendants of root node use d.depth conditional to how many levels deep to display on first render |
| 42 | + root.x0 = dy / 2; |
| 43 | + root.y0 = 0; |
| 44 | + root.descendants().forEach((d, i) => { |
| 45 | + d.id = i; |
| 46 | + d._children = d.children; |
| 47 | + // use to limit depth of children rendered |
| 48 | + //if (d.depth === 5) d.children = null; |
| 49 | + }); |
33 | 50 |
|
34 |
| - // creating a pseudo-class for reusability |
35 |
| - const g: any = svgContainer |
36 |
| - .append('g') |
37 |
| - .attr('transform', `translate(${x}, ${y}), scale(${k})`); // sets the canvas to the saved zoomState |
| 51 | + // Create Container for D3 Visualizations |
| 52 | + const svgContainer = d3 |
| 53 | + .select('#canvas') |
| 54 | + .attr('width', width) |
| 55 | + .attr('height', height); |
38 | 56 |
|
39 |
| - // creating the tree map |
40 |
| - const treeMap: any = d3.tree().nodeSize([width, height]); |
| 57 | + // create inner container to help with drag and zoom |
| 58 | + const svg: any = svgContainer |
| 59 | + .append('g') |
| 60 | + .attr('transform', `translate(${x}, ${y}), scale(${k})`); // sets the canvas to the saved zoomState |
41 | 61 |
|
42 |
| - // creating the nodes of the tree |
43 |
| - const hierarchyNodes: any = d3.hierarchy(snapshots[lastSnap]); |
| 62 | + // create links |
| 63 | + const gLink = svg |
| 64 | + .append('g') |
| 65 | + .attr('fill', 'none') |
| 66 | + .attr('stroke', '#555') |
| 67 | + .attr('stroke-opacity', 0.9) |
| 68 | + .attr('stroke-width', 1.5); |
44 | 69 |
|
45 |
| - // calling the tree function with nodes created from data |
46 |
| - const finalMap: any = treeMap(hierarchyNodes); |
| 70 | + // create nodes |
| 71 | + const gNode = svg |
| 72 | + .append('g') |
| 73 | + .attr('cursor', 'pointer') |
| 74 | + .attr('pointer-events', 'all'); |
47 | 75 |
|
48 |
| - // renders a flat array of objects containing all parent-child links |
49 |
| - // renders the paths onto the component |
50 |
| - let paths: any = finalMap.links(); |
| 76 | + // declare re render funciton to handle collapse and expansion of nodes |
| 77 | + const update = (source) => { |
| 78 | + const duration = 0; |
| 79 | + const nodes = root.descendants().reverse(); |
| 80 | + const links = root.links(); |
51 | 81 |
|
52 |
| - // this creates the paths to each node and its contents in the tree |
53 |
| - g.append('g') |
54 |
| - .attr('fill', 'none') |
55 |
| - .attr('stroke', '#646464') |
56 |
| - .attr('stroke-width', 5) |
57 |
| - .selectAll('path') |
58 |
| - .data(paths) |
59 |
| - .enter() |
60 |
| - .append('path') |
61 |
| - .attr( |
62 |
| - 'd', |
| 82 | + // Compute the new tree layout. |
| 83 | + tree(root); |
| 84 | + let left = root; |
| 85 | + let right = root; |
| 86 | + root.eachBefore((node) => { |
| 87 | + if (node.x < left.x) left = node; |
| 88 | + if (node.x > right.x) right = node; |
| 89 | + }); |
| 90 | + |
| 91 | + //use nodes to detrmine height |
| 92 | + const height = right.x - left.x + margin.top + margin.bottom; |
| 93 | + |
| 94 | + const transition = svg |
| 95 | + .transition() |
| 96 | + .duration(duration) |
| 97 | + .attr('viewBox', [-margin.left, left.x - margin.top, width, height]); |
| 98 | + // Update the nodes… |
| 99 | + const node = gNode.selectAll('g').data(nodes, (d) => d.id); |
| 100 | + |
| 101 | + // Enter any new nodes at the parent's previous position. |
| 102 | + const nodeEnter = node |
| 103 | + .enter() |
| 104 | + .append('g') |
| 105 | + .attr('transform', (d) => `translate(${source.y0},${source.x0})`) |
| 106 | + .attr('fill-opacity', 0) |
| 107 | + .attr('stroke-opacity', 1) |
| 108 | + .on('click', (d) => { |
| 109 | + d.children = d.children ? null : d._children; |
| 110 | + update(d); |
| 111 | + }); |
| 112 | + |
| 113 | + // paint circles, color based on children |
| 114 | + nodeEnter |
| 115 | + .append('circle') |
| 116 | + .attr('r', 10) |
| 117 | + .attr('fill', (d) => (d._children ? '#46edf2' : '#95B6B7')) |
| 118 | + .attr('stroke-width', 10) |
| 119 | + .attr('stroke-opacity', 1); |
| 120 | + |
| 121 | + // append node names |
| 122 | + nodeEnter |
| 123 | + .append('text') |
| 124 | + .attr('dy', '.31em') |
| 125 | + .attr('x', '-10') |
| 126 | + .attr('y', '-5') |
| 127 | + .attr('text-anchor', 'end') |
| 128 | + .text((d: any) => d.data.name.slice(0, 14)) // Limits Characters in Display |
| 129 | + .style('font-size', `.6rem`) |
| 130 | + .style('fill', 'white') |
| 131 | + .clone(true) |
| 132 | + .lower() |
| 133 | + .attr('stroke-linejoin', 'round') |
| 134 | + .attr('stroke', '#646464') |
| 135 | + .attr('stroke-width', 1); |
| 136 | + |
| 137 | + //TODO -> Alter incoming snapshots so there is useful data to show on hover. |
| 138 | + // nodeEnter.on('mouseover', function (d: any, i: number): any { |
| 139 | + // if (!d.children) { |
| 140 | + // d3.select(this) |
| 141 | + // .append('text') |
| 142 | + // .text(()=>{ |
| 143 | + // return JSON.stringify(d.data)}) |
| 144 | + // .style('fill', 'white') |
| 145 | + // .attr('x',0) |
| 146 | + // .attr('y', 0) |
| 147 | + // .style('font-size', '.6rem') |
| 148 | + // .style('text-align', 'center') |
| 149 | + // .attr('stroke', '#646464') |
| 150 | + // .attr('id', `popup${i}`); |
| 151 | + // } |
| 152 | + // }); |
| 153 | + // nodeEnter.on('mouseout', function (d: any, i: number): any { |
| 154 | + // d3.select(`#popup${i}`).remove(); |
| 155 | + // }); |
| 156 | + |
| 157 | + // Transition nodes to their new position. |
| 158 | + const nodeUpdate = node |
| 159 | + .merge(nodeEnter) |
| 160 | + .transition(transition) |
| 161 | + .attr('transform', (d) => `translate(${d.y},${d.x})`) |
| 162 | + .attr('fill-opacity', 1) |
| 163 | + .attr('stroke-opacity', 1); |
| 164 | + |
| 165 | + // Transition exiting nodes to the parent's new position. |
| 166 | + const nodeExit = node |
| 167 | + .exit() |
| 168 | + .transition(transition) |
| 169 | + .remove() |
| 170 | + .attr('transform', (d) => `translate(${source.y},${source.x})`) |
| 171 | + .attr('fill-opacity', 0) |
| 172 | + .attr('stroke-opacity', 0); |
| 173 | + |
| 174 | + // Update the links… |
| 175 | + const link = gLink.selectAll('path').data(links, (d) => d.target.id); |
| 176 | + |
| 177 | + // Enter any new links at the parent's previous position. |
| 178 | + const linkEnter = link |
| 179 | + .enter() |
| 180 | + .append('path') |
| 181 | + .attr('d', (d) => { |
| 182 | + const o = { x: source.x0, y: source.y0 }; |
| 183 | + return diagonal({ source: o, target: o }); |
| 184 | + }); |
| 185 | + |
| 186 | + // Transition links to their new position. |
| 187 | + link.merge(linkEnter).transition(transition).attr('d', diagonal); |
| 188 | + |
| 189 | + // Transition exiting nodes to the parent's new position. |
| 190 | + link |
| 191 | + .exit() |
| 192 | + .transition(transition) |
| 193 | + .remove() |
| 194 | + .attr('d', (d) => { |
| 195 | + const o = { x: source.x, y: source.y }; |
| 196 | + return diagonal({ source: o, target: o }); |
| 197 | + }); |
| 198 | + |
| 199 | + // Stash the old positions for transition. |
| 200 | + root.eachBefore((d) => { |
| 201 | + d.x0 = d.x; |
| 202 | + d.y0 = d.y; |
| 203 | + }); |
| 204 | + }; |
| 205 | + |
| 206 | + //______________ZOOM______________\\ |
| 207 | + |
| 208 | + // Sets starting zoom |
| 209 | + let zoom = d3.zoom().on('zoom', zoomed); |
| 210 | + |
| 211 | + svgContainer.call( |
| 212 | + zoom.transform, |
| 213 | + // Changes the initial view, (left, top) |
| 214 | + d3.zoomIdentity.translate(x, y).scale(k) |
| 215 | + ); |
| 216 | + |
| 217 | + // allows the canvas to be zoom-able |
| 218 | + svgContainer.call( |
63 | 219 | d3
|
64 |
| - .linkHorizontal() |
65 |
| - .x((d: any) => d.y) |
66 |
| - .y((d: any) => d.x) |
| 220 | + .zoom() |
| 221 | + .scaleExtent([0.15, 1.5]) // [zoomOut, zoomIn] |
| 222 | + .on('zoom', zoomed) |
| 223 | + |
67 | 224 | );
|
| 225 | + function zoomed(d: any) { |
| 226 | + svg.attr('transform', d3.event.transform) |
| 227 | + .on('mouseup', setZoomState(d3.zoomTransform(d3.select('#canvas').node())) |
| 228 | + ); |
| 229 | + } |
68 | 230 |
|
| 231 | + // allows the canvas to be draggable |
| 232 | + svg.call( |
| 233 | + d3 |
| 234 | + .drag() |
| 235 | + |
| 236 | + ); |
| 237 | + |
| 238 | + |
| 239 | +<<<<<<< HEAD |
69 | 240 | // returns a flat array of objects containing all the nodes and their information
|
70 | 241 | let nodes: any = hierarchyNodes.descendants();
|
71 | 242 |
|
@@ -170,12 +341,19 @@ const Map = (props) => {
|
170 | 341 |
|
171 | 342 |
|
172 | 343 | });
|
| 344 | +======= |
| 345 | + // call update on node click |
| 346 | + update(root); |
| 347 | + }, |
| 348 | + [data] |
| 349 | + ); |
| 350 | +>>>>>>> master |
173 | 351 |
|
174 | 352 | return (
|
175 | 353 | <div data-testid="canvas">
|
176 |
| - <div className="Visualizer"> |
177 |
| - <svg id="canvas"></svg> |
178 |
| - </div> |
| 354 | + <svg |
| 355 | + id="canvas" |
| 356 | + ></svg> |
179 | 357 | </div>
|
180 | 358 | );
|
181 | 359 | };
|
|
0 commit comments