Public
Edited
Nov 17, 2023
Insert cell
Insert cell
{
const margin = {
top: 25,
bottom: 25,
left: 50,
right: 25,
}

const width = 750
const height = 400

const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)

const g = svg.append("g")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top} )`)

const xScale = d3.scaleUtc(
[new Date("2023-09-01"), new Date("2023-09-30")],
[0, width]
)

// const maxY = Math.max(
// d3.max([...kjAverage, ...agAverage, ...spAverage, klSentralRidership].flatMap(d => d.ridership)),
// d3.max([...kjAverage, ...agAverage, ...spAverage].flatMap(d => d.max))
// )

const maxY = d3.max([
...kjAverage,
...agAverage,
...spAverage,
// klSentralRidership
].flatMap(d => d.ridership))

const yScale = d3.scaleLinear(
[0, maxY],
[height, 0]
).nice()

const path = d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))

const bisect = d3.bisector(function(d) { return d.date }).center

function mouseover(event) {
circleScrubber.style("opacity", 1)
yLineScrubber.style("opacity", 1)
}
function mousemove(event) {
const offset = event.clientX - event.offsetX
const mouseX = event.clientX - margin.left - offset
const x0 = xScale.invert(mouseX);
const i = bisect(kjAverage, x0, 1);
const selectedData = kjAverage[i]

// circle scrubber
circleScrubber
.transition()
.ease(d3.easeLinear)
.delay(0)
.duration(25)
.attr("cx", xScale(selectedData.date))
.attr("cy", yScale(selectedData.ridership))

// line scrubber
yLineScrubber
.attr("y1", yScale(0))
.attr("y2", yScale(maxY))
.transition()
.ease(d3.easeLinear)
.delay(0)
.duration(50)
.attr("x1", xScale(selectedData.date))
.attr("x2", xScale(selectedData.date))
}
function mouseout(event) {
circleScrubber.style("opacity", 0)
yLineScrubber.style("opacity", 0)
}

// circle scrubber
const circleScrubber = g.append("g")
.append("circle")
.attr("fill", "none")
.attr("stroke", "black")
.attr("r", 5)
.style("opacity", 0)

// x-axis line scrubber
const xScrubber = g.append("g")
.append("line")
.attr("height", height)
.attr("width", 1)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-dasharray", 4)
.style("opacity", 0)

// y-axis scrubber
const yLineScrubber = g.append("g")
.append("line")
.attr("height", height)
.attr("width", 1)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-dasharray", 4)
.style("opacity", 0)
// x-axis
g.append("g")
.attr("transform", `translate(0, ${height})`)
.call(d3.axisBottom(xScale)
.ticks(30)
.tickFormat(d3.utcFormat('%d'))
)

// y-axis
g.append("g")
.call(d3.axisLeft(yScale).tickFormat(d3.format(".0s")))

// KJ daily average
g.append("g")
.append("path")
.datum(kjAverage)
.attr("d", d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.ridership))
)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)

// KJ all lines
lineGroups.get("kelana-jaya").forEach((station) => {
const data = []
for (let i = 0; i < 30; i++) {
data.push({ x: station.dates[i], y: station.ridership[i] })
}
g.append("g")
.append("path")
.datum(data)
.attr("d", d3.line()
.x(d => xScale(d.x))
.y(d => yScale(d.y))
)
.attr("fill", "none")
.attr("stroke", "steelblue")
.attr("stroke-width", 1.5)
})

// KJ min max
// g.append("g")
// .append("path")
// .datum(kjAverage)
// .attr("d", d3.area()
// .x(d => xScale(d.date))
// .y0(d => yScale(d.min))
// .y1(d => yScale(d.max))
// )
// .attr("fill", "steelblue")
// .attr("fill-opacity", 0.15)

// AG daily average
g.append("g")
.append("path")
.datum(agAverage)
.attr("d", d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.ridership))
)
.attr("fill", "none")
.attr("stroke", "orange")
.attr("stroke-width", 1.5)

// SP daily average
g.append("g")
.append("path")
.datum(spAverage)
.attr("d", d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.ridership))
)
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-width", 1.5)

// KL Sentral ridership
// g.append("g")
// .append("path")
// .datum(klSentralRidership)
// .attr("d", d3.line()
// .x(d => xScale(d.date))
// .y(d => yScale(d.ridership))
// )
// .attr("fill", "none")
// .attr("stroke", "red")
// .attr("stroke-width", 1.5)

// invisble area to track mouse movement
g.append("g")
.append('rect')
.attr('width', width)
.attr('height', height)
.style("fill", "none")
.style("pointer-events", "all")
.on('mouseover', mouseover)
.on('mousemove', mousemove)
.on('mouseout', mouseout);
return svg.node()
}
Insert cell
lineGroups = d3.group(ridershipByDate, d => d.line)
Insert cell
kjAverage = getDailyAvgRidershipByLine("kelana-jaya")
Insert cell
agAverage = getDailyAvgRidershipByLine("ampang")
Insert cell
spAverage = getDailyAvgRidershipByLine("sri-petaling")
Insert cell
klSentralRidership = formatRidershipByStation("KL Sentral")
Insert cell
ridershipByDate = FileAttachment("20231112_2043_ridership-by-date.json").json()
Insert cell
function getDailyAvgRidershipByLine(line) {
const arr = []
for (let i = 0; i < 30; i++) {
const date = new Date(`2023-09-${(i + 1).toString().padStart(2, "0")}`)
const dailyRidership = lineGroups.get(line).flatMap(station => station.ridership[i])
const average = d3.mean(dailyRidership)
const min = d3.min(dailyRidership)
const max = d3.max(dailyRidership)
arr.push({ date, line, ridership: average, min, max})
}

return arr
}
Insert cell
function formatRidershipByStation(station) {
const stationData = ridershipByDate.find(d => d.station === station)

const arr = []

for (let i = 0; i < 30; i++) {
arr.push({
date: new Date(stationData.dates[i]),
ridership: stationData.ridership[i],
line: stationData.line
})
}

return arr
}
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