Published
Edited
May 28, 2021
4 forks
Importers
38 stars
Insert cell
Insert cell
Insert cell
viewof countyCodes = fipsbrush({
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 / 975;
const height = scale * 610;
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().scale(scale);
const basemapPath = d3.geoPath(projection, basemap);
const highlightsPath = d3.geoPath(projection, highlights);
const index = buildIndex(counties, 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.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.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[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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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