Published
Edited
May 26, 2022
5 forks
4 stars
Insert cell
Insert cell
Insert cell
chart = {

// process observable widget
let cluster = clusterWidget == "true" ? true : false;

// settings
const width = 800, height = 600;


// setup PIXI
const stage = new PIXI.Container();
const renderer = PIXI.autoDetectRenderer({
width: width,
height: height,
antialias: true,
autoResize:true,
resolution: 2,
backgroundColor: 0xFFFFFF
});
const context = new PIXI.Graphics();
stage.addChild(context);


// d3 color scaling function
const color = (function() {
let scale = d3.scaleOrdinal(d3.schemeCategory10);
return (num) => parseInt(scale(num).slice(1), 16);
})();


// pixi nodes and link graphics
const links = new PIXI.Graphics();
stage.addChild(links);
graph.nodes.forEach((node) => {
node.gfx = new PIXI.Graphics();
node.gfx.lineStyle(1.5, 0xFFFFFF);
node.gfx.beginFill(color(node.group));
node.gfx.drawCircle(0, 0, 5);
node.gfx.interactive = true;
node.gfx.buttonMode = true;
node.gfx.on("pointerover", ()=>pointerOver(node));
node.gfx.on("pointerout", ()=>pointerOut(node));
stage.addChild(node.gfx);
});


// d3 simulation
const simulation = d3.forceSimulation()
.nodes(graph.nodes)
.force('link', d3.forceLink().id((d) => d.id))
.force('charge', d3.forceManyBody().strength(-5))
.force('center', d3.forceCenter(width / 2, height / 2))
.force("forceInABox", forceInABox()
.strength(0.5)
.groupBy("group") // Node attribute to group
.enableGrouping(cluster)
.forceNodeSize(d => 8)
.size([width, height])
)
.on('tick', ticked);
simulation
.force('link')
.links(graph.links);
function ticked() {
graph.nodes.forEach((node) => {
let { x, y, gfx } = node;
gfx.position = new PIXI.Point(x, y);
});
links.clear();
links.alpha = 0.6;
graph.links.forEach((link) => {
let { source, target } = link;
links.lineStyle(Math.sqrt(link.value), 0xEEEEEE);
links.moveTo(source.x, source.y);
links.lineTo(target.x, target.y);
});
links.endFill();
renderer.render(stage);
}

// d3 drag
d3.select(renderer.view)
.call(d3.drag()
.container(renderer.view)
// Returns the node closest to the position with the given search radius
.subject((d)=>simulation.find(d.x, d.y, 10))
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended)
);
function dragstarted(e, d) {
hideTooltip();
if (!e.active) {
simulation
.alphaTarget(0.3)
.restart();
}
e.subject.fx = e.subject.x;
e.subject.fy = e.subject.y;
}

function dragged(e,d) {
e.subject.fx = e.x;
e.subject.fy = e.y;
}

function dragended(e,d) {
if (!e.active) {
simulation.alphaTarget(0);
}
e.subject.fx = null;
e.subject.fy = null;
}

function pointerOver(node){
showTooltip(node);
node.gfx.tint = 0x666666;
renderer.render(stage);
}

function pointerOut(node){
hideTooltip();
node.gfx.tint = 0xFFFFFF;
renderer.render(stage);
}
// tooltips
const tooltipDom = d3.select("body")
.append("div")
.attr("style",`
display: none;
font-family: sans-serif;
font-size: 12px;
position: absolute;
padding: 0.5em;
color: #fff;
background-color: #333;
z-index: +9;
`);

function showTooltip(node){
const offset = document.querySelector("canvas").getBoundingClientRect();
tooltipDom
.text(node.id)
.style("display","inline-block")
.style("left",`${offset.x + node.x + 10 }px`)
.style("top",`${offset.y + node.y + 10}px`)
}
function hideTooltip() {
tooltipDom
.text("")
.style("display","none")
.style("top","-100px")
.style("left","-100px");
}

return Object.assign(renderer.view, { style: `width: ${width}px` });
}
Insert cell
Insert cell
Insert cell
Insert cell
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