Published
Edited
Nov 5, 2021
3 forks
Importers
51 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
paris = FileAttachment("paris.json").json()
Insert cell
Insert cell
Insert cell
function coordinates(node) {
return [node.x, node.y];
}
Insert cell
coordinates(nodes[0]) // should be [-95.5357799, 33.6787973] for Paris, TX
Insert cell
Insert cell
projection0 = d3.geoMercator().fitSize([width, 500], {
type: "MultiPoint",
coordinates: nodes.map((d) => [d.x, d.y])
})
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0, 0, width, 500]);
const path = d3.geoPath(projection0).pointRadius(1.5);

svg
.append("path")
.attr(
"d",
path({ type: "MultiPoint", coordinates: nodes.map(coordinates) })
);

return svg.node();
}
Insert cell
Insert cell
Insert cell
nodesIndex = d3.index(nodes, (d) => d.osmid)
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0, 0, width, 500]);
const path = d3.geoPath(projection0);

svg
.append("path")
.attr(
"d",
path({
type: "MultiLineString",
coordinates: edges.map(({ u, v }) => [
coordinates(nodesIndex.get(u)),
coordinates(nodesIndex.get(v))
])
})
)
.attr("stroke", "currentColor");

return svg.node();
}
Insert cell
Insert cell
Insert cell
projection = d3
.geoMercator()
.translate([width / 2, height / 2])
.center(d3.geoCentroid({ type: "MultiPoint", coordinates: nodes.map((d) => [d.x, d.y]) }))
.scale(projection0.scale() * 1.8)
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
const path = d3.geoPath(projection);
const color = d3.scaleOrdinal(["residential", "other"], ["orange", "grey"]);

for (const [type, subset] of d3.group(edges, (d) =>
d.highway.match("residential") ? "residential" : "other"
)) {
svg
.append("path")
.attr(
"d",
path({
type: "MultiLineString",
coordinates: subset.map(({ u, v }) => [
coordinates(nodesIndex.get(u)),
coordinates(nodesIndex.get(v))
])
})
)
.attr("stroke", color(type));
}

return svg.node();
}
Insert cell
Insert cell
import { shortest_tree } from "@fil/dijkstra"
Insert cell
graph = {
const graph = {
sources: [],
targets: [],
costs: []
};
const nodesIndex = new Map(nodes.map(({ osmid }, i) => [osmid, i]));
for (const { u, v, length, highway } of edges) {
graph.sources.push(nodesIndex.get(u));
graph.targets.push(nodesIndex.get(v));
graph.costs.push(length * (highway.match("residential") ? 5 : 1));
}
return graph;
}
Insert cell
origins = (replay,
Array.from({ length: nOrigins }, () => (Math.random() * nodes.length) | 0))
Insert cell
tree = shortest_tree({ graph, origins })
Insert cell
Insert cell
isochrone = {
const color = d3
.scaleSequential((t) => d3.interpolateInferno(1 - t))
.domain([0, d3.quantile(tree.cost, 0.9)]);

const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

const path = d3.geoPath(projection);

path.pointRadius(2);
svg
.append("g")
.selectAll("path")
.data(d3.range(nodes.length))
.join("path")
.attr("d", (i) =>
path({
type: "Point",
coordinates: coordinates(nodes[i])
})
)
.attr("fill", (i) => color(tree.cost[i]));

return svg.node();
}
Insert cell
Insert cell
winner = {
const color = d3.scaleOrdinal(d3.schemeCategory10);

const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

const path = d3.geoPath(projection);

path.pointRadius(2);
svg
.append("g")
.selectAll("path")
.data(d3.range(nodes.length))
.join("path")
.attr("d", (i) =>
path({
type: "Point",
coordinates: coordinates(nodes[i])
})
)
.attr("fill", (i) => color(tree.origin[i]));

return svg.node();
}
Insert cell
Insert cell
{
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("background", "#333");

const path = d3.geoPath(projection);
const color = d3.scaleOrdinal(d3.shuffle(d3.schemeSet1));

// the whole graph as dimmed paths
// residential roads are dashed
const gGraph = svg
.append("g")
.style("stroke", "white")
.style("stroke-width", 0.125);

for (const [type, subset] of d3.group(
edges,
(d) => !!d.highway.match("residential")
)) {
gGraph
.append("path")
.datum({
type: "MultiLineString",
coordinates: subset.map(({ u, v }) => [
coordinates(nodesIndex.get(u)),
coordinates(nodesIndex.get(v))
])
})
.attr("d", path)
.style("stroke-dasharray", type ? [1, 3] : []);
}

svg
.append("g")
.attr("opacity", 0.7)
.selectAll("path")
.data(d3.range(nodes.length))
.join("path")
.attr("d", (i) =>
tree.predecessor[i] === -1
? null
: path({
type: "LineString",
coordinates: [
coordinates(nodes[tree.predecessor[i]]),
coordinates(nodes[i])
]
})
)
.attr("stroke", (i) => color(tree.origin[i]));

const gNodes = svg.append("g");
const sorted = d3.sort(d3.range(nodes.length), (i) => tree.cost[i]);

path.pointRadius(2);
gNodes
.selectAll("path")
.data(d3.range(nodes.length))
.join("path")
.attr("d", (i) =>
path({
type: "Point",
coordinates: coordinates(nodes[i])
})
)
.attr("fill", (i) => color(tree.origin[i]));

return svg.node();
}
Insert cell
Insert cell
// map() is called at the beginning of this notebook
// TODO: this would probably be faster if we binned nodes by cost, and animated the bins;
// Paris, TX is small so it's fast enough on a recent computer, but for Houston it crawls…
map = () => {
const delay0 = 300;

const svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("background", "#333");

const path = d3.geoPath(projection);
const color = d3.scaleOrdinal(d3.shuffle(d3.schemeSet1));

// the whole graph as dimmed paths
// residential roads are dashed
const gGraph = svg
.append("g")
.style("stroke", "white")
.style("stroke-width", 0.125);

for (const [type, subset] of d3.group(
edges,
(d) => !!d.highway.match("residential")
)) {
gGraph
.append("path")
.datum({
type: "MultiLineString",
coordinates: subset.map(({ u, v }) => [
coordinates(nodesIndex.get(u)),
coordinates(nodesIndex.get(v))
])
})
.attr("d", path)
.style("stroke-dasharray", type ? [1, 3] : []);
}

svg
.append("g")
.attr("opacity", 0.7)
.selectAll("path")
.data(d3.range(nodes.length))
.join("path")
.attr("d", (i) =>
tree.predecessor[i] === -1
? null
: path({
type: "LineString",
coordinates: [
coordinates(nodes[tree.predecessor[i]]),
coordinates(nodes[i])
]
})
)
.attr("stroke", (i) => color(tree.origin[i]))
// Animation starts here
.attr("opacity", 0)
.attr("stroke-dasharray", [0, 1000])
.transition()
.delay((i) => delay0 + tree.cost[i])
.attr("opacity", 1)
.attr("stroke-dasharray", (i) =>
tree.predecessor[i] === -1
? []
: [(tree.cost[i] - tree.cost[tree.predecessor[i]]) / 10, 1000]
);

const gNodes = svg.append("g");
const sorted = d3.sort(d3.range(nodes.length), (i) => tree.cost[i]);

path.pointRadius(2);
gNodes
.selectAll("path")
.data(d3.range(nodes.length))
.join("path")
.attr("d", (i) =>
path({
type: "Point",
coordinates: coordinates(nodes[i])
})
)
.attr("fill", (i) => color(tree.origin[i]))
// Animation of nodes
.attr("opacity", 0)
.transition()
.delay((i) => delay0 + tree.cost[i])
.duration(1000)
.attr("opacity", 1);

path.pointRadius(4);
svg
.append("g")
.selectAll("path")
.data(origins)
.join("path")
.attr("d", (i) =>
path({
type: "Point",
coordinates: coordinates(nodes[i])
})
)
.attr("stroke", (i) => color(tree.origin[i]))
.attr("fill", "white");

svg
.append("g")
.attr("transform", "translate(10, 20)")
.append("text")
.text("Paris, TX Voronoi")
.style("fill", "white")
.style("font-family", "sans-serif")
.style("font-size", "18px");

return svg.node();
}
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