let data = []; let zoomLevel = 0; const xy = d3.scaleLinear().domain([0, 1]).range([0, 100]); const simulation = d3 .forceSimulation() .force( "x", d3 .forceX((d) => xy(+d._x)) .strength((d) => (d.category === "tactic" ? 0 : 0.1)) ) .force( "y", d3 .forceY((d) => xy(+d._y)) .strength((d) => (d.category === "tactic" ? 0 : 0.1)) ) .force( "link", d3.forceLink().id((d) => d.id) ) .force("collide", d3.forceCollide().radius(75)) .on("tick", ticked) .velocityDecay(0.6) .alphaDecay(0.01) .stop(); const zoom = d3.zoom().on("zoom", zoomed); const main = d3.select("#main").call(zoom); const mainSvg = d3.select("#main-svg").style("background-color", "#F9F9F9"); const bbox = main.node().getBoundingClientRect(); const width = bbox.width; const height = bbox.height; const g1 = d3.select("#g1"); const g1Svg = mainSvg.select("g"); let item = g1.selectAll(".item"); let link = g1Svg.selectAll(".link"); d3.tsv("./data.tsv").then((_data) => { data = _data; update(makeClusters(data), []); }); main .call( zoom.transform, d3.zoomIdentity.translate(width / 2, height / 2).scale(0.01) ) .transition() .duration(1000) .call( zoom.transform, d3.zoomIdentity.translate(width / 2, height / 2).scale(0.15) ); function zoomed(e) { const { x, y, k } = e.transform; const previousZoom = zoomLevel; if (k < 0.2) { zoomLevel = 0; } else if (k < 0.5) { zoomLevel = 1; } else { zoomLevel = 2; } g1.style("transform", `translate(${x}px,${y}px) scale(${k})`); g1Svg.style("transform", `translate(${x}px,${y}px) scale(${k})`); if (previousZoom != zoomLevel) { switch (zoomLevel) { case 0: update(makeClusters(data), []); mainSvg.style("background-color", "#F9F9F9"); break; case 1: const projects = makeItems(data, previousZoom !== 2); update(projects, []); mainSvg.style("background-color", "#F5F5F5"); break; case 2: const net = makeNetworks(data); update(net.nodes, net.links); mainSvg.style("background-color", "#EBEBEB"); break; } } } function ticked() { item.style("top", (d) => d.y).style("left", (d) => d.x); link .attr("x1", (d) => d.source.x) .attr("y1", (d) => d.source.y) .attr("x2", (d) => d.target.x) .attr("y2", (d) => d.target.y); } function update(nodes, links) { item = item.data(nodes, (d) => d.id); item .exit() .transition() .duration(750) .style("opacity", "-0.5") .style("top", (d) => d.fading_y + "px") .style("left", (d) => d.fading_x + "px") .remove(); item = item .enter() .append("svg") .classed("item", true) .style("opacity", "0") .style("position", "absolute") .style("transform", "translate(-50%,-50%)") .attr("width", 100) .attr("height", 100) .style("background-color", (d) => d.category === "cluster" ? "#7765E3" : d.category === "tactic" ? "#FFFFFF" : "#E4FF1A" ) .merge(item); item.transition().duration(500).style("opacity", "1"); item .selectAll("text") .data( (d) => [d], (d) => d.id ) .join("text") .attr("fill", "black") .attr("x", 50) .attr("y", 60) .attr("font-size", 50) .attr("text-anchor", "middle") .text((d) => d.id); link = link.data(links, (d) => d.id); link .exit() .transition() .duration(250) .style("opacity", "-0.5") .remove(); link = link .enter() .append("line") .classed("line", true) .attr("stroke", "black") .style("opacity", "0") .merge(link); link.transition().delay(500).duration(500).style("opacity", "1"); simulation.nodes(nodes); simulation.force("link").links(links); simulation.alpha(1).restart(); } function makeClusters(data) { const clusters = d3 .flatRollup( data, (v) => [d3.mean(v, (d) => d._x), d3.mean(v, (d) => d._y)], (d) => d.cluster ) .map((d) => ({ id: d[0], _x: d[1][0], _y: d[1][1], x: xy(d[1][0]), y: xy(d[1][1]), fading_x: xy(d[1][0]), fading_y: xy(d[1][1]), category: "cluster", })); return clusters; } function makeItems(data, setCoordinates) { const clustersPositions = d3.flatRollup( data, (v) => [d3.mean(v, (d) => d._x), d3.mean(v, (d) => d._y)], (d) => d.cluster ); data.forEach((d) => { const _clusterPosition = clustersPositions.find((c) => c[0] === d.cluster); if (setCoordinates) { d.x = xy(_clusterPosition[1][0]); d.y = xy(_clusterPosition[1][1]); } d.fading_x = xy(_clusterPosition[1][0]); d.fading_y = xy(_clusterPosition[1][1]); }); return data; } function makeNetworks(data) { const clustersPositions = d3.flatRollup( data, (v) => [d3.mean(v, (d) => d._x), d3.mean(v, (d) => d._y)], (d) => d.cluster ); const tactics = d3.flatRollup( data, (v) => { const _arr = v.map((vv) => vv.alltactics.split(";")).flat(); const _cluster = clustersPositions.find((c) => c[0] === v[0].cluster); return d3 .flatGroup(_arr, (d) => d) .map((d) => ({ id: _cluster[0] + "-" + d[0], label: d[0], _x: _cluster[1][0], _y: _cluster[1][1], x: xy(_cluster[1][0]), y: xy(_cluster[1][1]), fading_x: xy(_cluster[1][0]) + 0, fading_y: xy(_cluster[1][1]) + 0, category: "tactic", })); }, (d) => d.cluster ); const flatTactics = tactics.map((d) => d[1]).flat(); const links = d3.flatRollup( data, (v) => { return v.map((d) => { const temp = d.cluster + "-" + d.id + "-"; return d.alltactics.split(";").map((t) => ({ id: temp + t, source: d, target: flatTactics.find((ft) => ft.id === d.cluster + "-" + t), })); }); }, (d) => d.cluster ); const flatLinks = links.map((d) => d[1].flat()).flat(); return { nodes: data.concat(flatTactics), links: flatLinks }; }