// Create some global variables var width, height, audioEl; var links = [], nodes = [], groups = []; let zoomLvl = 1; let lastK = 0; 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", }); } }); }); // 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 rScale = d3.scaleLinear().domain([0, 30000000]).range([2, 15]); const simulation = d3 .forceSimulation(nodes) .force("center", d3.forceCenter(width / 2, height / 2)) .force( "collision", d3 .forceCollide() // .radius((d) => rScale(d.size) + 1) .iterations(3) ) .force( "link", d3 .forceLink(links) .id((d) => d.id) .strength((d) => 0.2) // .distance((d) => (d.source.type === "directory" ? 80 : 20)) ) .force("charge", d3.forceManyBody().strength(-1)); // .alphaTarget(0.3); // stay hot const svg = d3 .create("svg") // .attr("width", width) .attr("height", height); const g = svg.append("g"); const labels = g .append("g") .selectAll("text") .data(nodes) .join("text") .attr("font-size", (d) => (d.type === "directory" ? 12 : 3)) .text((d) => d.id) .attr("cx", (d) => d.x) .attr("cy", (d) => d.y); 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.size)) .attr("r", 3) .attr("stroke", "black") .attr("fill", (d) => { return d.FileType === "directory" ? "orange" : "white"; }) // .attr("fill","rgba(255,255,255,.5)") .attr("label", (d) => d.id) .attr("path", (d) => d.id) .attr("group", (d) => d.group); // 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); }); // add zoom behavior svg.call( d3 .zoom() .extent([ [0, 0], [width, height], ]) .scaleExtent([1, 100]) .on("zoom", zoomed) ); // add some mouse behaviors svg .selectAll("circle") .on("mouseenter", function (e) { if (d3.select(this).attr("fill") !== "black") { d3.select(this).attr("fill", "grey"); } }) .on("click", function () { d3.selectAll("circle").attr("fill", "rgba(255,255,255,.5)"); d3.select(this).attr("fill", "black"); var path = this.getAttribute("path"); // do things based on the file type if (path.match(/\.(?:wav|mp3|flac)$/i)) { playAudio(path); } }) .on("mouseleave", function () { if (d3.select(this).attr("fill") !== "black") { d3.select(this).attr("fill", "rgba(255,255,255,.5)"); } }); 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 zoomed(e) { if (e.transform.k > 2 && lastK != e.transform.k) { lastK = e.transform.k; console.log("zoomed"); zoomLvl = Math.log2(e.transform.k); globalNode.attr("stroke-width", 1 / zoomLvl); link.attr("stroke-width", 1 / zoomLvl); labels.attr("font-size", (d) => (d.type === "directory" ? 12 : 3) / zoomLvl); } g.attr("transform", e.transform); } } function playAudio(src) { console.log("play audio: ", src); audioEl.src = src; audioEl.currentTime = 0; audioEl.play(); } window.onload = function () { // TODO: add resize function width = window.innerWidth; height = window.innerHeight; parseExifData(dataset); createGraph(); // clear loading screen document.querySelector(".loading").style.display = "none"; };