// Create some global variables var width, height, audioEl, textEl, sampEl; var mouseOverId; var tooltip; let currentTransform; var links = [], nodes = [], groups = []; let zoomLvl = 1; let lastK = 0; let svg; var zoom; let simulation; const ROOT = "https://hub.xpub.nl/chopchop/archive_non-tree/Active-Archive"; function getNameFromPath(path) { return path.split("/")[path.split("/").length - 1]; } function wrap(text, width) { text.each(function () { var text = d3.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineNumber = 0, lineHeight = 1.1, // ems x = text.attr("x"), y = text.attr("y"), dy = 0, //parseFloat(text.attr("dy")), tspan = text.text(null) .append("tspan") .attr("x", x) .attr("y", y) .attr("dy", dy + "em"); while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); line = [word]; tspan = text.append("tspan") .attr("x", x) .attr("y", y) .attr("dy", ++lineNumber * lineHeight + dy + "em") .text(word); } } }); } function parseExifData(files) { // loop trough all files in JSON and add each to the list of nodes files.forEach((file) => { file.id = file.SourceFile; nodes.push(file); // also include any mentioned directories in the list of groups and the list of nodes. var paths = file.Directory.split("/"); // the directory is split up and looped trough, so for every subdirectory, a node is created paths.forEach((p, index) => { var path = ""; for (var i = 0; i < index + 1; i++) { path += (i === 0 ? "" : "/") + paths[i]; } // we check if the directory is not already stored if (!groups.includes(path)) { groups.push(path); nodes.push({ id: path, FileType: "directory", FileName: getNameFromPath(path), }); } }); }); // Now that the groups are created, they need to be linked together by adding them to the array of links. groups.forEach((group) => { var path = group.split("/"); // link a folder to its parent when it exists if (path.length > 1) { var currentName = "/" + path[path.length - 1]; var parentName = group.replace(currentName, ""); var parent = groups.find((group) => { return group === parentName; }); // if there is a parent, create a link between the parent and the group. if (parent) { links.push({ source: group, target: parentName, }); } } // now, find any files of the folder and var children = files.filter((cFile) => { return cFile.Directory === group; }); // for each child, add a link between the directory and the file. children.forEach((sFile) => { links.push({ source: group, target: sFile.SourceFile, }); }); }); } function createGraph() { // Create a scale to set the radius based on a file size (0 to 3 GB) var max = d3.max(nodes, function (d) { return +d.FileSize; }); console.log(max); var rScale = d3.scaleLinear().domain([3, max]).range([2, 40]); currentTransform = [width / 2, height / 2, height]; simulation = d3 .forceSimulation(nodes) .force("center", d3.forceCenter(width / 2, height / 2)) .force( "collision", d3.forceCollide().radius((d) => rScale(d.FileSize) + 2) ) .force( "link", d3 .forceLink(links) .id((d) => d.id) .strength((d) => 0.82) .distance((d) => (d.source.FileType === "directory" ? 80 : 20)) ) // .force("charge", d3.forceManyBody().strength(-1)) .force("charge", d3.forceManyBody()); // .alphaTarget(0.3); // stay hot svg = d3 .create("svg") // .attr("width", width) .attr("height", height); const g = svg.append("g"); tooltip = d3.select("body").append("div").attr("class", "tooltip"); const link = g .append("g") // .attr("stroke", "#999") .attr("stroke-opacity", 0.6) .selectAll("line") .data(links) .join("line") .join("text") .text("aaa"); const globalNode = g.append("g"); const node = globalNode .selectAll("circle") .data(nodes) .join("circle") .attr("r", (d) => rScale(d.FileSize)) .attr("stroke", "black") .attr("fill", (d) => { if (d.FileType === "directory") { return "orange"; } else if (d.FileType === "TXT") { return "blue"; } else if (d.FileType === "PNG") { return "pink"; } else { return "white"; } }) .attr("label", (d) => d.id) .attr("path", (d) => d.id) .attr("file-type", (d) => d.FileType) .attr("group", (d) => d.group) .on("mouseenter", function (e, d) { // if (d3.select(this).attr("fill") !== "black") { // d3.select(this).attr("fill", "grey"); // } tooltip.style("display", "block").html(tooltipContents(d)); }) .on("click", function () { // d3.selectAll("circle").attr("fill", "rgba(255,255,255,.5)"); // d3.select(this).attr("fill", "black"); var path = this.getAttribute("path"); svg .transition() // .duration(300) .ease(d3.easeCubic) .call(zoom.translateTo, this.getAttribute("cx"), this.getAttribute("cy")); // do things based on the file type if (path.match(/\.(?:wav|mp3|flac)$/i)) { playAudio(path); } if (path.match(/\.(?:txt|png)$/i)) { playText(path); } }) .on("mouseleave", function () { if (d3.select(this).attr("fill") !== "black") { d3.select(this).attr("fill", "rgba(255,255,255,.5)"); } tooltip.style("display", "none"); }) .on("mousemove", function (event, d) { if (!event) return; tooltip.style("transform", `translate(${event.clientX}px, ${event.clientY}px)`); }); const labels = g .append("g") .selectAll("text") .data(nodes) .join("text") .attr("font-size", (d) => (d.FileType === "directory" ? 15 : 3)) .text((d) => d.FileName.replaceAll("-", " ")) .attr("cx", (d) => d.x) .attr("cy", (d) => d.y) .call(wrap, 50); // Add a drag behavior. node.call( d3 .drag() // .on("start", onDragStart) .on("drag", onDrag) .on("end", onDragEnd) ); // On tick, animate simulation.on("tick", () => { 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); node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); labels.attr("x", (d) => d.x).attr("y", (d) => d.y); // tooltip.attr("x", (d) => d.x).attr("y", (d) => d.y); }); // add zoom behavior var zoom = d3 .zoom() .extent([ [0, 0], [width, height], ]) .scaleExtent([1, 100]) .on("zoom", onZoom); svg.call(zoom); // add some mouse behaviors container.append(svg.node()); // Reheat the simulation when drag starts, and fix the subject position. function onDragStart(event) { if (!event.active) simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; } // Update the subject (onDrag node) position during drag. function onDrag(event) { event.subject.fx = event.x; event.subject.fy = event.y; } // Restore the target alpha so the simulation cools after dragging ends. // Unfix the subject position now that it’s no longer being onDrag. function onDragEnd(event) { if (!event.active) simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; } function onZoom(e) { if (e.transform.k > 2 && lastK != e.transform.k) { lastK = e.transform.k; zoomLvl = Math.log2(e.transform.k); globalNode.attr("stroke-width", 1 / zoomLvl); link.attr("stroke-width", 1 / zoomLvl); labels.attr("font-size", (d) => (d.FileType === "directory" ? 20 : 3) / zoomLvl); tooltip.attr("font-size", 3 / zoomLvl); } g.attr("transform", e.transform); } } function tooltipContents(d) { var string = "