212 lines
6.4 KiB
JavaScript
212 lines
6.4 KiB
JavaScript
/// <reference path="./d3.js" />
|
|
|
|
const ASPECT_RATIO = window.innerWidth / window.innerHeight;
|
|
const CANVAS_WIDTH = window.innerWidth ?? 1000;
|
|
// *0.9 for footer
|
|
const CANVAS_HEIGHT =
|
|
(ASPECT_RATIO > 1
|
|
? window.innerHeight * 0.9
|
|
: CANVAS_WIDTH * ASPECT_RATIO * 0.9) ?? 500;
|
|
const CHART_WIDTH = CANVAS_WIDTH * 0.8;
|
|
const CHART_HEIGHT = CANVAS_HEIGHT * 0.8;
|
|
const CHART_X_OFFSET = (CANVAS_WIDTH - CHART_WIDTH) / 2;
|
|
const CHART_Y_OFFSET = (CANVAS_HEIGHT - CHART_HEIGHT) / 2;
|
|
const DOPING_COLOR = "rgb(225, 145, 48)";
|
|
const NO_DOPING_COLOR = "rgb(20, 176, 160)";
|
|
const STROKE_WIDTH = CHART_WIDTH / 1000;
|
|
const STROKE_COLOR = "black";
|
|
const BASE_SIZE = CHART_Y_OFFSET * 0.3;
|
|
const FONT_FAMILY = "quicksand, Sans-serif";
|
|
|
|
const fetchData = async () => {
|
|
return await (await fetch("./data.json")).json();
|
|
};
|
|
|
|
fetchData()
|
|
.then((dataset) => {
|
|
const FIRST_YEAR = d3.min(dataset, (d) => d.Year);
|
|
const LAST_YEAR = d3.max(dataset, (d) => d.Year);
|
|
const MIN_TIME = d3.min(dataset, (d) => d.Seconds);
|
|
const MAX_TIME = d3.max(dataset, (d) => d.Seconds);
|
|
const xAxisScale = d3
|
|
.scaleLinear()
|
|
.domain([FIRST_YEAR - 1, LAST_YEAR + 1])
|
|
.range([0, CHART_WIDTH]);
|
|
const yAxisScale = d3
|
|
.scaleLinear()
|
|
.domain([MAX_TIME + 20, MIN_TIME - 20])
|
|
.range([CHART_HEIGHT, 0]);
|
|
|
|
const canvas = d3
|
|
.select("body")
|
|
.append("svg")
|
|
.attr("width", CANVAS_WIDTH)
|
|
.attr("height", CANVAS_HEIGHT);
|
|
|
|
const mouseover = (e) => {
|
|
const obj = JSON.parse(e.target.dataset.obj);
|
|
tooltip
|
|
.html(
|
|
`${obj.Name} from ${obj.Nationality}<br/>Year: ${obj.Year} Time: ${
|
|
obj.Time
|
|
}${obj.Doping.length ? "<br />" + obj.Doping : ""}`
|
|
)
|
|
.style("background-color", obj.Doping ? DOPING_COLOR : NO_DOPING_COLOR)
|
|
.style("opacity", 1)
|
|
.attr("data-year", obj.Year);
|
|
};
|
|
|
|
const mousemove = (e) => {
|
|
tooltip.style("left", e.pageX + "px").style("top", e.pageY + "px");
|
|
};
|
|
|
|
const mouseleave = (d) => {
|
|
tooltip.style("opacity", 0);
|
|
};
|
|
|
|
canvas
|
|
.append("text")
|
|
.attr("id", "title")
|
|
.attr("x", "50%")
|
|
.attr("text-anchor", "middle")
|
|
.attr("y", BASE_SIZE * 1.7)
|
|
.attr("font-size", BASE_SIZE * 1.7)
|
|
.text("Doping in Professional Bicycle Racing");
|
|
|
|
canvas
|
|
.append("text")
|
|
.attr("id", "sub-title")
|
|
.attr("x", "50%")
|
|
.attr("text-anchor", "middle")
|
|
.attr("y", BASE_SIZE * 3)
|
|
.attr("font-size", BASE_SIZE)
|
|
.text("35 Fastest times up Alpe d'Huez");
|
|
|
|
canvas
|
|
.selectAll("circle")
|
|
.data(dataset)
|
|
.enter()
|
|
.append("circle")
|
|
.attr("class", "dot")
|
|
.attr("data-xvalue", (d) => d.Year)
|
|
.attr("data-yvalue", (d) => new Date(d.Seconds * 1000))
|
|
.attr("data-obj", (d) => JSON.stringify(d))
|
|
.attr("r", CHART_WIDTH / 200)
|
|
.attr("cx", (d) => CHART_X_OFFSET + xAxisScale(Number(d.Year)))
|
|
.attr("cy", (d) => CHART_Y_OFFSET + yAxisScale(d.Seconds))
|
|
.style("fill", (d) => (d.Doping.length ? DOPING_COLOR : NO_DOPING_COLOR))
|
|
.attr("stroke", STROKE_COLOR)
|
|
.attr("stroke-width", STROKE_WIDTH)
|
|
.on("mouseover", mouseover)
|
|
.on("mousemove", mousemove)
|
|
.on("mouseleave", mouseleave);
|
|
|
|
const legend = canvas
|
|
.append("g")
|
|
.attr("id", "legend")
|
|
.attr(
|
|
"transform",
|
|
`translate(${CHART_X_OFFSET + CHART_WIDTH * 0.95}, ${
|
|
CHART_Y_OFFSET + CHART_HEIGHT * 0.1
|
|
})`
|
|
);
|
|
legend
|
|
.append("rect")
|
|
.attr("height", BASE_SIZE)
|
|
.attr("width", BASE_SIZE)
|
|
.attr("fill", DOPING_COLOR);
|
|
legend
|
|
.append("rect")
|
|
.attr("height", BASE_SIZE)
|
|
.attr("width", BASE_SIZE)
|
|
.attr("y", BASE_SIZE * 1.7)
|
|
.attr("fill", NO_DOPING_COLOR);
|
|
legend
|
|
.append("text")
|
|
.attr("font-size", BASE_SIZE)
|
|
.attr("x", -BASE_SIZE * 0.33)
|
|
.attr("y", BASE_SIZE)
|
|
.attr("text-anchor", "end")
|
|
.text("Riders with doping allegations");
|
|
legend
|
|
.append("text")
|
|
.attr("font-size", BASE_SIZE)
|
|
.attr("x", -BASE_SIZE * 0.33)
|
|
.attr("y", BASE_SIZE + BASE_SIZE * 1.7)
|
|
.attr("text-anchor", "end")
|
|
.text("No doping allegations");
|
|
|
|
canvas
|
|
.append("g")
|
|
.attr("id", "x-axis")
|
|
.style("font", BASE_SIZE + "px " + FONT_FAMILY)
|
|
.attr(
|
|
"transform",
|
|
`translate(${CHART_X_OFFSET}, ${CHART_HEIGHT + CHART_Y_OFFSET})`
|
|
)
|
|
.call(d3.axisBottom(xAxisScale).tickFormat(d3.format("d")));
|
|
|
|
canvas
|
|
.append("g")
|
|
.attr("id", "y-axis")
|
|
.style("font", BASE_SIZE + "px " + FONT_FAMILY)
|
|
.attr("transform", `translate(${CHART_X_OFFSET}, ${CHART_Y_OFFSET})`)
|
|
.call(
|
|
d3
|
|
.axisLeft(yAxisScale)
|
|
.tickFormat(
|
|
(d) =>
|
|
Math.trunc(d / 60) + ":" + (d % 60).toString().padStart(2, "0")
|
|
)
|
|
);
|
|
|
|
canvas.selectAll("path").style("stroke-width", STROKE_WIDTH);
|
|
canvas.selectAll("line").style("stroke-width", STROKE_WIDTH);
|
|
|
|
let tooltip = d3
|
|
.select("body")
|
|
.append("div")
|
|
.attr("id", "tooltip")
|
|
.attr("class", "tooltip")
|
|
.attr("data-year", "")
|
|
.style("opacity", 0)
|
|
.style("position", "absolute")
|
|
.style("padding", `${CHART_WIDTH / 200}px ${CHART_WIDTH / 100}px`)
|
|
.style("margin", `0 ${CHART_WIDTH / 50}px`)
|
|
.style("border-radius", `${CHART_WIDTH / 100}px`)
|
|
.style("font-size", BASE_SIZE + "px");
|
|
|
|
canvas
|
|
.append("text")
|
|
.attr("id", "y-label")
|
|
.style("font-size", BASE_SIZE + "px")
|
|
.attr("text-anchor", "end")
|
|
.attr("y", CHART_X_OFFSET * 0.2)
|
|
.attr("x", -CHART_Y_OFFSET - CHART_HEIGHT / 2)
|
|
.attr("text-anchor", "middle")
|
|
.attr("transform", "rotate(-90)")
|
|
.text("Time in Minutes");
|
|
|
|
canvas
|
|
.append("text")
|
|
.attr("id", "x-label")
|
|
.attr("x", "50%")
|
|
.attr("y", CHART_HEIGHT + CHART_Y_OFFSET * 1.9)
|
|
.attr("text-anchor", "middle")
|
|
.style("font-size", BASE_SIZE + "px")
|
|
.text("Year");
|
|
|
|
d3.select("body")
|
|
.append("footer")
|
|
.style("bottom", BASE_SIZE * 0.33 + "px")
|
|
.style("font-size", BASE_SIZE * 0.8 + "px")
|
|
.append("p")
|
|
.append("a")
|
|
.attr(
|
|
"href",
|
|
"https://radii.dev/freecodecamp-data-visualization/scatterplot-graph"
|
|
)
|
|
.text("</> Source Code & License");
|
|
})
|
|
.catch((e) => console.error("Error occurred!", e));
|