Skip to content
57 changes: 32 additions & 25 deletions public/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,9 @@ a {
user-select: none;
}

#population,
#cells,
#compass {
fill: none;
#cells,
#compass {
fill: none;
}

#landmass {
Expand Down Expand Up @@ -175,12 +174,13 @@ a {
}

t,
#regions,
#cults,
#relig,
#biomes,
#provincesBody,
#terrs,
#regions,
#cults,
#relig,
#biomes,
#population,
#provincesBody,
#terrs,
#tooltip,
#temperature,
#texture,
Expand Down Expand Up @@ -213,21 +213,28 @@ t,
stroke-linejoin: round;
}

#statesBody,
#provincesBody,
#relig,
#biomes,
#cults {
stroke-linejoin: round;
fill-rule: evenodd;
}

#statesBody,
#provincesBody,
#relig,
#cults {
mask: url(#land);
}
#statesBody,
#provincesBody,
#relig,
#biomes,
#population,
#cults {
stroke-linejoin: round;
fill-rule: evenodd;
}

#population,
#population path {
stroke-dasharray: none !important;
}

#statesBody,
#provincesBody,
#relig,
#population,
#cults {
mask: url(#land);
}

#borders {
stroke-linejoin: round;
Expand Down
2 changes: 1 addition & 1 deletion public/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ let texture = viewbox.append("g").attr("id", "texture");
let terrs = viewbox.append("g").attr("id", "terrs");
let lakes = viewbox.append("g").attr("id", "lakes");
let biomes = viewbox.append("g").attr("id", "biomes");
let population = viewbox.append("g").attr("id", "population");
let cells = viewbox.append("g").attr("id", "cells");
let gridOverlay = viewbox.append("g").attr("id", "gridOverlay");
let coordinates = viewbox.append("g").attr("id", "coordinates");
Expand Down Expand Up @@ -72,7 +73,6 @@ let goods = viewbox.append("g").attr("id", "goods").style("display", "none");
let markets = viewbox.append("g").attr("id", "markets");
let tradeAnimation = viewbox.append("g").attr("id", "tradeAnimation");
let prec = viewbox.append("g").attr("id", "prec").style("display", "none");
let population = viewbox.append("g").attr("id", "population");
let emblems = viewbox.append("g").attr("id", "emblems").style("display", "none");
let icons = viewbox.append("g").attr("id", "icons");
let labels = viewbox.append("g").attr("id", "labels");
Expand Down
5 changes: 4 additions & 1 deletion public/modules/io/load.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,10 @@ async function parseLoadedData(data, mapVersion) {
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
if (hasChildren(temperature)) turnOn("toggleTemperature");
if (hasChild(population, "line")) turnOn("togglePopulation");
if (hasChild(population, "path, line")) {
turnOn("togglePopulation");
drawPopulation();
}
if (isVisible(ice)) turnOn("toggleIce");
if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
Expand Down
144 changes: 89 additions & 55 deletions public/modules/ui/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,76 +359,110 @@ function drawPrecipitation() {
}

function togglePopulation(event) {
if (!population.selectAll("line").size()) {
if (!population.selectAll("path, line").size()) {
turnButtonOn("togglePopulation");
drawPopulation();
if (event && isCtrlClick(event)) editStyle("population");
} else {
if (event && isCtrlClick(event)) return editStyle("population");
turnButtonOff("togglePopulation");

const isD3data = population.select("line").datum();
if (!isD3data) {
// just remove
population.selectAll("line").remove();
} else {
// remove with animation
const hide = d3.transition().duration(1000).ease(d3.easeSinIn);
population
.select("#rural")
.selectAll("line")
.transition(hide)
.attr("y2", d => d[1])
.remove();
population
.select("#urban")
.selectAll("line")
.transition(hide)
.delay(1000)
.attr("y2", d => d[1])
.remove();
}
population.selectAll("*").remove();
}
Comment on lines 361 to 370
}

function drawPopulation() {
population.selectAll("line").remove();
TIME && console.time("drawPopulation");

biomes.node().after(population.node());

population
.attr("mask", "url(#land)")
.attr("stroke", null)
.attr("stroke-width", null)
.attr("stroke-dasharray", null)
.attr("stroke-linecap", null)
.style("stroke", null)
.style("stroke-width", null)
.style("stroke-dasharray", null);

const { cells, burgs } = pack;
const show = d3.transition().duration(2000).ease(d3.easeSinIn);
const urban = new Float64Array(cells.i.length);
const total = new Float64Array(cells.i.length);
let maxPopulation = 0;

for (const cellId of cells.i) {
if (cells.h[cellId] < 20) continue;

const burg = burgs[cells.burg[cellId]];
const ruralPopulation = cells.pop[cellId] * populationRate;
urban[cellId] =
burg && !burg.removed ? (burg.population || 0) * populationRate * urbanization : 0;
total[cellId] = ruralPopulation + urban[cellId];
if (total[cellId] > maxPopulation) maxPopulation = total[cellId];
}

const rural = Array.from(
cells.i.filter(i => cells.pop[i] > 0),
i => [...cells.p[i], cells.p[i][1] - cells.pop[i] / 5]
);
const colors = new Array(cells.i.length);
for (const cellId of cells.i) {
if (cells.h[cellId] < 20) continue;
colors[cellId] = getPopulationColor(urban[cellId], total[cellId], maxPopulation);
}

population
.select("#rural")
.selectAll("line")
.data(rural)
.enter()
.append("line")
.attr("x1", d => d[0])
.attr("y1", d => d[1])
.attr("x2", d => d[0])
.attr("y2", d => d[1])
.transition(show)
.attr("y2", d => d[2]);
const isolines = getIsolines(pack, cellId => colors[cellId] || null, { fill: true, waterGap: true });
const populationShapes = Object.entries(isolines).map(([color, { fill, waterGap }], index) => {
const closedFill = closeSvgPathRings(fill);
Comment on lines +404 to +412
const coastalClipId = `population-coast-clip${index}`;
const outsideFillClip = /* html */ `<clipPath id="${coastalClipId}"><path d="M0,0H${graphWidth}V${graphHeight}H0Z${closedFill}" clip-rule="evenodd" /></clipPath>`;
const coastalStroke = waterGap
? /* html */ `<path d="${waterGap}" fill="none" stroke="${color}" stroke-width="12" stroke-linecap="round" stroke-linejoin="round" clip-path="url(#${coastalClipId})" id="population-coast${index}" />`
: "";
const fillPath = /* html */ `<path d="${closedFill}" fill="${color}" stroke="none" id="population${index}" />`;
return { outsideFillClip, coastalStroke, fillPath };
});

const urban = burgs.filter(b => b.i && !b.removed).map(b => [b.x, b.y, b.y - (b.population / 5) * urbanization]);
population
.select("#urban")
.selectAll("line")
.data(urban)
.enter()
.append("line")
.attr("x1", d => d[0])
.attr("y1", d => d[1])
.attr("x2", d => d[0])
.attr("y2", d => d[1])
.transition(show)
.delay(500)
.attr("y2", d => d[2]);
const coastClips = populationShapes.map(({ outsideFillClip }) => outsideFillClip).join("");
const coastalStrokes = populationShapes.map(({ coastalStroke }) => coastalStroke).join("");
const fillPaths = populationShapes.map(({ fillPath }) => fillPath).join("");
ensureEl("population").innerHTML = `<defs>${coastClips}</defs>${coastalStrokes}${fillPaths}`;

TIME && console.timeEnd("drawPopulation");
}

function closeSvgPathRings(pathData) {
if (!pathData) return "";

return pathData
.split(/(?=M)/)
.filter(Boolean)
.map(ring => (/[zZ]\s*$/.test(ring) ? ring : `${ring}Z`))
.join("");
}

function getPopulationColor(urbanPopulation, totalPopulation, maxPopulation) {
if (!totalPopulation || !maxPopulation) return "#000000";

const urbanShare = urbanPopulation / totalPopulation;
const hue = 240 + 120 * urbanShare;
const value = (Math.log1p(totalPopulation) / Math.log1p(maxPopulation)) ** 1.5;
return hsvToHex(hue, 1, value);
}
Comment on lines +440 to +447

function hsvToHex(hueDegrees, saturation, value) {
const hue = ((hueDegrees % 360) + 360) % 360;
const chroma = value * saturation;
const segment = hue / 60;
const intermediate = chroma * (1 - Math.abs((segment % 2) - 1));

let [red, green, blue] = [0, 0, 0];
if (segment < 1) [red, green] = [chroma, intermediate];
else if (segment < 2) [red, green] = [intermediate, chroma];
else if (segment < 3) [green, blue] = [chroma, intermediate];
else if (segment < 4) [green, blue] = [intermediate, chroma];
else if (segment < 5) [red, blue] = [intermediate, chroma];
else [red, blue] = [chroma, intermediate];

const match = value - chroma;
const toHex = channel => Math.round((channel + match) * 255).toString(16).padStart(2, "0");
return `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
}

function toggleCells(event) {
Expand Down
2 changes: 1 addition & 1 deletion public/modules/ui/style-presets.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ function addStylePreset() {
"#landmass": ["opacity", "fill", "filter"],
"#markers": ["opacity", "rescale", "filter"],
"#prec": ["opacity", "stroke", "stroke-width", "fill", "filter"],
"#population": ["opacity", "stroke-width", "stroke-dasharray", "stroke-linecap", "filter"],
"#population": ["opacity", "filter"],
"#markets": ["opacity", "stroke-width", "fill-opacity", "stroke-opacity", "filter"],
"#goodsCells": ["opacity", "filter"],
"#goodsIcons": ["opacity", "stroke-width", "data-circle", "filter"],
Expand Down
14 changes: 0 additions & 14 deletions public/modules/ui/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ function selectStyleElement() {
"coordinates",
"gridOverlay",
"legend",
"population",
"routes",
"temperature",
"zones"
Expand All @@ -165,7 +164,6 @@ function selectStyleElement() {
"compass",
"coordinates",
"gridOverlay",
"population",
"prec",
"routes",
"temperature",
Expand Down Expand Up @@ -227,18 +225,6 @@ function selectStyleElement() {
styleReliefSet.value = terrain.attr("set");
}

if (styleElement === "population") {
stylePopulation.style.display = "block";
stylePopulationRuralStrokeInput.value = stylePopulationRuralStrokeOutput.value = population
.select("#rural")
.attr("stroke");
stylePopulationUrbanStrokeInput.value = stylePopulationUrbanStrokeOutput.value = population
.select("#urban")
.attr("stroke");
styleStrokeWidth.style.display = "block";
styleStrokeWidthInput.value = el.attr("stroke-width") || 0;
}

if (styleElement === "regions") {
styleStates.style.display = "block";
styleStatesBodyOpacity.value = statesBody.attr("opacity") || 1;
Expand Down
24 changes: 12 additions & 12 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
</style>
<link
rel="preload"
href="index.css?v=1.123.0-2"
href="index.css?v=1.123.0-population-stroke-2"
as="style"
onload="
this.onload = null;
Expand Down Expand Up @@ -468,6 +468,13 @@
>
<u>B</u>iomes
</li>
<li id="togglePopulation"
data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="N"
onclick="togglePopulation(event)"
>
Populatio<u>n</u>
</li>
<li
id="toggleCells"
data-tip="Cells structure: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
Expand Down Expand Up @@ -621,13 +628,6 @@
>
Precipit<u>a</u>tion
</li>
<li id="togglePopulation"
data-tip="Population map: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
data-shortcut="N"
onclick="togglePopulation(event)"
>
Populatio<u>n</u>
</li>
<li
id="toggleEmblems"
data-tip="Emblems: click to toggle, drag to raise or lower the layer. Ctrl + click to edit layer style"
Expand Down Expand Up @@ -8931,13 +8931,13 @@
<script defer src="libs/alea.min.js?v1.105.0"></script>
<script defer src="libs/polylabel.min.js?v1.105.0"></script>
<script defer src="libs/simplify.js?v1.105.6"></script>
<script defer src="modules/ui/layers.js?v=1.125.0"></script>
<script defer src="modules/ui/layers.js?v=1.125.0-population-coast-stroke-2"></script>
<script defer src="modules/ui/measurers.js?v=1.99.00"></script>
<script defer src="modules/ui/style-presets.js?v=1.125.0"></script>
<script defer src="modules/ui/style-presets.js?v=1.125.0-population-cells-1"></script>
<script defer src="modules/ui/general.js?v=1.125.0"></script>
<script defer src="modules/ui/options.js?v=1.125.0"></script>
<script defer src="main.js?v=1.125.1"></script>
<script defer src="modules/ui/style.js?v=1.125.0"></script>
<script defer src="main.js?v=1.125.1-population-stack-1"></script>
<script defer src="modules/ui/style.js?v=1.125.0-population-cells-1"></script>
<script defer src="modules/ui/editors.js?v=1.125.0"></script>
<script defer src="modules/ui/tools.js?v=1.125.1"></script>
<script defer src="modules/ui/world-configurator.js?v=1.120.5"></script>
Expand Down