Published
Edited
May 6, 2019
1 fork
Insert cell
Insert cell
viewof map = {
const container = html`<div style='height:460px;' />`;

// Give the container dimensions.
yield container;

const map = new mapboxgl.Map({
container,
center: [5.456252, 52.137609],
zoom: 6,
style: "mapbox://styles/mapbox/dark-v10",
antialias: true,
});

// Wait until the map loads.
map.on("load", () => {
map.addLayer(flowsLayer, 'road-label');
map.addLayer(locationsLayer, 'road-label');
});
}
Insert cell
flowsLayer = ({
id: 'flows',
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 vec3 a_origin;
attribute vec3 a_dest;
attribute vec4 a_color;
attribute vec3 a_square_vert;
varying vec2 v_square_pos;
varying vec4 v_color;
void main() {
v_square_pos = a_square_vert.xy; // position within [-1, 1] square
v_color = a_color;
gl_Position = u_matrix * vec4(mix(a_origin.xy, a_dest.xy, v_square_pos.x), 0.0, 1.0) +
vec4(a_square_vert.x, 10.0*a_square_vert.y, 0.0, 0.0);
}
`));
gl.attachShader(this.program, compileShader(gl, gl.FRAGMENT_SHADER, `
precision highp float;
varying vec2 v_square_pos;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
`));
gl.linkProgram(this.program);
// Square vertices
this.aSquareVert = gl.getAttribLocation(this.program, "a_square_vert");
this.squareVerts = new Float32Array([
0, -1, 0,
0, 1, 0,
1, -1, 0,
1, 1, 0,
]);
this.squareVertsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.squareVertsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.squareVerts, gl.STATIC_DRAW);

// Flows
this.aOrigin = gl.getAttribLocation(this.program, "a_origin");
this.aDest = gl.getAttribLocation(this.program, "a_dest");
this.aColor = gl.getAttribLocation(this.program, "a_color");
const originCoords = new Float32Array(flows.length * 3);
const destCoords = new Float32Array(flows.length * 3);
const colors = new Float32Array(flows.length * 4);
for (let i = 0; i < flows.length; i++) {
const { origin, dest, count } = flows[i];
originCoords[i * 3] = origin.x;
originCoords[i * 3 + 1] = origin.y;
originCoords[i * 3 + 2] = i;
destCoords[i * 3] = dest.x;
destCoords[i * 3 + 1] = dest.y;
destCoords[i * 3 + 2] = i;
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);
}
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);
},

render: function (gl, matrix) {
gl.useProgram(this.program);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);

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

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

gl.bindBuffer(gl.ARRAY_BUFFER, this.destCoordsBuffer);
gl.enableVertexAttribArray(this.aDest);
gl.vertexAttribPointer(this.aDest, 3, gl.FLOAT, false, 0, 0);
gl.vertexAttribDivisor(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);
gl.vertexAttribDivisor(this.aColor, 1); // instanced

gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, this.squareVerts.length / 3, flows.length);
}
})
Insert cell
maxFlowMagnitude = d3.max(flows, flow => flow.count)
Insert cell
flowOpacityScale = d3.scalePow()
.exponent(1/1000)
.range([0, 1.0])
.domain([0, maxFlowMagnitude])
Insert cell
flowColorScale = d3.scaleSequentialPow(d3.interpolateRgbBasis(d3.schemeYlGn[9].reverse()))
.exponent(1/3)
.domain([0, maxFlowMagnitude])
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 vec3 a_location;
attribute vec3 a_square_vert;
varying vec2 v_square_pos;
void main() {
v_square_pos = a_square_vert.xy; // position within [-1, 1] square
float radius = a_location.z;
float deform = 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 * deform, 0.0, 0.0);
}
`));
gl.attachShader(this.program, compileShader(gl, gl.FRAGMENT_SHADER, `
precision highp float;
varying vec2 v_square_pos;
void main() {
if (length(v_square_pos) > 1.0) { discard; }
if (length(v_square_pos) > 0.95) {
gl_FragColor = vec4(0.1, 0.1, 0.2, 0.5);
} else {
gl_FragColor = vec4(0.80, 0.90, 0.99, 0.98);
}
}
`));
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, 0,
-1, 1, 0,
1, 1, 0,
1, -1, 0,
]);
this.squareVertsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.squareVertsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this.squareVerts, gl.STATIC_DRAW);

// Location coords
this.aLocationCoords = gl.getAttribLocation(this.program, "a_location");
const locationCoords = new Float32Array(locations.length * 3);
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 * 3] = x;
locationCoords[i * 3 + 1] = y;
// radius and z
locationCoords[i * 3 + 2] = locationRadiusScale(locationTotals.max.get(id));
}
this.locationCoordsBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, this.locationCoordsBuffer);
gl.bufferData(gl.ARRAY_BUFFER, locationCoords, gl.STATIC_DRAW);
},

render: function (gl, matrix) {
gl.useProgram(this.program);
gl.uniformMatrix4fv(gl.getUniformLocation(this.program, "u_matrix"), false, matrix);

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

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

gl.drawArraysInstanced(gl.TRIANGLE_FAN, 0, this.squareVerts.length / 3, locations.length);
}
})
Insert cell
locationRadiusScale = d3.scalePow(1/2)
.range([1, 40])
.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 response = await fetch(
'https://docs.google.com/spreadsheets/d/1Oe3zM219uSfJ3sjdRT90SAK2kU3xIvzdcCW6cwTsAuc/gviz/tq?tq=SELECT%20A,B,C&tqx=out:csv&sheet=flows'
);
const flows = d3.csvParse(await response.text())
.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
d3 = require("d3@5")
Insert cell
mapboxgl = {
// Using a fork of mapbox-gl-js with WebGL 2.0 enabled
const response = await fetch("https://raw.githubusercontent.com/ilyabo/mapbox-gl-js/master/dist/mapbox-gl.js");
const blob = await response.blob();
const gl = await require(URL.createObjectURL(blob)).catch(() => window.mapboxgl);
if (!gl.accessToken) {
gl.accessToken =
"pk.eyJ1IjoiaWx5YWJvIiwiYSI6ImNqdjk0YXUwejBsMmY0ZHBmMHdjM25xdTIifQ.DIX2FlU4IC8uCGZfWeWWkA";
const href = await require.resolve("mapbox-gl@0.54/dist/mapbox-gl.css");
document.head.appendChild(html`<link href=${href} rel=stylesheet>`);
}
return gl;
}
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