Published
Edited
May 24, 2019
5 forks
20 stars
Insert cell
Insert cell
Insert cell
viewof map = {
const container = html`<div id='map' style='height:800px;' />`;
yield container; // Give the container dimensions
const map = new mapboxgl.Map({
container,
center: [4.9, 52.4],
zoom: 9.5,
bearing: 0,
pitch: 50,
style: "mapbox://styles/mapbox/dark-v10",
antialias: true,
});
map.on("load", () => {
map.addLayer(flowsLayer, 'road-label');
map.addLayer(locationsLayer, 'road-label');
map.setLayoutProperty('country-label', 'visibility', 'none');
});
while (true) {
map.jumpTo({ bearing: (map.getBearing() + 0.04)%360 });
yield container;
}
}
Insert cell
Insert cell
flowsLayer = ({
id: 'flows',
type: 'custom',

onAdd: function (map, gl) {
this.program = gl.createProgram();
gl.attachShader(this.program, compileShader(gl, gl.VERTEX_SHADER, `
#define WAVE_FACTOR 2000.0
#define WAVE_SPEED 0.005
precision highp float;
uniform mat4 u_matrix;
uniform int u_time;
attribute vec2 a_origin_loc;
attribute vec2 a_dest_loc;
attribute vec4 a_color;
attribute float a_thickness;
attribute float a_instance_index;
attribute vec2 a_square_vert;
varying vec2 v_square_pos;
varying vec4 v_color;
varying float v_source_to_target;
void main() {
v_square_pos = a_square_vert.xy; // position within [-1, 1] square
v_color = a_color;
float height = a_thickness * a_square_vert.y * mix(0.0, 1.0, v_square_pos.x);
gl_Position = u_matrix * vec4(mix(a_origin_loc.xy, a_dest_loc.xy, v_square_pos.x), 0.0, 1.0) +
vec4(a_square_vert.x, height, 0.0, 1.0);
// Adding a_instance_index to prevent the "pulsing" effect:
// when all waves depart from their origins at the same time
float t = (float(u_time) + a_instance_index) * WAVE_SPEED;
v_source_to_target = v_square_pos.x * length(a_origin_loc - a_dest_loc) * WAVE_FACTOR - t;
}
`));
gl.attachShader(this.program, compileShader(gl, gl.FRAGMENT_SHADER, `
precision highp float;
varying vec2 v_square_pos;
varying vec4 v_color;
varying float v_source_to_target;
void main() {
gl_FragColor = vec4(v_color.xyz, v_color.w * smoothstep(0.3, 1.0, fract(v_source_to_target)));
float soften = smoothstep(0.0, 1.0, v_square_pos.y);
gl_FragColor = vec4(gl_FragColor.rgb, gl_FragColor.a * soften);
}
`));
gl.linkProgram(this.program);
// Square vertices
this.aSquareVert = gl.getAttribLocation(this.program, "a_square_vert");
this.squareVerts = new Float32Array([
0, -1,
0, 1,
1, -1,
1, 1,
]);
this.squareVertsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.squareVertsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.squareVerts, gl.STATIC_DRAW);

// Instanced attrs
this.aOrigin = gl.getAttribLocation(this.program, "a_origin_loc");
this.aDest = gl.getAttribLocation(this.program, "a_dest_loc");
this.aColor = gl.getAttribLocation(this.program, "a_color");
this.aThickness = gl.getAttribLocation(this.program, "a_thickness");
this.aInstanceIndex = gl.getAttribLocation(this.program, "a_instance_index");

const originCoords = new Float32Array(flows.length * 2);
const destCoords = new Float32Array(flows.length * 2);
const colors = new Float32Array(flows.length * 4);
const thicknesses = new Float32Array(flows.length);
const instanceIndices = new Float32Array(flows.length);
for (let i = 0; i < flows.length; i++) {
const { origin, dest, count } = flows[i];
originCoords[i * 2] = origin.x;
originCoords[i * 2 + 1] = origin.y;
destCoords[i * 2] = dest.x;
destCoords[i * 2 + 1] = dest.y;
const {r,g,b} = d3.rgb(flowColorScale(count));
colors[i * 4] = r/255;
colors[i * 4 + 1] = g/255;
colors[i * 4 + 2] = b/255;
colors[i * 4 + 3] = flowOpacityScale(count);
thicknesses[i] = flowThicknessScale(count);
instanceIndices[i] = i;
}
this.originCoordsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.originCoordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, originCoords, gl.STATIC_DRAW);
this.destCoordsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.destCoordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, destCoords, gl.STATIC_DRAW);
this.colorsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
this.thicknessBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.thicknessBuffer);
gl.bufferData(gl.ARRAY_BUFFER, thicknesses, gl.STATIC_DRAW);
this.instanceIndexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceIndexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, instanceIndices, gl.STATIC_DRAW);
},

render: function (gl, matrix) {
const ext = gl.getExtension('ANGLE_instanced_arrays');
gl.useProgram(this.program);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
gl.uniform1i(gl.getUniformLocation(this.program, "u_time"), currentTime());

// Square vertices
gl.bindBuffer(gl.ARRAY_BUFFER, this.squareVertsBuffer);
gl.enableVertexAttribArray(this.squareVertsBuffer);
gl.vertexAttribPointer(this.aSquareVert, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aSquareVert, 0); // non-instanced

// Flows
gl.bindBuffer(gl.ARRAY_BUFFER, this.originCoordsBuffer);
gl.enableVertexAttribArray(this.aOrigin);
gl.vertexAttribPointer(this.aOrigin, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aOrigin, 1); // instanced

gl.bindBuffer(gl.ARRAY_BUFFER, this.destCoordsBuffer);
gl.enableVertexAttribArray(this.aDest);
gl.vertexAttribPointer(this.aDest, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aDest, 1); // instanced

gl.bindBuffer(gl.ARRAY_BUFFER, this.colorsBuffer);
gl.enableVertexAttribArray(this.aColor);
gl.vertexAttribPointer(this.aColor, 4, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aColor, 1); // instanced

gl.bindBuffer(gl.ARRAY_BUFFER, this.thicknessBuffer);
gl.enableVertexAttribArray(this.aThickness);
gl.vertexAttribPointer(this.aThickness, 1, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aThickness, 1); // instanced

gl.bindBuffer(gl.ARRAY_BUFFER, this.instanceIndexBuffer);
gl.enableVertexAttribArray(this.aInstanceIndex);
gl.vertexAttribPointer(this.aInstanceIndex, 1, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aInstanceIndex, 1); // instanced

gl.enable(gl.BLEND);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
ext.drawArraysInstancedANGLE(gl.TRIANGLE_STRIP, 0, this.squareVerts.length / 2, flows.length);
}
})
Insert cell
locationsLayer = ({
id: 'locations',
type: 'custom',

onAdd: function (map, gl) {
this.program = gl.createProgram();
gl.attachShader(this.program, compileShader(gl, gl.VERTEX_SHADER, `
precision highp float;
uniform mat4 u_matrix;
attribute vec4 a_location;
attribute vec2 a_square_vert;
varying vec2 v_square_pos;
void main() {
float radius = a_location.z;
float instance_index = a_location.w;
v_square_pos = a_square_vert.xy; // position within [-1, 1] square
float scale_y = u_matrix[1][1]/u_matrix[0][0];
gl_Position = u_matrix * vec4(a_location.xy, 0.0, 1.0) +
radius * vec4(a_square_vert.x, a_square_vert.y * scale_y, 0.0, 0.0);
}
`));
gl.attachShader(this.program, compileShader(gl, gl.FRAGMENT_SHADER, `
#define SOFT_OUTLINE 0.7
precision highp float;
varying vec2 v_square_pos;
varying float v_color_intensity;
uniform vec4 u_color;
void main() {
if (length(v_square_pos) > 1.0) { discard; }
float soften = smoothstep(0.0, SOFT_OUTLINE, 1.0 - length(v_square_pos));
gl_FragColor = vec4(u_color.xyz, u_color.w * soften);
}
`));
gl.linkProgram(this.program);
// Square vertices
this.aSquareVert = gl.getAttribLocation(this.program, "a_square_vert");
this.squareVerts = new Float32Array([
// a square to cover the unit circle
-1, -1,
-1, 1,
1, 1,
1, -1,
]);
this.squareVertsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.squareVertsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.squareVerts, gl.STATIC_DRAW);

// Locations
this.aLocationCoords = gl.getAttribLocation(this.program, "a_location");
const locationCoords = new Float32Array(locations.length * 4);
locations.sort((a, b) => d3.ascending(
locationTotals.max.get(a.id) || 0,
locationTotals.max.get(b.id) || 0,
));
for (let i = 0; i < locations.length; i++) {
const { id, x, y } = locations[i];
locationCoords[i * 4] = x;
locationCoords[i * 4 + 1] = y;
locationCoords[i * 4 + 2] = locationRadiusScale(locationTotals.max.get(id));
locationCoords[i * 4 + 3] = (1.0 - i / locations.length);
}
this.locationCoordsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.locationCoordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, locationCoords, gl.STATIC_DRAW);
},

render: function (gl, matrix) {
const ext = gl.getExtension('ANGLE_instanced_arrays');
gl.useProgram(this.program);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);
const {r,g,b} = d3.rgb(flowColorScale(maxFlowMagnitude));
gl.uniform4fv(gl.getUniformLocation(this.program, "u_color"), [r/255,g/255,b/255,1.0]);

// Square vertices
gl.bindBuffer(gl.ARRAY_BUFFER, this.squareVertsBuffer);
gl.enableVertexAttribArray(this.squareVertsBuffer);
gl.vertexAttribPointer(this.aSquareVert, 2, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aSquareVert, 0); // non-instanced

// Location coords
gl.bindBuffer(gl.ARRAY_BUFFER, this.locationCoordsBuffer);
gl.enableVertexAttribArray(this.aLocationCoords);
gl.vertexAttribPointer(this.aLocationCoords, 4, gl.FLOAT, false, 0, 0);
ext.vertexAttribDivisorANGLE(this.aLocationCoords, 1); // instanced
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

ext.drawArraysInstancedANGLE(gl.TRIANGLE_FAN, 0, this.squareVerts.length / 2, locations.length);
}
})
Insert cell
maxFlowMagnitude = d3.max(flows, flow => flow.count)
Insert cell
flowThicknessScale = d3.scaleLinear()
.range([4, 75])
.domain([0, maxFlowMagnitude])
Insert cell
flowOpacityScale = d3.scalePow()
.exponent(1/5)
.range([0, 1.0])
.domain([0, maxFlowMagnitude])
Insert cell
flowColorScale = d3.scaleSequentialPow(d3.interpolateYlGnBu)
.exponent(1/3)
.domain([maxFlowMagnitude,0])
Insert cell
locationRadiusScale = d3.scalePow(1/2)
.range([2, 150])
.domain([0, d3.max(Array.from(locationTotals.incoming.values()))])
Insert cell
locationTotals = {
const incoming = new Map();
const outgoing = new Map();
for (const { origin, dest, count } of flows) {
incoming.set(dest.id, (incoming.get(dest.id) || 0) + count);
outgoing.set(origin.id, (outgoing.get(origin.id) || 0) + count);
}
const max = new Map();
for (const { id } of locations) {
max.set(id, Math.max(incoming.get(id) || 0, outgoing.get(id) || 0));
}
return { incoming, outgoing, max };
}
Insert cell
function compileShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) return shader;
throw new Error(gl.getShaderInfoLog(shader));
}
Insert cell
locationsById = d3.nest().key(d => d.id).rollup(([d]) => d).object(locations)
Insert cell
locations = {
const response = await fetch(
'https://docs.google.com/spreadsheets/d/1Oe3zM219uSfJ3sjdRT90SAK2kU3xIvzdcCW6cwTsAuc/gviz/tq?tq=SELECT%20A,B,C,D&tqx=out:csv&sheet=locations'
);
return d3.csvParse(await response.text()).map(loc => ({
...loc,
...mapboxgl.MercatorCoordinate.fromLngLat({ lng: +loc.lon, lat: +loc.lat }),
}));
}
Insert cell
flows = {
const flows = flowsData
.map(({ origin, dest, count }) => ({
origin: locationsById[origin],
dest: locationsById[dest],
count: +count
}));
flows.sort((a, b) => d3.ascending(a.count, b.count));
return flows;
}
Insert cell
flowsData = {
const response = await fetch(
'https://docs.google.com/spreadsheets/d/1Oe3zM219uSfJ3sjdRT90SAK2kU3xIvzdcCW6cwTsAuc/gviz/tq?tq=SELECT%20A,B,C&tqx=out:csv&sheet=flows'
);
return d3.csvParse(await response.text())
}
Insert cell
Insert cell
d3 = require("d3@5")
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