/// 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}
Year: ${obj.Year} Time: ${ obj.Time }${obj.Doping.length ? "
" + 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));