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

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