Published
Edited
Oct 7, 2021
20 forks
69 stars
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);

svg.append("g")
.call(xAxis);

svg.append("g")
.call(yAxis);

const train = svg.append("g")
.attr("stroke-width", 1.5)
.selectAll("g")
.data(data)
.join("g");

train.append("path")
.attr("fill", "none")
.attr("stroke", d => colors[d.type])
.attr("d", d => line(d.stops));

train.append("g")
.attr("stroke", "white")
.attr("fill", d => colors[d.type])
.selectAll("circle")
.data(d => d.stops)
.join("circle")
.attr("transform", d => `translate(${x(d.station.distance)},${y(d.time)})`)
.attr("r", 2.5);

svg.append("g")
.call(tooltip);

return svg.node();
}
Insert cell
colors = ({
N: "rgb(34, 34, 34)",
L: "rgb(183, 116, 9)",
B: "rgb(192, 62, 29)",
W: "currentColor",
S: "currentColor"
})
Insert cell
line = d3.line()
.x(d => x(d.station.distance))
.y(d => y(d.time))
Insert cell
x = d3.scaleLinear()
.domain(d3.extent(stations, d => d.distance))
.range([margin.left + 10, width - margin.right])
Insert cell
y = d3.scaleUtc()
.domain([parseTime("4:30AM"), parseTime("1:30AM")])
.range([margin.top, height - margin.bottom])
Insert cell
xAxis = g => g
.style("font", "10px sans-serif")
.selectAll("g")
.data(stations)
.join("g")
.attr("transform", d => `translate(${x(d.distance)},0)`)
.call(g => g.append("line")
.attr("y1", margin.top - 6)
.attr("y2", margin.top)
.attr("stroke", "currentColor"))
.call(g => g.append("line")
.attr("y1", height - margin.bottom + 6)
.attr("y2", height - margin.bottom)
.attr("stroke", "currentColor"))
.call(g => g.append("line")
.attr("y1", margin.top)
.attr("y2", height - margin.bottom)
.attr("stroke-opacity", 0.2)
.attr("stroke-dasharray", "1.5,2")
.attr("stroke", "currentColor"))
.call(g => g.append("text")
.attr("transform", `translate(0,${margin.top}) rotate(-90)`)
.attr("x", 12)
.attr("dy", "0.35em")
.text(d => d.name))
.call(g => g.append("text")
.attr("text-anchor", "end")
.attr("transform", `translate(0,${height - margin.top}) rotate(-90)`)
.attr("x", -12)
.attr("dy", "0.35em")
.text(d => d.name))
Insert cell
yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y)
.ticks(d3.utcHour)
.tickFormat(d3.utcFormat("%-I %p")))
.call(g => g.select(".domain").remove())
.call(g => g.selectAll(".tick line").clone().lower()
.attr("stroke-opacity", 0.2)
.attr("x2", width))
Insert cell
tooltip = g => {
const formatTime = d3.utcFormat("%-I:%M %p");

const tooltip = g.append("g")
.style("font", "10px sans-serif");

const path = tooltip.append("path")
.attr("fill", "white");

const text = tooltip.append("text");

const line1 = text.append("tspan")
.attr("x", 0)
.attr("y", 0)
.style("font-weight", "bold");

const line2 = text.append("tspan")
.attr("x", 0)
.attr("y", "1.1em");

const line3 = text.append("tspan")
.attr("x", 0)
.attr("y", "2.2em");

g.append("g")
.attr("fill", "none")
.attr("pointer-events", "all")
.selectAll("path")
.data(stops)
.join("path")
.attr("d", (d, i) => voronoi.renderCell(i))
.on("mouseout", () => tooltip.style("display", "none"))
.on("mouseover", (event, d) => {
tooltip.style("display", null);
line1.text(`${d.train.number}${d.train.direction}`);
line2.text(d.stop.station.name);
line3.text(formatTime(d.stop.time));
path.attr("stroke", colors[d.train.type]);
const box = text.node().getBBox();
path.attr("d", `
M${box.x - 10},${box.y - 10}
H${box.width / 2 - 5}l5,-5l5,5
H${box.width + 10}
v${box.height + 20}
h-${box.width + 20}
z
`);
tooltip.attr("transform", `translate(${
x(d.stop.station.distance) - box.width / 2},${
y(d.stop.time) + 28
})`);
});
}
Insert cell
voronoi = d3.Delaunay
.from(stops, d => x(d.stop.station.distance), d => y(d.stop.time))
.voronoi([0, 0, width, height])
Insert cell
data = alldata.filter(d => days(d) && direction(d))
Insert cell
stations = alldata.stations
Insert cell
stops = d3.merge(data.map(d => d.stops.map(s => ({train: d, stop: s}))))
Insert cell
alldata = {
const data = d3.tsvParse(await FileAttachment("schedule.tsv").text());

// Extract the stations from the "stop|*" columns.
const stations = data.columns
.filter(key => /^stop\|/.test(key))
.map(key => {
const [, name, distance, zone] = key.split("|");
return {key, name, distance: +distance, zone: +zone};
});

return Object.assign(
data.map(d => ({
number: d.number,
type: d.type,
direction: d.direction,
stops: stations
.map(station => ({station, time: parseTime(d[station.key])}))
.filter(station => station.time !== null)
})),
{stations}
);
}
Insert cell
parseTime = {
const parseTime = d3.utcParse("%I:%M%p");
return string => {
const date = parseTime(string);
if (date !== null && date.getUTCHours() < 3) date.setUTCDate(date.getUTCDate() + 1);
return date;
};
}
Insert cell
height = 2400
Insert cell
margin = ({top: 120, right: 30, bottom: 120, left: 50})
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