Published
Edited
Mar 8, 2019
4 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function make_map(cities) {
const context = DOM.context2d(w, h),
canvas = context.canvas,
path = d3.geoPath().context(context),
geopath = d3.geoPath(projection).context(context);

cities = move_centers(cities, projection);

const best = place_labels(cities);
cities = best.cities;

context.font = font;

context.fillStyle = "#eee";
context.beginPath();
geopath({ type: "Sphere" });
context.fill();

context.fillStyle = "#fff";
context.beginPath();
geopath(world);
context.fill();
context.strokeStyle = "#eee";
context.stroke();
context.fillStyle = "#36e";
context.globalAlpha = 0.75;

//console.log(cities);
var regions = new Set();
cities.forEach(c => {
//console.log(c.region_wb);
regions.add(c.properties.region_wb);
});
console.log(regions);
var ordinalScale = d3.scaleOrdinal()
.domain(regions.values())
.range(d3.schemeDark2);
// .range(d3.schemeCategory10);
cities.forEach(c => {
context.beginPath();
context.fillStyle = ordinalScale(c.properties.region_wb);
path.pointRadius(pointRadius(c));
path({
type: "Point",
coordinates: [c.x, c.y]
});
context.fill();
});
context.globalAlpha = 1;

if (debug) {
context.fillText(`${best.i} iterations (score: ${best.score | 0})`, 10, 10);
}

cities.forEach(c => {
if (c.label) {
context.textBaseline = c.baseline;
context.textAlign = c.anchor;
context.fillStyle = positions[c.pos].fill || "black";
context.fillText(c.label, c.lx, c.ly);
}

if (debug) {
if (!c.label) {
context.textBaseline = c.baseline;
context.textAlign = c.anchor;
context.fillStyle = "orange";
context.fillText(c.label || c.properties.name, c.lx, c.ly);
}

context.beginPath();
context.moveTo(c.dimensions.x, c.dimensions.y);
context.lineTo(c.dimensions.x + c.dimensions.width, c.dimensions.y);
context.lineTo(
c.dimensions.x + c.dimensions.width,
c.dimensions.y + c.dimensions.height
);
context.lineTo(c.dimensions.x, c.dimensions.y + c.dimensions.height);
context.lineTo(c.dimensions.x, c.dimensions.y);
context.strokeStyle = c.label ? "green" : "orange";
context.stroke();
}
});

return canvas;
}
Insert cell
function prepare(c, pos) {
c.label = c.properties.name;

switch (pos) {
case 1:
c.anchor = "center";
c.baseline = "bottom";
c.lx = c.x;
c.ly = c.y - (c.r + 1);
break;

case 2:
c.anchor = "center";
c.baseline = "top";
c.lx = c.x;
c.ly = c.y + (c.r + 1);
break;

case 3:
c.anchor = "start";
c.baseline = "middle";
c.lx = c.x + (c.r + 1);
c.ly = c.y;
break;

case 4:
c.anchor = "end";
c.baseline = "middle";
c.lx = c.x - (c.r + 1);
c.ly = c.y;
break;

case 5:
c.anchor = "center";
c.baseline = "middle";
c.lx = c.x;
c.ly = c.y;
break;

default:
c.anchor = "center";
c.baseline = "middle";
c.lx = c.x;
c.ly = c.y;
c.label = "";
break;
}

c.dimensions = dimensions(c);
c.pos = pos;
return c;
}
Insert cell
function test_positions(pos, cities) {
const labelled = [];

let score = 0;

cities.forEach((c, i) => {
c = prepare(c, pos[i]);

if (
labelled.some(a => a.pos > 0 && intersect(a.dimensions, c.dimensions))
) {
c.label = "";
c.pos = pos[i] = 0;
}

score += c.r ** 2 * positions[c.pos].score;
labelled.push(c);
});
return { score, pos };
}
Insert cell
function place_labels(cities) {
cities = cities.sort((a, b) => d3.descending(+a.count, +b.count));
cities.forEach(c => {
c.block = {
x: c.x - c.r,
y: c.y - c.r * 0.9,
width: 2 * c.r,
height: 2 * c.r * 0.9
};
});

const borders = [
{ x: 0, width: 0, y: 0, height: w },
{ x: 0, width: w, y: 0, height: 0 },
{ x: w, width: 0, y: 0, height: w },
{ x: 0, width: w, y: w, height: 0 }
];

const possibilities = cities.map(c =>
Object.keys(positions).filter(p => {
p = +p;
if (!p) return true; // "hidden" is always a possibility
c = prepare(c, p);

if (p === 5) {
// console.warn(c);
return c.dimensions.width < 2 * c.r;
}

return !(
borders.some(a => intersect(a, c.dimensions)) ||
cities.some(a => intersect(a.block, c.dimensions))
);
})
);

let best = {
score: 0,
pos: Uint8Array.from({ length: cities.length })
};
for (let i = 0; i < 150 * cities.length; i++) {
const arrangement = best.pos.map((p, i) =>
Math.random() < 0.3
? possibilities[i][(Math.random() * possibilities[i].length) | 0]
: p
);
const test = test_positions(arrangement, cities);
if (test.score > best.score) {
best = test;
best.i = i;
}
}

best.cities = cities.map((c, i) => prepare(c, best.pos[i]));
return best;
}
Insert cell
function intersect(a, b) {
return !(
a.x + a.width < b.x ||
b.x + b.width < a.x ||
a.y + a.height < b.y ||
b.y + b.height < a.y
);
}
Insert cell
Insert cell
Insert cell
largestPolygon = function(d) {
var best = {};
var bestArea = 0;
d.geometry.coordinates.forEach( function(coords) {
var poly = {'type':'Polygon','coordinates':coords};
var area = d3.geoArea(poly);
if (area > bestArea) {
bestArea = area;
best = poly;
}
});
return best;
}
Insert cell
Insert cell
Insert cell
features = ({
type: "FeatureCollection",
features: world.features
.map(d => {
d.pop = +d.properties.pop_est;
d.text = d.properties.abbrev;
[d.lon, d.lat] = d3.geoCentroid( d.geometry.type == "Polygon" ? d : largestPolygon(d));
d.admin = d.properties.admin;
d.iso_a3 = d.properties.iso_a3;
d.iso_a2 = d.properties.iso_a2;
d.region_wb = d.properties.region_wb;
return d;
})
.sort((a, b) => d3.descending(a.pop, b.pop))
.map(d => ({
type: "Feature",
geometry: { type: "Point", coordinates: [+d.lon, +d.lat] },
properties: {
name: d.text,
group: +d.group,
pop: d.pop,
count: Math.pow(d.pop / 10e6, 1),
iso_a3: d.iso_a3,
admin: d.admin,
iso_a3: d.iso_a3,
region_wb: d.region_wb
}
}))
})
Insert cell
Insert cell
Insert cell
Insert cell
projection = projection1;
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