Public
Edited
Apr 28, 2024
Insert cell
Insert cell
prefix = "https://raw.githubusercontent.com/os-threat/images/main/img/"

Insert cell
viewof shape = Inputs.radio(["rect-", "norm-", "rnd-"], {label: "Icon Style: ", value: "rect-"})
Insert cell
viewof icon_size = Inputs.range([20, 60], {label: "Icon Size", step: 1})
Insert cell
viewof indentSpacing = Inputs.range([0, 100], {label: "Indent Spacing", step: 1})
Insert cell
viewof lineSpacing = Inputs.range([0, 100], {label: "Line Spacing", step: 1})
Insert cell
margin = ({top: 30, right: 80, bottom: 30, left: 30});
Insert cell
Insert cell
collapsibleIndent(sighting_index)
Insert cell
Insert cell
Insert cell
function collapsibleIndent(data, options = {}) {
const {
duration = 300,
radius = 6, // radius of curve for links
height = 1500,
width = 1500,
minHeight = 20,
boxSize = 9.5,
ease = d3.easeQuadInOut, // https://observablehq.com/@d3/easing-animations
marginLeft = Math.round(boxSize/2)+1,
marginRight = 120,
marginTop = 10,
marginBottom = 10,
} = options;
// WARNING: x and y are switched because the d3.tree is vertical rather than the default horizontal
// settings
let plus = {shapeFill: "black", shapeStroke: "black", textFill:"white", text: "+"}
let minus = {shapeFill: "white", shapeStroke: "black", textFill:"black", text: "−"}
//
let tree = d3.tree()
.nodeSize([lineSpacing, indentSpacing])
let root = d3.hierarchy(taskindex);
root.x0 = 0;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = null;
});

let index = -1;
root.eachBefore(function(n) {++index}) // counts original number of items

//resonsive size before icons had height = Math.max(minHeight, index * lineSpacing + marginTop + marginBottom )
const svg = d3.create("svg")
//.attr("viewBox", [-marginLeft, -marginTop, width, height])
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.style("font", "10px sans-serif")
.style("user-select", "none");

const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#AAA")
.attr("stroke-width", .75);

const gNode = svg.append("g")
.attr("cursor", "pointer")
.attr("pointer-events", "all");

let indexLast
function update(source) {
const nodes = root.descendants().reverse();
const links = root.links();

// Compute the new tree layout.
tree(root);
// node position function
index = -1;
root.eachBefore(function(n) {
n.x = ++index * lineSpacing;
n.y = n.depth * indentSpacing;
});

const height = Math.max(minHeight, index * lineSpacing + marginTop + marginBottom );

svg.transition().delay(indexLast<index ? 0 : duration).duration(0)
.attr("viewBox", [-marginLeft, - marginTop, width, height])

// Update the nodes…
const node = gNode.selectAll("g")
.data(nodes, d => d.id);

// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${d.y},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d) => {
d.children = d.children ? null : d._children;
update(d);

charge
.attr("fill", d => d._children ? ( d.children ? minus.textFill : plus.textFill ) : "none")
.text(d => d._children ? ( d.children ? minus.text : plus.text ) : "");

box.attr("fill", d => d._children ? ( d.children ? minus.shapeFill : plus.shapeFill ) : "none")
});

// check box
let box = nodeEnter.append("rect")
.attr("width", boxSize)
.attr("height", boxSize)
.attr("x", -boxSize/2)
.attr("y", -boxSize/2)
.attr("fill", d => d._children ? ( d.children ? minus.shapeFill : plus.shapeFill ) : "none")
.attr("stroke", d => d._children ? "black" : "none")
.attr("stroke-width", .5);

// check box symbol
let charge = nodeEnter.append("text")
.attr("x", 0 )
.attr("text-anchor", "middle")
.attr("alignment-baseline", "central")
.attr("fill", d => d._children ? ( d.children ? minus.textFill : plus.textFill ) : "none")
.text(d => d._children ? ( d.children ? "−" : "+" ) : "");

// attach icon
let image = nodeEnter.append("image")
.attr("x", 10+boxSize/2)
.attr('y', -icon_size / 2 - boxSize / 2)
.attr("dy", -icon_size)
.attr("xlink:href", function(d) { return (prefix + shape + d.data.icon + ".svg");})
.attr("width", function(d) { return icon_size + 5;})
.attr("height", function(d) { return icon_size + 5;});

// label text
let label = nodeEnter.append("text")
.attr("x", icon_size+20+boxSize/2)
.attr("text-anchor", "start")
.attr("dy", "0.32em")
.text(d => d.data.name);
// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition().duration(duration).ease(ease)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1)

// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition().duration(duration).ease(ease).remove()
.attr("transform", d => `translate(${d.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);

// Update the links…
const link = gLink.selectAll("path")
.data(links, d => d.target.id);

// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("stroke-opacity", 0)
.attr("d", d => makeLink([d.source.y,source.x] , [d.target.y + (d.target._children ? 0 : boxSize/2 ), source.x] , radius) );

// Transition links to their new position.
link.merge(linkEnter).transition().duration(duration).ease(ease)
.attr("stroke-opacity", 1)
.attr("d", d => makeLink([d.source.y,d.source.x] , [d.target.y + (d.target._children ? 0 : boxSize/2 ), d.target.x] , radius));

// Transition exiting nodes to the parent's new position.
link.exit().transition().duration(duration).ease(ease).remove()
.attr("stroke-opacity", 0)
.attr("d", d => makeLink([d.source.y, source.x] , [d.target.y + (d.target._children ? 0 : boxSize/2 ), source.x] , radius) );

// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});

indexLast = index // to know if viewbox is expanding or contracting
}

update(root);

return svg.node();
}
Insert cell
sighting_index = FileAttachment("sighting_index@1.json").json()
Insert cell
Insert cell
taskindex = FileAttachment("taskIndex@1.json").json()
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more