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

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more