281 lines
8.1 KiB
JavaScript
281 lines
8.1 KiB
JavaScript
/// <reference path="./d3.js" />
|
|
|
|
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]) => `<a href="/?data=${k}">${v.title}</a>`
|
|
);
|
|
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));
|