Public
Edited
Apr 11, 2023
Importers
2 stars
Insert cell
Insert cell
Insert cell
viewof year = Scrubber(years_tick_array, {
delay: options.animation_speed,
loop: false
})
Insert cell
animated_map(options)
Insert cell
options = ({
translate_x: 0,
translate_y: 0,
width_padding: 200,
colour_mode: 'light',
shape_behind: eng_sct_wls,
draw_sheetOutlines: false,
maps_baseColor: 'rgb(236,229,214)',
sheetsActive_colour: '#DB5461',
animation_speed: 1000
})
Insert cell
function animated_map(options) {
const svg = d3.select(
DOM.svg(
options.width_padding ? width + options.width_padding : width,
height
)
);

// Define colours
let background_colour,
shape_behind_colour,
shape_behind_opacity,
text_colour,
sheetsActive_colour;

// Make lightmode the default
if (options.colour_mode == 'dark') {
background_colour = 'black';
shape_behind_colour = 'white';
shape_behind_opacity = 0.4;
text_colour = 'white';
sheetsActive_colour = options.sheetsActive_colour || 'cyan';
} else {
background_colour = 'white';
shape_behind_colour = 'black';
shape_behind_opacity = 0.05;
text_colour = 'black';
sheetsActive_colour = options.sheetsActive_colour || 'pink';
}

let translate_x = options.translate_x || 0,
translate_y = options.translate_y || 0;

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

// Add background coloured rectangle
let background = svg
.append('rect')
.attr('x', '0')
.attr('y', '0')
.attr(
'width',
options.width_padding ? width + options.width_padding : width
)
.attr('height', height)
.attr('fill', background_colour);

// define SVG 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"/>'
);

let map_wrapper = svg
.append('g')
.attr('id', 'map_wrapper')
.style(
'transform',
options.width_padding
? `translate(${options.width_padding / 2 +
translate_x}px,${translate_y}px)`
: ""
);

// Draw pencil outline of shape data (here Great Britain) beneath revealing map sheets.
map_wrapper
.append('path')
.attr('id', 'land_background')
.attr(
'd',
path(options.shape_behind || { type: 'FeatureCollection', features: [] })
)
.attr('fill', options.colour_mode == 'light' ? shape_behind_colour : 'none')
.attr(
'stroke',
options.colour_mode == 'light' ? 'none' : shape_behind_colour
)
.attr('stroke-width', '2px')
.attr('opacity', shape_behind_opacity)
.attr('transform', 'scale(0.994)') // shrink the shape a very small amount because the noise blur expands beyond its outline and can be visible outside of the map polygon area
.attr('filter', 'url(#noise1)');

// Draw historical map imagery from map tiles
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', options.maps_baseColor || 'none')
.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);

////////////////
/// 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);

// Add text for the year
let text = svg
.append('text')
.attr('x', width - 110)
.style(
'transform',
options.width_padding ? `translate(${options.width_padding}px,0px)` : ''
)
.attr('y', 50)
.attr('fill', text_colour)
.attr('font-size', '50px')
.text(year);

// Add the maps
map_wrapper
.append('path')
.attr('id', 'land_active1')
.attr('fill', sheetsActive_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;

// Create 3 paths drawing a fade-out colour over the new sheets that year (opacity 1 → 0.5), year - 1 (fainter: opacity 0.5 → 0.2 ), and year - 2 (fades to disappear).
map_wrapper
.append('path')
.attr('id', 'land_active2')
.attr('fill', sheetsActive_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', sheetsActive_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', sheetsActive_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);

// Draw sheet boundary outline
let sheets_outline = options.draw_sheetOutlines
? map_wrapper
.append('path')
.attr('id', 'sheetOutlines_cumulative')
.attr('stroke', sheetsActive_colour)
.attr('opacity', 0.5)
.attr('fill', 'none')
.attr('d', path(map_data_yearCumulative[year]))
: null;

// 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 svg.node();
}
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

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