Public
Edited
Mar 26, 2023
Insert cell
Insert cell
incrementalMapContainer = htl.html`<div class="scrolling">`;
Insert cell
svgIncremental = getSvgIncremental(worldMapImage, colorTable, '.centralization[data-centralization="false"]')
Insert cell
makeScrollable(svgIncremental, incrementalMapContainer);
Insert cell
Insert cell
function makeScrollable(svg, container) {
const [ viewBoxWidth, viewBoxHeight ] = svg.getAttribute("viewBox").split(" ").slice(2, 4).map(c => parseInt(c));
const svgHeightRatio = viewBoxHeight / viewBoxWidth;
const height = Math.round(width * svgHeightRatio);
container.style.overflow = "hidden";
container.style.cursor = "grab";
container.style.transition = "transform 0.5s";
container.style.height = `${height}px`;
svg.style.display = "block";
svg.style.transformOrigin = "0 0";
svg.style.width = "auto";
container.innerHTML = "";
container.append(svg);

let zoomExtent = [[0,0],[width,height]];
let zoom = d3.zoom()
.extent(zoomExtent)
.scaleExtent([1, 32])
.translateExtent(zoomExtent)
.on("zoom", onZoom);
d3.select(container).call(zoom);

function onZoom({transform: d3transform}) {
svg.style.transform = `translate(${d3transform.x}px, ${d3transform.y}px) scale(${d3transform.k})`;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
worldMapImage.heightRatio
Insert cell
async function getSvgIncremental(worldMapImage, colorTable, removeSelector) {
let cancellationToken = getCancellationToken();
invalidation.then(() => cancellationToken.cancel = true);

const tmpWidth = 480;
const tmpHeight = tmpWidth * worldMapImage.heightRatio;
const svgMap = svg`
<svg viewBox="0 0 ${tmpWidth} ${tmpHeight}" xmlns="http://www.w3.org/2000/svg">
<style>
.spinner { animation: kf .8s linear infinite; animation-delay:-.8s }
.spinner.dot2 { animation-delay: -.65s }
.spinner.dot3 { animation-delay: -.5s }
@keyframes kf {
93.75%, 100% { opacity:.2 }
}
</style>
<g id="loading" transform="translate(${tmpWidth/2 - 12} ${tmpHeight/2 - 12})">
<circle class="spinner dot1" cx="4" cy="12" r="3"/>
<circle class="spinner dot2" cx="12" cy="12" r="3"/>
<circle class="spinner dot3" cx="20" cy="12" r="3"/>
</g>
</svg>`;

const slicer = new TileSlicer(worldMapImage);
const minScale = getMinScale(slicer);
const maxScale = getMaxScale(slicer);
getLowres().then(getHires);
return svgMap;

async function getLowres() {
let newMap = await getSvgInner(minScale);
svgMap.setAttribute("viewBox", newMap.getAttribute("viewBox"));
svgMap.replaceChildren(...newMap.children);
await pauseIfNeeded();
}

async function getHires() {
if (maxScale == minScale || cancellationToken.cancel) return;
const newMap = await getSvgInner(maxScale);
const currentPaths = extractSvgPaths(svgMap);
const newPaths = extractSvgPaths(newMap);
mergeSvgPaths(currentPaths, newPaths);
await new Promise(r => requestAnimationFrame(r));
}
async function getSvgInner(scale) {
let newMap = await getSvg(worldMapImage, colorTable, scale, cancellationToken);
if (removeSelector) [...newMap.querySelectorAll(removeSelector)].forEach(e => e.remove());
await pauseIfNeeded();
return newMap;
}

function extractSvgPaths(svgMap) {
const paths = {};
for (let g of svgMap.querySelectorAll("g.layer")) {
let layerName = g.getAttribute("class").split(" ").filter(c => c != "layer").join(" ");
let prop = mapSpec.layers[layerName].prop;
let layerPaths = paths[layerName] = new Map();
for (let path of g.querySelectorAll("path")) {
let value = path.dataset[prop];
layerPaths.set(value, path);
}
}
return paths;
}

function mergeSvgPaths(currentPaths, newPaths) {
for (let [layerName, newLayerPaths] of Object.entries(newPaths)) {
const currentLayerPaths = currentPaths[layerName];
let currentGroup = svgMap.querySelector(`g.layer.${layerName}`);
for (let [key, newPath] of newLayerPaths) {
const currentPath = currentLayerPaths.get(key);
if (!currentPath) {
currentGroup.prepend(newPath);
}
else {
const newPathString = newPath.getAttribute("d");
currentPath.setAttribute("d", newPathString)
}
}
}
}
}
Insert cell
getSvg(worldMapImage, colorTable, 4)
Insert cell
async function getSvg(worldMapImage, colorTable, downscaleFactor, cancellationToken) {
cancellationToken ??= getCancellationToken();
invalidation.then(() => cancellationToken.cancel = true);

const polygons = await tracePolygons(worldMapImage, colorTable, downscaleFactor, cancellationToken);
await pauseIfNeeded();
let topology = buildTopologyFromPolygons(polygons);
await pauseIfNeeded();
topology = smoothTopology(topology);
await pauseIfNeeded();
topology = splitTopology(topology);
await pauseIfNeeded();

if (downscaleFactor > 1) {
for (let arc of topology.arcs) {
for (let i = 0; i < arc.length; i++) {
arc[i] = arc[i].map(c => c * downscaleFactor);
}
}
topology.bbox = topology.bbox.map(c => c * downscaleFactor);
await pauseIfNeeded();
}

return getSvgFromTopology(topology);
}
Insert cell
mapSpec
Insert cell
function getSvgFromTopology(topology) {
function svgGroupFromLayer(topology, layerName) {
const features = topojson.feature(topology, layerName).features;
const className = mapSpec.layers[layerName].prop;
let paths = [];
for (let obj of features) {
let { colorNum, id } = obj.properties;
let path = svg`<path fill="${ColorNum.toHex6(colorNum)}">`;
paths.push(path);
let rings = obj.geometry.coordinates.flat();
let pathStrings = [];
for (let ring of rings) {
pathStrings.push(ring.map((c,i) => i?`${c[0]} ${c[1]}`:`M${c[0]} ${c[1]}`).join(" ") + "Z");
}
path.setAttribute("d", pathStrings.join(" "));
path.setAttribute("class", className);
path.dataset[className] = id;
}
return svg`<g class="layer ${layerName}">${paths}`
}

debugger;
const sortedKeys = _.sortBy(Object.entries(mapSpec.layers).map(e => [e[0], e[1].zIndex]), 1).map(e => e[0]);
const groups = sortedKeys.map(n => svgGroupFromLayer(topology, n));
return svg`<svg viewBox="${topology.bbox.join(" ")}" style="background-color: #ff00ff33">${groups}`
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colorTable.canvas
Insert cell
Insert cell
colorSwap.getLayerProp("states", 1103571455)
Insert cell
colorSwap = {
const provincesFlat = [];
for (let [colorNum, props] of mapSpec.provinceProps) {
const traceId = Object.values(mapSpec.layers).map(p => props[p.prop]).join("|");
provincesFlat.push({ colorNum, traceId, props });
}

const uniques = new Map();
for (let group of Object.values(_.groupBy(provincesFlat, "traceId"))) {
const props = group[0].props;
props.colorNum = group[0].colorNum;
const sourceColorNums = group.map(p => p.colorNum);
uniques.set(group[0].colorNum, { props, sourceColorNums });
}
debugger;
const colorSwap = {};
const sourceColors = colorSwap.sourceColors = new Map();
const traceColorProps = colorSwap.traceColorProps = new Map();
for (let [traceColorNum, { props, sourceColorNums }] of uniques) {
sourceColorNums.forEach(sc => colorSwap.sourceColors.set(sc, traceColorNum));
colorSwap.traceColorProps.set(traceColorNum, props);
}
colorSwap.getLayerProp = function(layerName, traceColorNum) {
const props = traceColorProps.get(traceColorNum);
if (!props) return {};
const prop = mapSpec.layers[layerName].prop;
return { name: prop, value: props[prop] };
}
debugger;
return colorSwap;

/*
const layerProps = Object.values(layers).map(l => l.prop);
return mapSpec.provinceProps
function getSwapKey(props) {
layerProps
}

for (let [colorNum, props] of colorSwap) {
}
const mapLayerProps = mapLayerNames.filter(l => l != "traced")
.map(l => [{
result[l] = rollupFields[l].singular;
});
return result;
}
*/
}
Insert cell
[...mapSpec.provinceProps.entries()].map(([colorNum, props]) => Object.assign({}, props, { colorNum }))
Insert cell
mapSpec
Insert cell
colorSwapOld
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more