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

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