2024-02-05 11:26:14 +00:00
|
|
|
document$.subscribe(function () {
|
|
|
|
|
|
|
|
const NODE_RADIUS = 8;
|
|
|
|
const NODE_COLOR = "#69b3a2";
|
|
|
|
const Parent_Node_COLOR = "#ff0000";
|
|
|
|
|
2024-02-08 10:43:17 +00:00
|
|
|
|
|
|
|
function parseFilteredTable(tf, allData) {
|
2024-02-05 11:26:14 +00:00
|
|
|
var data = [];
|
|
|
|
tf.getFilteredData().forEach((row, i) => {
|
2024-02-08 10:43:17 +00:00
|
|
|
sourcePath = allData[row[0] - 2].sourcePath;
|
|
|
|
targetPath = allData[row[0] - 2].targetPath;
|
|
|
|
data.push({
|
|
|
|
source: row[1][0],
|
|
|
|
sourcePath: sourcePath,
|
2024-02-27 14:08:28 +00:00
|
|
|
sourceGalaxy: row[1][1],
|
|
|
|
target: row[1][2],
|
2024-02-08 10:43:17 +00:00
|
|
|
targetPath: targetPath,
|
2024-02-27 14:08:28 +00:00
|
|
|
targetGalaxy: row[1][3],
|
|
|
|
level: row[1][4]
|
2024-02-08 10:43:17 +00:00
|
|
|
});
|
|
|
|
});
|
2024-02-05 11:26:14 +00:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseTable(table) {
|
|
|
|
var data = [];
|
|
|
|
table.querySelectorAll("tr").forEach((row, i) => {
|
|
|
|
if (i > 1) {
|
|
|
|
var cells = row.querySelectorAll("td");
|
2024-02-08 10:43:17 +00:00
|
|
|
var sourceAnchor = cells[0].querySelector("a");
|
|
|
|
var sourcePath = sourceAnchor ? sourceAnchor.getAttribute("href") : null;
|
2024-02-27 14:08:28 +00:00
|
|
|
var targetAnchor = cells[2].querySelector("a");
|
2024-02-08 10:43:17 +00:00
|
|
|
var targetPath = targetAnchor ? targetAnchor.getAttribute("href") : null;
|
|
|
|
data.push({
|
|
|
|
source: cells[0].textContent,
|
2024-02-27 14:08:28 +00:00
|
|
|
sourceGalaxy: cells[1].textContent,
|
|
|
|
target: cells[2].textContent,
|
|
|
|
targetGalaxy: cells[3].textContent,
|
2024-02-08 10:43:17 +00:00
|
|
|
sourcePath: sourcePath,
|
|
|
|
targetPath: targetPath,
|
2024-02-27 14:08:28 +00:00
|
|
|
level: cells[4].textContent
|
2024-02-08 10:43:17 +00:00
|
|
|
});
|
2024-02-05 11:26:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function processNewData(newData) {
|
2024-02-08 10:43:17 +00:00
|
|
|
var nodePaths = {};
|
|
|
|
newData.forEach(d => {
|
|
|
|
nodePaths[d.source] = d.sourcePath || null;
|
|
|
|
nodePaths[d.target] = d.targetPath || null;
|
|
|
|
});
|
2024-02-05 11:26:14 +00:00
|
|
|
var newNodes = Array.from(new Set(newData.flatMap(d => [d.source, d.target])))
|
2024-02-08 10:43:17 +00:00
|
|
|
.map(id => ({
|
|
|
|
id,
|
|
|
|
path: nodePaths[id]
|
|
|
|
}));
|
2024-02-05 11:26:14 +00:00
|
|
|
|
|
|
|
var newLinks = newData.map(d => ({ source: d.source, target: d.target }));
|
|
|
|
return { newNodes, newLinks };
|
|
|
|
}
|
|
|
|
|
2024-02-08 10:43:17 +00:00
|
|
|
function filterTableAndGraph(tf, simulation, data) {
|
|
|
|
var filteredData = parseFilteredTable(tf, data);
|
2024-02-07 10:29:15 +00:00
|
|
|
var { newNodes, newLinks } = processNewData(filteredData);
|
|
|
|
|
|
|
|
simulation.update({ newNodes: newNodes, newLinks: newLinks });
|
|
|
|
}
|
|
|
|
|
2024-02-05 11:26:14 +00:00
|
|
|
function createForceDirectedGraph(data, elementId) {
|
2024-02-08 10:43:17 +00:00
|
|
|
var nodePaths = {};
|
|
|
|
data.forEach(d => {
|
|
|
|
nodePaths[d.source] = d.sourcePath || null;
|
|
|
|
nodePaths[d.target] = d.targetPath || null;
|
|
|
|
});
|
|
|
|
|
2024-02-05 11:26:14 +00:00
|
|
|
var nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])))
|
2024-02-08 10:43:17 +00:00
|
|
|
.map(id => ({
|
|
|
|
id,
|
|
|
|
path: nodePaths[id]
|
|
|
|
}));
|
2024-02-05 11:26:14 +00:00
|
|
|
|
2024-02-27 14:40:34 +00:00
|
|
|
const Parent_Node = nodes[0];
|
|
|
|
|
2024-02-05 11:26:14 +00:00
|
|
|
var links = data.map(d => ({ source: d.source, target: d.target }));
|
|
|
|
|
|
|
|
var tooltip = d3.select("body").append("div")
|
|
|
|
.attr("class", "tooltip") // Add relevant classes for styling
|
|
|
|
.style("opacity", 0);
|
|
|
|
|
|
|
|
// Set up the dimensions of the graph
|
2024-02-08 10:43:17 +00:00
|
|
|
var width = 800, height = 1000;
|
2024-02-05 11:26:14 +00:00
|
|
|
|
|
|
|
var svg = d3.select(elementId).append("svg")
|
|
|
|
.attr("width", width)
|
|
|
|
.attr("height", height);
|
|
|
|
|
|
|
|
// Create a force simulation
|
2024-02-08 10:43:17 +00:00
|
|
|
linkDistance = Math.sqrt((width * height) / nodes.length);
|
|
|
|
|
2024-02-05 11:26:14 +00:00
|
|
|
var simulation = d3.forceSimulation(nodes)
|
2024-02-08 10:43:17 +00:00
|
|
|
.force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
|
|
|
|
.force("charge", d3.forceManyBody().strength(-50))
|
2024-02-05 11:26:14 +00:00
|
|
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
|
|
.alphaDecay(0.02); // A lower value, adjust as needed
|
|
|
|
|
|
|
|
// Create links
|
|
|
|
var link = svg.append("g")
|
|
|
|
.attr("stroke", "#999")
|
|
|
|
.attr("stroke-opacity", 0.6)
|
|
|
|
.selectAll("line")
|
|
|
|
.data(links)
|
|
|
|
.enter().append("line")
|
|
|
|
.attr("stroke-width", d => Math.sqrt(d.value));
|
|
|
|
|
|
|
|
// Create nodes
|
|
|
|
var node = svg.append("g")
|
|
|
|
.attr("stroke", "#fff")
|
|
|
|
.attr("stroke-width", 1.5)
|
|
|
|
.selectAll("circle")
|
|
|
|
.data(nodes)
|
|
|
|
.enter().append("circle")
|
|
|
|
.attr("r", function (d, i) {
|
2024-02-27 14:40:34 +00:00
|
|
|
return d.id === Parent_Node.id ? NODE_RADIUS + 5 : NODE_RADIUS;
|
2024-02-05 11:26:14 +00:00
|
|
|
})
|
|
|
|
.attr("fill", function (d, i) {
|
2024-02-27 14:40:34 +00:00
|
|
|
return d.id === Parent_Node.id ? Parent_Node_COLOR : NODE_COLOR;
|
2024-02-05 11:26:14 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
// Apply tooltip on nodes
|
|
|
|
node.on("mouseover", function (event, d) {
|
|
|
|
tooltip.transition()
|
|
|
|
.duration(200)
|
|
|
|
.style("opacity", .9);
|
|
|
|
tooltip.html(d.id)
|
|
|
|
.style("left", (event.pageX) + "px")
|
|
|
|
.style("top", (event.pageY - 28) + "px");
|
|
|
|
})
|
|
|
|
.on("mousemove", function (event) {
|
|
|
|
tooltip.style("left", (event.pageX) + "px")
|
|
|
|
.style("top", (event.pageY - 28) + "px");
|
|
|
|
})
|
|
|
|
.on("mouseout", function (d) {
|
|
|
|
tooltip.transition()
|
|
|
|
.duration(500)
|
|
|
|
.style("opacity", 0);
|
|
|
|
});
|
|
|
|
|
2024-02-08 10:43:17 +00:00
|
|
|
// Apply links on nodes
|
|
|
|
node.on("dblclick", function (event, d) {
|
|
|
|
location.href = d.path;
|
|
|
|
});
|
|
|
|
|
2024-02-05 11:26:14 +00:00
|
|
|
// Define drag behavior
|
|
|
|
var drag = d3.drag()
|
|
|
|
.on("start", dragstarted)
|
|
|
|
.on("drag", dragged)
|
|
|
|
.on("end", dragended);
|
|
|
|
|
|
|
|
// Apply drag behavior to nodes
|
|
|
|
node.call(drag);
|
|
|
|
|
|
|
|
function dragstarted(event, d) {
|
|
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
|
|
d.fx = d.x;
|
|
|
|
d.fy = d.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
function dragged(event, d) {
|
|
|
|
d.fx = event.x;
|
|
|
|
d.fy = event.y;
|
|
|
|
}
|
|
|
|
|
|
|
|
function dragended(event, d) {
|
|
|
|
// Do not reset the fixed positions
|
|
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update positions on each simulation 'tick'
|
|
|
|
simulation.on("tick", () => {
|
|
|
|
nodes.forEach(d => {
|
|
|
|
d.x = Math.max(NODE_RADIUS, Math.min(width - NODE_RADIUS, d.x));
|
|
|
|
d.y = Math.max(NODE_RADIUS, Math.min(height - NODE_RADIUS, d.y));
|
|
|
|
});
|
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
|
|
|
return Object.assign(svg.node(), {
|
|
|
|
update({ newNodes, newLinks }) {
|
|
|
|
const oldNodesMap = new Map(node.data().map(d => [d.id, d]));
|
|
|
|
nodes = newNodes.map(d => Object.assign(oldNodesMap.get(d.id) || {}, d));
|
|
|
|
|
|
|
|
// Update nodes with new data
|
|
|
|
node = node.data(nodes, d => d.id)
|
|
|
|
.join(
|
|
|
|
enter => enter.append("circle")
|
2024-02-06 12:56:08 +00:00
|
|
|
.attr("r", function (d, i) {
|
2024-02-27 14:40:34 +00:00
|
|
|
return d.id === Parent_Node.id ? NODE_RADIUS + 5 : NODE_RADIUS;
|
2024-02-06 12:56:08 +00:00
|
|
|
})
|
|
|
|
.attr("fill", function (d, i) {
|
2024-02-27 14:40:34 +00:00
|
|
|
return d.id === Parent_Node.id ? Parent_Node_COLOR : NODE_COLOR;
|
2024-02-06 12:56:08 +00:00
|
|
|
}),
|
2024-02-05 11:26:14 +00:00
|
|
|
update => update,
|
|
|
|
exit => exit.remove()
|
|
|
|
);
|
|
|
|
|
|
|
|
node.call(drag);
|
|
|
|
|
2024-02-06 12:56:08 +00:00
|
|
|
// Apply tooltip on nodes
|
|
|
|
node.on("mouseover", function (event, d) {
|
|
|
|
tooltip.transition()
|
|
|
|
.duration(200)
|
|
|
|
.style("opacity", .9);
|
|
|
|
tooltip.html(d.id)
|
|
|
|
.style("left", (event.pageX) + "px")
|
|
|
|
.style("top", (event.pageY - 28) + "px");
|
|
|
|
})
|
|
|
|
.on("mousemove", function (event) {
|
|
|
|
tooltip.style("left", (event.pageX) + "px")
|
|
|
|
.style("top", (event.pageY - 28) + "px");
|
|
|
|
})
|
|
|
|
.on("mouseout", function (d) {
|
|
|
|
tooltip.transition()
|
|
|
|
.duration(500)
|
|
|
|
.style("opacity", 0);
|
|
|
|
});
|
|
|
|
|
2024-02-08 10:43:17 +00:00
|
|
|
// Apply links on nodes
|
|
|
|
node.on("dblclick", function (event, d) {
|
|
|
|
console.log("Node: " + d.id);
|
|
|
|
console.log(d);
|
|
|
|
console.log("Source Path: " + d.sourcePath);
|
|
|
|
location.href = d.path;
|
|
|
|
});
|
|
|
|
|
2024-02-05 11:26:14 +00:00
|
|
|
// Process new links
|
|
|
|
const oldLinksMap = new Map(link.data().map(d => [`${d.source.id},${d.target.id}`, d]));
|
|
|
|
links = newLinks.map(d => Object.assign(oldLinksMap.get(`${d.source.id},${d.target.id}`) || {}, d));
|
|
|
|
|
|
|
|
// Update links with new data
|
|
|
|
link = link.data(links, d => `${d.source.id},${d.target.id}`)
|
|
|
|
.join(
|
|
|
|
enter => enter.append("line")
|
|
|
|
.attr("stroke-width", d => Math.sqrt(d.value)),
|
|
|
|
update => update,
|
|
|
|
exit => exit.remove()
|
|
|
|
);
|
|
|
|
|
|
|
|
// Restart the simulation with new data
|
|
|
|
simulation.nodes(nodes);
|
|
|
|
simulation.force("link").links(links);
|
|
|
|
simulation.alpha(1).restart();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find all tables that have a th with the class .graph and generate Force-Directed Graphs
|
|
|
|
document.querySelectorAll("table").forEach((table, index) => {
|
|
|
|
var graphHeader = table.querySelector("th.graph");
|
|
|
|
if (graphHeader) {
|
|
|
|
var tf = new TableFilter(table, {
|
2024-02-06 15:55:05 +00:00
|
|
|
base_path: "../../../../01_attachements/modules/tablefilter/",
|
2024-02-05 11:26:14 +00:00
|
|
|
highlight_keywords: true,
|
2024-02-27 14:08:28 +00:00
|
|
|
col_1: "checklist",
|
|
|
|
col_3: "checklist",
|
|
|
|
col_4: "checklist",
|
|
|
|
col_widths: ["180px", "180px", "180px", "180px", "100px"],
|
|
|
|
col_types: ["string", "string", "string", "string", "number"],
|
2024-02-05 11:26:14 +00:00
|
|
|
grid_layout: false,
|
|
|
|
responsive: false,
|
2024-02-27 14:08:28 +00:00
|
|
|
watermark: ["Filter table ...", "Filter table ...", "Filter table ...", "Filter table ..."],
|
2024-02-05 11:26:14 +00:00
|
|
|
auto_filter: {
|
|
|
|
delay: 100 //milliseconds
|
|
|
|
},
|
|
|
|
filters_row_index: 1,
|
|
|
|
state: false,
|
|
|
|
rows_counter: true,
|
|
|
|
status_bar: true,
|
|
|
|
themes: [{
|
|
|
|
name: "transparent",
|
|
|
|
}],
|
|
|
|
btn_reset: {
|
|
|
|
tooltip: "Reset",
|
|
|
|
toolbar_position: "right",
|
|
|
|
},
|
|
|
|
toolbar: true,
|
|
|
|
extensions: [{
|
|
|
|
name: "sort",
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'filtersVisibility',
|
|
|
|
description: 'Sichtbarkeit der Filter',
|
|
|
|
toolbar_position: 'right',
|
|
|
|
}],
|
|
|
|
});
|
|
|
|
|
|
|
|
tf.init();
|
|
|
|
var data = parseTable(table);
|
|
|
|
var graphId = "graph" + index;
|
|
|
|
var div = document.createElement("div");
|
|
|
|
div.id = graphId;
|
|
|
|
table.parentNode.insertBefore(div, table);
|
|
|
|
var simulation = createForceDirectedGraph(data, "#" + graphId);
|
|
|
|
|
|
|
|
// Listen for table filtering events
|
2024-02-07 10:29:15 +00:00
|
|
|
tf.emitter.on(['after-filtering'], function () {
|
2024-02-08 10:43:17 +00:00
|
|
|
filterTableAndGraph(tf, simulation, data);
|
2024-02-07 10:29:15 +00:00
|
|
|
});
|
2024-02-05 11:26:14 +00:00
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|