|
|
// 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";
|
|
|
};
|