Published
Edited
Nov 24, 2021
42 stars
Insert cell
Insert cell
Insert cell
basicMap = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
geoPath.context(ctx);
// because each state has a different fill color, we need to draw each separately
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// because all strokes are the same style, we can draw them all to one path
ctx.strokeStyle = '#ccc';
ctx.beginPath(); // start the path
states.forEach((state) => { geoPath(state); }); // draw all states
ctx.stroke(); // stroke once, at the end
return canvas;
}
Insert cell
Insert cell
Insert cell
fancyStrokes = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
// fill the whole background with a water color
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
geoPath.context(ctx);
// these styles are the same for all the next few strokes
ctx.lineJoin = 'round'; // this is easy to forget, but is usually a good idea
ctx.strokeStyle = waterGlowColor;
ctx.beginPath(); // start the path
states.forEach((state) => { geoPath(state); }); // draw all states
// draw the stroke several times with different widths and colors
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 40;
ctx.stroke();
ctx.strokeStyle = '#999';
ctx.lineWidth = 20;
ctx.stroke();
ctx.strokeStyle = '#666';
ctx.lineWidth = 10;
ctx.stroke();
// state fills
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// gray stroke
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.stroke();
return canvas;
}
Insert cell
Insert cell
coastalGlow = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
// fill the whole background with a water color
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
geoPath.context(ctx);
// first we'll draw all the states in a single path, with the shadow/glow
ctx.save(); // save() makes it handy to revert styles later
ctx.fillStyle = 'white';
ctx.shadowColor = waterGlowColor;
ctx.shadowBlur = 30;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.fill();
// we don't want shadows on anything after this
// restore() undoes the style settings that followed save() above; easier than resetting them one by one!
ctx.restore();
// now the normal fills
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// because all strokes are the same style, we can draw them all to one path
ctx.strokeStyle = '#ccc';
ctx.beginPath(); // start the path
states.forEach((state) => { geoPath(state); }); // draw all states
ctx.stroke(); // stroke once, at the end
return canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
offsetCoastLines = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
geoPath.context(ctx);
// these styles are the same for all the next few strokes
ctx.lineJoin = 'round';
ctx.strokeStyle = d3.color(waterGlowColor).darker();
ctx.beginPath(); // start the path
states.forEach((state) => { geoPath(state); }); // draw all states
// draw the offset coastlines. for each one, a big stroke...
ctx.lineWidth = 40;
ctx.stroke();
// ...followed by a slightly narrower stroke in destination-out mode
// this clips out most of the fat stroke and leaves the appearance of a thin, offset stroke
ctx.lineWidth = 38;
ctx.globalCompositeOperation = 'destination-out';
ctx.stroke();
// // then reset to source-over and repeat
ctx.globalCompositeOperation = 'source-over';
ctx.lineWidth = 20;
ctx.stroke();
ctx.lineWidth = 18;
ctx.globalCompositeOperation = 'destination-out';
ctx.stroke();
ctx.globalCompositeOperation = 'source-over';
ctx.lineWidth = 10;
ctx.stroke();
ctx.lineWidth = 8;
ctx.globalCompositeOperation = 'destination-out';
ctx.stroke();
ctx.globalCompositeOperation = 'source-over';
// the above is spelled out for demo purposes, but could be written quite neatly as a function and/or loop
// state fills as usual. these will cover up the interior parts of the offset strokes
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// gray stroke
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.stroke();
// finally, we can use the 'destination-over' mode to draw the water background behind everything
// (if we'd done this first, the coastline stuff would have clipped out pieces of the background)
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
return canvas;
}
Insert cell
Insert cell
innerGlow = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
geoPath.context(ctx);
// let's start with an outer coastal glow, as long as we're feeling glowy
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
ctx.save();
ctx.fillStyle = 'white';
ctx.shadowColor = waterGlowColor;
ctx.shadowBlur = 30;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.fill();
ctx.restore();
// we can draw the fills as usual, on the main canvas
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// now we draw each state stroke/glow
states.forEach((state) => {
// create a canvas for this state
const stateCanvas = DOM.canvas(width, height);
const stateCtx = stateCanvas.getContext('2d');
stateCtx.save(); // we'll need to revert a bunch of settings in a moment
geoPath.context(stateCtx); // the geo path now needs to draw to this canvas
/*
Now some extra complication. Because a blur filter isn't widely supported by all browsers,
to achieve the blurred stroke appearance we'll use a colored shadow instead. We can draw a stroke
way off screen somewhere, but offset its shadow back to the normal place.
*/
// the size of the blur is a combination of this line width and the shadow blur
// we don't see this line, but its size affects the shadow size
stateCtx.lineWidth = 10;
// stroke color doesn't matter because we don't even see it, only its shadow
stateCtx.shadowColor = state.properties.strokeColor;
stateCtx.shadowBlur = 15;
// this moves the drawing somewhere off out of view
stateCtx.setTransform(1, 0, 0, 1, -3000, -3000);
// but offset the shadow back into place
stateCtx.shadowOffsetX = 3000;
stateCtx.shadowOffsetY = 3000;
// draw the stroke
stateCtx.beginPath();
geoPath(state);
stateCtx.stroke();
// revert the shadow and transformation settings
stateCtx.restore();
// now clip the blurred stroke/shadow to the state interior by drawing a fill in destination-in mode
stateCtx.globalCompositeOperation = 'destination-in';
stateCtx.beginPath();
geoPath(state);
stateCtx.fill();
// finally, draw this state canvas onto the main canvas
ctx.drawImage(stateCanvas, 0, 0);
});
// a stroke on top is helpful for clarity
geoPath.context(ctx);
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.stroke();
return canvas;
}
Insert cell
Insert cell
interiorBorders = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
geoPath.context(ctx);
// we can draw the fills as usual, on the main canvas
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// now we draw each state
states.forEach((state) => {
// create a canvas for this state
const stateCanvas = DOM.canvas(width, height);
const stateCtx = stateCanvas.getContext('2d');
geoPath.context(stateCtx); // the geo path now needs to draw to this canvas
stateCtx.lineWidth = 4;
stateCtx.strokeStyle = '#999';
stateCtx.lineJoin = 'round';
// draw the stroke
stateCtx.beginPath();
geoPath(state);
stateCtx.stroke();
// in this case, we want only the exterior border, so destination-out mode
stateCtx.globalCompositeOperation = 'destination-out';
stateCtx.beginPath();
geoPath(state);
stateCtx.fill();
// finally, draw this state canvas onto the main canvas
ctx.drawImage(stateCanvas, 0, 0);
});
geoPath.context(ctx);
// now clip to land area with destination-in. this removes the outer stroke along the coast
ctx.globalCompositeOperation = 'destination-in';
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.fill();
// draw the water last, in destination-over mode so it goes below land
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
return canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
hatchMap = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
geoPath.context(ctx);
for (let x = 0; x < width; x += patternSize) {
for (let y = 0; y < height; y += patternSize) {
ctx.drawImage(pattern, x, y);
}
}
ctx.lineJoin = 'round';
ctx.lineWidth = 40;
ctx.globalCompositeOperation = 'destination-in';
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.stroke();
ctx.globalCompositeOperation = 'source-over';
// because each state has a different fill color, we need to draw each separately
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
// because all strokes are the same style, we can draw them all to one path
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath(); // start the path
states.forEach((state) => { geoPath(state); }); // draw all states
ctx.stroke(); // stroke once, at the end
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
return canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
fancierMap = {
const canvas = DOM.canvas(width, height);
const ctx = canvas.getContext('2d');
geoPath.context(ctx);
ctx.lineJoin = 'round';
ctx.save();
ctx.lineWidth = 5;
ctx.shadowColor = 'black';
ctx.shadowBlur = 30;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
// the shadow tends to look weak by default; stroking several times helps even out the fade from solid to transparent
ctx.stroke();
ctx.stroke();
ctx.stroke();
ctx.stroke();
ctx.restore();
// fill another canvas with the pattern
const patternCanvas = DOM.canvas(width, height);
const patternCtx = patternCanvas.getContext('2d');
for (let x = 0; x < width; x += patternSize) {
for (let y = 0; y < height; y += patternSize) {
patternCtx.drawImage(patternWithGaps, x, y);
}
}
// draw pattern canvas, keeping only the part overlapping the blurry coastline
ctx.globalCompositeOperation = 'source-in';
ctx.drawImage(patternCanvas, 0, 0);
ctx.globalCompositeOperation = 'source-over';
states.forEach((state) => {
ctx.fillStyle = state.properties.fillColor;
ctx.beginPath();
geoPath(state);
ctx.fill();
});
ctx.strokeStyle = '#ccc';
ctx.lineWidth = 1;
ctx.beginPath();
states.forEach((state) => { geoPath(state); });
ctx.stroke();
ctx.globalCompositeOperation = 'destination-over';
ctx.fillStyle = waterColor;
ctx.fillRect(0, 0, width, height);
return canvas;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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