/// /// 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 STROKE_WIDTH = CHART_WIDTH / 1000; const STROKE_COLOR = "black"; const BASE_SIZE = CHART_Y_OFFSET * 0.3; const FONT_FAMILY = "quicksand, Sans-serif"; const PERCENT_HEXs = [ "#e5f5e0", "#c7e9c0", "#a1d99b", "#74c476", "#41ab5d", "#238b45", "#006d2c", ]; const fetchData = async () => { return { education: await (await fetch("./education.json")).json(), map: await (await fetch("./map.json")).json(), }; }; const featuresToPath = (features) => { const paths = []; let currentPath = []; let currentObj = {}; const extractPolygons = (arr) => { if (Array.isArray(arr[0][0])) { arr.forEach((a) => { extractPolygons(a); }); } else { const polygon = []; arr.forEach((a) => { polygon.push(`${a[0]},${a[1]}`); }); currentPath.push("M" + polygon.join("L") + "Z"); } }; features.forEach((feature) => { currentObj = { ...feature }; delete currentObj.geometry; extractPolygons(feature.geometry.coordinates); paths.push({ ...currentObj, path: currentPath.join("") }); currentPath = []; }); return paths; }; const featuresToPolygon = (features) => { const polygons = []; let currentObj = {}; const extractPolygons = (arr) => { if (Array.isArray(arr[0][0])) { arr.forEach((a) => { extractPolygons(a); }); } else { const polygon = []; arr.forEach((a) => { polygon.push(a[0], a[1]); }); polygons.push({ ...currentObj, polygon: [...polygon] }); } }; features.forEach((feature) => { currentObj = { ...feature }; delete currentObj.geometry; extractPolygons(feature.geometry.coordinates); }); return polygons; }; fetchData() .then(({ education, map }) => { const MAP_WIDTH = map.bbox[2]; const MAP_HEIGHT = map.bbox[3]; const scaleByWidth = CHART_HEIGHT / MAP_HEIGHT > CHART_WIDTH / MAP_WIDTH; const scale = scaleByWidth ? CHART_WIDTH / MAP_WIDTH : CHART_HEIGHT / MAP_HEIGHT; const edObj = {}; education.forEach((county) => { edObj[county.fips] = { ...county }; }); const states = topojson.feature(map, map.objects.states); const statesBoundaries = featuresToPolygon(states.features); const counties = topojson.feature(map, map.objects.counties); const countiesBoundaries = featuresToPath(counties.features); const MIN_PERCENT = 3; const MAX_PERCENT = 66; // 0.000000001 is to ensure that max percent do not yield percentBucketIndex as PERCENT_BUCKET_SIZE const PERCENT_BUCKET_SIZE = Number( ((MAX_PERCENT - MIN_PERCENT + 0.000000001) / PERCENT_HEXs.length).toFixed( 10 ) ); const getFill = (percent) => { const percentBucketIndex = Math.trunc( (Math.min(Math.max(percent, MIN_PERCENT), MAX_PERCENT) - MIN_PERCENT) / PERCENT_BUCKET_SIZE ); return PERCENT_HEXs[percentBucketIndex]; }; const canvas = d3 .select("body") .append("svg") .attr("width", CANVAS_WIDTH) .attr("height", CANVAS_HEIGHT); 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("United States Educational Attainment"); canvas .append("text") .attr("id", "description") .attr("x", "50%") .attr("text-anchor", "middle") .attr("y", BASE_SIZE * 3.33) .attr("font-size", BASE_SIZE) .text( " Percentage of adults age 25 and older with a bachelor's degree or higher (2010-2014) " ); const chart = canvas .append("g") .attr("id", "map") .attr( "transform", `translate(${CHART_X_OFFSET * 1.8}, ${ CHART_Y_OFFSET * 1.5 }) scale(${scale})` ); const mouseover = (e) => { const obj = JSON.parse(JSON.stringify(e.target.dataset)); tooltip .html( `${edObj[obj.fips].area_name}, ${edObj[obj.fips].state}: ${ obj.education }%` ) .style("background-color", "#000000") .style("opacity", 1) .attr("data-education", obj.education); }; const mousemove = (e) => { tooltip.style("left", e.pageX + "px").style("top", e.pageY + "px"); }; const mouseleave = (d) => { tooltip.style("opacity", 0); }; chart .selectAll(null) .data(countiesBoundaries) .enter() .append("path") .attr("class", "county") .attr("d", (d) => d.path) .attr("data-fips", (d) => d.id) .attr("data-education", (d) => edObj[d.id].bachelorsOrHigher) .attr("stroke", "none") .attr("fill", (d) => getFill(edObj[d.id].bachelorsOrHigher)) .on("mouseover", mouseover) .on("mousemove", mousemove) .on("mouseleave", mouseleave); chart .selectAll(null) .data(statesBoundaries) .enter() .append("polygon") .attr("class", "state") .attr("points", (d) => d.polygon) .attr("stroke", "white") .attr("fill", "none"); const legend = canvas .append("g") .attr("id", "legend") .attr( "transform", `translate(${CHART_X_OFFSET + CHART_WIDTH * 0.55}, ${ CHART_Y_OFFSET * 1.5 })` ); legend .selectAll("rect") .data(PERCENT_HEXs) .enter() .append("rect") .attr("height", BASE_SIZE / 2) .attr("width", BASE_SIZE * 2) .attr("x", (d, i) => BASE_SIZE * 2 * i) .attr("fill", (d) => d); const legendScale = d3 .scaleLinear() // percentage to proportions .domain([MIN_PERCENT / 100, MAX_PERCENT / 100]) .range([0, PERCENT_HEXs.length * BASE_SIZE * 2]); legend .append("g") .attr("id", "legend-scale-ticks") .style("font", BASE_SIZE * 0.8 + "px " + FONT_FAMILY) .call( d3 .axisBottom(legendScale) .tickValues( d3.range( // percentage to proportions MIN_PERCENT / 100, MAX_PERCENT / 100 + PERCENT_BUCKET_SIZE / 100, PERCENT_BUCKET_SIZE / 100 ) ) .tickFormat(d3.format(".0%")) ) .call((g) => g.select(".domain").remove()) .call((g) => g.selectAll("line").attr("y2", BASE_SIZE * 0.75)) .call((g) => g.selectAll("text").attr("y", BASE_SIZE)); 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-fips", "") .attr("data-education", "") .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("text-align", "center") .style("font-size", BASE_SIZE + "px"); 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/choropleth" ) .text(" Source Code & License"); }) .catch((e) => console.error("Error occurred!", e));