Merge pull request #948 from NMD03/main

Add buttons for editing and hiding TOC + Nav
pull/949/head
Alexandre Dulaunoy 2024-03-15 13:19:24 +01:00 committed by GitHub
commit 5218a996d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 300 additions and 240 deletions

View File

@ -51,6 +51,10 @@ class Galaxy:
def _create_title_entry(self):
entry = ""
entry += f"[Hide Navigation](#){{ .md-button #toggle-navigation }}\n"
entry += f"[Hide TOC](#){{ .md-button #toggle-toc }}\n"
entry += f"<div class=\"clearfix\"></div>\n"
entry += f"[Edit :material-pencil:](https://github.com/MISP/misp-galaxy/edit/main/clusters/{self.json_file_name}){{ .md-button }}\n"
entry += f"# {self.galaxy_name}\n"
return entry

View File

@ -7,7 +7,7 @@ class Site:
def __init__(self, path, name) -> None:
self.path = path
self.name = name
self.content = ""
self.content = '[Hide Navigation](#){ .md-button #toggle-navigation }\n[Hide TOC](#){ .md-button #toggle-toc }\n<div class="clearfix"></div> \n\n'
def add_content(self, content):
self.content += content

View File

@ -76,16 +76,15 @@ document$.subscribe(function () {
simulation.update({ newNodes: newNodes, newLinks: newLinks });
}
function createForceDirectedGraph(data, elementId) {
var nodePaths = {};
data.forEach(d => {
nodePaths[d.source] = d.sourcePath || null;
nodePaths[d.target] = d.targetPath || null;
});
// Extract unique galaxy names from data
const galaxies = Array.from(new Set(data.flatMap(d => [d.sourceGalaxy, d.targetGalaxy])));
function extractNodePaths(data) {
return data.reduce((acc, d) => ({
...acc,
[d.source]: d.sourcePath || null,
[d.target]: d.targetPath || null,
}), {});
}
function defineColorScale(galaxies) {
const colorScheme = [
'#E63946', // Red
'#F1FAEE', // Off White
@ -108,8 +107,171 @@ document$.subscribe(function () {
'#FFBA08', // Selective Yellow
'#FFD60A' // Naples Yellow
];
const colorScale = d3.scaleOrdinal(colorScheme)
return d3.scaleOrdinal(colorScheme)
.domain(galaxies);
}
function initializeNodeInteractions(node, link, tooltip, simulation, links, Parent_Node, NODE_RADIUS) {
// Mouseover event handler
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");
node.style("opacity", 0.1);
link.style("opacity", 0.1);
d3.select(this)
.attr("r", parseFloat(d3.select(this).attr("r")) + 5)
.style("opacity", 1);
d3.selectAll(".legend-text.galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.style("font-weight", "bold")
.style("font-size", "14px");
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-width", 3)
.style("opacity", 1);
node.filter(n => n.id === d.id || links.some(l => (l.source.id === d.id && l.target.id === n.id) || (l.target.id === d.id && l.source.id === n.id)))
.style("opacity", 1);
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function (event, d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
node.style("opacity", 1);
link.style("opacity", 1);
d3.select(this).attr("r", d => d.id === Parent_Node.id ? NODE_RADIUS + 5 : NODE_RADIUS);
d3.selectAll(".legend-text.galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.style("font-weight", "normal")
.style("font-size", "12px");
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-width", 1);
node.filter(n => n.id === d.id || links.some(l => (l.source.id === d.id && l.target.id === n.id) || (l.target.id === d.id && l.source.id === n.id)));
})
.on("dblclick", function (event, d) {
location.href = d.path;
});
// 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) {
if (!event.active) simulation.alphaTarget(0);
}
}
function createGalaxyColorLegend(svg, width, galaxies, colorScale, node, link, tooltip) {
// Prepare legend data
const legendData = galaxies.map(galaxy => ({
name: galaxy,
color: colorScale(galaxy)
}));
const maxCharLength = 10; // Maximum number of characters to display in legend
// Create legend
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + (width - 100) + ",20)"); // Adjust position as needed
// Add legend title
legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-size", "13px")
.style("text-anchor", "start")
.style("fill", "grey")
.text("Galaxy Colors");
// Add colored rectangles and text labels for each galaxy
const legendItem = legend.selectAll(".legend-item")
.data(legendData)
.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 20})`);
legendItem.append("rect")
.attr("width", 12)
.attr("height", 12)
.style("fill", d => d.color)
.on("mouseover", mouseoverEffect)
.on("mouseout", mouseoutEffect);
legendItem.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", "0.35em")
.style("text-anchor", "start")
.style("fill", "grey")
.style("font-size", "12px")
.attr("class", d => "legend-text galaxy-" + d.name.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.text(d => d.name.length > maxCharLength ? d.name.substring(0, maxCharLength) + "..." : d.name)
.on("mouseover", mouseoverEffect)
.on("mouseout", mouseoutEffect);
function mouseoverEffect(event, d) {
// Dim the opacity of all nodes and links
node.style("opacity", 0.1);
link.style("opacity", 0.1);
// Highlight elements associated with the hovered galaxy
svg.selectAll(".galaxy-" + d.name.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.each(function () {
d3.select(this).style("opacity", 1); // Increase opacity for related elements
});
// Show tooltip
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.name)
.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
}
function mouseoutEffect(event, d) {
// Restore the opacity of nodes and links
node.style("opacity", 1);
link.style("opacity", 1);
// Hide tooltip
tooltip.transition()
.duration(500)
.style("opacity", 0);
}
}
function createForceDirectedGraph(data, elementId) {
const nodePaths = extractNodePaths(data);
// // Extract unique galaxy names from data
const galaxies = Array.from(new Set(data.flatMap(d => [d.sourceGalaxy, d.targetGalaxy])));
const colorScale = defineColorScale(data);
var nodes = Array.from(new Set(data.flatMap(d => [d.source, d.target])))
.map(id => ({
@ -119,8 +281,6 @@ document$.subscribe(function () {
}));
let header = document.querySelector('h1').textContent;
// const parentUUID = header.replace(/\s+/g, '-').charAt(0).toLowerCase() + header.replace(/\s+/g, '-').slice(1);
// console.log("Parent UUID: " + parentUUID);
const Parent_Node = nodes.find(node => node.id.includes(header));
var links = data.map(d => ({ source: d.source, target: d.target }));
@ -130,15 +290,17 @@ document$.subscribe(function () {
.style("opacity", 0);
// Set up the dimensions of the graph
var width = 800, height = 1000;
var width = document.querySelector('.md-content__inner').offsetWidth;
var height = width;
var svg = d3.select(elementId).append("svg")
.attr("width", width)
.attr("height", height);
var svg = d3.select("div#container")
.append("svg")
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 " + width + " " + height)
.classed("svg-content", true);
// Create a force simulation
linkDistance = Math.sqrt((width * height) / nodes.length);
var simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(linkDistance))
.force("charge", d3.forceManyBody().strength(-70))
@ -169,166 +331,8 @@ document$.subscribe(function () {
})
.attr("class", d => "node galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'));
// 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");
node.style("opacity", 0.1);
link.style("opacity", 0.1);
d3.select(this)
.attr("r", parseFloat(d3.select(this).attr("r")) + 5)
.style("opacity", 1);
svg.selectAll(".legend-text.galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.style("font-weight", "bold")
.style("font-size", "14px");
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-width", 3)
.style("opacity", 1);
node.filter(n => n.id === d.id || links.some(l => (l.source.id === d.id && l.target.id === n.id) || (l.target.id === d.id && l.source.id === n.id)))
.style("opacity", 1);
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function (event, d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
node.style("opacity", 1);
link.style("opacity", 1);
d3.select(this).attr("r", function (d, i) {
return d.id === Parent_Node.id ? NODE_RADIUS + 5 : NODE_RADIUS;
});
svg.selectAll(".legend-text.galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.style("font-weight", "normal")
.style("font-size", "12px");
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-width", 1);
node.filter(n => n.id === d.id || links.some(l => (l.source.id === d.id && l.target.id === n.id) || (l.target.id === d.id && l.source.id === n.id)))
});
// Apply links on nodes
node.on("dblclick", function (event, d) {
location.href = d.path;
});
// 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);
}
// Prepare legend data
const legendData = galaxies.map(galaxy => ({
name: galaxy,
color: colorScale(galaxy)
}));
const maxCharLength = 10; // Maximum number of characters to display in legend
// Create legend
const legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + (width - 100) + ",20)"); // Adjust position as needed
// Add legend title
legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-size", "13px")
.style("text-anchor", "start")
.style("fill", "grey")
.text("Galaxy Colors");
// Add colored rectangles and text labels for each galaxy
const legendItem = legend.selectAll(".legend-item")
.data(legendData)
.enter().append("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 20})`);
legendItem.append("rect")
.attr("width", 12)
.attr("height", 12)
.style("fill", d => d.color)
.on("mouseover", function (event, d) {
node.style("opacity", 0.1);
link.style("opacity", 0.1);
svg.selectAll(".galaxy-" + d.name.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.each(function () {
var currentRadius = d3.select(this).attr("r");
d3.select(this).style("opacity", 1);
});
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.name)
.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function (event, d) {
node.style("opacity", 1);
link.style("opacity", 1);
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
legendItem.append("text")
.attr("x", 24)
.attr("y", 9)
.attr("dy", "0.35em")
.style("text-anchor", "start")
.style("fill", "grey")
.style("font-size", "12px")
.attr("class", d => "legend-text galaxy-" + d.name.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.text(d => d.name.length > maxCharLength ? d.name.substring(0, maxCharLength) + "..." : d.name)
.on("mouseover", function (event, d) {
node.style("opacity", 0.1);
link.style("opacity", 0.1);
svg.selectAll(".galaxy-" + d.name.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.each(function () {
d3.select(this).style("opacity", 1);
});
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(d.name)
.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function (event, d) {
node.style("opacity", 1);
link.style("opacity", 1);
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
initializeNodeInteractions(node, link, tooltip, simulation, links, Parent_Node, NODE_RADIUS);
createGalaxyColorLegend(svg, width, galaxies, colorScale, node, link, tooltip);
// Update positions on each simulation 'tick'
simulation.on("tick", () => {
@ -367,59 +371,6 @@ document$.subscribe(function () {
exit => exit.remove()
);
node.call(drag);
// 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");
node.style("opacity", 0.1);
link.style("opacity", 0.1);
d3.select(this)
.attr("r", parseFloat(d3.select(this).attr("r")) + 5)
.style("opacity", 1);
svg.selectAll(".legend-text.galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.style("font-weight", "bold")
.style("font-size", "14px");
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-width", 3)
.style("opacity", 1);
node.filter(n => n.id === d.id || links.some(l => (l.source.id === d.id && l.target.id === n.id) || (l.target.id === d.id && l.source.id === n.id)))
.style("opacity", 1);
})
.on("mousemove", function (event) {
tooltip.style("left", (event.pageX) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function (event, d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
node.style("opacity", 1);
link.style("opacity", 1);
d3.select(this).attr("r", function (d, i) {
return d.id === Parent_Node.id ? NODE_RADIUS + 5 : NODE_RADIUS;
});
svg.selectAll(".legend-text.galaxy-" + d.galaxy.replace(/\s+/g, '-').replace(/[\s.]/g, '-'))
.style("font-weight", "normal")
.style("font-size", "12px");
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr("stroke-width", 1);
node.filter(n => n.id === d.id || links.some(l => (l.source.id === d.id && l.target.id === n.id) || (l.target.id === d.id && l.source.id === n.id)))
});
// 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;
});
// 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));
@ -433,6 +384,9 @@ document$.subscribe(function () {
exit => exit.remove()
);
initializeNodeInteractions(node, link, tooltip, simulation, links, Parent_Node, NODE_RADIUS);
createGalaxyColorLegend(svg, width, galaxies, colorScale, node, link, tooltip);
// Restart the simulation with new data
simulation.nodes(nodes);
simulation.force("link").links(links);
@ -453,10 +407,9 @@ document$.subscribe(function () {
col_1: "checklist",
col_3: "checklist",
col_4: "checklist",
col_widths: ["180px", "180px", "180px", "180px", "100px"],
col_types: ["string", "string", "string", "string", "number"],
grid_layout: false,
responsive: false,
responsive: true,
watermark: ["Filter table ...", "Filter table ...", "Filter table ...", "Filter table ..."],
auto_filter: {
delay: 100 //milliseconds
@ -491,9 +444,11 @@ document$.subscribe(function () {
} else {
data = allData;
}
var graphId = "graph" + index;
var graphId = "container";
var div = document.createElement("div");
// div.id = graphId;
div.id = graphId;
div.className = "svg-container";
table.parentNode.insertBefore(div, table);
var simulation = createForceDirectedGraph(data, "#" + graphId);

View File

@ -0,0 +1,22 @@
document.addEventListener('DOMContentLoaded', function () {
const body = document.body;
const toggleNavigationBtn = document.getElementById('toggle-navigation');
const toggleTocBtn = document.getElementById('toggle-toc');
function updateButtonText() {
toggleNavigationBtn.textContent = body.classList.contains('hide-navigation') ? '>>> Show Navigation' : '<<< Hide Navigation';
toggleTocBtn.textContent = body.classList.contains('hide-toc') ? 'Show TOC <<<' : 'Hide TOC >>>';
}
toggleNavigationBtn.addEventListener('click', function () {
body.classList.toggle('hide-navigation');
updateButtonText();
});
toggleTocBtn.addEventListener('click', function () {
body.classList.toggle('hide-toc');
updateButtonText();
});
updateButtonText(); // Initialize button text based on current state
});

View File

@ -0,0 +1,6 @@
.md-button {
font-size: 16px;
position: relative;
padding: 10px 20px;
float: right;
}

View File

@ -7,4 +7,24 @@
border-radius: 4px;
pointer-events: none;
color: black;
}
.svg-container {
display: inline-block;
position: relative;
width: 100%;
padding-bottom: 100%;
vertical-align: top;
overflow: hidden;
}
.svg-content {
display: inline-block;
position: absolute;
top: 0;
left: 0;
}
.md-typeset__table {
width: 100%;
}

View File

@ -0,0 +1,49 @@
.hide-navigation .md-sidebar--primary {
display: none;
}
.hide-toc .md-sidebar--secondary {
display: none;
}
#toggle-toc {
margin: 10px 5px;
padding: 5px 10px;
color: grey;
outline: none;
background-color: initial;
border-color: grey;
/* border: none; */
cursor: pointer;
float: right;
}
#toggle-toc:hover {
color: #5C6BC0;
border-color: #5C6BC0;
}
/* Additional styling for positioning the buttons next to each other */
#toggle-navigation {
margin: 10px 5px;
padding: 5px 10px;
color: grey;
outline: none;
background-color: initial;
border-color: grey;
/* border: none; */
cursor: pointer;
float: left;
}
#toggle-navigation:hover {
color: #5C6BC0;
border-color: #5C6BC0;
}
.clearfix::after {
content: "";
display: table;
clear: both;
}

View File

@ -24,6 +24,8 @@ theme:
- search.highlight
- search.share
- navigation.instant.preview
- navigation.instant.prefetch
- navigation.top
palette:
# Palette toggle for automatic mode
@ -66,18 +68,16 @@ extra:
generator: false
extra_javascript:
# - javascripts/tablefilter.js
# - "https://unpkg.com/tablefilter@0.7.3/dist/tablefilter/tablefilter.js"
# - "https://d3js.org/d3.v6.min.js"
- 01_attachements/javascripts/graph.js
- 01_attachements/javascripts/statistics.js
# - node_modules/tablefilter/dist/tablefilter/tablefilter.js
# - node_modules/d3/dist/d3.min.js
- 01_attachements/modules/d3.min.js
- 01_attachements/modules/tablefilter/tablefilter.js
- 01_attachements/javascripts/navigation.js
extra_css:
- 01_attachements/stylesheets/graph.css
- 01_attachements/stylesheets/buttons.css
- 01_attachements/stylesheets/navigation.css
plugins:
- search

View File

@ -69,7 +69,11 @@ def galaxy_transform_to_link(galaxy):
def generate_relations_table(cluster):
relationships = cluster.relationships
markdown = f"# {cluster.value} ({cluster.uuid}) \n\n"
markdown = ""
markdown += f"[Hide Navigation](#){{ .md-button #toggle-navigation }}\n"
markdown += f"[Hide TOC](#){{ .md-button #toggle-toc }}\n"
markdown += f"<div class=\"clearfix\"></div>\n"
markdown += f"# {cluster.value} ({cluster.uuid}) \n\n"
markdown += f"{cluster.description} \n\n"
markdown += "|Cluster A | Galaxy A | Cluster B | Galaxy B | Level { .graph } |\n"
markdown += "| --- | --- | --- | --- | --- |\n"