Published
Edited
Dec 9, 2020
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
Insert cell
Insert cell
timing = 1000
Insert cell
{
let i = 0;
while (true) {
await Promises.delay(timing);
move();
}
}
Insert cell
Insert cell
svg = {
//svg variables
stations;

//create SVG artboard
let svg = d3
.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr('id', 'mainArea');

let bg = svg
.append('rect')
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr('fill', '#002');

svg
.selectAll('.links')
.data(crossLinks)
.enter()
.append('line')
.attr('x1', d => d[0].x)
.attr('y1', d => d[0].y)
.attr('x2', d => d[1].x)
.attr('y2', d => d[1].y)
.attr('stroke', 'white')
.attr('class', 'links');

//draw city circles
svg
.selectAll('.stations')
.data(stations)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr('r', d => 5)
.attr('fill', d => d3.interpolateViridis(scooterScale(d.scooters)))
.attr('opacity', 1)
.attr('class', 'stations');

//draw city circles
svg
.selectAll('.stationLabels')
.data(stations)
.enter()
.append("text")
.attr("x", d => d.x + 10)
.attr("y", d => d.y)
.text(d => d.name)
.attr('fill', 'white')
.attr('font-family', 'courier')
.attr('opacity', 1)
.attr('text-anchor', 'start')
.attr('alignment-baseline', 'middle')
.attr('class', 'stationLabels');
//show visualization in Observable
return svg.node();
}
Insert cell
Insert cell
move = function() {
let fromStation = pick(stations);
let toStation = pick(stations);

//ensure start and end are not the same station
while (fromStation == toStation) {
fromStation = pick(stations);
toStation = pick(stations);
}

let distance = getDistance(
fromStation.x,
fromStation.y,
toStation.x,
toStation.y
);

let color = "white";

//animate movement
d3.select('#mainArea')
.append('circle')
.attr('cx', fromStation.x)
.attr('cy', fromStation.y)
.attr('r', 0)
.attr('fill', color)
.transition()
.duration(timing * .25)
.attr('r', 10)
.transition()
.duration(timing * .5)
.attr('cx', toStation.x)
.attr('cy', toStation.y)
.transition()
.duration(timing * .25)
.attr('r', 0)
.remove();

updateStations(fromStation, toStation);

//recolor stations
d3.selectAll('.stations')
.transition()
.duration(1000)
.attr('fill', d => d3.interpolateViridis(scooterScale(d.scooters)));

return "moved 1 scooter from " + fromStation.name + " to " + toStation.name;
}
Insert cell
function updateStations(from, to) {
from.scooters--;
to.scooters++;

from.popularity += .005;
to.popularity += .005;

if (from.type == "equity") {
equity.equity -= .5;
} else {
}

if (to.type == "nonequity") {
equity.equity++;
} else {
equity.equity--;
equity.nonequity += .5;
}
}
Insert cell
{
while (true) {
let equityStations = stations.filter(d => d.type == "equity");
yield d3.mean(equityStations, d => d.popularity);
}
}
Insert cell
Insert cell
scooterScale = d3
.scaleLinear()
.domain([0, scooterCount])
.range([0, 1])
Insert cell
Insert cell
//https://observablehq.com/@nextlevelshit/rejection-sampling-in-javascript

weight = function(arr) {
return [].concat(
...arr.map(obj => Array(Math.ceil(obj.popularity * 100)).fill(obj))
);
}
Insert cell
pick = function(arr) {
let weighted = weight(arr);
return weighted[Math.floor(Math.random() * weighted.length)];
}
Insert cell
getDistance = function(x1, y1, x2, y2) {
let xs = x2 - x1,
ys = y2 - y1;

xs *= xs;
ys *= ys;

return Math.sqrt(xs + ys);
}
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