Public
Edited
Nov 1, 2023
Insert cell
Insert cell
viewof n = Inputs.range(d3.extent(data.nodes.map(d=>d.nEdge)), {label: html`Top <i>n</i>`, step: 1, value: 10})
Insert cell
Insert cell
{
const Viz = new Visualization("#viz", data);
Viz.draw();
Viz.animate();
}
Insert cell
data = FileAttachment("networks_unnested@4.json").json()
Insert cell
class Visualization {

constructor (selector, data) {
this.selector = selector;
this.data = data;
this.data.nodes = this.data.nodes.filter(d => d.nEdge >= n);
this.rootDOM = document.querySelector(this.selector);
// this.width = d3.select(this.selector).node().getBoundingClientRect().width;
this.height = d3.select(this.selector).node().getBoundingClientRect().height;
this.width = 600;
this.margin = {'top': 10, 'bottom': 150, 'left': 75, 'right': 10};
// this.height = 400;
this.hoverNodes = [];
this.yAxisData = [
{'level': 4, 'name': "Activity or Actor"},
{'level': 3, 'name': "Process 3"},
{'level': 2, 'name': "Process 2"},
{'level': 1, 'name': "Process 1"}
];

this.labelStyle = {
align: "center",
fill: 0xffffff,
fontFamily: ["ibmplexsans-regular-webfont", "Plex", "Arial"],
fontSize: 11,
padding: 5,
textBaseline: "bottom",
wordWrap: true,
wordWrapWidth: 90,
leading: -2,
dropShadow: true, // add text drop shadow to labels
dropShadowAngle: 90,
dropShadowBlur: 5,
dropShadowDistance: 2,
dropShadowColor: 0x21252b
}

this.initApp();
this.initTooltip();
this.initScales();
this.initSimulation();
}

initApp() {
this.app = new PIXI.Application({ resizeTo: this.rootDOM,
resolution: devicePixelRatio,
width: this.width,
height: this.height
});
this.rootDOM.appendChild(this.app.view);

// this.viewport = new Viewport({
// screenWidth: this.width,
// screenHeight: this.height,
// worldWidth: this.width,
// worldHeight: this.height,
// // passiveWheel: false,
// // interaction: this.app.renderer.events, // the interaction module is important for wheel to work properly when renderer.view is placed or scaled
// // events: this.app.renderer.events
// });

// this.app.stage.addChild(this.viewport);
}

initTooltip () {
this.tooltip = d3.select(this.selector)
.append("div")
.attr("class", "tooltip");
}

initScales () {
const stat = d3.extent(this.data.nodes.map(d=>d.nEdge));
this.sizeScale = d3.scaleSqrt()
.domain(stat)
.range([.5, 15]);

this.xScale = d3.scaleLinear()
.domain(stat)
.range([this.margin.left, this.width - this.margin.right]);

this.yScale = d3.scaleBand()
.domain(this.yAxisData.map(d => d.level))
.range([this.height - this.margin.bottom, this.margin.top]);
}
initSimulation () {
this.simulation = d3.forceSimulation(this.data.nodes)
.force('center', d3.forceCenter(this.width / 2, this.height / 2))
// .force('charge', d3.forceManyBody().strength(5))
.force('x', d3.forceX().x(d => this.xScale(d.nEdge)).strength(1.5))
.force('y', d3.forceY().y(d => this.yScale(d.level)).strength(1.5))
.force('collision', d3.forceCollide().radius(d => this.sizeScale(d.nEdge)).strength(1))
}
drawNodes () {
this.containerNodes = new PIXI.Container();
// this.nodes = [];

this.data.nodes.forEach((node) => {

console.log(this.yScale(node.level))
node.gfx = new PIXI.Graphics();
node.gfx.lineStyle(0); // draw a circle, set the lineStyle to zero so the circle doesn't have an outline
node.gfx.beginFill(0xcbcbcb, 1);
node.gfx.drawCircle(0, 0, this.sizeScale(node.nEdge));
node.gfx.endFill();
node.gfx.eventMode = 'static';
node.gfx.cursor = 'pointer';

// this.nodes.push(node);
this.containerNodes.addChild(node.gfx);
})

this.app.stage.addChild(this.containerNodes);
}

drawYAxis() {
this.containerYAxis = new PIXI.Container();
// Line -- Y axis
const lineYAxis = new PIXI.Graphics();
lineYAxis.lineStyle(1, 0x919295, 1)
.moveTo(this.xScale(0), this.margin.top)
.lineTo(this.xScale(0), this.height - this.margin.bottom);
lineYAxis.alpha = 0.5;
this.containerYAxis.addChild(lineYAxis);
// Labels - YAxis
for (let d of this.yAxisData) {
const id = new PIXI.Text(String(d.name), this.labelStyle);
id.anchor.set(0, 0);
id.position.set(10, this.yScale(d.level));
id.zIndex = 100;
this.containerYAxis.addChild(id);

const minorY = new PIXI.Graphics();
minorY.lineStyle(1, 0x919295, 1)
.moveTo(this.xScale(0), this.yScale(d.level))
.lineTo(this.width - this.margin.right, this.yScale(d.level));
minorY.alpha = 0.5;
this.containerYAxis.addChild(minorY);
}

for (let d of [this.margin.top, this.height - this.margin.bottom]) {
const majorY = new PIXI.Graphics();
majorY.lineStyle(1, 0x919295, 1)
.moveTo(this.xScale(0), d)
.lineTo(this.width - this.margin.right, d);
this.containerYAxis.addChild(majorY);
}

this.app.stage.addChild(this.containerYAxis);
}

drawXAxis() {
this.containerXAxis = new PIXI.Container();
// Labels - YAxis
for (let d of [0, 50, 100, 150, 200, 250, 300, 350, 400, 450]) {
const id = new PIXI.Text(String(d), this.labelStyle);
id.anchor.set(.5, 0);
id.position.set(this.xScale(d), this.height - this.margin.bottom + 16);
id.zIndex = 100;
this.containerXAxis.addChild(id);

const tickX = new PIXI.Graphics();
tickX.lineStyle(1, 0x919295, 1)
.moveTo(this.xScale(d), this.height - this.margin.bottom)
.lineTo(this.xScale(d), this.height - this.margin.bottom + 10);
this.containerXAxis.addChild(tickX);
}

this.app.stage.addChild(this.containerXAxis);
}

// Adds glow to nodes when hovered over
hoverNetworkNodes (node) {

this.hoverNodes = this.data.nodes.filter(d => d.id === node.id);
this.hoverNodes
.forEach(node => {
const { gfx } = node;

gfx.filters = [
new GlowFilter.GlowFilter({
distance: 2,
innerStrength: 0,
outerStrength: 2,
color: 0xffffff,
quality: 1
})
];
gfx.zIndex = 1;
});
}

showTooltip (node) {
this.tooltip.style('visibility', 'visible')
.style('top', `${node.y}px`)
.style('left', `${node.x}px`)
.html(`${node.name}, ${node.nEdge} connections`);
}

hideTooltip () {
this.tooltip.style('visibility', 'hidden');
}

pointerOver (node) {
this.hoverNetworkNodes(node);
this.showTooltip(node);
}

pointerOut () {
this.hideTooltip();

this.hoverNodes
.forEach(node => {
const { gfx } = node;
gfx.filters.pop();
gfx.zIndex = 0;
});
}

updateNodePosition () {
this.data.nodes.forEach((node) => {
node.gfx.x = node.x;
node.gfx.y = node.y + this.yScale.bandwidth()/2 - this.margin.top;
node.gfx
.on('pointerover', () => this.pointerOver(node))
.on('pointerout', () => this.pointerOut());
});
}
draw () {
this.drawYAxis();
this.drawXAxis();
this.drawNodes();
// this.simulation.alpha(1).restart();
}
animate () {
this.app.ticker.add(() => {
this.updateNodePosition();
});
}
}

Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
style = html`<style>
#viz {
height: 600px;
width: 100%;
}

.tooltip {
position: absolute;
left: 0px;
top: 0px;
visibility: hidden;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: normal;
font-size: 11px;
padding: 2px;
pointer-events: none;
background-color: rgba(255, 255, 255, 0.8);
padding: 5px;
border-radius: 5px;
}

</style>`
Insert cell
PIXI = import("https://cdn.jsdelivr.net/npm/pixi.js@7.3.2/+esm")
Insert cell
GlowFilter = import('https://cdn.skypack.dev/@pixi/filter-glow@5.2.1?min')
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