Published
Edited
Sep 25, 2020
Importers
Insert cell
Insert cell
Insert cell
viewof countyCodes = fipsbrush({
width: width,
brushSize: 15,
value: ['35051', '35053', '35061'],
title: 'Filter Counties',
description:
'Hold your shift key to add to an existing selection, option key to subtract, and/or meta key to "lock" onto a state'
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function fipsbrush({
value = [],
title = null,
description = null,
highlightColor = "rgba(255, 0, 0, .2)",
width: widthOpt = Math.min(width, 500),
brushSize = 20,
getCountyColor = null,
deferUpdates = false
} = {}) {
const width = widthOpt;
const scale = width / 275;
const height = scale * 175;
const dpi = window.devicePixelRatio || 1;

const formEl = html`<form style="width: ${width}px; padding: 0.3em 0" />`;
const containerEl = html`<div style="position: relative; width: ${width}px; height: ${height}px; padding: 0.6em 0" />`;
formEl.value = new Set(value);

const basemap = DOM.context2d(width, height);
const highlights = DOM.context2d(width, height);
const brush = DOM.context2d(width, height);
basemap.canvas.style = `position: absolute; width: ${width}px`;
highlights.canvas.style = `position: absolute; width: ${width}px`;
brush.canvas.style = `position: absolute; width: ${width}px`;

const projection = d3
.geoIdentity()
// .fitSize([width - 10, height - 10], countyBordersMesh)
.scale([scale])
.translate([-1600, -700]);
const basemapPath = d3.geoPath(projection, basemap);
const highlightsPath = d3.geoPath(projection, highlights);
const index = buildIndex(counties_filtered, basemapPath);

basemap.setTransform(dpi, 0, 0, dpi, 0, 0);
highlights.setTransform(dpi, 0, 0, dpi, 0, 0);
brush.setTransform(dpi, 0, 0, dpi, 0, 0);

function drawBasemap() {
basemap.clearRect(0, 0, width, height);

if (getCountyColor) {
counties_filtered.forEach(county => {
basemap.beginPath();
basemapPath(county);
basemap.fillStyle = getCountyColor(county);
basemap.fill();
});
}

basemap.beginPath();
basemapPath(countyBordersMesh);
basemap.lineWidth = 0.5;
basemap.strokeStyle = "rgba(0,0,0,.3)";
basemap.stroke();

basemap.beginPath();
basemapPath(stateBordersMesh);
basemap.lineWidth = 0.5;
basemap.strokeStyle = "rgba(0,0,0,.7)";
basemap.stroke();

// basemap.beginPath();
// basemapPath(nationMesh);
// basemap.lineWidth = 0.5;
// basemap.strokeStyle = "#000";
// basemap.stroke();
}

function drawOverlay(value) {
highlights.clearRect(0, 0, width, height);
highlights.beginPath();
highlightsPath({
type: "FeatureCollection",
features: counties_filtered.filter(c => value.has(c.id))
});
highlights.fillStyle = highlightColor;
highlights.fill();
}

function drawBrushes(brushes = []) {
brush.clearRect(0, 0, width, height);

brushes.forEach(coords => {
brush.beginPath();
brush.rect(coords[0][0], coords[0][1], brushSize, brushSize);
brush.lineWidth = 0.5;
brush.strokeStyle = "#000";
brush.stroke();
});
}

// Drag state
let isDragging = false;
let valueAtStartOfDrag = null;
let valueDuringDrag = null;
let dragOperation = 'add';
let lockStateId = null;

function broadcastUpdate() {
formEl.value = valueDuringDrag;
formEl.dispatchEvent(new CustomEvent("input", { bubbles: true }));
}

function computeNextValue(prevIds, ids, operation) {
switch (operation) {
case 'add':
return new Set([...prevIds, ...ids]);
case 'subtract':
return new Set([...prevIds].filter(id => !ids.has(id)));
case 'replace':
default:
return new Set(ids);
}
}

function update(ids = new Set(), operation = dragOperation) {
const prevValue = valueDuringDrag;
const nextValue = computeNextValue(prevValue, ids, operation);

const intersection = [...nextValue].filter(id => prevValue.has(id));
const hasChanged =
intersection.length !== prevValue.size ||
intersection.length !== nextValue.size;

if (hasChanged) {
valueDuringDrag = nextValue;
drawOverlay(nextValue);

if (!deferUpdates) {
broadcastUpdate();
}
}
}

function updateFromBrush(brush) {
const [[minX, minY], [maxX, maxY]] = brush;
const matchedIds = index
.search(minX, minY, maxX, maxY)
.map(i => counties_filtered[i].id)
.filter(id => (lockStateId ? isSameState(id, lockStateId) : true));

update(new Set(matchedIds));
}

brush.canvas.onmousedown = ev => {
isDragging = true;
valueDuringDrag = formEl.value;

if (!ev.shiftKey && !ev.altKey) {
update(new Set(), 'replace');
}

if (ev.altKey) {
dragOperation = 'subtract';
} else {
dragOperation = 'add';
}

if (ev.metaKey) {
const x = ev.offsetX;
const y = ev.offsetY;
const result = index.neighbors(x, y, 1).map(i => counties[i]);
lockStateId = result.length > 0 ? result[0].id : null;
}

function stopDrawing() {
if (deferUpdates) {
broadcastUpdate();
}
isDragging = false;
lockStateId = null;
document.removeEventListener('mouseup', stopDrawing);
}

document.addEventListener('mouseup', stopDrawing);
};

brush.canvas.onmousemove = ev => {
const brush = eventToBrush(ev, brushSize);
if (isDragging) {
updateFromBrush(brush);
}
drawBrushes([brush]);
};

brush.canvas.onmouseleave = () => {
drawBrushes();
};

brush.canvas.ontouchstart = ev => {
if (!isDragging) {
update(new Set(), 'replace');
}

isDragging = true;
valueDuringDrag = formEl.value;

function maybeStopDrawing(ev) {
if (ev.touches.length === 0) {
if (deferUpdates) {
broadcastUpdate();
}
isDragging = false;
drawBrushes();
document.removeEventListener('touchend', maybeStopDrawing);
}
}

document.addEventListener('touchend', maybeStopDrawing);
};

brush.canvas.ontouchmove = ev => {
if (isDragging) {
ev.preventDefault();
const brushes = [];
forEachTouch(ev.touches, touch => {
const brush = eventToBrush(touch, brushSize);
brushes.push(brush);
updateFromBrush(brush);
});
drawBrushes(brushes);
}
};

if (title) {
formEl.append(
html`<div style="font: 700 0.9rem sans-serif; text-align: center;">${title}</div>`
);
}

formEl.append(containerEl);

containerEl.append(basemap.canvas);
containerEl.append(highlights.canvas);
containerEl.append(brush.canvas);

if (description) {
formEl.append(
html`<div style="font-size: 0.85rem; font-style: italic; text-align: center;">${description}</div>`
);
}

drawBasemap();
drawOverlay(formEl.value);

return formEl;
}
Insert cell
Insert cell
Insert cell
counties_filtered = counties.filter(c =>
state_fips.includes(c.id.substring(0, 2))
)
Insert cell
Insert cell
Insert cell
Insert cell
state_fips = ["17", "29"]
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