mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-17 01:41:22 +01:00
feat(charts): add quantization types
This commit is contained in:
parent
90b1a14bba
commit
9a9c8e700c
3 changed files with 217 additions and 63 deletions
|
|
@ -6,31 +6,36 @@ const entitiesMap = {
|
|||
label: "State",
|
||||
cellsData: pack.cells.state,
|
||||
getName: nameGetter("states"),
|
||||
getColors: colorsGetter("states")
|
||||
getColors: colorsGetter("states"),
|
||||
landOnly: true
|
||||
},
|
||||
cultures: {
|
||||
label: "Culture",
|
||||
cellsData: pack.cells.culture,
|
||||
getName: nameGetter("cultures"),
|
||||
getColors: colorsGetter("cultures")
|
||||
getColors: colorsGetter("cultures"),
|
||||
landOnly: true
|
||||
},
|
||||
religions: {
|
||||
label: "Religion",
|
||||
cellsData: pack.cells.religion,
|
||||
getName: nameGetter("religions"),
|
||||
getColors: colorsGetter("religions")
|
||||
getColors: colorsGetter("religions"),
|
||||
landOnly: true
|
||||
},
|
||||
provinces: {
|
||||
label: "Province",
|
||||
cellsData: pack.cells.province,
|
||||
getName: nameGetter("provinces"),
|
||||
getColors: colorsGetter("provinces")
|
||||
getColors: colorsGetter("provinces"),
|
||||
landOnly: true
|
||||
},
|
||||
biomes: {
|
||||
label: "Biome",
|
||||
cellsData: pack.cells.biome,
|
||||
getName: biomeNameGetter,
|
||||
getColors: biomeColorsGetter
|
||||
getColors: biomeColorsGetter,
|
||||
landOnly: false
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -38,32 +43,155 @@ 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()} people`
|
||||
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()} people`
|
||||
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()} people`
|
||||
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()}`
|
||||
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()} cells`
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -181,14 +309,32 @@ function renderChart(event) {
|
|||
|
||||
const entity = byId("chartsOverview__entitiesSelect").value;
|
||||
const plotBy = byId("chartsOverview__plotBySelect").value;
|
||||
const groupBy = byId("chartsOverview__groupBySelect").value;
|
||||
let groupBy = byId("chartsOverview__groupBySelect").value;
|
||||
const sorting = byId("chartsOverview__sortingSelect").value;
|
||||
|
||||
const noGrouping = groupBy === entity;
|
||||
const filterWater = true;
|
||||
const {
|
||||
label: plotByLabel,
|
||||
stringify,
|
||||
quantize,
|
||||
aggregate,
|
||||
formatTicks,
|
||||
stackable,
|
||||
landOnly: plotByLandOnly
|
||||
} = quantizationMap[plotBy];
|
||||
|
||||
const {label: plotByLabel, stringify, quantize, formatTicks} = quantizationMap[plotBy];
|
||||
const {label: entityLabel, getName: getEntityName, cellsData: entityCells} = entitiesMap[entity];
|
||||
if (!stackable && groupBy !== entity) {
|
||||
tip("Grouping is not supported for this chart type", 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}`;
|
||||
|
|
@ -204,31 +350,24 @@ function renderChart(event) {
|
|||
const groups = new Set();
|
||||
|
||||
for (const cellId of pack.cells.i) {
|
||||
if (filterWater && isWater(cellId)) continue;
|
||||
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] += value;
|
||||
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);
|
||||
}
|
||||
|
||||
// fill missing groups with 0
|
||||
for (const entityId in dataCollection) {
|
||||
for (const groupId of groups) {
|
||||
if (!dataCollection[entityId][groupId]) dataCollection[entityId][groupId] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const chartData = Object.entries(dataCollection)
|
||||
.map(([entityId, groupData]) => {
|
||||
const name = getEntityName(entityId);
|
||||
return Object.entries(groupData).map(([groupId, rawValue]) => {
|
||||
return Object.entries(groupData).map(([groupId, values]) => {
|
||||
const group = getGroupName(groupId);
|
||||
const value = rn(rawValue);
|
||||
const value = aggregate(values);
|
||||
return {name, group, value};
|
||||
});
|
||||
})
|
||||
|
|
@ -269,7 +408,7 @@ function plot(
|
|||
const yDomain = new Set(Y);
|
||||
const zDomain = new Set(Z);
|
||||
|
||||
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 yRange = [height - marginBottom, marginTop];
|
||||
|
|
@ -282,8 +421,8 @@ function plot(
|
|||
.order(d3.stackOrderNone)
|
||||
.offset(d3.stackOffsetDiverging)(rolled)
|
||||
.map(s => {
|
||||
const nonNull = s.filter(d => Boolean(d[1]));
|
||||
const data = nonNull.map(d => Object.assign(d, {i: d.data[1].get(s.key)}));
|
||||
const defined = s.filter(d => !isNaN(d[1]));
|
||||
const data = defined.map(d => Object.assign(d, {i: d.data[1].get(s.key)}));
|
||||
return {key: s.key, data};
|
||||
});
|
||||
|
||||
|
|
@ -324,7 +463,7 @@ function plot(
|
|||
.join("g")
|
||||
.attr("fill", d => colors[d.key])
|
||||
.selectAll("rect")
|
||||
.data(d => d.data)
|
||||
.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]))
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue