Public
Edited
May 8, 2023
Insert cell
Insert cell
Insert cell
draw_animated_map(nl_buffered, nlPolygon, 9)
Insert cell
nlPolygon
Insert cell
path = d3.geoPath(projection)
Insert cell
nlPolygon = topojson.merge(nl, nl.objects.provincie_2022.geometries)
Insert cell
nl = FileAttachment("provincie_2022.topojson").json()
Insert cell
function create_buffered_polygons_inWorker(data) {
let geojson = data.geojson,
total = data.total, // default
radial_distance = data.radial_distance, // default
spacing_change = data.spacing_change; // default

// Use turf.rewind to make sure that the geojson winding direction is correct for d3 (or else polygons can be inverted into holes) - more on this https://bl.ocks.org/mbostock/a7bdfeb041e850799a8d3dce4d8c50c8
function buffer(geojson, distance) {
return turf.rewind(turf.buffer(geojson, distance), { reverse: true });
}

let buffered_polygons = [];
let exp = d3
.scalePow()
.exponent(spacing_change)
.domain([0, total - 1])
.range([0, radial_distance]);
for (let i = 0; i < total; i++) {
let new_polygon = buffer(geojson, exp(i), "kilometers");
buffered_polygons.push(new_polygon);
}
return buffered_polygons;
}
Insert cell
nl_buffered = Generators.observe(
worker(
create_buffered_polygons_inWorker,
{
geojson: nlPolygon,
total: 18,
radial_distance: 250,
spacing_change: 4
}, // pass in the required libraries
`importScripts(${await require
.resolve("@turf/turf@5")
.then(JSON.stringify)}, ${await require
.resolve("d3@5")
.then(JSON.stringify)});`
)
)
Insert cell
animation_interval = 4000
Insert cell
async function* draw_animated_map(
buffered_polygons, // waterlines geojson to be drawn in the centre
polygon_orig, // original geojson to be drawn at the centre
waterline_index_start = 10, // the waterline index to start animating from upwards
reverse = false // waterlines direction of travel
) {
/////////////////////
/// Define scales ///
/////////////////////
const opacity = d3
.scalePow()
.exponent(1)
.domain([0, buffered_polygons.length - 1])
.range([1, 0]);

const stroke_width = d3
.scalePow()
.exponent(3)
.domain([0, buffered_polygons.length - 1])
.range([1.4, 0.3]);

// Define geo path generator and projection

// To avoid the problems with morphing between multi-part paths (separate islands),
// only keep the longest path
let buffered_polygon_paths = [];
for (let i = 0; i < buffered_polygons.length; i++) {
let buffer_path = path(buffered_polygons[i]);
let buffer_path_array = buffer_path.split("M");
buffer_path_array.sort(function (a, b) {
return b.length - a.length;
});
let buffer_path_longest = "M" + buffer_path_array[0];
buffered_polygon_paths.push(buffer_path_longest);
}
/////////////////////////
/// Draw original map ///
/////////////////////////

const svg = d3
.select(DOM.svg(width, 600))
.style("width", "100%")
.style("height", "auto");

const g = svg.append("g");

g.selectAll("path.waterLines")
.data(buffered_polygons)
.enter()
.append("g") // create g wrappers for the paths so we can apply concurrent animations (path and opacity) below
.attr("class", "waterLine_g")
.attr("opacity", 1)
.append("path")
.attr("d", path)
.attr("class", "waterLines")
.style("fill", "none")
.style("stroke", "teal");

g.append("path") // draw the original polygon on top of the waterlines
.datum(polygon_orig)
.attr("d", path)
.attr("class", "land")
.style("fill", "white")
.style("stroke", "teal");
///////////////////////////
/// Define transitions ///
//////////////////////////

let n = waterline_index_start;

// for waterlines emitting outwards

while (true) {
// remove the final waterline
g.selectAll("g.waterLine_g")
.filter((d, i) => i == buffered_polygons.length - 1)
.remove();

// create a new nth waterline
let n_plus_1th_DOM_node = g
.selectAll("g.waterLine_g")
.filter((d, i) => i == n)
.node();
g.insert("g")
// make the nth child
.each(function () {
this.parentNode.insertBefore(this, n_plus_1th_DOM_node);
})
.attr("class", "waterLine_g")
.insert("path")
.datum(buffered_polygons[n])
.attr("d", path)
.attr("class", "waterLines")
.attr("id", "new")
.style("fill", "none")
.style("stroke", "teal");

// Transition the waterline paths
g.selectAll("path.waterLines")
.filter((d, i) => i > n) // don't include waterlines before n
.transition()
.duration(animation_interval)
.ease(d3.easeLinear)
.attrTween("d", function (d, i) {
var startPath = buffered_polygon_paths[i + n],
endPath = buffered_polygon_paths[i + 1 + n];
return flubber.interpolate(startPath, endPath);
});

//Transition the (new) final waterline, fade it out, and remove
// apply this transition to the parent g element, rather than the path element to avoid clashing animations https://bl.ocks.org/mbostock/6081914
g.selectAll("g.waterLine_g")
.filter((d, i) => i == buffered_polygons.length - 1)
.transition()
.duration(animation_interval)
.ease(d3.easeLinear)
.style("opacity", 0)
.remove();

await Promises.tick(animation_interval);
yield svg.node();
}
}
Insert cell
// import // create_buffered_polygons_inWorker,
// // draw_animated_map
// "@oliviafvane/iii-animating-waterlines-svg-morphing"
Insert cell
import { worker } from "@fil/worker"
Insert cell
turf = require("@turf/turf@5")
Insert cell
flubber = require("flubber@0.4.2")
Insert cell
projection = d3
.geoMercator()
.fitSize([width, width / 2], topojson.feature(nl, nl.objects.provincie_2022))
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