Published
Edited
Aug 7, 2019
Importers
Insert cell
md`# MapScript Map Layout`
Insert cell
import {World} from '6e557ad8d4923730'
Insert cell
world = new World(['x', 'y'])
Insert cell
function xAnchorIsUnknown(d) {
return d.stage === 0
}
Insert cell
nodes = world.generator((w) => {
try {
const scenarios = w.scenarios()
function dedupe(uses, fn, fallbackValue, collisionValue) {
let value
let collision = false
for (const use of uses) {
const v = fn(use)
if (v === undefined) {
continue
}
if (value === undefined) {
value = v
continue
}
if (value != v) {
console.log("!!collision", value, "!=", v, fn, {use, uses, fallbackValue})
collision = true
break
}
}
if (collision) {
value = collisionValue
}

if (value === undefined) {
return fallbackValue
}
return value
}

if (scenarios.length === 0) {
return []
}
function asNumber(d) {
const value = d === undefined ? d : +d
if (typeof value === "number" && isNaN(value)) {
console.warn(`Got NaN from ${d}`)
}
return value
}

const scenario = scenarios[0].ui
const components = Array.from(scenario.components(), ({def, uses}) => {
// Not a fan of the need to do this canonicalDef vs def business...
if (def.canonicalDef) {
def = def.canonicalDef
}

const users = []
const usesByUser = new Map()
for (const use of uses) {
if (use.user === undefined || use.user === scenario.rootUse) {
continue
}
// Not a fan of the need to do this canonicalDef vs def business...
const userTemps = (use.user.canonicalDef || use.user.def).temps
let userUses = usesByUser.get(userTemps)
if (userUses === undefined) {
users.push(userTemps)
usesByUser.set(userTemps, userUses = [])
}
userUses.push(use)
}
const stage = dedupe(uses, use => asNumber(use.attrs.stage), 0)
const substage = dedupe(uses, use => asNumber(use.attrs.substage), 0.5)
delete def.temps.stage
Object.defineProperty(def.temps, 'stage', {
get() { return stage },
configurable: true,
set(v) {
console.log("setting stage to", v) //, def.rawAttrs.stage.sourceLocation)
return true
},
})
delete def.temps.substage
Object.defineProperty(def.temps, 'substage', {
get() { return substage },
configurable: true,
set(v) {
console.log("setting substage to", v) //, def.rawAttrs.stage.sourceLocation)
return true
},
})

return Object.assign(
def.temps,
{
key: def.key,
name: def.name,
meta: def.meta,
uses,
sourceLocation: def.sourceLocation,
users,
depth: dedupe(uses, use => asNumber(use.attrs.depth), 1),

label: dedupe(uses, use => use.attrs.label, def.name),
fill: dedupe(uses, use => use.attrs.fill, "white"),
fontFamily: dedupe(uses, use => use.attrs.fontFamily, undefined),
fontColor: dedupe(uses, use => use.attrs.fontColor, "#333"),
fontWeight: dedupe(uses, use => asNumber(use.attrs.fontWeight), 500),
fontStyle: dedupe(uses, use => asNumber(use.attrs.fontStyle), undefined),
fontSize: dedupe(uses, use => asNumber(use.attrs.fontSize), undefined),
stroke: dedupe(uses, use => use.attrs.stroke, "#333"),
strokeWidth: dedupe(uses, use => asNumber(use.attrs.strokeWidth), 2),
hidden: dedupe(uses, use => use.attrs.hidden, undefined),
contour: dedupe(uses, use => use.attrs.contour, undefined),
contourRadius: dedupe(uses, use => use.attrs.contourRadius, undefined),

showVoronoi: dedupe(uses, use => asNumber(use.attrs.showVoronoi), false),
showAnchor: dedupe(uses, use => asNumber(use.attrs.showAnchor), false),
radius: dedupe(uses, use => asNumber(use.attrs.radius), 7),
compactRadius: dedupe(uses, use => asNumber(use.attrs.compactRadius), 5),

userEdges: Array.from(usesByUser, ([sourceDef, userUses]) => ({
target: def.temps,
source: sourceDef,

need: def.temps,
user: sourceDef,

label: dedupe(userUses, use => use.attrs.edgeLabel),
stroke: dedupe(userUses, use => use.attrs.edgeStroke, "#777"),
strokeOpacity: dedupe(userUses, use => asNumber(use.attrs.edgeStrokeOpacity), 0.3),
strokeWidth: dedupe(userUses, use => asNumber(use.attrs.edgeStrokeWidth), 1.5),
})),
})
}
)
return components
} catch (e) { console.log(e); throw e }
})
Insert cell
identity = {
function identity(d) {
return d
}
return Object.assign(identity, {invert: identity})
}
Insert cell
function pipe(...fns) {
return Object.assign(
d => {
const r = fns.reduce((acc, fn) => {
const v = fn(acc)
return v
}, d)
return r
},
{
invert: (d) => {
let result = d
for (let i = fns.length - 1; i >= 0; i--) {
const fn = fns[i]
const o = fn.invert(result)
result = o
}
return result
},
},
)
}
Insert cell
viewof yAnchor = new View()
Insert cell
yAnchorUpdate = {
const yGetter = d => d.depth
const depthExtent = [0, d3.max([5, 2 + d3.max(nodes, yGetter)])]
const yAnchorValue = Object.assign(
pipe(
yGetter,
d3.scaleLinear(depthExtent, yExtent)
.clamp(true),
),
{
title: d => "Visible Value",
},
)
viewof yAnchor.value = yAnchorValue
return yAnchorValue
}
Insert cell
update = { yAnchorUpdate; xAnchorUpdate; updateMeridians; updatePositions }
Insert cell
sample = world.run() `

t = 1

cup-of-tea:
-> hot-water:
usage = 100 mL

cup-of-tea:
-> tea:
usage = 2g

tea:
varunit = kg
varunitcost = 200 usd
stroke = "green"
fill = "red"

hot-water:
-> kettle:
usage = 2 kettle minute
-> water

hot-water:
varunit = L

water:
varunitcost = 0.1 usd
varunit = L

kettle:
-> power:
usage = 1 kW
stage = 3

power:
varunitcost = 0.1 usd / hour
varunit = kW
stage = 4
`
Insert cell
yMax = yExtent[1]
Insert cell
yMin = yExtent[0]
Insert cell
yExtent = [0, 100]
Insert cell
xMax = xExtent[1]
Insert cell
xMin = xExtent[0]
Insert cell
xWidth = xExtent[1] - xExtent[0]
Insert cell
xExtent = [0, 100]
Insert cell
dragAlphaTarget = 0.2
Insert cell
initialAlphaTarget = 1.0
Insert cell
viewof xAnchor = new View()
Insert cell
function clamp(min, max) {
return d => Math.min(Math.max(min, d), max)
}
Insert cell
noAnchor = {
function noAnchor(d) {
return d.x
}
noAnchor.invert = function invert(v) { return {x: v} }
noAnchor.stops = []
return noAnchor
}
Insert cell
wardleyAnchor = {
let width, start, anchor, substageScale, type = "tick", substage = 0
const stops = [
{stage: 1, weight: 2, compactLabel: "I", label: "Genesis", substage, width, start, anchor, substageScale, type},
{stage: 2, weight: 2, compactLabel: "II", label: "Bespoke", substage, width, start, anchor, substageScale, type},
{stage: 3, weight: 3, compactLabel: "III", label: "Product", substage, width, start, anchor, substageScale, type},
{stage: 4, weight: 3, compactLabel: "IV", label: "Utility", substage, width, start, anchor, substageScale, type},
]

const indexClamp = clamp(1, 4)
function find(stage) {
return stops[indexClamp(stage) - 1]
}
let totalWeight = 0
for (const stop of stops) {
totalWeight += stop.weight
}

const substageClamp = clamp(0, 1)
let cumulativeStart = 0
for (const stop of stops) {
let start = cumulativeStart
let width = stop.weight / totalWeight
stop.width = width
stop.start = start
stop.anchor = substage => start + substageClamp(substage) * width
cumulativeStart = start + width
}
const totalWidth = xWidth
const scaledMin = xMin

function wardleyAnchor(d) {
return totalWidth * find(d.stage || 1).anchor(d.substage) + scaledMin
}

let prevStop
for (const stop of stops) {
stop.x = wardleyAnchor(stop)
if (prevStop) {
prevStop.xNext = stop.x
prevStop.xWidth = prevStop.xNext - prevStop.x
}
prevStop = stop
}
prevStop.xNext = xMax
prevStop.xWidth = prevStop.xNext - prevStop.x

wardleyAnchor.invert = function invert(x) {
let stage = 0
let substage = 0.9
for (const stop of stops) {
stage = stop.stage
if (x >= stop.x && x < stop.xNext) {
substage = Math.min(0.9, Math.round(10 * (x - stop.x) / stop.xWidth) / 10)
break
}
}
return {stage, substage}
}
wardleyAnchor.stops = stops

return wardleyAnchor
}
Insert cell
onlyShowUnknownRegion = {
let onlyShowUnknownRegion = true
for (const node of nodes) {
if (!xAnchorIsUnknown(node)) {
onlyShowUnknownRegion = false
}
}
return onlyShowUnknownRegion
}
Insert cell
xAnchorUpdate = {
const xAnchorValue = onlyShowUnknownRegion ? noAnchor : wardleyAnchor
viewof xAnchor.value = xAnchorValue
return xAnchorValue
}
Insert cell
simulation = {
let simulation = d3.forceSimulation()

invalidation.then(() => simulation.stop());
return simulation
}
Insert cell
function refreshForces() {
simulation.force("x", simulation.force("x"))
simulation.force("y", simulation.force("y"))
simulation.force("nocollide", simulation.force("nocollide"))
simulation.force("nostackx", simulation.force("nostackx"))
simulation.force("nostacky", simulation.force("nostacky"))
}
Insert cell
dragset = new Set()
Insert cell
viewof layout = new View()
Insert cell
viewof dragStage = new View(null)
Insert cell
viewof dragSubstage = new View(null)
Insert cell
viewof meridians = new View([])
Insert cell
drag = (invertX, invertY) => {
function dragged(d) {
const xAnchor$ = viewof xAnchor.value
const yAnchor$ = viewof yAnchor.value
if (xAnchor$ && yAnchor$) {
if (invertX) {
const fdata = xAnchor$.invert(invertX(d3.event.x))
d.fdata = fdata
viewof dragStage.value = fdata.stage
viewof dragSubstage.value = fdata.substage
const fx = xAnchor$(fdata)

if (fx != null) {
d.fx = fx
}
}

if (invertY) {
const fy = invertY(d3.event.y)
if (fy != null) {
d.fy = fy
}
}
}
}

function dragstarted(d) {
if (!d3.event.active) {
simulation.alphaTarget(dragAlphaTarget).restart();
d.dragging = true
dragset.add(d)
}
}
function dragended(d) {
if (d.fdata != null) {
d.stage = d.fdata.stage
d.substage = d.fdata.substage
}
delete d.fdata;
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
dragset.delete(d)
viewof dragStage.value = null
viewof dragSubstage.value = null
simulation.alphaTarget(0);
refreshForces();
}
}

return d3.drag()
.on("start", d => dragstarted(d))
.on("drag", d => dragged(d))
.on("end", d => dragended(d));
}
Insert cell
viewof positions = new View([])
Insert cell
positions
Insert cell
forces = {
const yStrength = 0.3
const xStrength = 0.3

simulation
.force("y", forceY().y(yAnchor).posStrength(2 * yStrength).negStrength(yStrength))

if (onlyShowUnknownRegion) {
simulation
.force("link", d3.forceLink().strength(0.1))
.force("x", d3.forceX().x((xExtent[0] + xExtent[1]) / 2).strength(.02))
.force("nostackx", forceManyBodyX().strength(-1).distanceMax(5))
.force("nostacky", forceManyBodyY().strength(-1).distanceMax(5))
.force("nocollide", forceManyBodySkew().skewVX(0.4).strength(-2.5).distanceMax(20))
} else {
simulation
.force("link", null)
.force("x", d3.forceX().x(xAnchor).strength(xStrength))
.force("nostackx", forceManyBodyX().strength(-0.05).distanceMax(20))
.force("nocollide", forceManyBodySkew().skewVX(0.1).strength(-2.5).distanceMax(20))
}
}
Insert cell
seedrandom = require('https://bundle.run/seedrandom@3.0.1')
Insert cell
forceHelpers = {
// NB: Use a fixed seed to create reproducible layouts in the face of jitter
const random = seedrandom.alea('someseed')
return {
x(d) {
return d.x
},
y(d) {
return d.y
},
jiggle() {
return (random() - 0.5) * 1e-6;
},
constant(x) {
return function() {
return x;
};
},
}
}
Insert cell
forceY = {
const {constant} = forceHelpers
return function forceY(y) {
var posStrength = constant(0.1),
negStrength = constant(0.1),
nodes,
posStrengths,
negStrengths,
yz;

if (typeof y !== "function") y = constant(y == null ? 0 : +y);

function force(alpha) {
for (var i = 0, n = nodes.length, node; i < n; ++i) {
node = nodes[i], node.vy += (yz[i] > node.y ? posStrengths[i] : negStrengths[i]) * (yz[i] - node.y) * alpha;
}
}

function initialize() {
if (!nodes) return;
var i, n = nodes.length;
posStrengths = new Array(n);
negStrengths = new Array(n);
yz = new Array(n);
for (i = 0; i < n; ++i) {
posStrengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +posStrength(nodes[i], i, nodes);
negStrengths[i] = isNaN(yz[i] = +y(nodes[i], i, nodes)) ? 0 : +negStrength(nodes[i], i, nodes);
}
}

force.initialize = function(_) {
nodes = _;
initialize();
};

force.posStrength = function(_) {
return arguments.length ? (posStrength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : posStrength;
};

force.negStrength = function(_) {
return arguments.length ? (negStrength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : negStrength;
};

force.y = function(_) {
return arguments.length ? (y = typeof _ === "function" ? _ : constant(+_), initialize(), force) : y;
};

return force;
}
}
Insert cell
function forceManyBodyX() {
return forceManyBodySkew().skewVY(0).skewDistanceY(0)
}
Insert cell
function forceManyBodyY() {
return forceManyBodySkew().skewVX(0).skewDistanceX(0)
}
Insert cell
forceManyBodySkew = {
const {x, y, jiggle, constant} = forceHelpers
const quadtree = d3.quadtree

function forceManyBody() {
var nodes,
node,
alpha,
skewVX = 1,
skewVY = 1,
skewDistanceX = 1,
skewDistanceY = 1,
strength = constant(-30),
strengths,
distanceMin2 = 1,
distanceMax2 = Infinity,
theta2 = 0.81;

function force(_) {
var i, n = nodes.length, tree = quadtree(nodes, x, y).visitAfter(accumulate);
for (alpha = _, i = 0; i < n; ++i) node = nodes[i], tree.visit(apply);
}

function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
strengths = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], strengths[node.index] = +strength(node, i, nodes);
}

function accumulate(quad) {
var strength = 0, q, c, weight = 0, x, y, i;

// For internal nodes, accumulate forces from child quadrants.
if (quad.length) {
for (x = y = i = 0; i < 4; ++i) {
if ((q = quad[i]) && (c = Math.abs(q.value))) {
strength += q.value, weight += c, x += c * q.x, y += c * q.y;
}
}
quad.x = x / weight;
quad.y = y / weight;
}

// For leaf nodes, accumulate forces from coincident quadrants.
else {
q = quad;
q.x = q.data.x;
q.y = q.data.y;
do strength += strengths[q.data.index];
while (q = q.next);
}

quad.value = strength;
}

function apply(quad, x1, _, x2) {
if (!quad.value) return true;

var x = quad.x - node.x,
y = quad.y - node.y,
w = x2 - x1,
l = skewDistanceX * x * x + skewDistanceY * y * y;

// Apply the Barnes-Hut approximation if possible.
// Limit forces for very close nodes; randomize direction if coincident.
if (w * w / theta2 < l) {
if (l < distanceMax2) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
node.vx += skewVX * x * quad.value * alpha / l;
node.vy += skewVY * y * quad.value * alpha / l;
}
return true;
}

// Otherwise, process points directly.
else if (quad.length || l >= distanceMax2) return;

// Limit forces for very close nodes; randomize direction if coincident.
if (quad.data !== node || quad.next) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
if (l < distanceMin2) l = Math.sqrt(distanceMin2 * l);
}

do if (quad.data !== node) {
w = strengths[quad.data.index] * alpha / l;
node.vx += skewVX * x * w;
node.vy += skewVY * y * w;
} while (quad = quad.next);
}

force.initialize = function(_) {
nodes = _;
initialize();
};

force.strength = function(_) {
return arguments.length ? (strength = typeof _ === "function" ? _ : constant(+_), initialize(), force) : strength;
};

force.skewVX = function(_) {
return arguments.length ? (skewVX = +_, force) : skewVX;
};

force.skewVY = function(_) {
return arguments.length ? (skewVY = +_, force) : skewVY;
};

force.skewDistanceX = function(_) {
return arguments.length ? (skewDistanceX = +_, force) : skewDistanceX;
};

force.skewDistanceY = function(_) {
return arguments.length ? (skewDistanceY = +_, force) : skewDistanceY;
};

force.distanceMin = function(_) {
return arguments.length ? (distanceMin2 = _ * _, force) : Math.sqrt(distanceMin2);
};

force.distanceMax = function(_) {
return arguments.length ? (distanceMax2 = _ * _, force) : Math.sqrt(distanceMax2);
};

force.theta = function(_) {
return arguments.length ? (theta2 = _ * _, force) : Math.sqrt(theta2);
};

return force;
}
return forceManyBody
}
Insert cell
forceCollideSkew = {
const {x, y, jiggle, constant} = forceHelpers
const quadtree = d3.quadtree

function forceCollideSkew(radius) {
var nodes,
radii,
skewVX = 1,
skewVY = 1,
strength = 1,
iterations = 1;

if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius);

function force() {
var i, n = nodes.length,
tree,
node,
xi,
yi,
ri,
ri2;

for (var k = 0; k < iterations; ++k) {
tree = quadtree(nodes, x, y).visitAfter(prepare);
for (i = 0; i < n; ++i) {
node = nodes[i];
ri = radii[node.index], ri2 = ri * ri;
xi = node.x + node.vx;
yi = node.y + node.vy;
tree.visit(apply);
}
}

function apply(quad, x0, y0, x1, y1) {
var data = quad.data, rj = quad.r, r = ri + rj;
if (data) {
if (data.index > node.index) {
var x = xi - data.x - data.vx,
y = yi - data.y - data.vy,
l = x * x + y * y;
if (l < r * r) {
if (x === 0) x = jiggle(), l += x * x;
if (y === 0) y = jiggle(), l += y * y;
l = (r - (l = Math.sqrt(l))) / l * strength;
node.vx += skewVX * (x *= l) * (r = (rj *= rj) / (ri2 + rj));
node.vy += skewVY * (y *= l) * r;
data.vx -= skewVX * x * (r = 1 - r);
data.vy -= skewVY * y * r;
}
}
return;
}
return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r;
}
}

function prepare(quad) {
if (quad.data) return quad.r = radii[quad.data.index];
for (var i = quad.r = 0; i < 4; ++i) {
if (quad[i] && quad[i].r > quad.r) {
quad.r = quad[i].r;
}
}
}

function initialize() {
if (!nodes) return;
var i, n = nodes.length, node;
radii = new Array(n);
for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes);
}

force.initialize = function(_) {
nodes = _;
initialize();
};

force.iterations = function(_) {
return arguments.length ? (iterations = +_, force) : iterations;
};

force.strength = function(_) {
return arguments.length ? (strength = +_, force) : strength;
};

force.skewVX = function(_) {
return arguments.length ? (skewVX = +_, force) : skewVX;
};

force.skewVY = function(_) {
return arguments.length ? (skewVY = +_, force) : skewVY;
};

force.radius = function(_) {
return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius;
};

return force;
}

return forceCollideSkew
}
Insert cell
updateMeridians = {
const dragMeridians = []
if (dragStage != null && dragStage !== 0) {
for (let substage = 0; substage < 10; substage++) {
const o = {
stage: dragStage,
substage: substage / 10,
type: substage / 10 == dragSubstage ? "dragsubstage" : "dragstage",
}
o.x = xAnchor(o)
dragMeridians.push(o)
}
}

viewof meridians.value = [
...xAnchor.stops,
...dragMeridians,
]
}
Insert cell
updatePositions = {
forces;
const xAnchor = xAnchorUpdate;
const yAnchor = yAnchorUpdate;

// Set initial positions and decide whether or not layout is unchanged
let noop = nodes.length === simulation.nodes().length
for (const d of nodes) {
const x = xAnchor(d) || (xWidth / 2)
const y = yAnchor(d) || yMax
// 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)) {
// console.log("initial x", x, d)
d.x = x
}
d.yAnchor = y
if (d.y == null || isNaN(d.y)) {
d.y = y
// console.log("initial y", y, d)
}
}

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

// Whether or not this was a noop, we should announce a single update to node positions.
viewof positions.value = nodes
viewof layout.value = ({
positions: viewof positions.value,
xAnchor,
yAnchor,
meridians: viewof meridians.value,
})
return
} else {
// console.log("almost returned early but simulation.alpha() >= simulation.alphaMin()", {alpha: simulation.alpha(), alphaMin: simulation.alphaMin(), alphaTarget: simulation.alphaTarget()})
}
}

refreshForces()

simulation
.nodes(nodes)
;

simulation.on("tick", () => {
viewof positions.value = nodes
})
simulation.on("end", () => {
viewof layout.value = ({
positions: viewof positions.value,
xAnchor,
yAnchor,
meridians: viewof meridians.value,
})
})
invalidation.then(() => {
simulation.on("tick", null)
})
simulation
.alpha(initialAlphaTarget)
.alphaTarget(0)
.restart()

viewof positions.value = nodes
}
Insert cell
d3 = require("d3@5")
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

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