diff --git a/index.css b/index.css index 4bd1a328..505b0716 100644 --- a/index.css +++ b/index.css @@ -263,7 +263,7 @@ i.icon-lock { } #labels { - text-anchor: start; + text-anchor: middle; dominant-baseline: central; cursor: pointer; } diff --git a/index.html b/index.html index b13b0df4..752da95b 100644 --- a/index.html +++ b/index.html @@ -138,7 +138,7 @@ } - + @@ -7947,7 +7947,7 @@ - + diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index d41ea9bc..ff433f46 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -502,223 +502,6 @@ window.BurgsAndStates = (function () { TIME && console.timeEnd("updateCulturesForBurgsAndStates"); }; - // calculate and draw curved state labels for a list of states - const drawStateLabelsOld = function (list) { - TIME && console.time("drawStateLabels"); - const {cells, features, states} = pack; - const paths = []; // text paths - lineGen.curve(d3.curveBundle.beta(1)); - const mode = options.stateLabelsMode || "auto"; - - for (const s of states) { - if (!s.i || s.removed || s.lock || !s.cells || (list && !list.includes(s.i))) continue; - - const used = []; - const visualCenter = findCell(s.pole[0], s.pole[1]); - const start = cells.state[visualCenter] === s.i ? visualCenter : s.center; - const hull = getHull(start, s.i, s.cells / 10); - const points = [...hull].map(v => pack.vertices.p[v]); - const delaunay = Delaunator.from(points); - const chain = connectCenters(voronoi.vertices, s.pole[1]); - const voronoi = new Voronoi(delaunay, points, points.length); - const relaxed = chain.map(i => voronoi.vertices.p[i]).filter((p, i) => i % 15 === 0 || i + 1 === chain.length); - paths.push([s.i, relaxed]); - - function getHull(start, state, maxLake) { - const queue = [start]; - const hull = new Set(); - - while (queue.length) { - const q = queue.pop(); - const sameStateNeibs = cells.c[q].filter(c => cells.state[c] === state); - - cells.c[q].forEach(function (c, d) { - const passableLake = features[cells.f[c]].type === "lake" && features[cells.f[c]].cells < maxLake; - if (cells.b[c] || (cells.state[c] !== state && !passableLake)) return hull.add(cells.v[q][d]); - - const hasCoadjacentSameStateCells = sameStateNeibs.some(neib => cells.c[c].includes(neib)); - if (hull.size > 20 && !hasCoadjacentSameStateCells && !passableLake) return hull.add(cells.v[q][d]); - - if (used[c]) return; - used[c] = 1; - queue.push(c); - }); - } - - return hull; - } - - function connectCenters(c, y) { - // check if vertex is inside the area - const inside = c.p.map(function (p) { - if (p[0] <= 0 || p[1] <= 0 || p[0] >= graphWidth || p[1] >= graphHeight) return false; // out of the screen - return used[findCell(p[0], p[1])]; - }); - - const pointsInside = d3.range(c.p.length).filter(i => inside[i]); - if (!pointsInside.length) return [0]; - const h = c.p.length < 200 ? 0 : c.p.length < 600 ? 0.5 : 1; // power of horyzontality shift - const end = - pointsInside[ - d3.scan( - pointsInside, - (a, b) => c.p[a][0] - c.p[b][0] + (Math.abs(c.p[a][1] - y) - Math.abs(c.p[b][1] - y)) * h - ) - ]; // left point - const start = - pointsInside[ - d3.scan( - pointsInside, - (a, b) => c.p[b][0] - c.p[a][0] - (Math.abs(c.p[b][1] - y) - Math.abs(c.p[a][1] - y)) * h - ) - ]; // right point - - // connect leftmost and rightmost points with shortest path - const queue = new PriorityQueue({comparator: (a, b) => a.p - b.p}); - const cost = [], - from = []; - queue.queue({e: start, p: 0}); - - while (queue.length) { - const next = queue.dequeue(), - n = next.e, - p = next.p; - if (n === end) break; - - for (const v of c.v[n]) { - if (v === -1) continue; - const totalCost = p + (inside[v] ? 1 : 100); - if (from[v] || totalCost >= cost[v]) continue; - cost[v] = totalCost; - from[v] = n; - queue.queue({e: v, p: totalCost}); - } - } - - // restore path - const chain = [end]; - let cur = end; - while (cur !== start) { - cur = from[cur]; - if (inside[cur]) chain.push(cur); - } - return chain; - } - } - - void (function drawLabels() { - const g = labels.select("#states"); - const t = defs.select("#textPaths"); - const displayed = layerIsOn("toggleLabels"); - if (!displayed) toggleLabels(); - - // remove state labels to be redrawn - for (const state of pack.states) { - if (!state.i || state.removed || state.lock) continue; - if (list && !list.includes(state.i)) continue; - - byId(`stateLabel${state.i}`)?.remove(); - byId(`textPath_stateLabel${state.i}`)?.remove(); - } - - const example = g.append("text").attr("x", 0).attr("x", 0).text("Average"); - const letterLength = example.node().getComputedTextLength() / 7; // average length of 1 letter - - paths.forEach(p => { - const id = p[0]; - const state = states[p[0]]; - const {name, fullName} = state; - - const path = p[1].length > 1 ? round(lineGen(p[1])) : `M${p[1][0][0] - 50},${p[1][0][1]}h${100}`; - const textPath = t - .append("path") - .attr("d", path) - .attr("id", "textPath_stateLabel" + id); - const pathLength = p[1].length > 1 ? textPath.node().getTotalLength() / letterLength : 0; // path length in letters - - const [lines, ratio] = getLines(mode, name, fullName, pathLength); - - // prolongate path if it's too short - if (pathLength && pathLength < lines[0].length) { - const points = p[1]; - const f = points[0]; - const l = points[points.length - 1]; - const [dx, dy] = [l[0] - f[0], l[1] - f[1]]; - const mod = Math.abs((letterLength * lines[0].length) / dx) / 2; - points[0] = [rn(f[0] - dx * mod), rn(f[1] - dy * mod)]; - points[points.length - 1] = [rn(l[0] + dx * mod), rn(l[1] + dy * mod)]; - textPath.attr("d", round(lineGen(points))); - } - - example.attr("font-size", ratio + "%"); - const top = (lines.length - 1) / -2; // y offset - const spans = lines.map((l, d) => { - example.text(l); - const left = example.node().getBBox().width / -2; // x offset - return `${l}`; - }); - - const el = g - .append("text") - .attr("id", "stateLabel" + id) - .append("textPath") - .attr("xlink:href", "#textPath_stateLabel" + id) - .attr("startOffset", "50%") - .attr("font-size", ratio + "%") - .node(); - - el.insertAdjacentHTML("afterbegin", spans.join("")); - if (mode === "full" || lines.length === 1) return; - - // check whether multilined label is generally inside the state. If no, replace with short name label - const cs = pack.cells.state; - const b = el.parentNode.getBBox(); - const c1 = () => +cs[findCell(b.x, b.y)] === id; - const c2 = () => +cs[findCell(b.x + b.width / 2, b.y)] === id; - const c3 = () => +cs[findCell(b.x + b.width, b.y)] === id; - const c4 = () => +cs[findCell(b.x + b.width, b.y + b.height)] === id; - const c5 = () => +cs[findCell(b.x + b.width / 2, b.y + b.height)] === id; - const c6 = () => +cs[findCell(b.x, b.y + b.height)] === id; - if (c1() + c2() + c3() + c4() + c5() + c6() > 3) return; // generally inside => exit - - // move to one-line name - const text = pathLength > fullName.length * 1.8 ? fullName : name; - example.text(text); - const left = example.node().getBBox().width / -2; // x offset - el.innerHTML = `${text}`; - - const correctedRatio = minmax(rn((pathLength / text.length) * 60), 40, 130); - el.setAttribute("font-size", correctedRatio + "%"); - }); - - example.remove(); - if (!displayed) toggleLabels(); - })(); - - function getLines(mode, name, fullName, pathLength) { - // short name - if (mode === "short" || (mode === "auto" && pathLength < name.length)) { - const lines = splitInTwo(name); - const ratio = pathLength / lines[0].length; - return [lines, minmax(rn(ratio * 60), 50, 150)]; - } - - // full name: one line - if (pathLength > fullName.length * 2.5) { - const lines = [fullName]; - const ratio = pathLength / lines[0].length; - return [lines, minmax(rn(ratio * 70), 70, 170)]; - } - - // full name: two lines - const lines = splitInTwo(fullName); - const ratio = pathLength / lines[0].length; - return [lines, minmax(rn(ratio * 60), 70, 150)]; - } - - TIME && console.timeEnd("drawStateLabels"); - }; - // calculate states data like area, population etc. const collectStatistics = function () { TIME && console.time("collectStatistics"); diff --git a/modules/dynamic/auto-update.js b/modules/dynamic/auto-update.js index 5bcb2c2b..fcd12273 100644 --- a/modules/dynamic/auto-update.js +++ b/modules/dynamic/auto-update.js @@ -698,4 +698,11 @@ export function resolveVersionConflicts(version) { } }); } + + if (version < 1.92) { + // v1.92 change labels text-anchor from 'start' to 'middle' + labels.selectAll("tspan").each(function () { + this.setAttribute("x", 0); + }); + } } diff --git a/modules/renderers/drawStatelabels.js b/modules/renderers/drawStatelabels.js index d51e3e66..0c82a4cd 100644 --- a/modules/renderers/drawStatelabels.js +++ b/modules/renderers/drawStatelabels.js @@ -1,6 +1,7 @@ "use strict"; -function drawStateLabels() { +// list - an optional array of stateIds to regenerate +function drawStateLabels(list) { console.time("drawStateLabels"); const {cells, states, features} = pack; @@ -22,7 +23,8 @@ function drawStateLabels() { const labelPaths = []; for (const state of states) { - if (!state.i || state.removed || state.locked) continue; + if (!state.i || state.removed || state.lock) continue; + if (list && !list.includes(state.i)) continue; const offset = getOffsetWidth(state.cells); const maxLakeSize = state.cells / 50; @@ -115,17 +117,17 @@ function drawStateLabels() { const textGroup = d3.select("g#labels > g#states"); const pathGroup = d3.select("defs > g#deftemp > g#textPaths"); - const testLabel = textGroup.append("text").attr("x", 0).attr("x", 0).text("Example"); + const testLabel = textGroup.append("text").attr("x", 0).attr("y", 0).text("Example"); const letterLength = testLabel.node().getComputedTextLength() / 7; // approximate length of 1 letter testLabel.remove(); for (const [stateId, pathPoints] of labelPaths) { const state = states[stateId]; - if (!state.i || state.removed) throw new Error("State must not be neutral"); + if (!state.i || state.removed) throw new Error("State must not be neutral or removed"); if (pathPoints.length < 2) throw new Error("Label path must have at least 2 points"); - textGroup.select("#textPath_stateLabel" + stateId).remove(); - pathGroup.select("#stateLabel" + stateId).remove(); + textGroup.select("#stateLabel" + stateId).remove(); + pathGroup.select("#textPath_stateLabel" + stateId).remove(); const textPath = pathGroup .append("path") diff --git a/modules/ui/labels-editor.js b/modules/ui/labels-editor.js index 8bd04cdd..d19de7ae 100644 --- a/modules/ui/labels-editor.js +++ b/modules/ui/labels-editor.js @@ -78,7 +78,9 @@ function editLabel() { } function updateValues(textPath) { - document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")].map(tspan => tspan.textContent).join("|"); + document.getElementById("labelText").value = [...textPath.querySelectorAll("tspan")] + .map(tspan => tspan.textContent) + .join("|"); document.getElementById("labelStartOffset").value = parseFloat(textPath.getAttribute("startOffset")); document.getElementById("labelRelativeSize").value = parseFloat(textPath.getAttribute("font-size")); } @@ -298,22 +300,15 @@ function editLabel() { function changeText() { const input = document.getElementById("labelText").value; const el = elSelected.select("textPath").node(); - const example = d3.select(elSelected.node().parentNode).append("text").attr("x", 0).attr("x", 0).attr("font-size", el.getAttribute("font-size")).node(); const lines = input.split("|"); - const top = (lines.length - 1) / -2; // y offset - const inner = lines - .map((l, d) => { - example.innerHTML = l; - const left = example.getBBox().width / -2; // x offset - return `${l}`; - }) - .join(""); + if (lines.length > 1) { + const top = (lines.length - 1) / -2; // y offset + el.innerHTML = lines.map((line, index) => `${line}`).join(""); + } else el.innerHTML = `${lines}`; - el.innerHTML = inner; - example.remove(); - - if (elSelected.attr("id").slice(0, 10) === "stateLabel") tip("Use States Editor to change an actual state name, not just a label", false, "warning"); + if (elSelected.attr("id").slice(0, 10) === "stateLabel") + tip("Use States Editor to change an actual state name, not just a label", false, "warning"); } function generateRandomName() { diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 4f00d983..e1d554da 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -570,9 +570,8 @@ function addLabelOnClick() { .attr("data-size", 18) .attr("filter", null); - const example = group.append("text").attr("x", 0).attr("x", 0).text(name); + const example = group.append("text").attr("x", 0).attr("y", 0).text(name); const width = example.node().getBBox().width; - const x = width / -2; // x offset; example.remove(); group.classed("hidden", false); @@ -584,7 +583,7 @@ function addLabelOnClick() { .attr("startOffset", "50%") .attr("font-size", "100%") .append("tspan") - .attr("x", x) + .attr("x", 0) .text(name); defs diff --git a/versioning.js b/versioning.js index ab4fb4e1..fccb2bac 100644 --- a/versioning.js +++ b/versioning.js @@ -1,7 +1,7 @@ "use strict"; // version and caching control -const version = "1.91.05"; // generator version, update each time +const version = "1.92.00"; // generator version, update each time { document.title += " v" + version;