Public
Edited
Jun 21, 2024
Insert cell
Insert cell
Insert cell
Insert cell
{
const svg = d3.create("svg").attr("width", width).attr("height", height);

const stationsToShow = ["서울", "수원", "대전", "동대구", "울산", "부산"];

const stationGrid = svg
.selectAll(".stationGrid")
.data(stationsKorean)
.enter()
.append("line")
.attr("y1", margin.top)
.attr("y2", height - margin.bottom)
.attr("x1", (d, i) => xScale(i))
.attr("x2", (d, i) => xScale(i))
.style("stroke", (d) => (stationsToShow.includes(d) ? "#bbb" : "#efefef"));

const stationLabels = svg
.selectAll(".stationLabels")
.data(stationsKorean)
.enter()
.append("text")
.text((d) => d)
.attr("x", (d, i) => xScale(i))
.attr("y", margin.top - 10)
.attr("class", "stationLabels")
.style("display", (d) => (stationsToShow.includes(d) ? "block" : "none"))
.style("fill", (d) => (d == "서울" || d == "부산" ? "#222" : "#222"));

const hoursGrid = svg
.selectAll(".hoursGrid")
.data(hours)
.enter()
.append("line")
.attr("y1", (d) => yScale(timeParse(d)))
.attr("y2", (d) => yScale(timeParse(d)))
.attr("x1", margin.left)
.attr("x2", margin.left - 5)
.style("stroke", (d) =>
getTime(d) == "12:00" || getTime(d) == "24:00" ? "#777" : "#777"
)
.style("stroke-dasharray", (d) =>
getTime(d) == "12:00" || getTime(d) == "24:00" ? "3, 3" : "3, 0"
);

const hoursLabel = svg
.selectAll(".hoursLabel")
.data(hours)
.enter()
.append("text")
.text((d) => timeFormat(timeParse(d)))
.attr("x", margin.left - 8)
.attr("y", (d) => yScale(timeParse(d)) + 4)
.attr("class", "hoursLabel")
.style("fill", (d) =>
getTime(d) == "12:00" || getTime(d) == "24:00" ? "#222" : "#222"
)
.style("font-weight", (d) =>
getTime(d) == "12:00" || getTime(d) == "24:00" ? "600" : "400"
);

const currentTimeLine = svg
.append("line")
.attr("x1", xScale(0))
.attr("x2", xScale(stations.length - 1))
.attr("y1", yScale(timeParse("2024-05-01, 04:30")))
.attr("y2", yScale(timeParse("2024-05-01, 04:30")))
.style("stroke", "#000")
.style("stroke-width", 0.75);

let isClicked = false;
const rectForInteraction = svg
.append("rect")
// .attr("x", xScale(0))
// .attr("y", yScale(timeParse("2024-05-01, 03:00")))
// .attr("width", xScale(stations.length - 1) - xScale(0))
// .attr(
// "height",
// yScale(timeParse("2024-05-02, 03:00")) -
// yScale(timeParse("2024-05-01, 03:00"))
// )
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.style("fill", "rgba(0,0,0,0)")
.on("mousemove", function (event, d) {
// mouse의 page 내 좌표 -> event.pageX, event.pageY
// currentTimeLine.attr("y1", event.pageY).attr("y2", event.pageY);

// mouse의 svg 내 좌표 -> [xPos, yPos] = d3.pointer(event)
const [xPos, yPos] = d3.pointer(event);
if (xPos > xScale(0) && xPos < xScale(stations.length - 1)) {
if (
yPos > yScale(timeParse("2024-05-01, 03:00")) &&
yPos < yScale(timeParse("2024-05-02, 03:00"))
) {
currentTimeLine.attr("y1", yPos).attr("y2", yPos);

const currentTime = timeFormat(yScale.invert(yPos));
currentTimeLabel.attr("y", yPos + 4).text(currentTime);
currentTimeLabelBG.attr("y", yPos + 4).text(currentTime);
}
}
})
.on("click", function () {
isClicked = false;
seoulToBusan.map((train) =>
train.style("opacity", 0.9).style("stroke-width", 1.2)
);
busanToSeoul.map((train) =>
train.style("opacity", 0.9).style("stroke-width", 1.2)
);
});

//////// Line function
const lineFunc = d3
.line()
.x((d) => xScale(d.stationIndex))
.y((d) => yScale(d.time))
.curve(d3.curveLinear);
// .curve(d3.curveCatmullRom);
// .curve(d3.curveCardinal);

//////// Seoul to Busan
let seoulToBusan = [];
data.map((d, i) => {
const stationData = stations
.map((s, i) => {
const obj = {};
obj.time = d[s] ? d[s] : null;
obj.station = s;
obj.stationIndex = stations.indexOf(s);

return obj;
})
.filter((d) => d.time != null);

seoulToBusan[i] = svg
.append("path")
.datum(stationData)
.attr("d", lineFunc)
.attr("fill", "none")
.attr("stroke", lineColor)
.style("opacity", 0.9)
.attr("stroke-width", 1.2)
.style("display", direction != "Busan to Seoul" ? "block" : "none")
.on("mouseover", function () {
if (!isClicked) {
d3.select(this).style("stroke-width", 2.8);
}
})
.on("mouseout", function () {
if (!isClicked) {
d3.select(this).style("stroke-width", 1.2);
}
})
.on("click", function () {
isClicked = true;
seoulToBusan.map((train) =>
train.style("opacity", 0.2).style("stroke-width", 1.2)
);

busanToSeoul.map((train) =>
train.style("opacity", 0.2).style("stroke-width", 1.2)
);

d3.select(this).style("opacity", 0.9).style("stroke-width", 2.8);
});
});

//////// Busan to Seoul
let busanToSeoul = [];
dataBusan.map((d, i) => {
const stationData = stations
.map((s, i) => {
const obj = {};
obj.time = d[s] ? d[s] : null;
obj.station = s;
obj.stationIndex = stations.indexOf(s);

return obj;
})
.filter((d) => d.time != null);

busanToSeoul[i] = svg
.append("path")
.datum(stationData)
.attr("d", lineFunc)
.attr("fill", "none")
.attr("stroke", "#E87200")
.style("opacity", 0.9)
.attr("stroke-width", 1.2)
.style("display", direction != "Seoul to Busan" ? "block" : "none")
.on("mouseover", function () {
if (!isClicked) {
d3.select(this).style("stroke-width", 2.8);
}
})
.on("mouseout", function () {
if (!isClicked) {
d3.select(this).style("stroke-width", 1.2);
}
})
.on("click", function () {
isClicked = true;

seoulToBusan.map((train) =>
train.style("opacity", 0.2).style("stroke-width", 1.2)
);

busanToSeoul.map((train) =>
train.style("opacity", 0.2).style("stroke-width", 1.2)
);

d3.select(this).style("opacity", 0.9).style("stroke-width", 2.8);
});
});

const currentTimeLabelBG = svg
.append("text")
.attr("x", xScale(stations.length - 1) + 5)
.attr("y", yScale(timeParse("2024-05-01, 04:30")) + 4)
.text(timeFormat(timeParse("2024-05-01, 04:30")))
.attr("class", "timeLabel")
.style("font-weight", 800)
.style("fill", "white")
.style("stroke", "white")
.style("stroke-width", 3);

const currentTimeLabel = svg
.append("text")
.attr("x", xScale(stations.length - 1) + 5)
.attr("y", yScale(timeParse("2024-05-01, 04:30")) + 4)
.text(timeFormat(timeParse("2024-05-01, 04:30")))
.attr("class", "timeLabel");

return svg.node();
}
Insert cell
lineColor = "#ea7300" // purple '#a37dd9', blue "#3d67ac", orange '#ea7300'
Insert cell
thisWidth = width > 500 ? 500 : width
Insert cell
getTime = (date) => {
return date.split(", ")[1];
}
Insert cell
height = 1400
Insert cell
"2024-05-01, 4:00".split(", ")[1]
Insert cell
Insert cell
xScale = d3
.scaleLinear()
.domain([0, stations.length - 1])
.range([margin.left, thisWidth - margin.right])
Insert cell
yScale = d3
.scaleTime()
.domain([timeParse("2024-05-01, 03:00"), timeParse("2024-05-02, 03:00")])
.range([margin.top, height - margin.bottom])
Insert cell
margin = ({ top: 60, right: 60, left: 90, bottom: 30 })
Insert cell
Insert cell
Insert cell
Insert cell
// timeFormat = d3.timeFormat("%H:%M")
timeFormat = d3.timeFormat("%I:%M %p")
Insert cell
// timeParse = d3.utcParse("%Y-%m-%d, %H:%M")
timeParse = d3.timeParse("%Y-%m-%d, %H:%M")
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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