Published
Edited
Dec 15, 2020
1 star
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
// config
const SCREEN_WIDTH = width;
const SCREEN_HEIGHT = height;
const RESOLUTION = window.devicePixelRatio;
const NODE_RADIUS = 5;
const NODE_BORDER_WIDTH = 0;
const NODE_BORDER_COLOR = 0xffffff;
const NODE_BORDER_RADIUS = NODE_RADIUS + NODE_BORDER_WIDTH;
const NODE_HIT_WIDTH = 5;
const NODE_HIT_RADIUS = NODE_RADIUS + NODE_HIT_WIDTH;
const NODE_HOVER_BORDER_COLOR = 0x000000;
const TEXTURE_COLOR = 0xffffff;
const CIRCLE = 'CIRCLE';
const CIRCLE_BORDER = 'CIRCLE_BORDER';
// compute static layout
yield html`<div style="height: ${SCREEN_HEIGHT}px; font-size: 0.85rem">Computing layout...</div>`;
const { positions, layoutTime } = await runLayout(graph, config.layout);
const { nodes, links } = graph;
nodes.forEach(nodeData => {
const position = positions[nodeData.id];
nodeData.x = position.x + getRandomArbitrary(-10, 10);
nodeData.y = position.y + getRandomArbitrary(-10, 10);
});
// normalize node positions
yield html`<div style="height: ${SCREEN_HEIGHT}px; font-size: 0.85rem">Rendering...</div>`;
const minNodeX = Math.min(...nodes.map(nodeData => nodeData.x));
const maxNodeX = Math.max(...nodes.map(nodeData => nodeData.x));
const minNodeY = Math.min(...nodes.map(nodeData => nodeData.y));
const maxNodeY = Math.max(...nodes.map(nodeData => nodeData.y));
const graphWidth = Math.abs(maxNodeX - minNodeX);
const graphHeight = Math.abs(maxNodeY - minNodeY);
const WORLD_WIDTH = Math.max(SCREEN_WIDTH * 2, graphWidth * 1.1);
const WORLD_HEIGHT = Math.max(SCREEN_HEIGHT * 2, graphHeight * 1.1);
nodes.forEach(nodeData => {
nodeData.x = nodeData.x - minNodeX - graphWidth / 2 + WORLD_WIDTH / 2;
nodeData.y = nodeData.y - minNodeY - graphHeight / 2 + WORLD_HEIGHT / 2;
});

// create PIXI application
console.log(graphWidth, graphHeight, SCREEN_WIDTH, SCREEN_HEIGHT, WORLD_WIDTH, WORLD_HEIGHT, RESOLUTION);
const app = new PIXI.Application({
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
resolution: RESOLUTION,
transparent: true,
antialias: true,
autoDensity: true,
autoStart: false // disable automatic rendering by ticker, render manually instead, only when needed
});
app.view.style.width = `${SCREEN_WIDTH}px`;
//app.view.style.transform = `translate(${SCREEN_WIDTH/2}, ${SCREEN_HEIGHT}px`;
// manual rendering
// app.renderer.on('postrender', () => { console.log('render'); });
let renderRequestId = undefined;
const requestRender = () => {
if (renderRequestId) {
return;
}
renderRequestId = window.requestAnimationFrame(() => {
app.render();
renderRequestId = undefined;
});
}
// create PIXI viewport
const viewport = new Viewport({
screenWidth: SCREEN_WIDTH,
screenHeight: SCREEN_HEIGHT,
worldWidth: WORLD_WIDTH,
worldHeight: WORLD_HEIGHT,
backgroundColor : 0x061639,
interaction: app.renderer.plugins.interaction,
});
const zoomIn = () => {
viewport.zoom(-WORLD_WIDTH / 10, true);
};
const zoomOut = () => {
viewport.zoom(WORLD_WIDTH / 10, true);
};
const resetViewport = () => {
viewport.center = new PIXI.Point(WORLD_WIDTH / 2, WORLD_HEIGHT / 2);
viewport.fitWorld(true);
};
app.stage.addChild(viewport);
viewport.drag().pinch().wheel().decelerate().clampZoom({ minWidth: SCREEN_WIDTH, minHeight: SCREEN_HEIGHT });
// create layers: links, front links, nodes, front nodes, front labels
const nodesLayer = new PIXI.Container();
viewport.addChild(nodesLayer);
const frontNodesLayer = new PIXI.Container();
viewport.addChild(frontNodesLayer);

// create textures: circle, circle border, icons
const circleGraphics = new PIXI.Graphics();
circleGraphics.beginFill(TEXTURE_COLOR);
circleGraphics.drawCircle(NODE_RADIUS, NODE_RADIUS, NODE_RADIUS);
const circleTexture = app.renderer.generateTexture(circleGraphics, PIXI.settings.SCALE_MODE, RESOLUTION);
const circleBorderGraphics = new PIXI.Graphics();
circleBorderGraphics.lineStyle(NODE_BORDER_WIDTH, TEXTURE_COLOR);
circleBorderGraphics.drawCircle(NODE_BORDER_RADIUS, NODE_BORDER_RADIUS, NODE_RADIUS);
const circleBorderTexture = app.renderer.generateTexture(circleBorderGraphics, PIXI.settings.SCALE_MODE, RESOLUTION);

// state
let nodeDataToNodeGfx = new WeakMap();
let nodeGfxToNodeData = new WeakMap();
let hoveredNodeData = undefined;
let clickedNodeData = undefined;

const updatePositions = () => {
nodes.forEach(nodeData => {
const nodeGfx = nodeDataToNodeGfx.get(nodeData);

nodeGfx.x = nodeData.x;
nodeGfx.y = nodeData.y;
});

requestRender();
};
const round = value => Math.round(value * 1000) / 1000;
const updateVisibility = () => {
// culling
const cull = new PIXI.Cull();
cull.addAll(nodesLayer.children);
cull.cull(app.renderer.screen);
// console.log(
// [...cull._targetList].filter(x => x.visible === true).length,
// [...cull._targetList].filter(x => x.visible === false).length
// );
// levels of detail
const zoom = viewport.scale.x;
const zoomSteps = [0.1, 0.2, 0.4, Infinity];
const zoomStep = zoomSteps.findIndex(zoomStep => zoom <= zoomStep);
nodes.forEach(nodeData => {
const nodeGfx = nodeDataToNodeGfx.get(nodeData);
const circleBorder = nodeGfx.getChildByName(CIRCLE_BORDER);
circleBorder.visible = zoomStep >= 1;
});
};
// event handlers
const hoverNode = nodeData => {
if (clickedNodeData) {
return;
}
if (hoveredNodeData === nodeData) {
return;
}
hoveredNodeData = nodeData;
const nodeGfx = nodeDataToNodeGfx.get(nodeData);

// move to front layer
nodesLayer.removeChild(nodeGfx);
frontNodesLayer.addChild(nodeGfx);

// add hover effect
const circleBorder = nodeGfx.getChildByName(CIRCLE_BORDER);
circleBorder.tint = NODE_HOVER_BORDER_COLOR;

requestRender();
};
const unhoverNode = nodeData => {
if (clickedNodeData) {
return;
}
if (hoveredNodeData !== nodeData) {
return;
}
hoveredNodeData = undefined;
const nodeGfx = nodeDataToNodeGfx.get(nodeData);

// move back from front layer
frontNodesLayer.removeChild(nodeGfx);
nodesLayer.addChildAt(nodeGfx, nodes.indexOf(nodeData));

// clear hover effect
const circleBorder = nodeGfx.getChildByName(CIRCLE_BORDER);
circleBorder.tint = NODE_BORDER_COLOR;
requestRender();
};

const moveNode = (nodeData, point) => {
const nodeGfx = nodeDataToNodeGfx.get(nodeData);
nodeData.x = point.x;
nodeData.y = point.y;
updatePositions();
};
const appMouseMove = event => {
if (!clickedNodeData) {
return;
}
moveNode(clickedNodeData, viewport.toWorld(event.data.global));
};
const clickNode = nodeData => {
clickedNodeData = nodeData;
// enable node dragging
app.renderer.plugins.interaction.on('mousemove', appMouseMove);
// disable viewport dragging
viewport.pause = true;
};
const unclickNode = () => {
clickedNodeData = undefined;
// disable node dragging
app.renderer.plugins.interaction.off('mousemove', appMouseMove);
// enable viewport dragging
viewport.pause = false;
};
// create node graphics
const nodeDataGfxPairs = nodes.map(nodeData => {
const nodeGfx = new PIXI.Container();
nodeGfx.x = nodeData.x;
nodeGfx.y = nodeData.y;
nodeGfx.interactive = true;
nodeGfx.buttonMode = true;
nodeGfx.hitArea = new PIXI.Circle(0, 0, NODE_HIT_RADIUS);
//nodeGfx.on('mouseover', event => hoverNode(nodeGfxToNodeData.get(event.currentTarget)));
//nodeGfx.on('mouseout', event => unhoverNode(nodeGfxToNodeData.get(event.currentTarget)));
//nodeGfx.on('mousedown', event => clickNode(nodeGfxToNodeData.get(event.currentTarget)));
//nodeGfx.on('mouseup', () => unclickNode());
//nodeGfx.on('mouseupoutside', () => unclickNode());
const circle = new PIXI.Sprite(circleTexture);
circle.name = CIRCLE;
circle.x = -circle.width / 2;
circle.y = -circle.height / 2;
circle.tint = '#000';
circle.alpha = 0.5;
nodeGfx.addChild(circle);

const circleBorder = new PIXI.Sprite(circleBorderTexture);
circleBorder.name = CIRCLE_BORDER;
circleBorder.x = -circleBorder.width / 2;
circleBorder.y = -circleBorder.height / 2;
circleBorder.tint = NODE_BORDER_COLOR;
nodeGfx.addChild(circleBorder);
nodesLayer.addChild(nodeGfx);

return [nodeData, nodeGfx];
});
// create lookup tables
nodeDataToNodeGfx = new WeakMap(nodeDataGfxPairs.map(([nodeData, nodeGfx]) => [nodeData, nodeGfx]));
nodeGfxToNodeData = new WeakMap(nodeDataGfxPairs.map(([nodeData, nodeGfx]) => [nodeGfx, nodeData]));

// initial draw
resetViewport();
updatePositions();
viewport.on('frame-end', () => {
if (viewport.dirty) {
updateVisibility();
requestRender();
viewport.dirty = false;
}
});

// destroy PIXI application on Observable cell invalidation
invalidation.then(() => {
if (renderRequestId) {
window.cancelAnimationFrame(renderRequestId);
renderRequestId = undefined;
}
app.destroy(true, true);
});

// prevent body scrolling
app.view.addEventListener('wheel', event => { event.preventDefault(); });
let tick = 0
// Listen for animate update
app.ticker.add((delta) => {

// rotate the container!
// use delta to create frame-independent transform
//nodesLayer.rotation -= 0.001 * delta;

//nodesLayer.x = WORLD_WIDTH / 2;
//nodesLayer.y = WORLD_HEIGHT / 2;

//nodesLayer.pivot.x = WORLD_WIDTH / 2;
//nodesLayer.pivot.y = WORLD_HEIGHT / 2;
//nodes.forEach(nodeData => {
//const nodeGfx = nodeDataToNodeGfx.get(nodeData);
//const labelGfx = nodeDataToLabelGfx.get(nodeData);

//let randX = getRandomArbitrary(-1, 1)
//let randY = getRandomArbitrary(-1, 1)
//let speed = (2 + Math.random() * 2) * 0.2;

//nodeGfx.position.x = (nodeData.x + randX);
//nodeGfx.position.y = (nodeData.y + randY);
//nodeGfx.position.x += randX;
//nodeGfx.position.y += randY;
//});
let nodeIDs = [18, 39, 80, 120, 180, 190]
nodes.slice(200,400).forEach((d,i)=>{
getMoveNodes(i)
})

tick += 1
function getMoveNodes(id){
let N = nodeDataToNodeGfx.get(nodes[id])
let xDist, yDist
if(nodes[id].x > WORLD_WIDTH/2){
xDist = -WORLD_WIDTH/2 + nodes[id].x + getRandomArbitrary(-200, 200)
} else {
xDist = nodes[id].x - WORLD_WIDTH/2 + getRandomArbitrary(-200, 200)
}
if(nodes[id].y > WORLD_HEIGHT/2){
yDist = -WORLD_HEIGHT/2 + nodes[id].y + getRandomArbitrary(-200, 200)
} else {
yDist = nodes[id].y - WORLD_HEIGHT/2 + getRandomArbitrary(-200, 200)
}

let steps = 50
let scaleTo = 3
if(tick <= 50) {
//N.position.x += xDist/steps
//N.position.y += yDist/steps
//N.scale.set((scaleTo/steps*tick),(scaleTo/steps*tick));
const circle = N.getChildByName(CIRCLE);
circle.tint = 0xff0000;
circle.x -= xDist/steps
circle.y -= yDist/steps
}
}
});
app.ticker.start()
// create container
const container = html`<div style="position: relative"></div>`;
const toolbar = html`<div style="position: absolute; top: 0; left: 0; margin: 5px"></div>`;
container.appendChild(toolbar);
const zoomInButton = html`<button>+</button>`;
zoomInButton.addEventListener('click', () => zoomIn());
toolbar.appendChild(zoomInButton);
const zoomOutButton = html`<button>−</button>`;
zoomOutButton.addEventListener('click', () => zoomOut());
toolbar.appendChild(zoomOutButton);
const resetButton = html`<button>Reset</button>`;
resetButton.addEventListener('click', () => resetViewport());
toolbar.appendChild(resetButton);
const infoText = html`<div style="margin-top: 10px; font-size: 0.85rem; text-shadow: 1px 1px white, 1px -1px white, -1px 1px white, -1px -1px white">
<strong>Layout stats:</strong><br>
Layout: ${config.layout}<br>
Time: ${formatInt(layoutTime)} ms<br>
Readability score: <span id="readability-score">Computing...</span><br>
</div>`;
const readabilityScoreText = infoText.querySelector('#readability-score');
toolbar.appendChild(infoText);
if (config.showFpsMeter) {
const fpsMeterContainer = html`<div style="position: absolute; top: 0; right: 0; margin: 5px"></div>`;
fpsMeterContainer.appendChild(fpsMeter());
container.appendChild(fpsMeterContainer);
}
container.appendChild(app.view);
yield container;
readabilityScoreText.innerHtml = 'Computing...';
const layoutReadability = await getLayoutReadability(graph);
readabilityScoreText.innerText = formatFloat(layoutReadability);
}
Insert cell
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
Insert cell
graphs = {
const lesMiserables = await d3.json("https://gist.githubusercontent.com/mbostock/4062045/raw/5916d145c8c048a6e3086915a6be464467391c62/miserables.json");
const socfbCaltech36 = await d3.json("https://gist.githubusercontent.com/zakjan/eb366020e5477c578456d8cf9e8e5d01/raw/373d82875ac329e0543f3f723010c53423a0588a/socfb-Caltech36.json");
return {
'Point': point(),
'Line': line(10),
'Circle': circle(10),
'Star': star(10),
'Wheel': wheel(10),
'Complete': complete(10),
'Triangle Lattice': triangleLattice(5),
'Square Lattice': squareLattice(5),
'Sierpinski Triangle': sierpinskiTriangle(3),
'Les Misérables': lesMiserables,
'socfb-Caltech36': socfbCaltech36,
};
}
Insert cell
graph = hyper(
multiply(
graphs[config.graph],
parseInt(config.multiply, 10)
),
parseInt(config.hyper, 10)
)
Insert cell
height = 600
Insert cell
color = {
const scale = d3.scaleOrdinal(d3.schemeCategory10);
return nodeData => scale(nodeData.group);
}
Insert cell
colorToNumber = color => parseInt(color.slice(1), 16)
Insert cell
layouts = ['d3-force', 'd3-force-reuse', 'd3-force-sampled', 'cytoscape-cola', 'cytoscape-cose', 'cytoscape-cose-bilkent', 'cytoscape-fcose', 'cytoscape-euler', 'sigma.layout.fruchtermanReingold', 'sigma.layout.forceAtlas2', 'sigma.layout.paragraphl', 'graphology-layout-forceatlas2', 'ngraph.forcelayout', 'springy', 'random']
Insert cell
runLayout = (...args) => {
return new Promise((resolve, reject) => {
const workerCode = `
// hotfix for Sigma
self.document = {
documentElement: {},
createElement: function(tagName) {
// hotfix for sigma.layout.paragraphl
if (tagName === 'canvas') {
return new OffscreenCanvas(100, 100);
}
}
};
self.HTMLElement = function() {};
// hotfix for sigma.layout.paragraphl
self.alert = console.error;
Object.defineProperty(self, 'vizit', { get: function() { return window.vizit; } });

function randomString() {
return Math.floor(Math.random() * 10e12).toString(36);
}

function runD3Layout(data, d3ForceManyBodyFunc) {
const { nodes, links } = data;
const iterations = 300;
const nodeRepulsion = -250;

//d3.forceSimulation(nodes)
//.force('link', d3.forceLink(links).id(linkData => linkData.id))
//.force('charge', d3ForceManyBodyFunc().strength(nodeRepulsion))
//.force('center', d3.forceCenter())
//.stop()
//.tick(iterations);

d3.forceSimulation(nodes)
.force("charge", d3.forceCollide().radius(9))
.force("r", d3.forceRadial(function(d,i) { return i<100 ? 300 : 600; }))
.force('center', d3.forceCenter(0, 0))
.stop()
.tick(iterations);

const positions = Object.fromEntries(nodes.map(node => {
return [node.id, { x: node.x, y: node.y }];
}));

return positions;
}

function runCytoscapeLayout(data, cytoscapeLayoutName) {
return new Promise(resolve => {
const { nodes, links } = data;
const iterations = 300;
const nodeRepulsion = 1000000;

const cytoscapeGraph = [
...nodes.map(node => ({ group: 'nodes', data: node })),
...links.map(link => ({ group: 'edges', data: link })),
];

cytoscape({
headless: true,
styleEnabled: false,
layout: {
name: cytoscapeLayoutName,
// numIter: iterations,
nodeRepulsion: nodeRepulsion,
randomize: true,
animate: false,
stop: event => {
const positions = Object.fromEntries(event.cy.elements('node').map(node => {
return [node.id(), node.position()];
}));

resolve(positions);
},
},
elements: cytoscapeGraph,
});
});
}

async function runSigmaLayout(data, sigmaLayoutFunc) {
const { nodes, links } = data;
const iterations = 300;
const scalingRatio = 40;

const sigmaGraph = {
nodes: nodes.map(node => ({ ...node, x: Math.random(), y: Math.random() })),
edges: links.map(link => ({ id: randomString(), ...link }))
};

const s = new sigma({ graph: sigmaGraph });

await sigmaLayoutFunc(s, { iterations, scalingRatio });

const positions = Object.fromEntries(s.graph.nodes().map(node => {
return [node.id, { x: node.x, y: node.y }];
}));

return positions;
}

function runGraphologyLayout(data, graphologyLayoutFunc) {
const { nodes, links } = data;
const iterations = 300;
const scalingRatio = 80;

const graph = new graphology.Graph();
nodes.forEach(node => {
graph.addNode(node.id);
});
links.forEach(link => {
graph.addEdge(link.source, link.target);
});

graph.forEachNode(nodeId => {
graph.setNodeAttribute(nodeId, 'x', Math.random());
graph.setNodeAttribute(nodeId, 'y', Math.random());
});

const positions = graphologyLayoutFunc(graph, {
iterations,
settings: {
...graphologyLayoutFunc.inferSettings(graph),
scalingRatio,
},
});

return positions;
}

function runNgraphLayout(data, ngraphLayoutFunc) {
const { nodes, links } = data;
const iterations = 300;
const gravity = -100;

const graph = createGraph();
nodes.forEach(node => {
graph.addNode(node.id);
});
links.forEach(link => {
graph.addLink(link.source, link.target);
});

const layout = ngraphLayoutFunc(graph, { gravity: gravity });
for (let i = 0; i < iterations; i++) {
layout.step();
}

const positions = Object.fromEntries(nodes.map(node => {
return [node.id, layout.getNodePosition(node.id)];
}));

return positions;
}

function runSpringyLayout(data, springyLayoutFunc) {
return new Promise(resolve => {
const { nodes, links } = data;

const graph = new Springy.Graph();
const nodesMap = new Map(nodes.map(node => {
return [node.id, graph.newNode({ id: node.id })];
}));
links.forEach(link => {
graph.newEdge(nodesMap.get(link.source), nodesMap.get(link.target));
});

const layout = new springyLayoutFunc(graph, 400, 400, 0.5, 0.00001);
layout.start(undefined, () => {
const positions = Object.fromEntries(Array.from(nodesMap.values()).map(node => {
const point = layout.point(node);
return [node.data.id, { x: point.p.x * 20, y: point.p.y * 20 }];
}));

resolve(positions);
});
});
}

function runRandomLayout(data) {
const { nodes } = data;

const positions = Object.fromEntries(nodes.map(node => {
return [node.id, { x: Math.random() * 1000, y: Math.random() * 1000 }];
}));

return positions;
}

async function runLayout(data, layoutName) {
const layoutFuncs = new Map([
['d3-force', {
importUrls: [
'https://unpkg.com/d3@5.14.2/dist/d3.min.js',
],
run: data => runD3Layout(data, d3.forceManyBody),
}],
['d3-force-reuse', {
importUrls: [
'https://unpkg.com/d3@5.14.2/dist/d3.min.js',
'https://unpkg.com/d3-force-reuse@1.0.1/build/d3-force-reuse.js',
],
run: data => runD3Layout(data, d3.forceManyBodyReuse),
}],
['d3-force-sampled', {
importUrls: [
'https://unpkg.com/d3@5.14.2/dist/d3.min.js',
'https://unpkg.com/d3-force-sampled@1.0.0/build/d3-force-sampled.js',
],
run: data => runD3Layout(data, d3.forceManyBodySampled),
}],
['cytoscape-cose', {
importUrls: [
'https://unpkg.com/cytoscape@3.13.0/dist/cytoscape.min.js',
],
run: data => runCytoscapeLayout(data, 'cose'),
}],
['cytoscape-cola', {
importUrls: [
'https://unpkg.com/cytoscape@3.13.0/dist/cytoscape.min.js',
'https://bundle.run/cytoscape-cola@2.3.0',
],
run: data => runCytoscapeLayout(data, 'cola'),
}],
['cytoscape-cose-bilkent', {
importUrls: [
'https://unpkg.com/cytoscape@3.13.0/dist/cytoscape.min.js',
'https://unpkg.com/layout-base@1.0.2/layout-base.js',
'https://unpkg.com/cose-base@1.0.1/cose-base.js',
'https://unpkg.com/cytoscape-cose-bilkent@4.1.0/cytoscape-cose-bilkent.js',
],
run: data => runCytoscapeLayout(data, 'cose-bilkent'),
}],
['cytoscape-fcose', {
importUrls: [
'https://unpkg.com/cytoscape@3.13.0/dist/cytoscape.min.js',
'https://unpkg.com/numeric@1.2.6/numeric-1.2.6.js',
'https://unpkg.com/layout-base@1.0.2/layout-base.js',
'https://unpkg.com/cose-base@1.0.1/cose-base.js',
'https://unpkg.com/cytoscape-fcose@1.2.0/cytoscape-fcose.js',
],
run: data => runCytoscapeLayout(data, 'fcose'),
}],
['cytoscape-euler', {
importUrls: [
'https://unpkg.com/cytoscape@3.13.0/dist/cytoscape.min.js',
'https://unpkg.com/cytoscape-euler@1.2.2/cytoscape-euler.js',
],
run: data => runCytoscapeLayout(data, 'euler'),
}],
['sigma.layout.fruchtermanReingold', {
importUrls: [
'https://unpkg.com/sigma@1.2.1/build/sigma.require.js',
'https://unpkg.com/sigma@1.2.1/build/plugins/sigma.plugins.animate.min.js',
'https://unpkg.com/linkurious@1.5.2/dist/plugins/sigma.layouts.fruchtermanReingold.min.js'
],
run: data => runSigmaLayout(
data,
async (s, config) => {
const listener = sigma.layouts.fruchtermanReingold.configure(s, {
maxIterations: config.iterations
});
const promise = new Promise(resolve => listener.bind('stop', resolve));
sigma.layouts.fruchtermanReingold.start(s);
await promise;

const scalingRatio = 10;
s.graph.nodes().forEach(node => {
node.x = node.x * scalingRatio;
node.y = node.y * scalingRatio;
});
}
),
}],
['sigma.layout.forceAtlas2', {
importUrls: [
'https://unpkg.com/sigma@1.2.1/build/sigma.require.js',
'https://unpkg.com/sigma@1.2.1/build/plugins/sigma.layout.forceAtlas2.min.js'
],
run: data => runSigmaLayout(
data,
async (s, config) => {
s.startForceAtlas2({
startingIterations: config.iterations,
iterationsPerRender: 0,
scalingRatio: config.scalingRatio
});
await new Promise(resolve => setTimeout(resolve, 500));
s.killForceAtlas2();
}
),
}],
['sigma.layout.paragraphl', {
importUrls: [
'https://unpkg.com/sigma@1.2.1/build/sigma.require.js',
'https://unpkg.com/sigma@1.2.1/build/plugins/sigma.plugins.animate.min.js',
'https://cdn.jsdelivr.net/gh/nblintao/ParaGraphL@master/GPGPUtility.js',
'https://cdn.jsdelivr.net/gh/nblintao/ParaGraphL@master/sigma.layout.paragraphl.js'
],
run: data => runSigmaLayout(
data,
async (s, config) => {
var listener = sigma.layouts.paragraphl.configure(s, {
iterations: config.iterations
});
const promise = new Promise(resolve => listener.bind('stop', resolve));
sigma.layouts.paragraphl.start(s);
await promise;

const scalingRatio = 10;
s.graph.nodes().forEach(node => {
node.x = node.x * scalingRatio;
node.y = node.y * scalingRatio;
});
}
)
}],
['graphology-layout-forceatlas2', {
importUrls: [
'https://unpkg.com/graphology@0.17.0/dist/graphology.umd.js',
'https://unpkg.com/graphology-layout-forceatlas2@0.4.2/build/graphology-layout-forceatlas2.min.js',
],
run: data => runGraphologyLayout(data, forceAtlas2),
}],
['ngraph.forcelayout', {
importUrls: [
'https://unpkg.com/ngraph.graph@19.0.2/dist/ngraph.graph.min.js',
'https://bundle.run/ngraph.forcelayout@1.0.0/index.js',
],
run: data => runNgraphLayout(data, ngraph_forcelayout),
}],
['springy', {
importUrls: [
'https://unpkg.com/springy@2.8.0/springy.js',
],
run: data => runSpringyLayout(data, Springy.Layout.ForceDirected),
}],
['random', {
run: data => runRandomLayout(data),
}],
]);

const layoutFunc = layoutFuncs.get(layoutName);
if (!layoutFunc) {
throw new Error('Unknown layout ' + layoutName);
}

// import layout dependencies
if (layoutFunc.importUrls) {
layoutFunc.importUrls.forEach(importUrl => {
importScripts(importUrl);
});
}

// run layout and measure time
const startTime = performance.now();
const positions = await layoutFunc.run(data);
const endTime = performance.now();
const layoutTime = endTime - startTime;
const result = { positions, layoutTime };

return result;
};

self.onmessage = async event => {
const result = await runLayout.apply(undefined, event.data);
self.postMessage(result);
}
`;

const workerBlob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(workerBlob)
const worker = new Worker(workerUrl);

worker.onmessage = event => {
resolve(event.data);
worker.terminate();
URL.revokeObjectURL(workerUrl);
};
worker.postMessage(args);
});
}
Insert cell
getLayoutReadability = (...args) => {
return new Promise((resolve, reject) => {
const workerCode = `
importScripts('https://cdn.jsdelivr.net/gh/rpgove/greadability@master/greadability.js');

function getLayoutReadability(graph) {
const layoutQuality = greadability.greadability(
JSON.parse(JSON.stringify(graph.nodes)),
JSON.parse(JSON.stringify(graph.links)),
d => d.id,
);
const layoutReadability = (layoutQuality.crossing + layoutQuality.crossingAngle + layoutQuality.angularResolutionMin + layoutQuality.angularResolutionDev) / 4;

return layoutReadability;
}

self.onmessage = async event => {
const result = await getLayoutReadability.apply(undefined, event.data);
self.postMessage(result);
}
`;

const workerBlob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(workerBlob)
const worker = new Worker(workerUrl);

worker.onmessage = event => {
resolve(event.data);
worker.terminate();
URL.revokeObjectURL(workerUrl);
};
worker.postMessage(args);
});
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more