|
|
// 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 = "<ul>";
|
|
|
for (var prop in d) {
|
|
|
string += `<li><b>${prop}</b>: ${d[prop]}</li>`;
|
|
|
}
|
|
|
string += "</ul>";
|
|
|
return string;
|
|
|
}
|
|
|
|
|
|
function playAudio(src) {
|
|
|
audioEl.src = ROOT + "/" + src;
|
|
|
audioEl.currentTime = 0;
|
|
|
audioEl.play();
|
|
|
}
|
|
|
|
|
|
function playText(src) {
|
|
|
textEl.setAttribute("active", "true");
|
|
|
textEl.querySelector(".fn-title").textContent = src.split("/")[src.split("/").length - 1];
|
|
|
textEl.querySelector("iframe").src = ROOT + "/" + src;
|
|
|
window.setTimeout(function () {
|
|
|
svg.on("click", onBodyClick);
|
|
|
}, 300);
|
|
|
}
|
|
|
|
|
|
function onBodyClick() {
|
|
|
textEl.setAttribute("active", "false");
|
|
|
svg.on("click", null);
|
|
|
}
|
|
|
|
|
|
function updateWindow() {
|
|
|
width = window.innerWidth - 10;
|
|
|
height = window.innerHeight - 10;
|
|
|
|
|
|
simulation.force("center", d3.forceCenter(width / 2, height / 2));
|
|
|
|
|
|
svg.attr("width", width).attr("height", height);
|
|
|
}
|
|
|
|
|
|
window.onload = function () {
|
|
|
// TODO: add resize function
|
|
|
width = window.innerWidth - 10;
|
|
|
height = window.innerHeight - 10;
|
|
|
|
|
|
audioEl = document.querySelector(".fn-audio");
|
|
|
textEl = document.querySelector(".fn-text");
|
|
|
sampEl = document.querySelector("samp");
|
|
|
|
|
|
parseExifData(dataset);
|
|
|
createGraph();
|
|
|
|
|
|
// clear loading screen
|
|
|
document.querySelector(".loading").style.display = "none";
|
|
|
|
|
|
// Add resize features
|
|
|
d3.select(window).on("resize.updatesvg", updateWindow);
|
|
|
};
|