+
@@ -7779,89 +7781,89 @@
-
+
-
+
-
-
+
+
-
+
-
-
+
+
-
-
+
+
-
+
-
+
-
-
-
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
+
+
+
+
diff --git a/modules/dynamic/editors/cultures-editor.js b/modules/dynamic/editors/cultures-editor.js
index 9c651acc..4b5bc788 100644
--- a/modules/dynamic/editors/cultures-editor.js
+++ b/modules/dynamic/editors/cultures-editor.js
@@ -630,7 +630,7 @@ function togglePercentageMode() {
async function showHierarchy() {
if (customization) return;
- const HeirarchyTree = await import("../hierarchy-tree.js?v=19062022");
+ const HeirarchyTree = await import("../hierarchy-tree.js?v=1.87.00");
const getDescription = culture => {
const {name, type, rural, urban} = culture;
diff --git a/modules/dynamic/editors/religions-editor.js b/modules/dynamic/editors/religions-editor.js
index 53d625fe..ebd75e18 100644
--- a/modules/dynamic/editors/religions-editor.js
+++ b/modules/dynamic/editors/religions-editor.js
@@ -533,7 +533,7 @@ function togglePercentageMode() {
async function showHierarchy() {
if (customization) return;
- const HeirarchyTree = await import("../hierarchy-tree.js?v=19062022");
+ const HeirarchyTree = await import("../hierarchy-tree.js?v=1.87.00");
const getDescription = religion => {
const {name, type, form, rural, urban} = religion;
diff --git a/modules/dynamic/heightmap-selection.js b/modules/dynamic/heightmap-selection.js
index 5ca94e61..9b78d118 100644
--- a/modules/dynamic/heightmap-selection.js
+++ b/modules/dynamic/heightmap-selection.js
@@ -42,7 +42,8 @@ export function open() {
}
function appendStyleSheet() {
- const styles = /* css */ `
+ const style = document.createElement("style");
+ style.textContent = /* css */ `
div.dialog > div.heightmap-selection {
width: 70vw;
height: 70vh;
@@ -145,8 +146,6 @@ function appendStyleSheet() {
}
`;
- const style = document.createElement("style");
- style.appendChild(document.createTextNode(styles));
document.head.appendChild(style);
}
diff --git a/modules/dynamic/hierarchy-tree.js b/modules/dynamic/hierarchy-tree.js
index dc161e43..6cd25f0e 100644
--- a/modules/dynamic/hierarchy-tree.js
+++ b/modules/dynamic/hierarchy-tree.js
@@ -62,7 +62,11 @@ export function open(props) {
}
function appendStyleSheet() {
- const styles = /* css */ `
+ const style = document.createElement("style");
+ style.textContent = /* css */ `
+ #hierarchyTree_selectedOrigins > button {
+ margin: 0 2px;
+ }
#hierarchyTree {
display: flex;
@@ -74,10 +78,6 @@ function appendStyleSheet() {
height: 100%;
}
- #hierarchyTree_selectedOrigins > button {
- margin: 0 2px;
- }
-
.hierarchyTree_selectedOrigins {
margin-right: 15px;
}
@@ -141,8 +141,6 @@ function appendStyleSheet() {
}
`;
- const style = document.createElement("style");
- style.appendChild(document.createTextNode(styles));
document.head.appendChild(style);
}
diff --git a/modules/dynamic/overview/charts-overview.js b/modules/dynamic/overview/charts-overview.js
new file mode 100644
index 00000000..3fe5464e
--- /dev/null
+++ b/modules/dynamic/overview/charts-overview.js
@@ -0,0 +1,665 @@
+import {rollups} from "../../../utils/functionUtils.js";
+
+const entitiesMap = {
+ states: {
+ label: "State",
+ cellsData: pack.cells.state,
+ getName: nameGetter("states"),
+ getColors: colorsGetter("states"),
+ landOnly: true
+ },
+ cultures: {
+ label: "Culture",
+ cellsData: pack.cells.culture,
+ getName: nameGetter("cultures"),
+ getColors: colorsGetter("cultures"),
+ landOnly: true
+ },
+ religions: {
+ label: "Religion",
+ cellsData: pack.cells.religion,
+ getName: nameGetter("religions"),
+ getColors: colorsGetter("religions"),
+ landOnly: true
+ },
+ provinces: {
+ label: "Province",
+ cellsData: pack.cells.province,
+ getName: nameGetter("provinces"),
+ getColors: colorsGetter("provinces"),
+ landOnly: true
+ },
+ biomes: {
+ label: "Biome",
+ cellsData: pack.cells.biome,
+ getName: biomeNameGetter,
+ getColors: biomeColorsGetter,
+ landOnly: false
+ }
+};
+
+const quantizationMap = {
+ total_population: {
+ label: "Total population",
+ quantize: cellId => getUrbanPopulation(cellId) + getRuralPopulation(cellId),
+ aggregate: values => rn(d3.sum(values)),
+ formatTicks: value => si(value),
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ },
+ urban_population: {
+ label: "Urban population",
+ quantize: getUrbanPopulation,
+ aggregate: values => rn(d3.sum(values)),
+ formatTicks: value => si(value),
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ },
+ rural_population: {
+ label: "Rural population",
+ quantize: getRuralPopulation,
+ aggregate: values => rn(d3.sum(values)),
+ formatTicks: value => si(value),
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ },
+ area: {
+ label: "Land area",
+ quantize: cellId => getArea(pack.cells.area[cellId]),
+ aggregate: values => rn(d3.sum(values)),
+ formatTicks: value => `${si(value)} ${getAreaUnit()}`,
+ stringify: value => `${value.toLocaleString()} ${getAreaUnit()}`,
+ stackable: true,
+ landOnly: true
+ },
+ cells: {
+ label: "Number of cells",
+ quantize: () => 1,
+ aggregate: values => d3.sum(values),
+ formatTicks: value => value,
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ },
+ burgs_number: {
+ label: "Number of burgs",
+ quantize: cellId => (pack.cells.burg[cellId] ? 1 : 0),
+ aggregate: values => d3.sum(values),
+ formatTicks: value => value,
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ },
+ average_elevation: {
+ label: "Average elevation",
+ quantize: cellId => pack.cells.h[cellId],
+ aggregate: values => d3.mean(values),
+ formatTicks: value => getHeight(value),
+ stringify: value => getHeight(value),
+ stackable: false,
+ landOnly: false
+ },
+ max_elevation: {
+ label: "Maximum mean elevation",
+ quantize: cellId => pack.cells.h[cellId],
+ aggregate: values => d3.max(values),
+ formatTicks: value => getHeight(value),
+ stringify: value => getHeight(value),
+ stackable: false,
+ landOnly: false
+ },
+ min_elevation: {
+ label: "Minimum mean elevation",
+ quantize: cellId => pack.cells.h[cellId],
+ aggregate: values => d3.min(values),
+ formatTicks: value => getHeight(value),
+ stringify: value => getHeight(value),
+ stackable: false,
+ landOnly: false
+ },
+ average_temperature: {
+ label: "Annual mean temperature",
+ quantize: cellId => grid.cells.temp[pack.cells.g[cellId]],
+ aggregate: values => d3.mean(values),
+ formatTicks: value => convertTemperature(value),
+ stringify: value => convertTemperature(value),
+ stackable: false,
+ landOnly: false
+ },
+ max_temperature: {
+ label: "Mean annual maximum temperature",
+ quantize: cellId => grid.cells.temp[pack.cells.g[cellId]],
+ aggregate: values => d3.max(values),
+ formatTicks: value => convertTemperature(value),
+ stringify: value => convertTemperature(value),
+ stackable: false,
+ landOnly: false
+ },
+ min_temperature: {
+ label: "Mean annual minimum temperature",
+ quantize: cellId => grid.cells.temp[pack.cells.g[cellId]],
+ aggregate: values => d3.min(values),
+ formatTicks: value => convertTemperature(value),
+ stringify: value => convertTemperature(value),
+ stackable: false,
+ landOnly: false
+ },
+ average_precipitation: {
+ label: "Annual mean precipitation",
+ quantize: cellId => grid.cells.prec[pack.cells.g[cellId]],
+ aggregate: values => rn(d3.mean(values)),
+ formatTicks: value => getPrecipitation(rn(value)),
+ stringify: value => getPrecipitation(rn(value)),
+ stackable: false,
+ landOnly: true
+ },
+ max_precipitation: {
+ label: "Mean annual maximum precipitation",
+ quantize: cellId => grid.cells.prec[pack.cells.g[cellId]],
+ aggregate: values => rn(d3.max(values)),
+ formatTicks: value => getPrecipitation(rn(value)),
+ stringify: value => getPrecipitation(rn(value)),
+ stackable: false,
+ landOnly: true
+ },
+ min_precipitation: {
+ label: "Mean annual minimum precipitation",
+ quantize: cellId => grid.cells.prec[pack.cells.g[cellId]],
+ aggregate: values => rn(d3.min(values)),
+ formatTicks: value => getPrecipitation(rn(value)),
+ stringify: value => getPrecipitation(rn(value)),
+ stackable: false,
+ landOnly: true
+ },
+ coastal_cells: {
+ label: "Number of coastal cells",
+ quantize: cellId => (pack.cells.t[cellId] === 1 ? 1 : 0),
+ aggregate: values => d3.sum(values),
+ formatTicks: value => value,
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ },
+ river_cells: {
+ label: "Number of river cells",
+ quantize: cellId => (pack.cells.r[cellId] ? 1 : 0),
+ aggregate: values => d3.sum(values),
+ formatTicks: value => value,
+ stringify: value => value.toLocaleString(),
+ stackable: true,
+ landOnly: true
+ }
+};
+
+const plotTypeMap = {
+ stackedBar: {offset: d3.stackOffsetDiverging},
+ normalizedStackedBar: {offset: d3.stackOffsetExpand, formatX: value => rn(value * 100) + "%"}
+};
+
+appendStyleSheet();
+insertHtml();
+addListeners();
+changeViewColumns();
+
+export function open() {
+ const charts = byId("chartsOverview__charts").childElementCount;
+ if (!charts) renderChart();
+ $("#chartsOverview").dialog({title: "Data Charts"});
+}
+
+function appendStyleSheet() {
+ const style = document.createElement("style");
+ style.textContent = /* 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 0;
+ display: grid;
+ grid-template-columns: auto auto;
+ grid-gap: 0.3em;
+ align-items: start;
+ justify-items: end;
+ }
+
+ @media (max-width: 600px) {
+ #chartsOverview__form {
+ font-size: 1em;
+ grid-template-columns: 1fr;
+ justify-items: normal;
+ }
+ }
+
+ #chartsOverview__charts {
+ overflow: auto;
+ scroll-behavior: smooth;
+ display: grid;
+ }
+
+ #chartsOverview__charts figure {
+ margin: 0;
+ }
+
+ #chartsOverview__charts figcaption {
+ font-size: 1.2em;
+ margin: 0 1% 0 4%;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ }
+ `;
+
+ document.head.appendChild(style);
+}
+
+function insertHtml() {
+ const entities = Object.entries(entitiesMap).map(([entity, {label}]) => [entity, label]);
+ const plotBy = Object.entries(quantizationMap).map(([plotBy, {label}]) => [plotBy, label]);
+
+ const createOption = ([value, label]) => `
`;
+ const createOptions = values => values.map(createOption).join("");
+
+ const html = /* 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() {
+ byId("chartsOverview__form").on("submit", renderChart);
+ byId("chartsOverview__viewColumns").on("change", changeViewColumns);
+}
+
+function renderChart(event) {
+ if (event) event.preventDefault();
+
+ const entity = byId("chartsOverview__entitiesSelect").value;
+ const plotBy = byId("chartsOverview__plotBySelect").value;
+ let groupBy = byId("chartsOverview__groupBySelect").value;
+ const sorting = byId("chartsOverview__sortingSelect").value;
+ const type = byId("chartsOverview__chartType").value;
+
+ const {
+ label: plotByLabel,
+ stringify,
+ quantize,
+ aggregate,
+ formatTicks,
+ stackable,
+ landOnly: plotByLandOnly
+ } = quantizationMap[plotBy];
+
+ if (!stackable && groupBy !== entity) {
+ tip(`Grouping is not supported for ${plotByLabel}`, false, "warn", 4000);
+ groupBy = entity;
+ }
+
+ const noGrouping = groupBy === entity;
+
+ const {
+ label: entityLabel,
+ getName: getEntityName,
+ cellsData: entityCells,
+ landOnly: entityLandOnly
+ } = entitiesMap[entity];
+ 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, percentage) => {
+ const entityTip = `${entityLabel}: ${entity}`;
+ const groupTip = noGrouping ? "" : `${groupLabel}: ${group}`;
+ let valueTip = `${plotByLabel}: ${stringify(value)}`;
+ if (!noGrouping) valueTip += ` (${rn(percentage * 100)}%)`;
+ return [entityTip, groupTip, valueTip].filter(Boolean);
+ };
+
+ const dataCollection = {};
+ const groups = new Set();
+
+ for (const cellId of pack.cells.i) {
+ if ((entityLandOnly || plotByLandOnly) && isWater(cellId)) continue;
+ const entityId = entityCells[cellId];
+ const groupId = groupCells[cellId];
+ const value = quantize(cellId);
+
+ if (!dataCollection[entityId]) dataCollection[entityId] = {[groupId]: [value]};
+ else if (!dataCollection[entityId][groupId]) dataCollection[entityId][groupId] = [value];
+ else dataCollection[entityId][groupId].push(value);
+
+ groups.add(groupId);
+ }
+
+ const chartData = Object.entries(dataCollection)
+ .map(([entityId, groupData]) => {
+ const name = getEntityName(entityId);
+ return Object.entries(groupData).map(([groupId, values]) => {
+ const group = getGroupName(groupId);
+ const value = aggregate(values);
+ return {name, group, value};
+ });
+ })
+ .flat();
+
+ const colors = getColors();
+ const {offset, formatX = formatTicks} = plotTypeMap[type];
+
+ const chart = createStackedBarChart(chartData, {sorting, colors, tooltip, offset, formatX});
+ insertChart(chart, title);
+
+ byId("chartsOverview__charts").lastChild.scrollIntoView();
+ updateDialog();
+}
+
+// based on observablehq.com/@d3/stacked-horizontal-bar-chart
+function createStackedBarChart(data, {sorting, colors, tooltip, offset, formatX}) {
+ const sortedData = sortData(data, sorting);
+
+ const X = sortedData.map(d => d.value);
+ const Y = sortedData.map(d => d.name);
+ const Z = sortedData.map(d => d.group);
+
+ const yDomain = new Set(Y);
+ const zDomain = new Set(Z);
+ const I = d3.range(X.length).filter(i => yDomain.has(Y[i]) && zDomain.has(Z[i]));
+
+ const entities = Array.from(yDomain);
+ const groups = Array.from(zDomain);
+
+ const yScaleMinWidth = getTextMinWidth(entities);
+ const legendRows = calculateLegendRows(groups, WIDTH - yScaleMinWidth - 15);
+
+ const margin = {top: 30, right: 15, bottom: legendRows * 20 + 10, left: yScaleMinWidth};
+ const xRange = [margin.left, WIDTH - margin.right];
+ const height = yDomain.size * 25 + margin.top + margin.bottom;
+ const yRange = [height - margin.bottom, margin.top];
+
+ const rolled = rollups(...[I, ([i]) => i, i => Y[i], i => Z[i]]);
+
+ const series = d3
+ .stack()
+ .keys(groups)
+ .value(([, I], z) => X[new Map(I).get(z)])
+ .order(d3.stackOrderNone)
+ .offset(offset)(rolled)
+ .map(s => {
+ const defined = s.filter(d => !isNaN(d[1]));
+ const data = defined.map(d => Object.assign(d, {i: new Map(d.data[1]).get(s.key)}));
+ return {key: s.key, data};
+ });
+
+ const xDomain = d3.extent(series.map(d => d.data).flat(2));
+
+ const xScale = d3.scaleLinear(xDomain, xRange);
+ const yScale = d3.scaleBand(entities, yRange).paddingInner(Y_PADDING);
+
+ const xAxis = d3.axisTop(xScale).ticks(WIDTH / 80, null);
+ const yAxis = d3.axisLeft(yScale).tickSizeOuter(0);
+
+ const svg = d3
+ .create("svg")
+ .attr("version", "1.1")
+ .attr("xmlns", "http://www.w3.org/2000/svg")
+ .attr("viewBox", [0, 0, WIDTH, height])
+ .attr("style", "max-width: 100%; height: auto; height: intrinsic;");
+
+ svg
+ .append("g")
+ .attr("transform", `translate(0,${margin.top})`)
+ .call(xAxis)
+ .call(g => g.select(".domain").remove())
+ .call(g => g.selectAll("text").text(d => formatX(d)))
+ .call(g =>
+ g
+ .selectAll(".tick line")
+ .clone()
+ .attr("y2", height - margin.top - margin.bottom)
+ .attr("stroke-opacity", 0.1)
+ );
+
+ const bar = svg
+ .append("g")
+ .attr("stroke", "#666")
+ .attr("stroke-width", 0.5)
+ .selectAll("g")
+ .data(series)
+ .join("g")
+ .attr("fill", d => colors[d.key])
+ .selectAll("rect")
+ .data(d => d.data.filter(([x1, x2]) => x1 !== x2))
+ .join("rect")
+ .attr("x", ([x1, x2]) => Math.min(xScale(x1), xScale(x2)))
+ .attr("y", ({i}) => yScale(Y[i]))
+ .attr("width", ([x1, x2]) => Math.abs(xScale(x1) - xScale(x2)))
+ .attr("height", yScale.bandwidth());
+
+ const totalZ = Object.fromEntries(
+ rollups(...[I, ([i]) => i, i => Y[i], i => X[i]]).map(([y, yz]) => [y, d3.sum(yz, yz => yz[0])])
+ );
+ const getTooltip = ({i}) => tooltip(Y[i], Z[i], X[i], X[i] / totalZ[Y[i]]);
+
+ bar.append("title").text(d => getTooltip(d).join("\r\n"));
+ bar.on("mouseover", d => tip(getTooltip(d).join(". ")));
+
+ svg
+ .append("g")
+ .attr("transform", `translate(${xScale(0)},0)`)
+ .call(yAxis);
+
+ const rowElements = Math.ceil(groups.length / legendRows);
+ const columnWidth = WIDTH / (rowElements + 0.5);
+
+ const ROW_HEIGHT = 20;
+
+ const getLegendX = (d, i) => (i % rowElements) * columnWidth;
+ const getLegendLabelX = (d, i) => getLegendX(d, i) + LABEL_GAP;
+ const getLegendY = (d, i) => Math.floor(i / rowElements) * ROW_HEIGHT;
+
+ const legend = svg
+ .append("g")
+ .attr("stroke", "#666")
+ .attr("stroke-width", 0.5)
+ .attr("dominant-baseline", "central")
+ .attr("transform", `translate(${margin.left},${height - margin.bottom + 15})`);
+
+ legend
+ .selectAll("circle")
+ .data(groups)
+ .join("rect")
+ .attr("x", getLegendX)
+ .attr("y", getLegendY)
+ .attr("width", 10)
+ .attr("height", 10)
+ .attr("transform", "translate(-5, -5)")
+ .attr("fill", d => colors[d]);
+
+ legend
+ .selectAll("text")
+ .data(groups)
+ .join("text")
+ .attr("x", getLegendLabelX)
+ .attr("y", getLegendY)
+ .text(d => d);
+
+ 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 = /* html */ `
+
+ Figure ${figureNo}. ${title}
+
+
+
+
+
+ `;
+
+ $figure.appendChild(chart);
+ $figure.appendChild($caption);
+ $chartContainer.appendChild($figure);
+
+ const downloadChart = () => {
+ const name = `${getFileName(title)}.svg`;
+ downloadFile(chart.outerHTML, name);
+ };
+
+ const removeChart = () => {
+ $figure.remove();
+ updateDialog();
+ };
+
+ $figure.querySelector("button.icon-download").on("click", downloadChart);
+ $figure.querySelector("button.icon-trash").on("click", removeChart);
+}
+
+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: "svg"}});
+}
+
+// config
+const NEUTRAL_COLOR = "#ccc";
+const EMPTY_NAME = "no";
+
+const WIDTH = 800;
+const Y_PADDING = 0.2;
+
+const RESERVED_PX_PER_CHAR = 7;
+const LABEL_GAP = 10;
+
+function getTextMinWidth(entities) {
+ return d3.max(entities.map(name => name.length)) * RESERVED_PX_PER_CHAR;
+}
+
+function calculateLegendRows(groups, availableWidth) {
+ const minWidth = LABEL_GAP + getTextMinWidth(groups);
+ const maxInRow = Math.floor(availableWidth / minWidth);
+ const legendRows = Math.ceil(groups.length / maxInRow);
+ return legendRows;
+}
+
+function nameGetter(entity) {
+ return i => pack[entity][i].name || EMPTY_NAME;
+}
+
+function colorsGetter(entity) {
+ return () => Object.fromEntries(pack[entity].map(({name, color}) => [name || EMPTY_NAME, color || NEUTRAL_COLOR]));
+}
+
+function biomeNameGetter(i) {
+ return biomesData.name[i] || EMPTY_NAME;
+}
+
+function biomeColorsGetter() {
+ return Object.fromEntries(biomesData.i.map(i => [biomesData.name[i], biomesData.color[i]]));
+}
+
+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) {
+ return pack.cells.pop[cellId] * populationRate;
+}
+
+function sortData(data, sorting) {
+ if (sorting === "natural") return data;
+
+ if (sorting === "name") {
+ return data.sort((a, b) => {
+ if (a.name !== b.name) return b.name.localeCompare(a.name); // reversed as 1st element is the bottom
+ return a.group.localeCompare(b.group);
+ });
+ }
+
+ if (sorting === "value") {
+ const entitySum = {};
+ const groupSum = {};
+ for (const {name, group, value} of data) {
+ entitySum[name] = (entitySum[name] || 0) + value;
+ groupSum[group] = (groupSum[group] || 0) + value;
+ }
+
+ return data.sort((a, b) => {
+ if (a.name !== b.name) return entitySum[a.name] - entitySum[b.name]; // reversed as 1st element is the bottom
+ return groupSum[b.group] - groupSum[a.group];
+ });
+ }
+
+ return data;
+}
diff --git a/modules/ui/editors.js b/modules/ui/editors.js
index af3e3028..403a77a2 100644
--- a/modules/ui/editors.js
+++ b/modules/ui/editors.js
@@ -1180,12 +1180,12 @@ async function editStates() {
async function editCultures() {
if (customization) return;
- const Editor = await import("../dynamic/editors/cultures-editor.js?v=19062022");
+ const Editor = await import("../dynamic/editors/cultures-editor.js?v=1.87.00");
Editor.open();
}
async function editReligions() {
if (customization) return;
- const Editor = await import("../dynamic/editors/religions-editor.js?v=19062022");
+ const Editor = await import("../dynamic/editors/religions-editor.js?v=1.87.00");
Editor.open();
}
diff --git a/modules/ui/general.js b/modules/ui/general.js
index ad46d184..fa030418 100644
--- a/modules/ui/general.js
+++ b/modules/ui/general.js
@@ -121,7 +121,11 @@ function showMapTooltip(point, e, i, g) {
if (group === "emblems" && e.target.tagName === "use") {
const parent = e.target.parentNode;
const [g, type] =
- parent.id === "burgEmblems" ? [pack.burgs, "burg"] : parent.id === "provinceEmblems" ? [pack.provinces, "province"] : [pack.states, "state"];
+ parent.id === "burgEmblems"
+ ? [pack.burgs, "burg"]
+ : parent.id === "provinceEmblems"
+ ? [pack.provinces, "province"]
+ : [pack.states, "state"];
const i = +e.target.dataset.i;
if (event.shiftKey) highlightEmblemElement(type, g[i]);
@@ -161,8 +165,10 @@ function showMapTooltip(point, e, i, g) {
if (group === "ruler") {
const tag = e.target.tagName;
const className = e.target.getAttribute("class");
- if (tag === "circle" && className === "edge") return tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point");
- if (tag === "circle" && className === "control") return tip("Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point");
+ if (tag === "circle" && className === "edge")
+ return tip("Drag to adjust. Hold Ctrl and drag to add a point. Click to remove the point");
+ if (tag === "circle" && className === "control")
+ return tip("Drag to adjust. Hold Shift and drag to keep axial direction. Click to remove the point");
if (tag === "circle") return tip("Drag to adjust the measurer");
if (tag === "polyline") return tip("Click on drag to add a control point");
if (tag === "path") return tip("Drag to move the measurer");
@@ -248,10 +254,19 @@ function updateCellInfo(point, i, g) {
infoTemp.innerHTML = convertTemperature(grid.cells.temp[g]);
infoPrec.innerHTML = cells.h[i] >= 20 ? getFriendlyPrecipitation(i) : "n/a";
infoRiver.innerHTML = cells.h[i] >= 20 && cells.r[i] ? getRiverInfo(cells.r[i]) : "no";
- infoState.innerHTML = cells.h[i] >= 20 ? (cells.state[i] ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})` : "neutral lands (0)") : "no";
- infoProvince.innerHTML = cells.province[i] ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})` : "no";
+ infoState.innerHTML =
+ cells.h[i] >= 20
+ ? cells.state[i]
+ ? `${pack.states[cells.state[i]].fullName} (${cells.state[i]})`
+ : "neutral lands (0)"
+ : "no";
+ infoProvince.innerHTML = cells.province[i]
+ ? `${pack.provinces[cells.province[i]].fullName} (${cells.province[i]})`
+ : "no";
infoCulture.innerHTML = cells.culture[i] ? `${pack.cultures[cells.culture[i]].name} (${cells.culture[i]})` : "no";
- infoReligion.innerHTML = cells.religion[i] ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})` : "no";
+ infoReligion.innerHTML = cells.religion[i]
+ ? `${pack.religions[cells.religion[i]].name} (${cells.religion[i]})`
+ : "no";
infoPopulation.innerHTML = getFriendlyPopulation(i);
infoBurg.innerHTML = cells.burg[i] ? pack.burgs[cells.burg[i]].name + " (" + cells.burg[i] + ")" : "no";
infoFeature.innerHTML = f ? pack.features[f].group + " (" + f + ")" : "n/a";
@@ -300,8 +315,7 @@ function getFriendlyHeight([x, y]) {
function getHeight(h, abs) {
const unit = heightUnit.value;
let unitRatio = 3.281; // default calculations are in feet
- if (unit === "m") unitRatio = 1;
- // if meter
+ if (unit === "m") unitRatio = 1; // if meter
else if (unit === "f") unitRatio = 0.5468; // if fathom
let height = -990;
@@ -312,10 +326,14 @@ function getHeight(h, abs) {
return rn(height * unitRatio) + " " + unit;
}
+function getPrecipitation(prec) {
+ return prec * 100 + " mm";
+}
+
// get user-friendly (real-world) precipitation value from map data
function getFriendlyPrecipitation(i) {
const prec = grid.cells.prec[pack.cells.g[i]];
- return prec * 100 + " mm";
+ return getPrecipitation(prec);
}
function getRiverInfo(id) {
@@ -399,7 +417,8 @@ function highlightEmblemElement(type, el) {
// assign lock behavior
document.querySelectorAll("[data-locked]").forEach(function (e) {
e.addEventListener("mouseover", function (event) {
- if (this.className === "icon-lock") tip("Click to unlock the option and allow it to be randomized on new map generation");
+ if (this.className === "icon-lock")
+ tip("Click to unlock the option and allow it to be randomized on new map generation");
else tip("Click to lock the option and always use the current value on new map generation");
event.stopPropagation();
});
@@ -476,7 +495,10 @@ function showInfo() {
const Patreon = link("https://www.patreon.com/azgaar", "Patreon");
const Armoria = link("https://azgaar.github.io/Armoria", "Armoria");
- const QuickStart = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial", "Quick start tutorial");
+ const QuickStart = link(
+ "https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Quick-Start-Tutorial",
+ "Quick start tutorial"
+ );
const QAA = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Q&A", "Q&A page");
const VideoTutorial = link("https://youtube.com/playlist?list=PLtgiuDC8iVR2gIG8zMTRn7T_L0arl9h1C", "Video tutorial");
diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js
index 6cba97a0..d8fcaaf8 100644
--- a/modules/ui/hotkeys.js
+++ b/modules/ui/hotkeys.js
@@ -48,6 +48,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();
diff --git a/modules/ui/options.js b/modules/ui/options.js
index 48ddf7bd..0683a694 100644
--- a/modules/ui/options.js
+++ b/modules/ui/options.js
@@ -635,7 +635,7 @@ function changeEra() {
}
async function openTemplateSelectionDialog() {
- const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=290520222");
+ const HeightmapSelectionDialog = await import("../dynamic/heightmap-selection.js?v=1.87.00");
HeightmapSelectionDialog.open();
}
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/utils/functionUtils.js b/utils/functionUtils.js
new file mode 100644
index 00000000..845673a8
--- /dev/null
+++ b/utils/functionUtils.js
@@ -0,0 +1,25 @@
+// extracted d3 code to bypass version conflicts
+// https://github.com/d3/d3-array/blob/main/src/group.js
+
+export function rollups(values, reduce, ...keys) {
+ return nest(values, Array.from, 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);
+}
diff --git a/utils/unitUtils.js b/utils/unitUtils.js
index 141fa432..a9a933df 100644
--- a/utils/unitUtils.js
+++ b/utils/unitUtils.js
@@ -2,27 +2,20 @@
// FMG utils related to units
// conver temperature from °C to other scales
+const temperatureConversionMap = {
+ "°C": temp => rn(temp) + "°C",
+ "°F": temp => rn((temp * 9) / 5 + 32) + "°F",
+ K: temp => rn(temp + 273.15) + "K",
+ "°R": temp => rn(((temp + 273.15) * 9) / 5) + "°R",
+ "°De": temp => rn(((100 - temp) * 3) / 2) + "°De",
+ "°N": temp => rn((temp * 33) / 100) + "°N",
+ "°Ré": temp => rn((temp * 4) / 5) + "°Ré",
+ "°Rø": temp => rn((temp * 21) / 40 + 7.5) + "°Rø"
+};
+
function convertTemperature(temp) {
- switch (temperatureScale.value) {
- case "°C":
- return rn(temp) + "°C";
- case "°F":
- return rn((temp * 9) / 5 + 32) + "°F";
- case "K":
- return rn(temp + 273.15) + "K";
- case "°R":
- return rn(((temp + 273.15) * 9) / 5) + "°R";
- case "°De":
- return rn(((100 - temp) * 3) / 2) + "°De";
- case "°N":
- return rn((temp * 33) / 100) + "°N";
- case "°Ré":
- return rn((temp * 4) / 5) + "°Ré";
- case "°Rø":
- return rn((temp * 21) / 40 + 7.5) + "°Rø";
- default:
- return rn(temp) + "°C";
- }
+ const scale = temperatureScale.value || "°C";
+ return temperatureConversionMap[scale](temp);
}
// corvent number to short string with SI postfix
diff --git a/versioning.js b/versioning.js
index 4e7f3589..f725265a 100644
--- a/versioning.js
+++ b/versioning.js
@@ -1,7 +1,7 @@
"use strict";
// version and caching control
-const version = "1.86.10"; // generator version, update each time
+const version = "1.87.00"; // generator version, update each time
{
document.title += " v" + version;
@@ -28,6 +28,7 @@ const version = "1.86.10"; // generator version, update each time
Latest changes:
+ - Data Charts screen
- Сultures and religions can have multiple parents in hierarchy tree
- Heightmap selection screen
- Dialogs optimization for mobile
@@ -36,9 +37,6 @@ const version = "1.86.10"; // generator version, update each time
- Ability to install the App
- 14 new default fonts
- Caching for faster startup
- - Submap tool by Goteguru
- - Resample tool by Goteguru
- - Pre-defined heightmaps
Join our Discord server and Reddit community to ask questions, share maps, discuss the Generator and Worlbuilding, report bugs and propose new features.