mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 09:41:24 +01:00
feat(charts): stack bar chart UI
This commit is contained in:
parent
d30171b054
commit
b00b9db3b5
1 changed files with 236 additions and 104 deletions
|
|
@ -1,48 +1,112 @@
|
||||||
import {rollup} from "../../../utils/functionUtils.js";
|
import {rollup} from "../../../utils/functionUtils.js";
|
||||||
import {stack} from "https://cdn.skypack.dev/d3-shape@3";
|
import {stack} from "https://cdn.skypack.dev/d3-shape@3";
|
||||||
|
|
||||||
const entities = ["states", "cultures", "religions"];
|
const entitiesMap = {
|
||||||
const quantitatives = ["total_population", "urban_population", "rural_population", "area", "cells"];
|
states: {
|
||||||
const groupings = ["cultures", "states", "religions"];
|
label: "State",
|
||||||
|
array: pack.states,
|
||||||
const dataMap = {
|
cellsData: pack.cells.state,
|
||||||
states: {array: pack.states, getName: i => pack.states[i].name, cellsData: pack.cells.state},
|
getName: nameGetter("states"),
|
||||||
cultures: {array: pack.cultures, getName: i => pack.cultures[i].name, cellsData: pack.cells.culture},
|
getColors: colorsGetter("states")
|
||||||
religions: {array: pack.religions, getName: i => pack.religions[i].name, cellsData: pack.cells.religion}
|
},
|
||||||
|
cultures: {
|
||||||
|
label: "Culture",
|
||||||
|
array: pack.cultures,
|
||||||
|
cellsData: pack.cells.culture,
|
||||||
|
getName: nameGetter("cultures"),
|
||||||
|
getColors: colorsGetter("cultures")
|
||||||
|
},
|
||||||
|
religions: {
|
||||||
|
label: "Religion",
|
||||||
|
array: pack.religions,
|
||||||
|
cellsData: pack.cells.religion,
|
||||||
|
getName: nameGetter("religions"),
|
||||||
|
getColors: colorsGetter("religions")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const quantizationMap = {
|
const quantizationMap = {
|
||||||
total_population: cellId => getUrbanPopulation(cellId) + getRuralPopulation(cellId),
|
total_population: {
|
||||||
urban_population: getUrbanPopulation,
|
label: "Total population",
|
||||||
rural_population: getRuralPopulation,
|
quantize: cellId => getUrbanPopulation(cellId) + getRuralPopulation(cellId),
|
||||||
area: cellId => getArea(pack.cells.area[cellId]),
|
formatTicks: value => si(value),
|
||||||
cells: () => 1
|
stringify: value => `${value.toLocaleString()} people`
|
||||||
};
|
},
|
||||||
|
urban_population: {
|
||||||
const sortingMap = {
|
label: "Urban population",
|
||||||
value: (a, b) => b.value - a.value,
|
quantize: getUrbanPopulation,
|
||||||
name: (a, b) => a.name.localeCompare(b.name)
|
formatTicks: value => si(value),
|
||||||
|
stringify: value => `${value.toLocaleString()} people`
|
||||||
|
},
|
||||||
|
rural_population: {
|
||||||
|
label: "Rural population",
|
||||||
|
quantize: getRuralPopulation,
|
||||||
|
formatTicks: value => si(value),
|
||||||
|
stringify: value => `${value.toLocaleString()} people`
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
label: "Land area",
|
||||||
|
quantize: cellId => getArea(pack.cells.area[cellId]),
|
||||||
|
formatTicks: value => `${si(value)} ${getAreaUnit()}`,
|
||||||
|
stringify: value => `${value.toLocaleString()} ${getAreaUnit()}`
|
||||||
|
},
|
||||||
|
cells: {
|
||||||
|
label: "Number of cells",
|
||||||
|
quantize: () => 1,
|
||||||
|
formatTicks: value => value,
|
||||||
|
stringify: value => `${value.toLocaleString()} cells`
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
appendStyleSheet();
|
appendStyleSheet();
|
||||||
|
|
||||||
insertHtml();
|
insertHtml();
|
||||||
const $entitiesSelect = byId("chartsOverview__entitiesSelect");
|
|
||||||
const $plotBySelect = byId("chartsOverview__plotBySelect");
|
|
||||||
const $groupBySelect = byId("chartsOverview__groupBySelect");
|
|
||||||
updateSelectorOptions();
|
|
||||||
addListeners();
|
addListeners();
|
||||||
|
changeViewColumns();
|
||||||
|
|
||||||
export function open() {
|
export function open() {
|
||||||
renderChart();
|
const charts = byId("chartsOverview__charts").childElementCount;
|
||||||
|
if (!charts) renderChart();
|
||||||
|
|
||||||
$("#chartsOverview").dialog({
|
$("#chartsOverview").dialog({title: "Data Charts"});
|
||||||
title: "Charts"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendStyleSheet() {
|
function appendStyleSheet() {
|
||||||
const styles = /* css */ `
|
const styles = /* css */ `
|
||||||
|
#chartsOverview {
|
||||||
|
max-width: 90vw !important;
|
||||||
|
max-height: 90vh !important;
|
||||||
|
overflow: hidden;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chartsOverview__form {
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin: 0.3em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chartsOverview__charts {
|
||||||
|
overflow: auto;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chartsOverview__charts figure {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#chartsOverview__charts figcaption {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin-left: 4%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartsOverview__bars {
|
||||||
|
stroke: #666;
|
||||||
|
stroke-width: 0.5;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
|
|
@ -51,48 +115,83 @@ function appendStyleSheet() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertHtml() {
|
function insertHtml() {
|
||||||
const createOption = value => `<option value="${value}">${value.replaceAll("_", " ")}</option>`;
|
const entities = Object.entries(entitiesMap).map(([entity, {label}]) => [entity, label]);
|
||||||
|
const plotBy = Object.entries(quantizationMap).map(([plotBy, {label}]) => [plotBy, label]);
|
||||||
|
|
||||||
|
const createOption = ([value, label]) => `<option value="${value}">${label}</option>`;
|
||||||
const createOptions = values => values.map(createOption).join("");
|
const createOptions = values => values.map(createOption).join("");
|
||||||
|
|
||||||
const html = /* html */ `<div id="chartsOverview" style="overflow: disabled;">
|
const html = /* html */ `<div id="chartsOverview">
|
||||||
|
<form id="chartsOverview__form">
|
||||||
<div>
|
<div>
|
||||||
<span>Plot</span>
|
<button type="submit">Plot</button>
|
||||||
<select id="chartsOverview__entitiesSelect">${createOptions(entities)}</select>
|
<select id="chartsOverview__entitiesSelect">${createOptions(entities)}</select>
|
||||||
|
|
||||||
<span>by</span>
|
<span>by</span>
|
||||||
<select id="chartsOverview__plotBySelect">${createOptions(quantitatives)}</select>
|
<select id="chartsOverview__plotBySelect">${createOptions(plotBy)}</select>
|
||||||
|
|
||||||
<span>grouped by</span>
|
<span>grouped by</span>
|
||||||
<select id="chartsOverview__groupBySelect">${createOptions(groupings)}</select>
|
<select id="chartsOverview__groupBySelect">${createOptions(entities)}</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="chartsOverview__svgContainer"></div>
|
<span>sorted</span>
|
||||||
|
<select id="chartsOverview__sortingSelect">
|
||||||
|
<option value="value">by value</option>
|
||||||
|
<option value="name">by name</option>
|
||||||
|
<option value="natural">naturally</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Columns</span>
|
||||||
|
<select id="chartsOverview__viewColumns">
|
||||||
|
<option value="1" selected>1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section id="chartsOverview__charts"></section>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
byId("dialogs").insertAdjacentHTML("beforeend", html);
|
byId("dialogs").insertAdjacentHTML("beforeend", html);
|
||||||
|
|
||||||
|
// set defaults
|
||||||
|
byId("chartsOverview__entitiesSelect").value = "states";
|
||||||
|
byId("chartsOverview__plotBySelect").value = "total_population";
|
||||||
|
byId("chartsOverview__groupBySelect").value = "cultures";
|
||||||
}
|
}
|
||||||
|
|
||||||
function addListeners() {
|
function addListeners() {
|
||||||
$entitiesSelect.on("change", renderChart);
|
byId("chartsOverview__form").on("submit", renderChart);
|
||||||
$plotBySelect.on("change", renderChart);
|
byId("chartsOverview__viewColumns").on("change", changeViewColumns);
|
||||||
$groupBySelect.on("change", renderChart);
|
|
||||||
|
|
||||||
$entitiesSelect.on("change", updateSelectorOptions);
|
|
||||||
$groupBySelect.on("change", updateSelectorOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderChart() {
|
function renderChart(event) {
|
||||||
const entity = $entitiesSelect.value;
|
if (event) event.preventDefault();
|
||||||
const plotBy = $plotBySelect.value;
|
|
||||||
const groupBy = $groupBySelect.value;
|
const entity = byId("chartsOverview__entitiesSelect").value;
|
||||||
|
const plotBy = byId("chartsOverview__plotBySelect").value;
|
||||||
|
const groupBy = byId("chartsOverview__groupBySelect").value;
|
||||||
|
const sorting = byId("chartsOverview__sortingSelect").value;
|
||||||
|
|
||||||
|
const noGrouping = groupBy === entity;
|
||||||
|
|
||||||
const filterWater = true;
|
const filterWater = true;
|
||||||
const filterZeroes = true;
|
const filterZeroes = true;
|
||||||
const sorting = sortingMap["value"];
|
|
||||||
|
|
||||||
const {getName: getEntityName, cellsData: entityCells} = dataMap[entity];
|
const {label: plotByLabel, stringify, quantize, formatTicks} = quantizationMap[plotBy];
|
||||||
const {getName: getGroupName, cellsData: groupCells} = dataMap[groupBy];
|
const {label: entityLabel, getName: getEntityName, cellsData: entityCells} = entitiesMap[entity];
|
||||||
const quantize = quantizationMap[plotBy];
|
const {label: groupLabel, getName: getGroupName, cellsData: groupCells, getColors} = entitiesMap[groupBy];
|
||||||
|
|
||||||
|
const title = `${capitalize(entity)} by ${plotByLabel}${noGrouping ? "" : " grouped by " + groupLabel}`;
|
||||||
|
|
||||||
|
const tooltip = (entity, group, value) => {
|
||||||
|
const entityTip = `${entityLabel}: ${entity}`;
|
||||||
|
const groupTip = noGrouping ? "" : `${groupLabel}: ${group}`;
|
||||||
|
const valueTip = `${plotByLabel}: ${stringify(value)}`;
|
||||||
|
tip([entityTip, groupTip, valueTip].filter(Boolean).join(". "));
|
||||||
|
};
|
||||||
|
|
||||||
const dataCollection = {};
|
const dataCollection = {};
|
||||||
for (const cellId of pack.cells.i) {
|
for (const cellId of pack.cells.i) {
|
||||||
|
|
@ -118,77 +217,66 @@ function renderChart() {
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
const chartDataFiltered = filterZeroes ? chartData.filter(({value}) => value > 0) : chartData;
|
const chartDataFiltered = filterZeroes ? chartData.filter(({value}) => value > 0) : chartData;
|
||||||
|
const colors = getColors();
|
||||||
|
|
||||||
const chart = plot(chartDataFiltered, {sorting});
|
const chart = plot(chartDataFiltered, {sorting, colors, formatTicks, tooltip});
|
||||||
byId("chartsOverview__svgContainer").appendChild(chart);
|
insertChart(chart, title);
|
||||||
|
|
||||||
|
byId("chartsOverview__charts").lastChild.scrollIntoView();
|
||||||
|
updateDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelectorOptions() {
|
// based on observablehq.com/@d3/stacked-horizontal-bar-chart
|
||||||
const entity = $entitiesSelect.value;
|
|
||||||
$groupBySelect.querySelector("option[disabled]")?.removeAttribute("disabled");
|
|
||||||
$groupBySelect.querySelector(`option[value="${entity}"]`)?.setAttribute("disabled", "");
|
|
||||||
|
|
||||||
const group = $groupBySelect.value;
|
|
||||||
$entitiesSelect.querySelector("option[disabled]")?.removeAttribute("disabled");
|
|
||||||
$entitiesSelect.querySelector(`option[value="${group}"]`)?.setAttribute("disabled", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on https://observablehq.com/@d3/grouped-bar-chart
|
|
||||||
function plot(
|
function plot(
|
||||||
data,
|
data,
|
||||||
{
|
{
|
||||||
marginTop = 30, // top margin, in pixels
|
marginTop = 30, // top margin, in pixels
|
||||||
marginRight = 0, // right margin, in pixels
|
marginRight = 10, // right margin, in pixels
|
||||||
marginBottom = 40, // bottom margin, in pixels
|
marginBottom = 10, // bottom margin, in pixels
|
||||||
marginLeft = 100, // left margin, in pixels
|
marginLeft = 80, // left margin, in pixels
|
||||||
width = 800, // outer width, in pixels
|
width = 800, // outer width, in pixels
|
||||||
xRange = [marginLeft, width - marginRight], // [xmin, xmax]
|
xRange = [marginLeft, width - marginRight], // [xmin, xmax]
|
||||||
yPadding = 0.2,
|
yPadding = 0.2,
|
||||||
xFormat,
|
sorting,
|
||||||
xLabel = "Population (millions) →",
|
colors,
|
||||||
sorting
|
formatTicks,
|
||||||
|
tooltip
|
||||||
} = {}
|
} = {}
|
||||||
) {
|
) {
|
||||||
const X = data.map(d => d.value);
|
const X = data.map(d => d.value);
|
||||||
const Y = data.map(d => d.name);
|
const Y = data.map(d => d.name);
|
||||||
const Z = data.map(d => d.group);
|
const Z = data.map(d => d.group);
|
||||||
|
|
||||||
const yDomain = new Set(Y); // get from parent, already sorted
|
const sortedY = sortData({array: Y, sorting, data, dataKey: "name", reverse: true});
|
||||||
|
const sortedZ = sortData({array: Z, sorting, data, dataKey: "group", reverse: false});
|
||||||
|
const yDomain = new Set(sortedY);
|
||||||
const zDomain = new Set(Z);
|
const zDomain = new Set(Z);
|
||||||
|
|
||||||
// omit any data not present in both the y- and z-domain
|
const I = d3.range(X.length).filter(i => X[i] > 0 && yDomain.has(Y[i]) && zDomain.has(Z[i]));
|
||||||
const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));
|
|
||||||
|
|
||||||
const height = yDomain.size * 25 + marginTop + marginBottom;
|
const height = yDomain.size * 25 + marginTop + marginBottom;
|
||||||
const yRange = [height - marginBottom, marginTop];
|
const yRange = [height - marginBottom, marginTop];
|
||||||
|
|
||||||
const offset = d3.stackOffsetDiverging;
|
const rolled = rollup(...[I, ([i]) => i, i => Y[i], i => Z[i]]);
|
||||||
const order = d3.stackOrderNone;
|
|
||||||
|
|
||||||
const series = stack()
|
const series = stack()
|
||||||
.keys(zDomain)
|
.keys(zDomain)
|
||||||
.value(([, I], z) => X[I.get(z)])
|
.value(([, I], z) => X[I.get(z)])
|
||||||
.order(order)
|
.order(d3.stackOrderNone)
|
||||||
.offset(offset)(
|
.offset(d3.stackOffsetDiverging)(rolled)
|
||||||
rollup(
|
.map(s => {
|
||||||
I,
|
const nonNull = s.filter(d => Boolean(d[1]));
|
||||||
([i]) => i,
|
const data = nonNull.map(d => Object.assign(d, {i: d.data[1].get(s.key)}));
|
||||||
i => Y[i],
|
return {key: s.key, data};
|
||||||
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 xDomain = d3.extent(series.map(d => d.data).flat(2));
|
||||||
|
|
||||||
const xScale = d3.scaleLinear(xDomain, xRange);
|
const xScale = d3.scaleLinear(xDomain, xRange);
|
||||||
const yScale = d3.scaleBand(Array.from(yDomain), yRange).paddingInner(yPadding);
|
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 xAxis = d3.axisTop(xScale).ticks(width / 80, null);
|
||||||
const title = i => `${Y[i]}\n${Z[i]}\n${formatValue(X[i])}`;
|
const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
|
||||||
|
|
||||||
const svg = d3
|
const svg = d3
|
||||||
.create("svg")
|
.create("svg")
|
||||||
|
|
@ -202,48 +290,77 @@ function plot(
|
||||||
.attr("transform", `translate(0,${marginTop})`)
|
.attr("transform", `translate(0,${marginTop})`)
|
||||||
.call(xAxis)
|
.call(xAxis)
|
||||||
.call(g => g.select(".domain").remove())
|
.call(g => g.select(".domain").remove())
|
||||||
|
.call(g => g.selectAll("text").text(d => formatTicks(d)))
|
||||||
.call(g =>
|
.call(g =>
|
||||||
g
|
g
|
||||||
.selectAll(".tick line")
|
.selectAll(".tick line")
|
||||||
.clone()
|
.clone()
|
||||||
.attr("y2", height - marginTop - marginBottom)
|
.attr("y2", height - marginTop - marginBottom)
|
||||||
.attr("stroke-opacity", 0.1)
|
.attr("stroke-opacity", 0.1)
|
||||||
)
|
|
||||||
.call(g =>
|
|
||||||
g
|
|
||||||
.append("text")
|
|
||||||
.attr("x", width - marginRight)
|
|
||||||
.attr("y", -22)
|
|
||||||
.attr("fill", "currentColor")
|
|
||||||
.attr("text-anchor", "end")
|
|
||||||
.text(xLabel)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const bar = svg
|
const bar = svg
|
||||||
.append("g")
|
.append("g")
|
||||||
|
.attr("class", "chartsOverview__bars")
|
||||||
.selectAll("g")
|
.selectAll("g")
|
||||||
.data(series)
|
.data(series)
|
||||||
.join("g")
|
.join("g")
|
||||||
.attr("fill", ([{i}]) => color(Z[i]))
|
.attr("fill", d => colors[d.key])
|
||||||
.selectAll("rect")
|
.selectAll("rect")
|
||||||
.data(d => d.filter(d => d.i !== undefined))
|
.data(d => d.data)
|
||||||
.join("rect")
|
.join("rect")
|
||||||
.attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
|
.attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
|
||||||
.attr("y", ({i}) => yScale(Y[i]))
|
.attr("y", ({i}) => yScale(Y[i]))
|
||||||
.attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
|
.attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
|
||||||
.attr("height", yScale.bandwidth());
|
.attr("height", yScale.bandwidth());
|
||||||
|
|
||||||
bar.append("title").text(({i}) => title(i));
|
bar.on("mouseover", ({i}) => tooltip(Y[i], Z[i], X[i]));
|
||||||
|
|
||||||
svg
|
svg
|
||||||
.append("g")
|
.append("g")
|
||||||
.attr("transform", `translate(${xScale(0)},0)`)
|
.attr("transform", `translate(${xScale(0)},0)`)
|
||||||
.call(yAxis);
|
.call(yAxis);
|
||||||
|
|
||||||
return Object.assign(svg.node(), {scales: {color}});
|
return svg.node();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertChart(chart, title) {
|
||||||
|
const $chartContainer = byId("chartsOverview__charts");
|
||||||
|
|
||||||
|
const $figure = document.createElement("figure");
|
||||||
|
const $caption = document.createElement("figcaption");
|
||||||
|
|
||||||
|
const figureNo = $chartContainer.childElementCount + 1;
|
||||||
|
$caption.innerHTML = `<strong>Figure ${figureNo}</strong>. ${title}`;
|
||||||
|
|
||||||
|
$figure.appendChild(chart);
|
||||||
|
$figure.appendChild($caption);
|
||||||
|
|
||||||
|
$chartContainer.appendChild($figure);
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeViewColumns() {
|
||||||
|
const columns = byId("chartsOverview__viewColumns").value;
|
||||||
|
const $charts = byId("chartsOverview__charts");
|
||||||
|
$charts.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
||||||
|
updateDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDialog() {
|
||||||
|
$("#chartsOverview").dialog({position: {my: "center", at: "center", of: window}});
|
||||||
|
}
|
||||||
|
|
||||||
|
// config
|
||||||
|
const NEUTRAL_COLOR = "#ccc";
|
||||||
|
|
||||||
// helper functions
|
// helper functions
|
||||||
|
function nameGetter(entity) {
|
||||||
|
return i => pack[entity][i].name;
|
||||||
|
}
|
||||||
|
function colorsGetter(entity) {
|
||||||
|
return () => Object.fromEntries(pack[entity].map(({name, color}) => [name, color || NEUTRAL_COLOR]));
|
||||||
|
}
|
||||||
|
|
||||||
function getUrbanPopulation(cellId) {
|
function getUrbanPopulation(cellId) {
|
||||||
const burgId = pack.cells.burg[cellId];
|
const burgId = pack.cells.burg[cellId];
|
||||||
if (!burgId) return 0;
|
if (!burgId) return 0;
|
||||||
|
|
@ -254,3 +371,18 @@ function getUrbanPopulation(cellId) {
|
||||||
function getRuralPopulation(cellId) {
|
function getRuralPopulation(cellId) {
|
||||||
return pack.cells.pop[cellId] * populationRate;
|
return pack.cells.pop[cellId] * populationRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sortData({array, sorting, data, dataKey, reverse}) {
|
||||||
|
if (sorting === "natural") return array;
|
||||||
|
if (sorting === "name") return array.sort((a, b) => (reverse ? b.localeCompare(a) : a.localeCompare(b)));
|
||||||
|
|
||||||
|
if (sorting === "value") {
|
||||||
|
return [...new Set(array)].sort((a, b) => {
|
||||||
|
const valueA = d3.sum(data.filter(d => d[dataKey] === a).map(d => d.value));
|
||||||
|
const valueB = d3.sum(data.filter(d => d[dataKey] === b).map(d => d.value));
|
||||||
|
return reverse ? valueA - valueB : valueB - valueA;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue