Unlisted
Edited
Sep 23, 2024
22 forks
Importers
96 stars
Insert cell
Insert cell
chart = SpikeMap(population, {
value: ([population]) => +population,
position([, stateid, countyid]) {
const county = countymap.get(stateid + countyid);
return county && centroid(county);
},
title([population, stateid, countyid]) {
const state = statemap.get(stateid);
const county = countymap.get(stateid + countyid);
return `${county?.properties.name}, ${state?.properties.name}\n${(+population).toLocaleString("en")}`;
},
features: nation,
borders: statemesh,
width: 975,
height: 610
})
Insert cell
Insert cell
population = FileAttachment("population.json").json()
Insert cell
Insert cell
centroid = {
const path = d3.geoPath();
return feature => path.centroid(feature);
}
Insert cell
Insert cell
us = FileAttachment("counties-albers-10m.json").json()
Insert cell
nation = topojson.feature(us, us.objects.nation)
Insert cell
statemap = new Map(topojson.feature(us, us.objects.states).features.map(d => [d.id, d]))
Insert cell
countymap = new Map(topojson.feature(us, us.objects.counties).features.map(d => [d.id, d]))
Insert cell
Insert cell
statemesh = topojson.mesh(us, us.objects.states, (a, b) => a !== b)
Insert cell
Insert cell
// Copyright 2022 Observable, Inc.
// Released under the ISC license.
// https://observablehq.com/@d3/spike-map
function SpikeMap(data, {
position = d => d, // given d in data, returns the [longitude, latitude]
value = () => undefined, // given d in data, returns the quantitative value
title, // given a datum d, returns the hover text
scale = d3.scaleLinear, // type of length scale
domain, // [0, max] values; input of length scale; must start at zero
maxLength = 200, // maximum length of spikes
width = 640, // outer width, in pixels
height, // outer height, in pixels
projection, // a D3 projection; null for pre-projected geometry
features, // a GeoJSON feature collection for the background
borders, // a GeoJSON object for stroking borders
spike = (length, width = 7) => `M${-width / 2},0L0,${-length}L${width / 2},0`,
outline = projection && projection.rotate ? {type: "Sphere"} : null, // a GeoJSON object for the background
backgroundFill = "#e0e0e0", // fill color for background
backgroundStroke = "white", // stroke color for borders
backgroundStrokeWidth, // stroke width for borders
backgroundStrokeOpacity, // stroke width for borders
backgroundStrokeLinecap = "round", // stroke line cap for borders
backgroundStrokeLinejoin = "round", // stroke line join for borders
fill = "red", // fill color for spikes
fillOpacity = 0.3, // fill opacity for spikes
stroke = "red", // stroke color for spikes
strokeWidth, // stroke width for spikes
strokeOpacity, // stroke opacity for spikes
legendX = width - 20,
legendY = height - 20,
} = {}) {
// Compute values.
const I = d3.map(data, (_, i) => i);
const V = d3.map(data, value).map(d => d == null ? NaN : +d);
const P = d3.map(data, position);
const T = title == null ? null : d3.map(data, title);

// Compute default domains.
if (domain === undefined) domain = [0, d3.max(V)];

// Construct scales.
const length = scale(domain, [0, maxLength]);

// Compute the default height. If an outline object is specified, scale the projection to fit
// the width, and then compute the corresponding height.
if (height === undefined) {
if (outline === undefined) {
height = 400;
} else {
const [[x0, y0], [x1, y1]] = d3.geoPath(projection.fitWidth(width, outline)).bounds(outline);
const dy = Math.ceil(y1 - y0), l = Math.min(Math.ceil(x1 - x0), dy);
projection.scale(projection.scale() * (l - 1) / l).precision(0.2);
height = dy;
}
}

// Construct a path generator.
const path = d3.geoPath(projection);

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "width: 100%; height: auto; height: intrinsic;");

if (outline != null) svg.append("path")
.attr("fill", "white")
.attr("stroke", "currentColor")
.attr("d", path(outline));

svg.append("path")
.datum(features)
.attr("fill", backgroundFill)
.attr("d", path);

if (borders != null) svg.append("path")
.attr("pointer-events", "none")
.attr("fill", "none")
.attr("stroke", backgroundStroke)
.attr("stroke-linecap", backgroundStrokeLinecap)
.attr("stroke-linejoin", backgroundStrokeLinejoin)
.attr("stroke-width", backgroundStrokeWidth)
.attr("stroke-opacity", backgroundStrokeOpacity)
.attr("d", path(borders));

const legend = svg.append("g")
.attr("fill", "#777")
.attr("text-anchor", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.selectAll("g")
.data(length.ticks(4).slice(1).reverse())
.join("g")
.attr("transform", (d, i) => `translate(${legendX - i * 18},${legendY})`);

legend.append("path")
.attr("fill", "red")
.attr("fill-opacity", 0.3)
.attr("stroke", "red")
.attr("d", d => spike(length(d)));

legend.append("text")
.attr("dy", "1.3em")
.text(length.tickFormat(4, "s"));

svg.append("g")
.attr("fill", fill)
.attr("fill-opacity", fillOpacity)
.attr("stroke", stroke)
.attr("stroke-width", strokeWidth)
.attr("stroke-opacity", strokeOpacity)
.selectAll("path")
.data(d3.range(data.length)
.filter(i => P[i])
.sort((i, j) => d3.ascending(P[i][1], P[j][1]) || d3.ascending(P[i][0], P[j][0])))
.join("path")
.attr("transform", projection == null
? i => `translate(${P[i]})`
: i => `translate(${projection(P[i])})`)
.attr("d", i => spike(length(V[i])))
.call(T ? path => path.append("title").text(i => T[i]) : () => {});

return svg.node();
}
Insert cell
import {howto} from "@d3/example-components"
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