Published
Edited
Apr 6, 2021
3 stars
Insert cell
md`# Ukraine Population Movement Visualization`
Insert cell
ukraineForm = []
Insert cell
function setRandomCoord(points, offset) {
let number = Math.round(Math.random() * data.length);
let i = -1;
const rad = 0.2;

while (true) {
i++;
if (i >= data.length) {
i = 0;
}
const city = data[i];

if (city.value > 0) continue;
number--;
if (number < 0) {
const phi = Math.random() * Math.PI * 2;
points[offset] = city.lon + rad * Math.cos(phi);
points[offset + 1] = city.lat + (rad / 2) * Math.sin(phi);
return city;
}
}
}
Insert cell
function shouldBeRecreated(points, offset) {
const lon = points[offset];
const lat = points[offset + 1];
let minDistance = Infinity;
for (let i = 0; i < data.length; i++) {
const city = data[i];
if (city.value < 0) continue;
const dist = (city.lat - lat) ** 2 + (city.lon - lon) ** 2;
if (dist < minDistance) {
minDistance = dist;
}
}
return minDistance < 0.01;
}
Insert cell
chart = {
const canvas = d3
.create('canvas')
.attr('width', width)
.attr('height', height)
.attr(
'style',
'border: 1px solid #eaeaea; background-image: url(https://i2.rozetka.ua/goods/20190919/253286261_images_20190919481.jpg); background-size: 110%; background-position-y: -150px; background-position-x: -20px;'
);

const lonScale = d3
.scaleLinear()
.domain([minLon, maxLon])
.range([paddingLeft, width - paddingRight]);

const latScale = d3
.scaleLinear()
.domain([minLat, maxLat])
.range([height - paddingBottom, paddingTop]);

const ctx = canvas.node().getContext('2d');
const STRUCT_SIZE = 5;

const points = Array.from({ length: pointsNumber * STRUCT_SIZE }, ind => 0);
function getRandomLon() {
return Math.random() * (maxLon - minLon) + minLon;
}
function getRandomLat() {
return Math.random() * (maxLat - minLat) + minLat;
}
let currentSecond = Date.now() / 1000;

function init() {
currentSecond = Date.now() / 1000;
for (let i = 0; i < pointsNumber * STRUCT_SIZE; i += STRUCT_SIZE) {
points[i] = getRandomLon();
points[i + 1] = getRandomLat();
points[i + 2] = 0;
points[i + 3] = 0;
points[i + 4] = Date.now() / 1000;
}
}

init();

canvas.node().addEventListener('click', init);

const dt = 5e-3;
while (true) {
currentSecond = Date.now() / 1000;
// update

for (let i = 0; i < pointsNumber * STRUCT_SIZE; i += STRUCT_SIZE) {
const creationTime = points[i + 4];
const lon = points[i];
const lat = points[i + 1];
const force = getForce(lon, lat);
points[i + 2] = force[0] * dt;
points[i + 3] = force[1] * dt;
points[i] += points[i + 2] * dt;
points[i + 1] += points[i + 3] * dt;
const v2 = points[i + 2] ** 2 + points[i + 3] ** 2;

if (shouldBeRecreated(points, i)) {
setRandomCoord(points, i);
points[i + 2] = 0;
points[i + 3] = 0;
points[i + 4] = currentSecond;
}
}

// draw
ctx.clearRect(0, 0, width, height);

ctx.fillStyle = '#333';
for (let i = 0; i < pointsNumber * STRUCT_SIZE; i += STRUCT_SIZE) {
const lon = points[i];
const lat = points[i + 1];
const x = lonScale(lon);
const y = latScale(lat);
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2, false);
ctx.fill();
ctx.beginPath();
ctx.moveTo(x, y);
const nextX = lonScale(points[i] + points[i + 2]);
const nextY = latScale(points[i + 1] + points[i + 3]);
const dist = Math.sqrt((nextX - x) ** 2 + (nextY - y) ** 2);
const unitX = (nextX - x) / dist;
const unitY = (nextY - y) / dist;
ctx.lineTo(x + unitX * 5, y + unitY * 5);
ctx.stroke();
}

ctx.save();
ctx.fillStyle = '#fff';
for (const d of data) {
const x = lonScale(d.lon);
const y = latScale(d.lat);
if (d.value > 0) {
ctx.strokeStyle = 'crimson';
} else {
ctx.strokeStyle = '#369';
}
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2, false);
ctx.fill();
ctx.stroke();
}
ctx.restore();

yield canvas.node();
}
canvas.node().removeEventListener('click', init);
}
Insert cell
maxParticleLiveDuration = 6
Insert cell
function getForce(lon, lat) {
let alon = 0;
let alat = 0;
for (let j = 0; j < data.length; j++) {
const { lat: cityLat, lon: cityLon, value } = data[j];
const dist = Math.sqrt((cityLon - lon) ** 2 + (cityLat - lat) ** 2);
const unitLon = (cityLon - lon) / dist;
const unitLat = (cityLat - lat) / dist;
alon += (unitLon * value) / dist / dist;
alat += (unitLat * value) / dist / dist;
}
return [alon, alat];
}
Insert cell
function euclidSquare(x, y) {
return x ** 2 + y ** 2;
}
Insert cell
function getForce2(lon, lat) {
const dlon = 0.01;
const dlat = 0.01;
const centerForce = getForce(lon, lat);
const rightForce = getForce(lon + dlon, lat);
const topForce = getForce(lon, lat + dlat);
const dfdlon =
(euclidSquare(...rightForce) - euclidSquare(...centerForce)) / dlon;
const dfdlat =
(euclidSquare(...topForce) - euclidSquare(...centerForce)) / dlat;
return [-dfdlon, -dfdlat].map(x => x * 0.1);
}
Insert cell
pointsNumber = 5000
Insert cell
paddingLeft = 10
Insert cell
paddingRight = 10
Insert cell
paddingTop = 20
Insert cell
paddingBottom = 20
Insert cell
width = 768
Insert cell
height = (1 / (maxLon - minLon)) * (maxLat - minLat) * width * 2
Insert cell
minLon = Math.min(...R.pluck('lon', data))
Insert cell
maxLon = Math.max(...R.pluck('lon', data))
Insert cell
minLat = Math.min(...R.pluck('lat', data))
Insert cell
maxLat = Math.max(...R.pluck('lat', data))
Insert cell
data = arr.map(({ place, delta }) => ({
value: delta,
place,
lon: coordinatesByCityName[place].lon,
lat: coordinatesByCityName[place].lat
}))
Insert cell
Insert cell
Insert cell
Insert cell
d3 = require('d3')
Insert cell
R = require('ramda')
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