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));
 |