Published
Edited
Jul 1, 2021
Importers
Insert cell
Insert cell
choroplethMap({
data: dairy,
geography: wiCounties,
topographyBorder: border,
breaks: breaks,
colorBins: colorBins,
ordinalLabels: ['a','b','c','d','e','f'],
colorVariable: 'milk_pounds_per_cow',
geographyIdVariable: 'fips',
dataIdVariable: 'fips',
legend: {
title: 'Annual milk production per cow in pounds',
},
hoverFn: f => neighbors[f]
})
Insert cell
mutable hoverTarget = null
Insert cell
Insert cell
wiCounties = FileAttachment("wi-counties@1.json").json();
Insert cell
wiCountiesBuf = FileAttachment("wi-counties@1.json").arrayBuffer();
Insert cell
mapId = geoda.readGeoJSON(wiCountiesBuf)
Insert cell
weights = geoda.getQueenWeights(mapId)
Insert cell
neighbors = getNeighbors(wiCounties, 'fips', weights)
Insert cell
function getNeighbors(geojsonData, idCol, weights){
let neighborsObj = {}
// loop through
for (let i=0; i<geojsonData.features.length;i++){
// snag ID column
const currId = geojsonData.features[i].properties[idCol]
// find neighbors
const currNeighbors = geoda.getNeighbors(weights, i)
neighborsObj[currId] = [currId, ...(currNeighbors.map(i => geojsonData.features[i].properties[idCol]))]
}
return neighborsObj
}
Insert cell
jsgeoda = import('https://cdn.skypack.dev/jsgeoda')
Insert cell
geoda = await jsgeoda.New()
Insert cell
breaks = geoda.naturalBreaks(6, dairy.map(o => o.milk_pounds_per_cow||0))
Insert cell
colorBins = ['#ffffb2','#fed976','#feb24c','#fd8d3c','#f03b20','#bd0026']
Insert cell
Insert cell
border = {
const topo = topojson.topology({ geography: wiCounties });
return topojson.merge(topo, topo.objects.geography.geometries);
}
Insert cell
dairy
Insert cell
function choroplethMap({
data = data,
geography = geography,
topographyBorder = {},
tickSize = 6,
width = 960,
height = 600,
marginTop = 60,
marginRight = 10,
marginBottom = 10,
marginLeft = 10,
colorVariable = 'value',
breaks = [],
colorBins = [],
colorInterpolator = d3.interpolateYlGnBu,
geographyIdVariable = 'id',
dataIdVariable = 'id',
titleFontSize = 20,
titleFontFamily = 'sans-serif',
titleFontWeight = 'bold',
titleMaxCharsPerLine = 65,
legend: legendOptions = {},
strokeColor = '#fff',
ordinalLabels = [],
hoverFn = null
} = {}) {
let dataById = {}

for (let i=0; i<data.length;i++){
dataById[data[i][dataIdVariable]] = data[i]
}
const extent = [
[marginLeft, marginTop],
[width - marginRight - marginLeft, height - marginBottom - marginTop]
];
const projection = d3.geoMercator()
.fitExtent(extent, geography);
const path = d3.geoPath()
.projection(projection);
const colorScale = d3.scaleThreshold()
.domain(breaks)
.range(colorBins);
const svg = d3.create('svg')
.attr('viewBox', [0, 0, 975, 610]);
svg.append('g')
.attr('fill', 'none')
.selectAll('path')
.data(geography.features)
.join('path')
.attr('d', path)
.attr('fill', (d) => {
const id = d.properties[geographyIdVariable];
if (dataById.hasOwnProperty(id)) {
const datum = dataById[id];
return colorScale(datum[colorVariable]);
}
})
.attr("stroke", d => strokeColor)
.style('opacity', d => hoverTarget === null || hoverTarget.includes(d.properties[geographyIdVariable]) ? 1 : .5)
.on('click', d => hoverFn !== null ? mutable hoverTarget = hoverFn(d.properties[geographyIdVariable]) : '')
.attr('class', 'feature');
svg.append('path')
.datum(topographyBorder)
.attr('fill', 'none')
.attr('stroke', '#434343')
.attr('stroke-width', 1)
.attr('d', path);
const options = Object.assign({
color: ordinalLabels.length
? d3.scaleOrdinal(ordinalLabels, colorBins)
: colorScale,
},legendOptions);
const legendSVG = legend(options);

const legendWidth = +d3.select(legendSVG).attr('width');
const legendHeight = +d3.select(legendSVG).attr('height');
const legendNode = svg.append('g')
.attr('transform', `translate(${width / 2 - legendWidth / 2}, 0)`);
Array.from(legendSVG.children).forEach((child) => {
legendNode.node().appendChild(child);
});
legendNode.select('g').selectAll('text')
.filter(function() { return d3.select(this).attr('font-weight') === 'bold'; })
.attr('font-size', 14);

// thanks! https://observablehq.com/@duynguyen1678/choropleth-with-tooltip
const tooltip = svg.append("g")

svg
.selectAll(".feature")
.on("touchmove mousemove", (d, event) => {
console.log(event)
tooltip.call(
callout,
`ID: ${d.properties[geographyIdVariable]}`
);
tooltip.attr("transform", `translate(${d3.pointer(event, this)})`);
d3.select(this)
.raise();
})
.on("touchend mouseleave", function() {
tooltip.call(callout, null);
d3.select(this)
.attr("stroke", null)
.lower();
});
return svg.node();
}
Insert cell
function deckLineMap({
initialViewState={
latitude:0,
longitude:0,
zoom:0
},
geography={},
controller=true,
width=500,
height=500,
customDeckParams={},
strokeColor="#999999",
title="",
connectivityData={},
}){
const container = html `<div style="height:${height}px;width:${width}px"></div>`
const deckgl = new deck.DeckGL({
container: container,
map: false,
// Viewport settings
initialViewState,
controller,
glOptions: {
preserveDrawingBuffer: true // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
}
});

const layers = [new deck.GeoJsonLayer({
id: `geojson-${Math.random()}`,
data: geography,
pickable: true,
getFillColor: [255,255,255],
getLineColor: [40,40,40],
lineWidthScale: 1,
lineWidthMinPixels: 1,
getLineWidth: 1,
autoHighlight: true,
...customDeckParams
}),
connectivityData.arcs && new deck.LineLayer({
data: connectivityData.arcs,
pickable: false,
getWidth: d => d.value,
getSourcePosition: d => d.source,
getTargetPosition: d => d.target,
getColor: [0,0,0,120]
}),
connectivityData.targets && new deck.ScatterplotLayer({
data: connectivityData.targets,
opacity: 0.8,
stroked: false,
filled: true,
radiusScale: 6,
radiusMinPixels: 5,
radiusMaxPixels: 10,
getPosition: d => d.position,
getRadius: d => 1,
getFillColor: d => [255, 140, 0],
getLineColor: d => [0, 0, 0]
})
]
deckgl.setProps({layers: layers});
return html`<div style="height:${height}px;width:${width}px">
<span id="meta" style="z-index:5">
<h2>${title}</h2>
</span>
${container}
</div>`
}
Insert cell
// thanks! https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
Insert cell
deck = require.alias({
h3: {},
s2Geometry: {}
})('deck.gl@^8.0.0/dist.min.js')
Insert cell
Insert cell
Insert cell
Insert cell
import {callout} from '@d3/line-chart-with-tooltip'
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