var audioEl; var links = [], nodes = [], groups = []; let zoomLvl = 1; let lastK = 0; const c = { circle: { fill: "rgba(255,255,255,.5)", hover: "grey", active: "black", }, text: { size: { xl: 12, md: 3, }, }, }; function parseData(set) { for (var i = 0; i < set.length; i++) { nodes.push(set[i]); set[i].children?.forEach((child) => { links.push({ source: set[i].id, target: child.id, depth: set[i].depth, }); }); if (set[i].children) { parseData(set[i].children); } } } 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 (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 getGroups() { nodes.forEach((item) => { if (!groups.includes(item.group)) { groups.push(item.group); } }); } function createGraph() { // 0 to 3 GB var rScale = d3.scaleLinear().domain([0, 30000000]).range([2, 15]); const width = window.innerWidth; const height = window.innerHeight; var colorS = d3 .scaleLinear() // .domain([1, groups.length]) .range(["red", "green"]); 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" ? c.text.size.xl : c.text.size.md)) .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", c.circle.fill) .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", dragstarted).on("drag", dragged).on("end", dragended)); 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); }); svg.call( d3 .zoom() .extent([ [0, 0], [width, height], ]) .scaleExtent([1, 100]) .on("zoom", zoomed) ); container.append(svg.node()); svg .selectAll("circle") .on("mouseenter", function (e) { if (d3.select(this).attr("fill") !== c.circle.active) { d3.select(this).attr("fill", c.circle.hover); } }) .on("click", function () { d3.selectAll("circle").attr("fill", c.circle.fill); d3.select(this).attr("fill", c.circle.active); var path = this.getAttribute("path"); console.log(path); if (path.match(/\.(?:wav|mp3|flac)$/i)) { playAudio(path); } }) .on("mouseleave", function () { if (d3.select(this).attr("fill") !== c.circle.active) { d3.select(this).attr("fill", c.circle.fill); } }); Object.assign(svg.node(), { update({ nodes, links }) { // Make a shallow copy to protect against mutation, while // recycling old nodes to preserve position and velocity. const old = new Map(node.data().map((d) => [d.id, d])); nodes = nodes.map((d) => ({ ...old.get(d.id), ...d })); links = links.map((d) => ({ ...d })); node = node .data(nodes, (d) => d.id) .join((enter) => enter .append("circle") .attr("r", 5) .call(drag(simulation)) .call((node) => node.append("title").text((d) => d.id)) ); link = link.data(links, (d) => [d.source, d.target]).join("line"); simulation.nodes(nodes); simulation.force("link").links(links); simulation.alpha(1).restart().tick(); ticked(); // render now! }, }); // Reheat the simulation when drag starts, and fix the subject position. function dragstarted(event) { if (!event.active) simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x; event.subject.fy = event.subject.y; } // Update the subject (dragged node) position during drag. function dragged(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 dragged. function dragended(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" ? c.text.size.xl : c.text.size.md) / zoomLvl); } g.attr("transform", e.transform); } } function playAudio(src) { console.log("play audio: ", src); audioEl.src = src; audioEl.currentTime = 0; audioEl.play(); } function prepareAudio() { // var context = window.AudioContext || window.webkitAudioContext; // var ctx = new AudioContext(); audioEl = document.querySelector("audio"); // var track = ctx.createMediaElementSource(audioEl); // var gainNode = ctx.createGain(); // track.connect(gainNode); // track.connect(ctx.destination); // gainNode.gain.value to change volume } window.onload = function () { parseExifData(dataset); // getGroups(); createGraph(); document.querySelector(".loading").style.display = "none"; prepareAudio(); };