Public
Edited
Nov 21, 2023
Paused
Importers
1 star
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
async function Chart()
{
const div = d3.create("div");
const info_div = div.append("div");
info_div.style("display", "none")
.style("position", "absolute")
.style("font-size", "85%")
.style("background", "white")
.style("border", "1px solid #bbb")
.style("border-radius", ".3em")
.style("box-shadow", "1px 1px 4px rgba(0, 0, 0, .05)")
.style("padding", "0 .5em");
const svg = div.append("svg")
.style("display", "block")
.attr("viewBox", [0, 0, chart_size[0], chart_size[1]]);
svg.append("defs").append("pattern")
.attr('id','bgimage')
.attr("width", "100%")
.attr("height", "100%")
.attr("x", "0")
.attr("y", "0")
.attr("viewBox", `0 0 ${img_size[0]} ${img_size[1]}`)
.append('image')
.attr('width', img_size[0])
.attr('height', img_size[1])
.attr('xlink:href', await FileAttachment("roads2.webp").url())

var defs_extra;
var transform_timeout;
var k = 1;
var clip_rect = svg.append("defs")
.append('clipPath')
.attr('id', 'map-clip-box')
.append('rect')
.attr('x', chart_content_rect[0][0])
.attr('y', chart_content_rect[0][1])
.attr('width', chart_content_rect[1][0])
.attr('height', chart_content_rect[1][1]);
var clip_layer = svg.append('g').attr('clip-path', 'url(#map-clip-box)');

const map_layer = clip_layer.append("g");

const sa_layer = map_layer.append("g");
const img_layer = clip_layer.append("g");
img_layer.append("path")
.datum(img_bounds_path)
.attr("d", path)
.attr("style", "pointer-events: none;")
.attr('fill','url(#bgimage)')

const select_layer = clip_layer.append("g")
.attr("stroke-width", 1.5);

// click event
function sa_clicked(event, d)
{
select_layer.selectAll("*").remove();
select_layer.append("path")
.datum(d)
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-linejoin", "round");
select_layer.append("path")
.datum(d)
.attr("id", "select-strokes")
.attr("d", path)
.attr("fill", "none")
.attr("stroke", "blue")
.attr("stroke-dasharray", [3/k, 3/k])
.attr("stroke-linejoin", "round");
// read back title from DOM
var text = d3.select(this).select('title').text().split(/\r?\n/);

info_div.selectAll("*").remove();
info_div.style("display", "block");
info_div.append("span")
.style("cursor", "pointer")
.style("margin-left", "1em")
.style("margin-right", "-.5em")
.style("float", "right")
.text("✖")
.on("click", close_info);
text.forEach(l =>
info_div.append("span").text(l) && info_div.append("br"));
}
function close_info()
{
select_layer.selectAll("*").remove();
info_div.selectAll("*").remove();
info_div.style("display", "none");
}
var obj = {
node: div.node(),
svg: svg,
map_layer: map_layer,
img_layer: img_layer,
transform_callback: null };
obj.update_fill = function(collection, fill_f, title_f)
{
sa_layer.selectAll("path")
.data(collection.features)
.join("path")
.attr("fill", fill_f)
.attr("d", path)
.on("click", sa_clicked)
.selectAll("title")
.data(f => [f])
.join("title")
.text(title_f);
}
obj.zoomTransform = function()
{
return d3.zoomTransform(obj.svg.node());
}
function zoomed({transform}) {
k = transform.k;
map_layer.attr("transform", transform);
img_layer.attr("transform", transform);
select_layer.attr("transform", transform)
.attr("stroke-width", 1.5 / k);
select_layer.selectAll("#select-strokes")
.attr("stroke-dasharray", [3/k, 3/k]);
clip_rect
.attr('x', transform.x + transform.k * chart_content_rect[0][0])
.attr('y', transform.y + transform.k * chart_content_rect[0][1])
.attr('width', transform.k * chart_content_rect[1][0])
.attr('height', transform.k * chart_content_rect[1][1])
}

function zoomed_end({transform})
{
// update list of definitions after a delay
// (updating patterns is potentially slow)
if (obj.transform_callback)
{
clearTimeout(transform_timeout);
transform_timeout = setTimeout(function() {
obj.transform_callback(transform, obj)
transform_timeout = undefined;
}, 1000);
}
}

obj.set_transform_callback = function(f)
{
obj.transform_callback = f;
zoomed_end({transform: obj.zoomTransform()});
}
obj.trigger_transform_callback = function()
{
zoomed_end({transform: obj.zoomTransform()});
};
// zoom behaviour
svg.call(d3.zoom()
.extent([[0, 0], chart_size])
// ideally we would exclude the areas outside the clipping box but
// that doesn’t work well.
.translateExtent([[0, 0], chart_size])
.scaleExtent([1, max_zoom * img_size[0] / width])
.on("zoom", zoomed)
.on("end", zoomed_end));
return obj;
}
Insert cell
chart = Chart()
Insert cell
chart_updater = chart.update_fill(
census_collection, d => color(map_value(d)), title_text);
Insert cell
max_zoom = 3
Insert cell
Insert cell
color = current_census_header.key == "dep" ?
nzdep_color :
x => color_scheme(x / current_census_header.max);
Insert cell
// slightly subdued version of D3 preset:
nzdep_scheme = d3.range(10).map(x => d3.interpolateRdYlBu(0.5 - 0.06*(x-4.5)))
Insert cell
function nzdep_color(x)
{
// no data
if (!x || x < 1) { return 'rgb(200, 200, 200)'; }
return nzdep_scheme[x-1];
}
Insert cell
function color_scheme(x)
{
// no data
if (x < 0) { return 'rgb(240, 240, 240)'; }
// for off-the-chart values, use dark purple or black
if (x > 2) return "black";
if (x > 1.5) return "rgb(70, 0, 110 )";

x = Math.max(0, Math.min(1, x));

var list = [
'rgb(255, 255, 255)' ,
'rgb(255, 249, 200)' ,
'rgb(255, 239, 160)' ,
'rgb(255, 220, 130)' ,
'rgb(255, 200, 90 )',
'rgb(255, 180, 45 )',
'rgb(255, 140, 45 )',
'rgb(255, 100, 90 )',
'rgb(220, 50, 100 )',
'rgb(180, 0, 200 )',
'rgb(120, 0, 200 )'];
x *= (list.length - 1);
return list[Math.round(x)];
}
Insert cell
function map_value(d)
{
// ratio and diverging: cut off to avoid very large and noisy patches
if (d.properties.density < density_cutoff &&
!current_census_header.all) return -1;

// custom function
if (current_census_header.f)
{
return current_census_header.f(d.properties);
}
// direct value
if (current_census_header.absolute || current_census_header.diverging)
{
return d.properties[current_census_header.key];
}
var n = commute_group == 'work' ? d.properties.n : d.properties.sn;
if (n == 0) return -1;
return d.properties[current_census_header.key] / n;
}
Insert cell
Insert cell
param = {
var params = new URLSearchParams(html`<a href>`.search)
return function(key, def)
{
var v = params.get(key);
if (v == null) return def;
return v;
}
}
Insert cell
Insert cell
mode_choices = commute_group == 'work' ?
[
{ "title":"Car", "key":"n_car" , "q":"car" , "max": 1},
{ "title":"Bicycle", "key":"n_cycle" , "q":"cycle", "max": .15},
{ "title":"Work From Home", "key":"n_home" , "q":"home" , "max": .3},
{ "title":"Public Transport", "key":"n_pt" , "q":"pt" , "max": .45},
{ "title":"Walk", "key":"n_walk" , "q":"walk" , "max": .6},
{ "title":"Response rate", "key":"response" , "max": 1 , "f":prop => (+prop.n / +prop.population).toFixed(1)},
] : commute_group == 'study' ? [
{ "title":"Car", "key":"sn_car" , "q":"car" , "max": 1},
{ "title":"Bicycle", "key":"sn_cycle" , "q":"cycle", "max": .15},
{ "title":"Study From Home", "key":"sn_home" , "q":"home" , "max": .15},
{ "title":"Public Transport", "key":"sn_pt" , "q":"pt" , "max": .45},
{ "title":"Walk", "key":"sn_walk" , "q":"walk" , "max": .8},
{ "title":"School bus", "key":"sn_schoolbus" , "q":"schoolbus" , "max": .30},
{ "title":"Response rate", "key":"response" , "max": 1, "f":prop => (+prop.sn / +prop.population).toFixed(1)},
] : [
{"title": "Population density", "key": "density", "q": "density", "max": 1e4, "absolute": true, "all": true},
{"title": "Population density (CBD)", "key": "density", "q": "density-high", "max": 5e4, "absolute": true, "all": true},
{"title": "Cars per capita", "key": "cars_capita", "max": 1, "absolute": true,
f:props => (+props.cars_count / +props.population).toFixed(2)},
{"title": "Cars per 20+", "key": "cars_adult", "max": 1, "absolute": true,
f:props => (+props.cars_count / (+props.prop_20_plus * +props.population)).toFixed(2)},
{"title": "NZiDep", "key": "dep", "q": "dep", "max": 10, "diverging": true, "all": true},
];
Insert cell
dataset_choices = ({
work: "Commute to work",
study: "Commute to study",
misc: "Other data"
})
Insert cell
async function get_census_input_data(what, sa_type) {
var use = k => !what || what[k];
var pd = use('pop_data');
var pds = use('pop_data_s');
var car = use('car');
var parents = use('parents');
switch (sa_type)
{
case 1:
return {
pop_data: pd ? await d3.csv(await FileAttachment("akl-2018-sh1.csv").url()) : undefined,
pop_data_s: pds ? await d3.csv(await FileAttachment("akl-2018-s-sh1@1.csv").url()) : undefined,
car: car ? await d3.csv(await FileAttachment("akl-2018-cars-sa1.csv").url()) : undefined,
parents: parents ? await d3.csv(await FileAttachment("akl-2018-parent-sa1.csv").url()) : undefined
};
case 2:
return {
pop_data: pd ? await d3.csv(await FileAttachment("akl-2018-sh2.csv").url()) : undefined,
pop_data_s: pds ? await d3.csv(await FileAttachment("akl-2018-s-sh2@1.csv").url()) : undefined,
car: car ? await d3.csv(await FileAttachment("akl-2018-cars-sa2.csv").url()) : undefined,
parents: parents ? await d3.csv(await FileAttachment("akl-2018-parent-sa2.csv").url()) : undefined
};

case 3:
return {
pop_data: pd ? await d3.csv(await FileAttachment("akl-2018-lb.csv").url()) : undefined,
pop_data_s: pds ? await d3.csv(await FileAttachment("akl-2018-s-lb.csv").url()) : undefined,
car: car ? await d3.csv(await FileAttachment("akl-2018-cars-lb@1.csv").url()) : undefined
};
}
}
Insert cell
census_input_data = get_census_input_data({
'pop_data': true,
'pop_data_s': commute_group == 'study',
'car': commute_group == 'misc'
}, sa_type)
Insert cell
function as_map(list) {
if (!list) return {};
var m = {};
list.forEach(x => m[x.ID] = x);
return m;
}
Insert cell
census_collection = {
var geometry = sa_type == 1
? d3.json(await FileAttachment("akl-2018-sa1@1.geojson").url())
: sa_type == 2
? d3.json(await FileAttachment("akl-2018-sa2@1.geojson").url())
: d3.json(await FileAttachment("akl-2018-lb@1.geojson").url());
geometry = await geometry;
// fill in properties of each geometry object:
var map_pop_data = as_map(census_input_data.pop_data);
var map_pop_data_s = as_map(census_input_data.pop_data_s);
var map_car_data = as_map(census_input_data.car);
geometry.features.forEach(g =>
g.properties = {
...g.properties,
...map_pop_data[g.properties.ID],
...map_pop_data_s[g.properties.ID],
...map_car_data[g.properties.ID]} );
return geometry;
}
Insert cell
// well I don't know if this is correct, I ended up not needing it since
// all source data is projected
_nzgd_2000 = d3.geoTransverseMercator()
.rotate([173,0]);
Insert cell
path = d3.geoPath(projection);
Insert cell
// the image is projected exactly to the SVG area
projection = d3.geoIdentity().reflectY(true).fitSize(chart_size, img_bounds_path);
Insert cell
Insert cell
Insert cell
img_bounds = {
var x1 = 1736687;
var y2 = 5939107;
var x2 = x1 + 39735;
var y1 = y2 - 44010;
return[[x1, y1], [x2, y2]];
}
Insert cell
img_bounds_path = ({
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
img_bounds[0],
[img_bounds[0][0],img_bounds[1][1]],
img_bounds[1],
[img_bounds[1][0], img_bounds[0][1]],
img_bounds[0]
]
]
},
"properties": {}
});
Insert cell
// here you can regulate the maximal amount of space we take up:
max_chart_h = 650
Insert cell
chart_size = {
var h = Math.round(width * img_size[1] / img_size[0]);
if (h < max_chart_h) return [width, h];
return [width, max_chart_h];
}
Insert cell
chart_content_rect = {
if (chart_size[1] < max_chart_h) return [[0, 0], chart_size];
var w = Math.round(max_chart_h * img_size[0] / img_size[1]);
return [[(chart_size[0] - w) / 2, 0], [w, chart_size[1]]];
}
Insert cell
Insert cell
function title_text(d)
{
var n = commute_group == 'work' ? d.properties.n : d.properties.sn;
var lines = [
`${d.properties.name} — ${d.properties.density} ppl / km²`,
];
if (commute_group == 'misc') {
lines.push(`\u2002• population: ${d.properties.population}`);
}
else {
lines.push(`responses: ${n} out of ${d.properties.population}`);
}
for (var i = 0; i < mode_choices.length; ++i)
{
var h = mode_choices[i];
if (h.q == 'density-high' || h.key == 'response') {
// exclude
}
else if (h.absolute || h.diverging) {
var v = h.f ? h.f(d.properties) : d.properties[h.key];
lines.push(`\u2002• ${h.title}: ${v}`)
}
else {
var v = h.f ? h.f(d.properties) : d.properties[h.key];
lines.push(`\u2002• ${h.title}: ${v} (${fmt_percent(v, n)})`)
}
}
return lines.join("\n");
}
Insert cell
function fmt_percent(a, b)
{
return b > 0 ? (100 * a / b).toFixed(1) + '%' : '—';
}
Insert cell
Insert cell
d3 = require("d3")
Insert cell
import {legend} from "@d3/color-legend"
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