Published
Edited
Jun 4, 2019
1 star
Insert cell
md`# Node Force Layout`
Insert cell
dragAlphaTarget = 0.3
Insert cell
initialAlphaTarget = 1.0
Insert cell
minV = 8e-4
Insert cell
yStrength = 0.001 * (yMax - yMin)
Insert cell
xStrength = 0.001 * (xMax - xMin)
Insert cell
linkStrength = 0.1
Insert cell
chargeDistanceMax = 10
Insert cell
chargeDistanceMin = 1
Insert cell
chargeStrength = -0.5
Insert cell
gravityYAnchor = yMax
Insert cell
gravityYStrength = 0.1
Insert cell
center = true
Insert cell
yExtent = [0, 100]
Insert cell
xExtent = [0, 100]
Insert cell
xMin = xExtent[0]
Insert cell
xMax = xExtent[1]
Insert cell
yMin = yExtent[0]
Insert cell
yMax = yExtent[1]
Insert cell
forces = {
simulation
.force("y", yStrength === 0 ? null : d3.forceY().y(yAnchor).strength(yStrength))
.force("x", xStrength === 0 ? null : d3.forceX().x(xAnchor).strength(xStrength))
.force("charge", chargeStrength === 0 ? null : d3.forceManyBody().strength(chargeStrength).distanceMin(chargeDistanceMin).distanceMax(chargeDistanceMax))
.force("link", linkStrength === 0 ? null : d3.forceLink().strength(linkStrength))
.force("center", center ? d3.forceCenter(d3.mean([xMin, xMax]), d3.mean([yMin, yMax])) : null)
.force("gravity", gravityYStrength === 0 ? null : d3.forceY().y(gravityYAnchor).strength(gravityYStrength))
;
}
Insert cell
drag = (dragX, dragY) => {
function dragged(d) {
if (dragX) {
const fx = dragX(d3.event.x)
if (fx != null) {
d.fx = fx
}
}
if (dragY) {
const fy = dragY(d3.event.y)
if (fy != null) {
d.fy = fy
}
}
}

function dragstarted(d) {
if (!d3.event.active) {
simulation.alphaTarget(dragAlphaTarget).restart();
d.dragging = true
}
}
function dragended(d) {
if (d.fx != null) {
d.x = d.fx;
}
delete d.fx;

if (d.fy != null) {
d.y = d.fy;
}
delete d.fy;
if (!d3.event.active) {
d.dragging = false
simulation.alphaTarget(0);
refreshForces();
}
}

return d3.drag()
.on("start", d => dragstarted(d))
.on("drag", d => dragged(d))
.on("end", d => dragended(d));
}
Insert cell
function refreshForces() {
simulation.force("x", simulation.force("x"))
simulation.force("y", simulation.force("y"))
}
Insert cell
viewof positions = new View([])
Insert cell
updatePositions = {
console.log("updatePositions")
forces;

// Initial positions
let noop = true
for (const d of nodes) {
const x = xAnchor(d)
const y = yAnchor(d)
// Avoid restarting the simulation if all nodes are in the same position as last time
if (noop &&
(d.xAnchor !== x || (d.fx !== undefined && d.fx !== x) ||
(d.yAnchor !== y || (d.fy !== undefined && d.fy !== y)))) {
noop = false
}

d.xAnchor = x
if (d.x == null || isNaN(d.x)) {
d.x = x
}
d.yAnchor = y
if (d.y == null || isNaN(d.y)) {
d.y = y
}
}

if (noop && simulation.alpha() == simulation.alphaTarget()) {
console.log("returning early!!!")
console.log({
noop,
alpha: simulation.alpha(),
alphaTarget: simulation.alphaTarget(),
atTarget: simulation.alpha() == simulation.alphaTarget()
})

return
}

refreshForces()
refreshPolygons(nodes)

simulation
.nodes(nodes)
;

const linkForce = simulation.force('link')
linkForce && linkForce.links(links)

simulation.on("tick", () => {
refreshPolygons(nodes)
viewof positions.value = nodes
})
invalidation.then(() => {
simulation.on("tick", null)
})
simulation
.alpha(initialAlphaTarget)
.alphaTarget(0)
.restart()

viewof positions.value = nodes
}
Insert cell
md `---

## Test values`
Insert cell
nodes = {
const raw = [
{
name: "water",
attrs: {
depth: 20,
phase: [null, 0.5],
},
temps: {
},
sources: {},
users: ["tea"],
},
{
name: "tea",
attrs: {
depth: 1,
phase: [null, 0.5],
},
temps: {
},
sources: {},
users: [],
},
]
const index = Object.fromEntries(raw.map(component => [component.name, component]))
raw.forEach(component => component.users = component.users.map(name => index[name]))
console.log(raw)
return raw.map(
component => Object.assign(
component.temps,
{
name: component.name,
attrs: component.attrs,
sources: component.sources,
users: component.users.map(user => user.temps),
}))
}
Insert cell
Insert cell
xStableOrder = new WeakMap()
Insert cell
yStableOrder = new WeakMap()
Insert cell
function lookup(wm, obj, fn) {
if (!wm.has(obj)) {
wm.set(obj, fn(obj))
}
return wm.get(obj)
}
Insert cell
function yAnchor(d, i, arr) {
return lookup(yStableOrder, d, () => (Math.random() * (yMax - yMin)) + yMin)
}
Insert cell
function xAnchor(d) {
return lookup(xStableOrder, d, () => (Math.random() * (xMax - xMin)) + xMin)
}
Insert cell
positions
Insert cell
Insert cell
simulation = {
let simulation = d3.forceSimulation()
.force("collide", d3.forceCollide().radius(2))
// .force("gravity", d3.forceY().y(height).strength(.01))

invalidation.then(() => simulation.stop());
return simulation
}
Insert cell
function refreshPolygons(nodes) {
if (nodes.length < 3) {
nodes.forEach(n => n.polygon = [])
return
}

try {
let voronoisPolygons = d3.Delaunay.from(nodes, d => d.x, d => d.y)
.voronoi([xMin, yMin, xMax, yMax]);
nodes.forEach((d, i) => {
d.polygon = voronoisPolygons.cellPolygon(i)
})
} catch (e) {
console.log("nodes", nodes)
nodes.forEach(n => n.polygon = [])
console.error(e)
return
}
}

Insert cell
class View {
constructor(value) {
Object.defineProperties(this, {
_list: {value: [], writable: true},
_value: {value, writable: true}
});
}
get value() {
return this._value;
}
set value(value) {
this._value = value;
this.dispatchEvent({type: "input", value});
}
addEventListener(type, listener) {
if (type != "input" || this._list.includes(listener)) return;
this._list = [listener].concat(this._list);
}
removeEventListener(type, listener) {
if (type != "input") return;
this._list = this._list.filter(l => l !== listener);
}
dispatchEvent(event) {
const p = Promise.resolve(event);
this._list.forEach(l => p.then(l));
}
}
Insert cell
d3 = require("d3@5", "d3-array@2", "https://files-5xxzmmll5.now.sh/d3-delaunay.js")
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