Published
Edited
Apr 6, 2019
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
simulation = {
let simulation = d3.forceSimulation()
// .force("link", d3.forceLink().id(d => d.id).distance().strength(.0002))
.force("charge", d3.forceManyBody().strength(-100).distanceMax(50))
.force("gravity", d3.forceY().y(height).strength(.01))

invalidation.then(() => simulation.stop());
return simulation
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.select(DOM.svg(width, height))
.attr("font-family", "BlinkMacSystemFont")
.attr("font-size", 12)
.attr("font-weight", "normal")
.attr("shape-rendering", "geometricPrecision")
.style("user-select", "none")

svg.append("defs")
.call(defs => defs.append("linearGradient")
.attr("id", "bg")
.call(lg => lg.append("stop").attr("offset", "0%").attr("stop-color", "#dadada"))
.call(lg => lg.append("stop").attr("offset", "20%").attr("stop-color", "#fbfbfb"))
.call(lg => lg.append("stop").attr("offset", "80%").attr("stop-color", "#fbfbfb"))
.call(lg => lg.append("stop").attr("offset", "100%").attr("stop-color", "#dadada"))
)
.call(defs => defs.append("marker")
.attr("id", "arrowhead")
.attr("viewBox", "0 -5 10 11")
.attr("refX", 9)
.attr("refY", 0)
.attr("markerWidth", "1em")
.attr("markerHeight", "1em")
.attr("markerUnits", "userSpaceOnUse")
.attr("orient", "auto-start-reverse")
.append("path")
.attr("d", "M0,-5L11,0,L0,5")
)
;
svg.append("rect")
.attr("x", xAxisPadding)
.attr("width", width - 2*xAxisPadding)
.attr("y", height * 0.08)
.attr("height", height * 0.84)
.attr("fill", "url(#bg)")
svg.append("g")
.attr("class", "yaxis")

svg.append("g")
.attr("class", "xaxis")

svg.append("g")
.attr("class", "meridians")

const linkGroup = svg.append("g")
.attr("stroke", "#000")
.attr("stroke-width", 1.5)
.attr("stroke-opacity", 0.3);

const nodeGroup = svg.append("g")
.style("text-shadow", "rgba(255, 255, 255, 1) 0 0 5px")
.attr("stroke", "#fff")
.attr("stroke-width", 2);

var voronoiEdges = svg.append("g")
.style("stroke", "red")
.style("fill", "none")
.attr("class", "voronoi-edges");

var voronoiCentroids = svg.append("g")
.attr("stroke", "orange")
.attr("class", "voronoi-centroids");

svg.append("g")
.append("text")
.attr("class", "xaxis-title");

svg.append("g")
.append("text")
.attr("class", "yaxis-title");
svg.on("click", function() {
console.log("click")
});
let setSelection = (values) => {
svg.node().value = values
svg.node().dispatchEvent(new CustomEvent("input"))
}
function updateData(data, xScale) {
linkGroup
.selectAll("line")
.data(data.links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value))
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y)

const node = nodeGroup
.selectAll("g")
.data(data.nodes)
.join(
enter => enter.append("g")
.call(drag(simulation, xScale))
.call(enter => enter.append('circle'))
.call(enter => enter.append('text')),
update => update,
exit => exit.remove(),
)
;

node
.attr("transform", d => translate(d.x, d.y))
.style("cursor", d => d.props.phase[0] === undefined ? "arrow" : "ew-resize")
.on("click", function() {
data.nodes.forEach(d => (d.selected = false))
var selected = d3.select(this).data()
selected.forEach(d => (d.selected = true))
setSelection(selected)
});

;

node.select("circle")
.attr("r", 7)
.attr("fill", "white")
.attr("stroke", color)
.attr("stroke-width", d => d.selected ? 4 : 2)
.append("title")
.text(d => d.id);

node.select("text")
.attr("dy", 15)
.attr("y", 0)
.attr("font-weight", 500)
.attr("stroke", "none")
.text(d => d.id)
.each(function(d) {
if (!d.polygon) {
d3.select(this).call(orient.botton)
return
}
const {x, y} = d, [cx, cy] = d3.polygonCentroid(d.polygon);
const angle = Math.round(Math.atan2(cy - y, cx - x) / Math.PI * 2);
// console.log("angle", angle, d)
d3.select(this).call(
angle === 0 ? orient.right
: angle === -1 ? orient.top
: angle === 1 ? orient.bottom
: orient.left);
})
;

const meridians = xScale.stops()
let shownMeridians =
(meridians.length === 1 && meridians[0] === undefined) ? []
: meridians.map(d => ({
x: xScale(d),
value: d,
stroke: "#aaa",
strokeDasharray: "1 2",
}))

data.nodes.forEach(d => {
if (!d.dragging) {
return
}

let phase = d.dragging
console.log("phase", phase)
d3.range(0, 1, 0.1).forEach(
i => {
let x = xScale([phase, i])
shownMeridians.push(
{
x,
stroke: x == d.fx ? "#000" : "#ccc",
strokeDasharray: x == d.fx ? "1" : "1 2",
}
)
}
)
})


// console.log("shownMeridians", shownMeridians, meridians)
svg.select("g.meridians")
.selectAll("line")
.data(shownMeridians)
.join("line")
.attr("stroke", d => d.stroke)
.attr("stroke-dasharray", d => d.strokeDasharray)
.attr("x1", d => d.x)
.attr("x2", d => d.x)
.attr("y1", 2*yAxisPadding)
.attr("y2", height - yAxisPadding)
;

if (debugVoronoi) {
voronoiEdges.selectAll("path")
.data(data.nodes)
.join("path")
.attr("d", d => `M${d.polygon.join("L")}Z`)
;
voronoiCentroids.selectAll("path")
.data(data.nodes)
.join("path")
.attr("d", d => `M${d3.polygonCentroid(d.polygon)}L${d.x},${d.y}`);
}
}

function updateAll(data, xScale, yScale) {
svg.select("text.yaxis-title")
.attr("transform", translate(xAxisPadding, height/2 - yAxisPadding) + " rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-size", 16)
.attr("font-weight", "bold")
.attr("alignment-baseline", "baseline")
.attr("dy", "-0.5em")
.text(yScale.title())
;

svg.select("text.xaxis-title")
.attr("transform", translate(width - xAxisPadding, height - 2*yAxisPadding))
.attr("text-anchor", "end")
.attr("font-size", 16)
.attr("font-weight", "bold")
.attr("alignment-baseline", "hanging")
.attr("dy", "1.5em")
.text(xScale.title())
;

let tickValues = xScale.stops()
if (tickValues.length === 1 && tickValues[0][0] === undefined) {
tickValues = []
}

svg.select("g.xaxis")
.attr("transform", translate(0, yScale(d3.max(yScale.domain()))))
.call(
d3.axisBottom(xScale)
.tickValues(tickValues)
.tickSize(0)
.tickFormat(d => {
switch (d[0]) {
case undefined:
return "unknown"
default:
return d[0]
}
})
)
.call(g => g.select("path")
.style("dominant-baseline", "central")
.attr("stroke-width", d => xScale.title() ? 2 : 0)
.attr("marker-end", d => xScale.title() ? "url(#arrowhead)" : undefined)
)
.call(g => g.selectAll("text")
.attr("font-size", 12)
.attr("font-style", "italic")
.attr("dx", "0.5em")
.attr("y", "0.5em")
.style("text-transform", "capitalize")
.style("text-anchor", "start")
.style("user-select", "none")
)
;

svg.select("g.yaxis")
.call(
d3.axisLeft(yScale)
.tickValues(d3.extent(yScale.domain()))
.tickFormat(d => d === 0 ? "Visible" : "Invisible")
.tickSize(0)
)
.call(g => g.selectAll("text")
.attr("transform", "rotate(-90)")
.attr("dy", "-0.75em")
.attr("dx", d => d === 0 ? "-0.5em" : "0.5em")
.attr("font-size", 12)
// .attr("font-style", "italic")
.attr("text-anchor", d => {
// console.log("text-anchor", d)
return d === 0 ? "end" : "start"
})
)
.attr("transform", translate(xScale(xScale.stops()[0]), 0))
.call(g => g.select("path")
.style("dominant-baseline", "central")
.attr("stroke-width", 2)
.attr("marker-start", "url(#arrowhead)")
)
;
updateData(data, xScale)
}

return {svg, updateAll, updateData}
}
Insert cell
function scaleOrdinal() {
let scale = d3.scaleOrdinal()

scale.invert = function(y) {
let range = this.range()
let yInt = Math.floor(y)
let yFrac = y - yInt
yFrac = Math.round(yFrac * 10.) / 10.0
if (yFrac > 0.9) {
yFrac = 0.9
}
if (yInt < range[0]) {
yInt = range[0]
yFrac = 0
} else if (yInt > range[range.length - 1]) {
yInt = range[range.length - 1]
yFrac = 0.9
}
// console.log("invert ordinal yFrac", y, yFrac, yInt)
let v = [this.domain().find((d, i) => yInt == range[i]), yFrac]
// console.log("invert ordinal", y, v, range, this.domain())
return v
}
let oldCopy = scale.copy
scale.copy = function() {
let n = oldCopy.apply(this)
n.invert = this.invert
return n
}
return scale
}

Insert cell
function composeScales(f, g) {
let h = d => {
let v = g(f(d))
// console.log("compose", d, f(d), g(f(d)))
return v !== undefined ? v : g(f(undefined))
}
h.domain = () => f.domain()
h.range = () => g.range()
h.copy = () => {
console.log("copy")
return composeScales(f.copy(), g.copy())
}
if (f.invert !== undefined && g.invert !== undefined) {
h.invert = d => {
// console.log("invert", d, "g.invert", g.invert(d), "f.invert", f.invert(g.invert(d)))
return f.invert(g.invert(d))
}
} else {
console.log("f", f, ".invert", f.invert, f.domain(), f.range())
console.log("g", g, ".invert", g.invert, f.domain(), f.range())
}
return h
}
Insert cell
function chain(scale1, scale2) {
let range1 = scale1.range()
if (range1.length !== 2 && scale2.range().length === 2) {
range1 = d3.extent(range1)
if (range1[0] === range1[1]) {
range1[1] += 0.01
}
}
scale2.domain(range1)

return composeScales(scale1, scale2)
}
Insert cell
Insert cell
Insert cell
Insert cell
xDomain = {
let totalRawWeights = d3.sum(regionMaxWidths)
let title = undefined
let hRegions = hDomain

if (regionMaxWidths[0] === totalRawWeights) {
hRegions = [undefined]
}
if (regionMaxWidths[0] !== totalRawWeights) {
title = "Evolution"
}

if (totalRawWeights > 0 && regionMaxWidths[0] === 0) {
hRegions = hRegions.slice(1)
}

// widthWeights = xDomain.values.map(d => {
// let m = widthsByDepthByPhase.get(d)
// return m === undefined ? 0 : d3.max(Array.from(m.values()))
// }).map(d => d3.max([0.5, d])).map(d => 1)

return {
title: title,
stops: hRegions,
weights: new Array(hRegions.length).fill(1),
}
}
Insert cell
xScale = {
function newXScale() {
let innerWidth = width - 2*xAxisPadding
let totalWeight = d3.sum(xDomain.weights)
let scaledWeights = xDomain.weights.map(d => d / totalWeight)
let rollingWeights = scaledWeights.reduce((acc, v, i) => { acc.push(acc[i]+v); return acc }, [0])

let evoScale = scaleOrdinal()
.domain(xDomain.stops);
evoScale.range(d3.range(0, evoScale.domain().length));
// console.log("evoScale", evoScale.domain(), evoScale.range(), evoScale.invert)

let evoWidths = scaleOrdinal()
.domain(xDomain.stops)
.range(scaledWeights.map(d => d * innerWidth))
console.log("evoWidths", evoWidths.domain(), evoWidths.range())

let innerScale = evoScale
if (evoScale.range().length !== 1) {
innerScale = chain(
evoScale,
d3.scaleLinear()
.range(rollingWeights))
}
let chained = chain(
innerScale,
d3.scaleLinear()
.range([xAxisPadding, width - xAxisPadding]))

let f = d => {
return chained(d[0]) + evoWidths(d[0]) * d[1]
}
f.invert = d => chained.invert(d)
f.range = () => chained.range()
f.stops = () => xDomain.stops.map(d => [d, 0])
f.copy = newXScale
f.width = d => evoWidths(d)
f.title = d => xDomain.title
f.round = () => undefined
return f
}

return newXScale()
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class NodeResolver {
constructor() {
this.nodesById = new Map()
this.backup = new Map()
}
flip() {
this.backup = this.nodesById
this.nodesById = new Map()
}
expunge() {
let newNodes = []
this.nodesById.forEach(node => node.x === undefined && newNodes.push(node))
this.backup.forEach(node => {
let newNode = newNodes.pop()
if (newNode) {
newNode.x = node.x
newNode.y = node.y
newNode.vx = node.vx
newNode.vy = node.vy
// newNode.index = node.index
console.log("recovered", newNode, newNode.x)
}
})
this.backup = new Map()
}
resolve(id) {
let node = this.nodesById.get(id)
if (node === undefined) {
node = this.backup.get(id)
if (node !== undefined) {
this.nodesById.set(id, node)
this.backup.delete(id)
node.parents = new Set()
node.children = new Set()
node.props = {}
node.propIntervals = {}
// console.log("reusing", node, node.x, node.dragx)
} else {
node = {
id: id,
group: 1,
props: {},
propIntervals: {},
parents: new Set(),
children: new Set(),
}
this.nodesById.set(id, node)
console.log("new", node, node.x)
}
node.props.phase = [undefined, 0.5]
}
return node
}
nodeDependsOn(id, depId) {
let node = this.resolve(id)
let depNode = this.resolve(depId)
node.children.add(depNode)
depNode.parents.add(node)
}
nodeHasProperty(id, property, value, interval) {
// console.log("nodeHasProperty", id, mod, property, value, interval)
let node = this.resolve(id)
node.props[property] = value
node.propIntervals[property] = interval
}
nodes() {
return Array.from(this.nodesById.values()).sort(d => d.index)
}

links() {
let links = []
this.nodesById.forEach(
node => node.children.forEach(
ch => links.push({source: node, target: ch})))
return links
}
data() {
return {
links: this.links(),
nodes: this.nodes(),
}
}
}
Insert cell
class EpochViewer {
constructor() {
this.nr = new NodeResolver()
}
setEpoch(epoch) {
let validPhases = ["genesis", "custom", "product", "utility"]
this.nr.flip()
console.log("epoch", epoch)
epoch.stanzas.forEach(({name, deps, props}) => {
// console.log("each", name, deps)
this.nr.resolve(name)
deps && deps.forEach(dep => this.nr.nodeDependsOn(name, dep.name))
props && props.forEach(prop => {
let value = prop.value
value[0] = validPhases.find(p => p.startsWith(value[0])) || value[0]
this.nr.nodeHasProperty(name, prop.name, value, prop.source)
})
})
this.nr.expunge()
}
data() {
let data = this.nr.data()
let cache = {}
function nodeDepth(node) {
if (cache[node.id] === undefined) {
let depth = 1+Array.from(node.parents).reduce(
(acc, child) => Math.max(acc, nodeDepth(child)),
0)
cache[node.id] = depth
node.depth = depth
}
return cache[node.id]
}

data.nodes.forEach(nodeDepth)

return data
}
}
Insert cell
ev = new EpochViewer()
Insert cell
data = {
let match = grammar.match(source)
let epochs = semantics(match).ast()
epochs[0] && ev.setEpoch(epochs[0])
return ev.data()
}
Insert cell
function refreshPolygons(nodes) {
try {
let voronoisPolygons = d3delaunay.Delaunay.from(nodes.map(d => [d.x, d.y]))
.voronoi([xAxisPadding, 0, width + 1, height + 1]);
nodes.forEach((d, i) => {
d.polygon = voronoisPolygons.cellPolygon(i)
})
} catch (e) {
console.log("nodes", nodes)
data.nodes.forEach(n => n.polygon = [])
console.error(e)
return
}
}

Insert cell
function xAnchor(d, i) {
let phase = d.props.phase
let v = xScale(phase)
return v
}
Insert cell
function yAnchor(d) {
return yScale(d.depth)
}

Insert cell
Insert cell
update = {
initNodePositions(data.nodes)
refreshPolygons(data.nodes)

simulation
.nodes(data.nodes)
// .force('link').links(data.links)
;

simulation
.force("y", d3.forceY().y(yAnchor).strength(yStrength))
.force("x", d3.forceX()
.x(xAnchor)
.strength(d => d.props.phase[0] === undefined ? 0.04 : 1))
;
simulation.on("tick", () => {
refreshPolygons(data.nodes)
chart.updateData(data, xScale)
})
chart.updateAll(data, xScale, yScale)

simulation
.alpha(1.0)
.alphaTarget(0)
.restart()
}
Insert cell
drag = (simulation, xScale) => {
function dragstarted(d) {
if (d.props.phase[0] === undefined) {
return
}

if (!d3.event.active) simulation.alphaTarget(0.3).restart();
let p = xScale.invert(d3.event.x)
d.dragging = p[0]
d.fx = xScale(p)
// d.fy = d.y;
}
function dragged(d) {
if (d.props.phase[0] === undefined) {
return
}

// d.fx = d3.event.x;
// d.fy = d3.event.y;
let p = xScale.invert(d3.event.x)
d.dragging = p[0]
d.fx = xScale(p)
}
function dragended(d) {
if (d.props.phase[0] === undefined) {
return
}

if (!d3.event.active) {
simulation.alphaTarget(0);
}
console.log("dragended", d.x, d.fx)
if (d.propIntervals.phase) {
updateSource("phase", d.propIntervals.phase, xScale.invert(d.fx))
d.props.phase = xScale.invert(d.fx)
}
d.x = d.fx;
d.dragx = d.x
simulation.force("x", simulation.force("x"))
delete d.fx;
delete d.dragging;
// d.fy = null;
}
return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
Insert cell
Insert cell
Insert cell
function translate(x, y) {
if (isNaN(x) || isNaN(y)) {
console.log("translate", x, y)
return ""
}
return `translate(${x},${y})`
}
Insert cell
Insert cell
Insert cell
Insert cell
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