Public
Edited
Oct 4, 2021
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
// Text annotations based on ‘A Scottish paper landscape: Ordnance Survey Maps - Six-inch 1st edition, Scotland, 1843-1882’https://maps.nls.uk/os/6inch/os_info1.html, an essay by Christopher Fleet (National Library of Scotland) and Charles W J Withers (University of Edinburgh).

animated_map = {
const wrapper_div = DOM.element('div');
const svg_el = DOM.svg(width, height);
const annotation_p = DOM.element('p');

wrapper_div.appendChild(annotation_p);
wrapper_div.appendChild(svg_el);

const svg = d3.select(svg_el);
let countdown_rect_fullWidth = 360;

//////////////
/// STATIC ///
//////////////

// Add background
let background = svg
.append('rect')
.attr('x', '0')
.attr('y', '0')
.attr('width', width)
.attr('height', height)
.attr('fill', background_colour);

// define defs
let defs = svg.append('defs');
defs
.append('clipPath')
.attr('id', 'clip')
.append('use')
.attr('xlink:href', location + '#land_cumulative');

defs
.append('filter')
.attr('id', 'noise1')
.html(
'<feTurbulence baseFrequency="0.3"/><feDisplacementMap in="SourceGraphic" scale="10"/>'
);

// Create interpolate functions for background pencil outline transform + opacity to handle transitioning over 2 ticks
let scaling_interpolate = d3.interpolate(
'translate(80,-30)scale(1)',
'translate(-70,-100)scale(1.7)'
);

let opacity_interpolate = d3.interpolate(
pencil_outline_opacity1,
pencil_outline_opacity2
);

let map_wrapper = svg
.append('g')
.attr('id', 'map_wrapper')
.attr(
'transform',
year == years_tick_array[0]
? scaling_interpolate(0)
: year == years_tick_array[1]
? scaling_interpolate(0.5)
: scaling_interpolate(1)
);

// background pencil outline of Britain
let background_shape = map_wrapper
.append('g')
.attr('id', 'land_background_wrapper')
.append('path')
.attr('id', 'land_background')
.attr('d', path(land))
.attr('fill', pencil_outline_colour)
.attr('stroke', pencil_outline_colour)
.attr('stroke-width', '2px')
.attr(
'opacity',
year <= years_tick_array[1]
? opacity_interpolate(0)
: year == years_tick_array[2]
? opacity_interpolate(0.5)
: opacity_interpolate(1)
)
.attr('filter', 'url(#noise1)');
// opacity transition at the beginning
if (year == years_tick_array[1]) {
background_shape
.attr('opacity', opacity_interpolate(0))
.transition()
.duration(tickDuration)
.ease(d3.easeLinear)
.attr('opacity', opacity_interpolate(0.5));
} else if (year == years_tick_array[2]) {
background_shape
.attr('opacity', opacity_interpolate(0.5))
.transition()
.duration(tickDuration)
.ease(d3.easeLinear)
.attr('opacity', opacity_interpolate(1));
}

let tiles_wrapper = map_wrapper
.append('g')
.attr('id', 'tile_wrapper')
.attr('clip-path', 'url(' + location + '#clip)');

tiles_wrapper
.append('path')
.attr('id', 'offWhite_background')
.attr('fill', offWhite_colour)
.attr(
'd',
year == years_tick_array[0]
? path(map_data_yearCumulative[year])
: path(map_data_yearCumulative[year - 1])
);

let tiles_wrapper_images = tiles_wrapper
.append('g')
.attr('id', 'tile_wrapper_images');

// To avoid aliasing artifacts (thin white lines) between tiles, two layers of tiles are drawn, with the lower layer’s tiles enlarged by one pixel — From Mike Bostock's https://observablehq.com/@d3/clipped-map-tiles
tiles_wrapper_images
.selectAll('.tiles2')
.data(tiles)
.join('image')
.attr('xlink:href', d => url(d[0], d[1], d[2]))
.attr('onerror', "this.style.display='none'")
.attr('x', d => Math.round((d[0] + tiles.translate[0]) * tiles.scale - 0.5))
.attr('y', d => Math.round((d[1] + tiles.translate[1]) * tiles.scale - 0.5))
.attr('width', tiles.scale + 1)
.attr('height', tiles.scale + 1)
.attr('class', 'tiles2');

tiles_wrapper_images
.selectAll('.tiles1')
.data(tiles)
.join('image')
.attr('xlink:href', d => url(d[0], d[1], d[2]))
.attr('onerror', "this.style.display='none'")
.attr('x', d => Math.round((d[0] + tiles.translate[0]) * tiles.scale))
.attr('y', d => Math.round((d[1] + tiles.translate[1]) * tiles.scale))
.attr('width', tiles.scale)
.attr('height', tiles.scale)
.attr('class', 'tiles1');

map_wrapper
.append('use')
.attr('xlink:href', location + '#land_cumulative')
.attr('fill', 'none')
.attr('stroke', 'lightgray')
.attr('stroke-width', 0.1);

// Text annotation styling
const text_font = 'Neuton';

annotation_p.style['font-size'] = "26px";
annotation_p.style.width = "360px";
annotation_p.style.position = "fixed";
annotation_p.style.transform = 'translate(18px, 0px)';
annotation_p.style['font-family'] = text_font;
annotation_p.style['line-height'] = 'normal';
annotation_p.style.color = text_colour;

let countdown_rect = svg
.append('rect')
.attr('x', 10)
.attr('y', 10)
.attr('width', countdown_rect_fullWidth)
.attr('height', 5)
.attr('fill', 'white')
.style('transform', 'translate(8px,9px)');

////////////////
/// ANIMATED ///
////////////////

defs
.append('path')
.attr('id', 'land_cumulative')
.attr(
'd',
year == years_tick_array[0]
? path(map_data_yearCumulative[year])
: path(map_data_yearCumulative[year - 1])
);

// update map clip - put a delay on this so it doesn't look like a flash with the land_active colour fading in
setTimeout(function() {
defs
.select('#land_cumulative')
.attr('d', path(map_data_yearCumulative[year]));

d3.select('#offWhite_background').attr(
'd',
path(map_data_yearCumulative[year])
);
}, tickDuration / 1.5);

if (viewof year[0].textContent != "Play" && year == years_tick_array[1]) {
map_wrapper
.attr('transform', scaling_interpolate(0))
.transition()
.ease(d3.easeQuadIn)
.duration(tickDuration)
.attr('transform', scaling_interpolate(0.5));
} else if (
viewof year[0].textContent != "Play" &&
year == years_tick_array[2]
) {
map_wrapper
.attr('transform', scaling_interpolate(0.5))
.transition()
.ease(d3.easeQuadOut)
.duration(tickDuration)
.attr('transform', scaling_interpolate(1));
}

// Add text for the year
let text = svg
.append('text')
.attr('x', width - 117)
.attr('y', 52)
.attr('fill', text_colour)
.attr('font-size', '55px')
.attr('font-family', text_font)
.text(year);

// Add text annotation
let current_annotation = annotations_processed
.filter(d => d.year <= year)
.reverse()[0];
annotation_p.textContent = current_annotation.text;
// Transition countdown bar

if (year == year_max) {
countdown_rect.attr('width', countdown_rect_fullWidth);
} else {
countdown_rect.attr(
'width',
countdown_rect_fullWidth *
(1 -
(year - current_annotation.year) / current_annotation.years_to_next)
);
}

// If the scrubber is not paused - transition bar
if (viewof year[0].textContent != "Play" && year != year_max) {
countdown_rect
.transition()
.ease(d3.easeLinear)
.duration(tickDuration)
.attr(
'width',
countdown_rect_fullWidth *
(1 -
(year - current_annotation.year + 1) /
current_annotation.years_to_next)
);
}

// fade in the text annotation if it is a new one
if (year == current_annotation.year) {
d3.select(annotation_p)
.style('opacity', 0)
.transition()
.style('opacity', 1);
}
// Add the maps
map_wrapper
.append('path')
.attr('id', 'land_active1')
.attr('fill', active_tiles_colour)
.attr('opacity', 0)
.attr('d', path(map_data_yearSlice[year]))
.transition()
.duration(tickDuration / 3)
.ease(d3.easeLinear)
.attr('opacity', 1);

let fading_colour_duration = tickDuration * 0.8;

map_wrapper
.append('path')
.attr('id', 'land_active2')
.attr('fill', active_tiles_colour)
.attr('opacity', 1)
.attr(
'd',
year == years_tick_array[0] ? null : path(map_data_yearSlice[year - 1])
)
.transition()
.duration(fading_colour_duration)
.ease(d3.easeLinear)
.attr('opacity', 0.5);

map_wrapper
.append('path')
.attr('id', 'land_active3')
.attr('fill', active_tiles_colour)
.attr('opacity', 0.5)
.attr(
'd',
year <= years_tick_array[1] ? null : path(map_data_yearSlice[year - 2])
)
.transition()
.duration(fading_colour_duration)
.ease(d3.easeLinear)
.attr('opacity', 0.2);

map_wrapper
.append('path')
.attr('id', 'land_active4')
.attr('fill', active_tiles_colour)
.attr('opacity', 0.2)
.attr(
'd',
year <= years_tick_array[2] ? null : path(map_data_yearSlice[year - 3])
)
.transition()
.duration(fading_colour_duration)
.ease(d3.easeLinear)
.attr('opacity', 0);

// sheets outline
let sheets_outline = map_wrapper
.append('path')
.attr('id', 'sheetOutlines_cumulative')
.attr('stroke', sheets_outline_colour)
.attr('fill', 'none')
.attr('d', path(map_data_yearCumulative[year]));

// Draw outline from current annotation
if ('outline' in current_annotation) {
let annotation_outline = map_wrapper
.append('path')
.attr('id', 'annotation_outline')
.attr('fill', 'none')
.attr('stroke', chroma(active_tiles_colour).saturate(1))
.attr('stroke-width', '5px')
.attr('d', path(current_annotation.outline));
if (year == current_annotation.year) {
annotation_outline
.attr('opacity', 0)
.transition()
.duration(tickDuration / 3)
.ease(d3.easeLinear)
.attr('opacity', 1);
}
}

// If the scrubber is paused
if (viewof year[0].textContent == "Play" || year == year_max) {
setTimeout(function() {
d3.selectAll('#land_active1, #land_active2, #land_active3, #land_active4')
.transition()
.duration(tickDuration)
.ease(d3.easeLinear)
.attr('opacity', 0)
.remove();
}, tickDuration);
}

return wrapper_div;
}
Insert cell
Insert cell
Insert cell
Insert cell
annotations = [
{
year: 1843,
text:
"Surveying finally commenced in Scotland in 1843. The Survey swept from South West to North East over time."
},
{
year: 1852,
text:
"The Isle of Lewis in the Outer Hebrides was mapped out of sequence because Sir James Matheson, the landowner, paid the OS to map it early.",
// An annotation can include an optional attribute 'outline', which is geojson data describing a shape that will be outlined in the animated graphic simultaneously with the text annotation. (Here, it outlines the map sheets for the Isle of Lewis).
outline: lewis_sheets
},
{
year: 1860,
text:
"Delays were caused by a dispute over the choice of cartographic scale, a general lack of funds and staff, and the difficult Highland terrain and weather."
},
{
year: 1870,
text:
"Highland deer forest owners did not want surveyors on their land in deer stalking season. This was a challenge as it clashed with survey season."
},
{
year: 1878,
text:
"The survey of Scotland took almost four decades to complete with the final maps of Orkney and Shetland published in 1882."
}
]
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import {
url_oneInch as url,
one_inch_byYear as year_data_features,
one_inch_byYear_cumulative as year_data_features_cumulative
} from '235c58cad97f5dda'
Insert cell
Insert cell
// Import Neuton Font from Google Fonts
html`<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Neuton&display=swap" rel="stylesheet">`
Insert cell
Insert cell
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