mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
feat(charts): stack bar chart basic
This commit is contained in:
parent
be5589daeb
commit
d30171b054
2 changed files with 118 additions and 66 deletions
|
|
@ -1,3 +1,6 @@
|
||||||
|
import {rollup} from "../../../utils/functionUtils.js";
|
||||||
|
import {stack} from "https://cdn.skypack.dev/d3-shape@3";
|
||||||
|
|
||||||
const entities = ["states", "cultures", "religions"];
|
const entities = ["states", "cultures", "religions"];
|
||||||
const quantitatives = ["total_population", "urban_population", "rural_population", "area", "cells"];
|
const quantitatives = ["total_population", "urban_population", "rural_population", "area", "cells"];
|
||||||
const groupings = ["cultures", "states", "religions"];
|
const groupings = ["cultures", "states", "religions"];
|
||||||
|
|
@ -16,6 +19,11 @@ const quantizationMap = {
|
||||||
cells: () => 1
|
cells: () => 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sortingMap = {
|
||||||
|
value: (a, b) => b.value - a.value,
|
||||||
|
name: (a, b) => a.name.localeCompare(b.name)
|
||||||
|
};
|
||||||
|
|
||||||
appendStyleSheet();
|
appendStyleSheet();
|
||||||
|
|
||||||
insertHtml();
|
insertHtml();
|
||||||
|
|
@ -78,26 +86,40 @@ function renderChart() {
|
||||||
const plotBy = $plotBySelect.value;
|
const plotBy = $plotBySelect.value;
|
||||||
const groupBy = $groupBySelect.value;
|
const groupBy = $groupBySelect.value;
|
||||||
|
|
||||||
const {array: entityArray, getName: getEntityName, cellsData: entityCells} = dataMap[entity];
|
const filterWater = true;
|
||||||
|
const filterZeroes = true;
|
||||||
|
const sorting = sortingMap["value"];
|
||||||
|
|
||||||
|
const {getName: getEntityName, cellsData: entityCells} = dataMap[entity];
|
||||||
const {getName: getGroupName, cellsData: groupCells} = dataMap[groupBy];
|
const {getName: getGroupName, cellsData: groupCells} = dataMap[groupBy];
|
||||||
const quantize = quantizationMap[plotBy];
|
const quantize = quantizationMap[plotBy];
|
||||||
|
|
||||||
const chartData = entityArray
|
const dataCollection = {};
|
||||||
.filter(element => !element.removed)
|
for (const cellId of pack.cells.i) {
|
||||||
.map(({i}) => {
|
if (filterWater && isWater(cellId)) continue;
|
||||||
const cells = pack.cells.i.filter(cellId => entityCells[cellId] === i);
|
const entityId = entityCells[cellId];
|
||||||
const name = getEntityName(i);
|
const groupId = groupCells[cellId];
|
||||||
|
const value = quantize(cellId);
|
||||||
|
|
||||||
return Array.from(cells).map(cellId => {
|
if (!dataCollection[entityId]) dataCollection[entityId] = {[groupId]: value};
|
||||||
const group = getGroupName(groupCells[cellId]);
|
else if (!dataCollection[entityId][groupId]) dataCollection[entityId][groupId] = value;
|
||||||
const value = quantize(cellId);
|
else dataCollection[entityId][groupId] += value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData = Object.entries(dataCollection)
|
||||||
|
.map(([entityId, groupData]) => {
|
||||||
|
const name = getEntityName(entityId);
|
||||||
|
return Object.entries(groupData).map(([groupId, rawValue]) => {
|
||||||
|
const group = getGroupName(groupId);
|
||||||
|
const value = rn(rawValue);
|
||||||
return {name, group, value};
|
return {name, group, value};
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
console.log(chartData);
|
const chartDataFiltered = filterZeroes ? chartData.filter(({value}) => value > 0) : chartData;
|
||||||
const chart = plot(chartData, {});
|
|
||||||
|
const chart = plot(chartDataFiltered, {sorting});
|
||||||
byId("chartsOverview__svgContainer").appendChild(chart);
|
byId("chartsOverview__svgContainer").appendChild(chart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -115,54 +137,58 @@ function updateSelectorOptions() {
|
||||||
function plot(
|
function plot(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
title, // given d in data, returns the title text
|
|
||||||
marginTop = 30, // top margin, in pixels
|
marginTop = 30, // top margin, in pixels
|
||||||
marginRight = 0, // right margin, in pixels
|
marginRight = 0, // right margin, in pixels
|
||||||
marginBottom = 40, // bottom margin, in pixels
|
marginBottom = 40, // bottom margin, in pixels
|
||||||
marginLeft = 100, // left margin, in pixels
|
marginLeft = 100, // left margin, in pixels
|
||||||
width = 2400, // outer width, in pixels
|
width = 800, // outer width, in pixels
|
||||||
height = 400, // outer height, in pixels
|
|
||||||
xRange = [marginLeft, width - marginRight], // [xmin, xmax]
|
xRange = [marginLeft, width - marginRight], // [xmin, xmax]
|
||||||
xPadding = 0.1, // amount of x-range to reserve to separate groups
|
yPadding = 0.2,
|
||||||
yType = d3.scaleLinear, // type of y-scale
|
xFormat,
|
||||||
yRange = [height - marginBottom, marginTop], // [ymin, ymax]
|
xLabel = "Population (millions) →",
|
||||||
zPadding = 0.05, // amount of x-range to reserve to separate bars
|
sorting
|
||||||
yFormat, // a format specifier string for the y-axis
|
|
||||||
yLabel, // a label for the y-axis
|
|
||||||
colors = d3.schemeCategory10 // array of colors
|
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
const X = data.map(d => d.name);
|
const X = data.map(d => d.value);
|
||||||
const Y = data.map(d => d.value);
|
const Y = data.map(d => d.name);
|
||||||
const Z = data.map(d => d.group);
|
const Z = data.map(d => d.group);
|
||||||
|
|
||||||
const xDomain = new Set(X);
|
const yDomain = new Set(Y); // get from parent, already sorted
|
||||||
const yDomain = [0, d3.max(Y)];
|
|
||||||
const zDomain = new Set(Z);
|
const zDomain = new Set(Z);
|
||||||
|
|
||||||
// omit any data not present in both the x- and z-domain
|
// omit any data not present in both the y- and z-domain
|
||||||
const I = d3.range(X.length).filter(i => xDomain.has(X[i]) && zDomain.has(Z[i]));
|
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));
|
||||||
|
|
||||||
const xDomainArray = Array.from(xDomain);
|
const height = yDomain.size * 25 + marginTop + marginBottom;
|
||||||
const zDomainArray = Array.from(zDomain);
|
const yRange = [height - marginBottom, marginTop];
|
||||||
|
|
||||||
// Construct scales, axes, and formats
|
const offset = d3.stackOffsetDiverging;
|
||||||
const xScale = d3.scaleBand(xDomainArray, xRange).paddingInner(xPadding);
|
const order = d3.stackOrderNone;
|
||||||
const xzScale = d3.scaleBand(zDomainArray, [0, xScale.bandwidth()]).padding(zPadding);
|
|
||||||
const yScale = yType(yDomain, yRange);
|
|
||||||
const zScale = d3.scaleOrdinal(zDomainArray, colors);
|
|
||||||
const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);
|
|
||||||
const yAxis = d3.axisLeft(yScale).ticks(height / 60, yFormat);
|
|
||||||
|
|
||||||
// Compute titles
|
const series = stack()
|
||||||
if (title === undefined) {
|
.keys(zDomain)
|
||||||
const formatValue = yScale.tickFormat(100, yFormat);
|
.value(([, I], z) => X[I.get(z)])
|
||||||
title = i => `${X[i]}\n${Z[i]}\n${formatValue(Y[i])}`;
|
.order(order)
|
||||||
} else {
|
.offset(offset)(
|
||||||
const O = d3.map(data, d => d);
|
rollup(
|
||||||
const T = title;
|
I,
|
||||||
title = i => T(O[i], i, data);
|
([i]) => i,
|
||||||
}
|
i => Y[i],
|
||||||
|
i => Z[i]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(s => s.map(d => Object.assign(d, {i: d.data[1].get(s.key)})));
|
||||||
|
|
||||||
|
const xDomain = d3.extent(series.flat(2));
|
||||||
|
|
||||||
|
const xScale = d3.scaleLinear(xDomain, xRange);
|
||||||
|
const yScale = d3.scaleBand(Array.from(yDomain), yRange).paddingInner(yPadding);
|
||||||
|
const color = d3.scaleOrdinal(Array.from(zDomain), d3.schemeCategory10);
|
||||||
|
const xAxis = d3.axisTop(xScale).ticks(width / 80, xFormat);
|
||||||
|
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
|
||||||
|
|
||||||
|
const formatValue = xScale.tickFormat(100, xFormat);
|
||||||
|
const title = i => `${Y[i]}\n${Z[i]}\n${formatValue(X[i])}`;
|
||||||
|
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.create("svg")
|
.create("svg")
|
||||||
|
|
@ -173,47 +199,48 @@ function plot(
|
||||||
|
|
||||||
svg
|
svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("transform", `translate(${marginLeft},0)`)
|
.attr("transform", `translate(0,${marginTop})`)
|
||||||
.call(yAxis)
|
.call(xAxis)
|
||||||
.call(g => g.select(".domain").remove())
|
.call(g => g.select(".domain").remove())
|
||||||
.call(g =>
|
.call(g =>
|
||||||
g
|
g
|
||||||
.selectAll(".tick line")
|
.selectAll(".tick line")
|
||||||
.clone()
|
.clone()
|
||||||
.attr("x2", width - marginLeft - marginRight)
|
.attr("y2", height - marginTop - marginBottom)
|
||||||
.attr("stroke-opacity", 0.1)
|
.attr("stroke-opacity", 0.1)
|
||||||
)
|
)
|
||||||
.call(g =>
|
.call(g =>
|
||||||
g
|
g
|
||||||
.append("text")
|
.append("text")
|
||||||
.attr("x", -marginLeft)
|
.attr("x", width - marginRight)
|
||||||
.attr("y", 10)
|
.attr("y", -22)
|
||||||
.attr("fill", "currentColor")
|
.attr("fill", "currentColor")
|
||||||
.attr("text-anchor", "start")
|
.attr("text-anchor", "end")
|
||||||
.text(yLabel)
|
.text(xLabel)
|
||||||
);
|
);
|
||||||
|
|
||||||
const bar = svg
|
const bar = svg
|
||||||
.append("g")
|
.append("g")
|
||||||
|
.selectAll("g")
|
||||||
|
.data(series)
|
||||||
|
.join("g")
|
||||||
|
.attr("fill", ([{i}]) => color(Z[i]))
|
||||||
.selectAll("rect")
|
.selectAll("rect")
|
||||||
.data(I)
|
.data(d => d.filter(d => d.i !== undefined))
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", i => xScale(X[i]) + xzScale(Z[i]))
|
.attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
|
||||||
.attr("y", i => yScale(Y[i]))
|
.attr("y", ({i}) => yScale(Y[i]))
|
||||||
.attr("width", xzScale.bandwidth())
|
.attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
|
||||||
.attr("height", i => yScale(0) - yScale(Y[i]))
|
.attr("height", yScale.bandwidth());
|
||||||
.attr("fill", i => zScale(Z[i]));
|
|
||||||
|
|
||||||
if (title) bar.append("title").text(title);
|
bar.append("title").text(({i}) => title(i));
|
||||||
|
|
||||||
svg
|
svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("transform", `translate(0,${height - marginBottom})`)
|
.attr("transform", `translate(${xScale(0)},0)`)
|
||||||
.call(xAxis);
|
.call(yAxis);
|
||||||
|
|
||||||
const chart = Object.assign(svg.node(), {scales: {color: zScale}});
|
return Object.assign(svg.node(), {scales: {color}});
|
||||||
console.log(chart);
|
|
||||||
return chart;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
|
|
@ -225,6 +252,5 @@ function getUrbanPopulation(cellId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuralPopulation(cellId) {
|
function getRuralPopulation(cellId) {
|
||||||
const populationPoints = pack.cells.pop[cellId] * populationRate;
|
return pack.cells.pop[cellId] * populationRate;
|
||||||
return populationPoints * populationRate;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
26
utils/functionUtils.js
Normal file
26
utils/functionUtils.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
function identity(x) {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rollup(values, reduce, ...keys) {
|
||||||
|
return nest(values, identity, reduce, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nest(values, map, reduce, keys) {
|
||||||
|
return (function regroup(values, i) {
|
||||||
|
if (i >= keys.length) return reduce(values);
|
||||||
|
const groups = new Map();
|
||||||
|
const keyof = keys[i++];
|
||||||
|
let index = -1;
|
||||||
|
for (const value of values) {
|
||||||
|
const key = keyof(value, ++index, values);
|
||||||
|
const group = groups.get(key);
|
||||||
|
if (group) group.push(value);
|
||||||
|
else groups.set(key, [value]);
|
||||||
|
}
|
||||||
|
for (const [key, values] of groups) {
|
||||||
|
groups.set(key, regroup(values, i));
|
||||||
|
}
|
||||||
|
return map(groups);
|
||||||
|
})(values, 0);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue