diff --git a/index.html b/index.html index 3c9dcacf..82c0b8db 100644 --- a/index.html +++ b/index.html @@ -1967,6 +1967,13 @@

Click to overview:

+ diff --git a/modules/dynamic/hierarchy-tree.js b/modules/dynamic/hierarchy-tree.js index 73b9f173..e3f06dde 100644 --- a/modules/dynamic/hierarchy-tree.js +++ b/modules/dynamic/hierarchy-tree.js @@ -1,6 +1,5 @@ appendStyleSheet(); insertHtml(); -addListeners(); const MARGINS = {top: 10, right: 10, bottom: -5, left: 10}; @@ -159,8 +158,6 @@ function insertHtml() { byId("dialogs").insertAdjacentHTML("beforeend", html); } -function addListeners() {} - function getRoot() { const root = d3 .stratify() diff --git a/modules/dynamic/overview/charts-overview.js b/modules/dynamic/overview/charts-overview.js new file mode 100644 index 00000000..3bee1ebf --- /dev/null +++ b/modules/dynamic/overview/charts-overview.js @@ -0,0 +1,230 @@ +const entities = ["states", "cultures", "religions"]; +const quantitatives = ["total_population", "urban_population", "rural_population", "area", "cells"]; +const groupings = ["cultures", "states", "religions"]; + +const dataMap = { + states: {array: pack.states, getName: i => pack.states[i].name, cellsData: pack.cells.state}, + cultures: {array: pack.cultures, getName: i => pack.cultures[i].name, cellsData: pack.cells.culture}, + religions: {array: pack.religions, getName: i => pack.religions[i].name, cellsData: pack.cells.religion} +}; + +const quantizationMap = { + total_population: cellId => getUrbanPopulation(cellId) + getRuralPopulation(cellId), + urban_population: getUrbanPopulation, + rural_population: getRuralPopulation, + area: cellId => getArea(pack.cells.area[cellId]), + cells: () => 1 +}; + +appendStyleSheet(); + +insertHtml(); +const $entitiesSelect = byId("chartsOverview__entitiesSelect"); +const $plotBySelect = byId("chartsOverview__plotBySelect"); +const $groupBySelect = byId("chartsOverview__groupBySelect"); +updateSelectorOptions(); +addListeners(); + +export function open() { + renderChart(); + + $("#chartsOverview").dialog({ + title: "Charts" + }); +} + +function appendStyleSheet() { + const styles = /* css */ ` + `; + + const style = document.createElement("style"); + style.appendChild(document.createTextNode(styles)); + document.head.appendChild(style); +} + +function insertHtml() { + const createOption = value => ``; + const createOptions = values => values.map(createOption).join(""); + + const html = /* html */ `
+
+ Plot + + + by + + + grouped by + +
+ +
+
`; + + byId("dialogs").insertAdjacentHTML("beforeend", html); +} + +function addListeners() { + $entitiesSelect.on("change", renderChart); + $plotBySelect.on("change", renderChart); + $groupBySelect.on("change", renderChart); + + $entitiesSelect.on("change", updateSelectorOptions); + $groupBySelect.on("change", updateSelectorOptions); +} + +function renderChart() { + const entity = $entitiesSelect.value; + const plotBy = $plotBySelect.value; + const groupBy = $groupBySelect.value; + + const {array: entityArray, getName: getEntityName, cellsData: entityCells} = dataMap[entity]; + const {getName: getGroupName, cellsData: groupCells} = dataMap[groupBy]; + const quantize = quantizationMap[plotBy]; + + const chartData = entityArray + .filter(element => !element.removed) + .map(({i}) => { + const cells = pack.cells.i.filter(cellId => entityCells[cellId] === i); + const name = getEntityName(i); + + return Array.from(cells).map(cellId => { + const group = getGroupName(groupCells[cellId]); + const value = quantize(cellId); + return {name, group, value}; + }); + }) + .flat(); + + console.log(chartData); + const chart = plot(chartData, {}); + byId("chartsOverview__svgContainer").appendChild(chart); +} + +function updateSelectorOptions() { + 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( + data, + { + title, // given d in data, returns the title text + marginTop = 30, // top margin, in pixels + marginRight = 0, // right margin, in pixels + marginBottom = 40, // bottom margin, in pixels + marginLeft = 100, // left margin, in pixels + width = 2400, // outer width, in pixels + height = 400, // outer height, in pixels + xRange = [marginLeft, width - marginRight], // [xmin, xmax] + xPadding = 0.1, // amount of x-range to reserve to separate groups + yType = d3.scaleLinear, // type of y-scale + yRange = [height - marginBottom, marginTop], // [ymin, ymax] + zPadding = 0.05, // amount of x-range to reserve to separate bars + 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 Y = data.map(d => d.value); + const Z = data.map(d => d.group); + + const xDomain = new Set(X); + const yDomain = [0, d3.max(Y)]; + const zDomain = new Set(Z); + + // omit any data not present in both the x- and z-domain + const I = d3.range(X.length).filter(i => xDomain.has(X[i]) && zDomain.has(Z[i])); + + const xDomainArray = Array.from(xDomain); + const zDomainArray = Array.from(zDomain); + + // Construct scales, axes, and formats + const xScale = d3.scaleBand(xDomainArray, xRange).paddingInner(xPadding); + 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 + if (title === undefined) { + const formatValue = yScale.tickFormat(100, yFormat); + title = i => `${X[i]}\n${Z[i]}\n${formatValue(Y[i])}`; + } else { + const O = d3.map(data, d => d); + const T = title; + title = i => T(O[i], i, data); + } + + const svg = d3 + .create("svg") + .attr("width", width) + .attr("height", height) + .attr("viewBox", [0, 0, width, height]) + .attr("style", "max-width: 100%; height: auto; height: intrinsic;"); + + svg + .append("g") + .attr("transform", `translate(${marginLeft},0)`) + .call(yAxis) + .call(g => g.select(".domain").remove()) + .call(g => + g + .selectAll(".tick line") + .clone() + .attr("x2", width - marginLeft - marginRight) + .attr("stroke-opacity", 0.1) + ) + .call(g => + g + .append("text") + .attr("x", -marginLeft) + .attr("y", 10) + .attr("fill", "currentColor") + .attr("text-anchor", "start") + .text(yLabel) + ); + + const bar = svg + .append("g") + .selectAll("rect") + .data(I) + .join("rect") + .attr("x", i => xScale(X[i]) + xzScale(Z[i])) + .attr("y", i => yScale(Y[i])) + .attr("width", xzScale.bandwidth()) + .attr("height", i => yScale(0) - yScale(Y[i])) + .attr("fill", i => zScale(Z[i])); + + if (title) bar.append("title").text(title); + + svg + .append("g") + .attr("transform", `translate(0,${height - marginBottom})`) + .call(xAxis); + + const chart = Object.assign(svg.node(), {scales: {color: zScale}}); + console.log(chart); + return chart; +} + +// helper functions +function getUrbanPopulation(cellId) { + const burgId = pack.cells.burg[cellId]; + if (!burgId) return 0; + const populationPoints = pack.burgs[burgId].population; + return populationPoints * populationRate * urbanization; +} + +function getRuralPopulation(cellId) { + const populationPoints = pack.cells.pop[cellId] * populationRate; + return populationPoints * populationRate; +} diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js index 9c9a4a7f..2c5a9a3a 100644 --- a/modules/ui/hotkeys.js +++ b/modules/ui/hotkeys.js @@ -49,6 +49,7 @@ function handleKeyup(event) { else if (shift && code === "KeyY") openEmblemEditor(); else if (shift && code === "KeyQ") editUnits(); else if (shift && code === "KeyO") editNotes(); + else if (shift && code === "KeyA") overviewCharts(); else if (shift && code === "KeyT") overviewBurgs(); else if (shift && code === "KeyV") overviewRivers(); else if (shift && code === "KeyM") overviewMilitary(); @@ -114,12 +115,17 @@ function pressNumpadSign(key) { let brush = null; if (document.getElementById("brushRadius")?.offsetParent) brush = document.getElementById("brushRadius"); - else if (document.getElementById("biomesManuallyBrush")?.offsetParent) brush = document.getElementById("biomesManuallyBrush"); - else if (document.getElementById("statesManuallyBrush")?.offsetParent) brush = document.getElementById("statesManuallyBrush"); - else if (document.getElementById("provincesManuallyBrush")?.offsetParent) brush = document.getElementById("provincesManuallyBrush"); - else if (document.getElementById("culturesManuallyBrush")?.offsetParent) brush = document.getElementById("culturesManuallyBrush"); + else if (document.getElementById("biomesManuallyBrush")?.offsetParent) + brush = document.getElementById("biomesManuallyBrush"); + else if (document.getElementById("statesManuallyBrush")?.offsetParent) + brush = document.getElementById("statesManuallyBrush"); + else if (document.getElementById("provincesManuallyBrush")?.offsetParent) + brush = document.getElementById("provincesManuallyBrush"); + else if (document.getElementById("culturesManuallyBrush")?.offsetParent) + brush = document.getElementById("culturesManuallyBrush"); else if (document.getElementById("zonesBrush")?.offsetParent) brush = document.getElementById("zonesBrush"); - else if (document.getElementById("religionsManuallyBrush")?.offsetParent) brush = document.getElementById("religionsManuallyBrush"); + else if (document.getElementById("religionsManuallyBrush")?.offsetParent) + brush = document.getElementById("religionsManuallyBrush"); if (brush) { const value = minmax(+brush.value + change, +brush.min, +brush.max); @@ -133,18 +139,26 @@ function pressNumpadSign(key) { function toggleMode() { if (zonesRemove?.offsetParent) { - zonesRemove.classList.contains("pressed") ? zonesRemove.classList.remove("pressed") : zonesRemove.classList.add("pressed"); + zonesRemove.classList.contains("pressed") + ? zonesRemove.classList.remove("pressed") + : zonesRemove.classList.add("pressed"); } } function removeElementOnKey() { - const fastDelete = Array.from(document.querySelectorAll("[role='dialog'] .fastDelete")).find(dialog => dialog.style.display !== "none"); + const fastDelete = Array.from(document.querySelectorAll("[role='dialog'] .fastDelete")).find( + dialog => dialog.style.display !== "none" + ); if (fastDelete) fastDelete.click(); - const visibleDialogs = Array.from(document.querySelectorAll("[role='dialog']")).filter(dialog => dialog.style.display !== "none"); + const visibleDialogs = Array.from(document.querySelectorAll("[role='dialog']")).filter( + dialog => dialog.style.display !== "none" + ); if (!visibleDialogs.length) return; - visibleDialogs.forEach(dialog => dialog.querySelectorAll("button").forEach(button => button.textContent === "Remove" && button.click())); + visibleDialogs.forEach(dialog => + dialog.querySelectorAll("button").forEach(button => button.textContent === "Remove" && button.click()) + ); } function closeAllDialogs() { diff --git a/modules/ui/tools.js b/modules/ui/tools.js index aad90578..27678921 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -19,6 +19,7 @@ toolsContent.addEventListener("click", function (event) { else if (button === "editUnitsButton") editUnits(); else if (button === "editNotesButton") editNotes(); else if (button === "editZonesButton") editZones(); + else if (button === "overviewChartsButton") overviewCharts(); else if (button === "overviewBurgsButton") overviewBurgs(); else if (button === "overviewRiversButton") overviewRivers(); else if (button === "overviewMilitaryButton") overviewMilitary(); @@ -855,3 +856,8 @@ function viewCellDetails() { position: {my: "right top", at: "right-10 top+10", of: "svg", collision: "fit"} }); } + +async function overviewCharts() { + const Overview = await import("../dynamic/overview/charts-overview.js"); + Overview.open(); +} diff --git a/versioning.js b/versioning.js index ba8f7187..c5e0dd86 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.86.07"; // generator version, update each time +const version = "1.87.00"; // generator version, update each time { document.title += " v" + version;