/// const DATA_SOURCES = { movies: { title: "Movie Sales", description: "Top 100 Highest Grossing Movies Grouped By Genre", source: "movie-sales-data.json", }, videogames: { title: "Video Game Sales", description: "Top 100 Most Sold Video Games Grouped by Platform", source: "video-game-sales-data.json", }, kickstarter: { title: "Kickstarter Pledges", description: "Top 100 Most Pledged Kickstarter Campaigns Grouped By Category", source: "kickstarter-funding-data.json", }, }; const ASPECT_RATIO = window.innerWidth / window.innerHeight; const CANVAS_WIDTH = window.innerWidth ?? 1000; const CANVAS_HEIGHT = window.innerHeight ?? 500; const CHART_WIDTH = CANVAS_WIDTH * 0.8; const CHART_HEIGHT = CANVAS_HEIGHT * 0.8; const BASE_SIZE = CHART_WIDTH > CHART_HEIGHT ? CHART_HEIGHT * 0.03 : CHART_WIDTH * 0.03; const CHART_X_OFFSET = (CANVAS_WIDTH - CHART_WIDTH) / 2; const CHART_Y_OFFSET = BASE_SIZE * 4; const STROKE_WIDTH = BASE_SIZE * 0.03; const STROKE_COLOR = "black"; const FONT_FAMILY = "quicksand, Sans-serif"; const binaryTree = (parent, x0, x1, y0, y1) => { // source: https://github.com/d3/d3-hierarchy/blob/main/src/treemap/binary.js const sums = [0]; for (let i = 0; i < parent.children.length; i++) { sums.push(sums[i] + Number(parent.children[i].value)); } const partition = (first, last, x0, x1, y0, y1) => { const value = sums[last + 1] - sums[first]; if (first >= last || value <= 0) { for (let i = first; i <= last; i++) { parent.children[i].x0 = x0; parent.children[i].x1 = x1; parent.children[i].y0 = y0; parent.children[i].y1 = y1; } return; } const offsetValue = sums[first]; const midValue = (sums[last + 1] + offsetValue) / 2; let i = first, j = last, mid; while (i < j) { mid = Math.trunc((i + j) / 2); if (sums[mid + 1] > midValue) { j = mid; } else { i = mid + 1; } } if (mid === undefined) { throw new Error("mid cannot be undefined"); } const leftValue = sums[mid + 1] - offsetValue; const rightValue = value - leftValue; if (x1 - x0 > y1 - y0) { const xMid = (x0 * rightValue + x1 * leftValue) / value; partition(first, mid, x0, xMid, y0, y1); partition(mid + 1, last, xMid, x1, y0, y1); } else { const yMid = (y0 * rightValue + y1 * leftValue) / value; partition(first, mid, x0, x1, y0, yMid); partition(mid + 1, last, x0, x1, yMid, y1); } }; partition(0, parent.children.length - 1, x0, x1, y0, y1); }; const fetchData = async () => { const params = new URLSearchParams(window.location.search); const id = params.get("data"); let data_source = DATA_SOURCES.kickstarter; if (id && DATA_SOURCES[id]) { data_source = DATA_SOURCES[id]; } return { dataset: await (await fetch(data_source.source)).json(), metadata: data_source, }; }; // max is exclusive const randomInt = (min, max) => Math.trunc(Math.random() * (max - min) + min); fetchData() .then(({ dataset, metadata }) => { const menuHtml = Object.entries(DATA_SOURCES).map( ([k, v]) => `${v.title}` ); d3.select("body") .append("header") .style("top", BASE_SIZE * 0.33 + "px") .style("font-size", BASE_SIZE * 1.25 + "px") .append("p") .html(menuHtml.join(" | ")); console.log(metadata); console.log(dataset); for (let child of dataset.children) { // source: https://gist.github.com/bendc/76c48ce53299e6078a76 child.color = `hsl(${randomInt(0, 361)}, ${randomInt( 50, 101 )}%, ${randomInt(25, 56)}%)`; child.value = child.children.reduce( (sum, childObj) => sum + Number(childObj.value), 0 ); } console.log( window.innerWidth, window.innerHeight, CHART_X_OFFSET, CHART_WIDTH, CHART_Y_OFFSET, CHART_HEIGHT ); binaryTree(dataset, 0, CHART_WIDTH, 0, CHART_HEIGHT); const legendColumns = 4; const canvas = d3 .select("body") .append("svg") .attr("width", CANVAS_WIDTH) // Additional height based on number of legend items .attr( "height", CANVAS_HEIGHT + Math.trunc(dataset.children.length / legendColumns) * BASE_SIZE * 1.5 ); 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(metadata.title); canvas .append("text") .attr("id", "description") .attr("x", "50%") .attr("text-anchor", "middle") .attr("y", BASE_SIZE * 3) .attr("font-size", BASE_SIZE) .text(metadata.description); const mouseover = (e) => { const obj = JSON.parse(JSON.stringify(e.target.dataset)); tooltip .html(`${obj.name}, ${obj.category}, ${obj.value}`) .style("background-color", "#000000") .style("opacity", 1) .attr("data-value", obj.value); }; const mousemove = (e) => { tooltip.style("left", e.pageX + "px").style("top", e.pageY + "px"); }; const mouseleave = (d) => { tooltip.style("opacity", 0); }; const treeMap = canvas .append("g") .attr("id", "treemap") .attr("transform", `translate(${CHART_X_OFFSET}, ${CHART_Y_OFFSET})`); let i = 0; for (let child of dataset.children) { binaryTree(child, child.x0, child.x1, child.y0, child.y1); treeMap .selectAll(null) .data(child.children) .enter() .append("rect") .attr("class", "tile") .attr("data-name", (d) => d.name) .attr("data-category", (d) => d.category) .attr("data-value", (d) => d.value) .attr("width", (d) => d.x1 - d.x0) .attr("height", (d) => d.y1 - d.y0) .attr("x", (d) => d.x0) .attr("y", (d) => d.y0) .attr("fill", (d) => child.color) .style("stroke-width", STROKE_WIDTH) .style("stroke", "#ffffff") .on("mouseover", mouseover) .on("mousemove", mousemove) .on("mouseleave", mouseleave); i++; } 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-value", "") .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"); const legend = canvas .append("g") .attr("id", "legend") .attr( "transform", `translate(${CHART_X_OFFSET}, ${CHART_Y_OFFSET * 1.2 + CHART_HEIGHT})` ); legend .selectAll(null) .data(dataset.children) .enter() .append("rect") .attr("class", "legend-item") .attr("x", (d, i) => (i % legendColumns) * (CHART_WIDTH / legendColumns)) .attr("y", (d, i) => Math.trunc(i / legendColumns) * BASE_SIZE * 1.5) .attr("width", BASE_SIZE) .attr("height", BASE_SIZE) .style("fill", (d) => d.color); legend .selectAll(null) .data(dataset.children) .enter() .append("text") .attr( "x", (d, i) => (i % legendColumns) * (CHART_WIDTH / legendColumns) + BASE_SIZE * 1.5 ) .attr( "y", (d, i) => BASE_SIZE * 0.8 + Math.trunc(i / legendColumns) * BASE_SIZE * 1.5 ) .attr("font-size", BASE_SIZE) .text((d) => d.name); 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/treemap") .text(" Source Code & License"); }) .catch((e) => console.error("Error occurred!", e));