Details
Nearest (letter) Neighbors
Click a state to find the states that have the most number of matching letters in their names.
I'm using bitty for the basic wiring.
The HTML
<script src="/scripts/d3/d3.v7.min.js"></script>
<script src="/scripts/topojson/topojson-3.0.2.min.js"></script>
<bitty-2-0 data-connect="TheMap" data-send="init">
<div class="two-columns">
<div>
<div class="xsmall-font" data-receive="pick"></div>
<ul data-receive="neighbors" class="default-top-margin small-font default-flow reportList"></ul>
</div>
<div id="container"></div>
</div>
</bitty-2-0>
The JavaScript
let hoist;
const s = {
pickedState: null,
distance: null,
names: {
"Alabama": "Alabama0000000",
"Alaska": "Alaska00000000",
"Arizona": "Arizona0000000",
"Arkansas": "Arkansas000000",
"California": "California0000",
"Colorado": "Colorado000000",
"Connecticut": "Connecticut000",
"Delaware": "Delaware000000",
"Florida": "Florida0000000",
"Georgia": "Georgia0000000",
"Hawaii": "Hawaii00000000",
"Idaho": "Idaho000000000",
"Illinois": "Illinois000000",
"Indiana": "Indiana0000000",
"Iowa": "Iowa0000000000",
"Kansas": "Kansas00000000",
"Kentucky": "Kentucky000000",
"Louisiana": "Louisiana00000",
"Maine": "Maine000000000",
"Maryland": "Maryland000000",
"Massachusetts": "Massachusetts0",
"Michigan": "Michigan000000",
"Minnesota": "Minnesota00000",
"Mississippi": "Mississippi000",
"Missouri": "Missouri000000",
"Montana": "Montana0000000",
"Nebraska": "Nebraska000000",
"Nevada": "Nevada00000000",
"New Hampshire": "New Hampshire0",
"New Jersey": "New Jersey0000",
"New Mexico": "New Mexico0000",
"New York": "New York000000",
"North Carolina": "North Carolina",
"North Dakota": "North Dakota00",
"Ohio": "Ohio0000000000",
"Oklahoma": "Oklahoma000000",
"Oregon": "Oregon00000000",
"Pennsylvania": "Pennsylvania00",
"Rhode Island": "Rhode Island00",
"South Carolina": "South Carolina",
"South Dakota": "South Dakota00",
"Tennessee": "Tennessee00000",
"Texas": "Texas000000000",
"Utah": "Utah0000000000",
"Vermont": "Vermont0000000",
"Virginia": "Virginia000000",
"Washington": "Washington0000",
"West Virginia": "West Virginia0",
"Wisconsin": "Wisconsin00000",
"Wyoming": "Wyoming0000000",
},
};
function getSpans(a, b) {
const aLetters = a.split("");
const bLetters = b.split("").map((l) => l.toLowerCase());
return aLetters.map((l) => {
if (bLetters.includes(l.toLowerCase())) {
return `<span class="hit">${l}</span>`;
} else {
return `<span class="miss">${l}</span>`;
}
}).join("");
}
function getSpans2(a, b) {
const aLetters = a.split("");
const bLetters = b.split("").map((l) => l.toLowerCase());
return aLetters.map((l) => {
if (bLetters.includes(l.toLowerCase())) {
return `<span class="hit-neighbor">${l}</span>`;
} else {
return `<span>${l}</span>`;
}
}).join("");
}
function formatText(input) {
if (input.length === 1) {
return input[0];
} else if (input.length === 2) {
return `${input[0]} and ${input[1]}`;
} else {
const lastItem = input.pop();
return `${input.join(", ")}, and ${lastItem}`;
}
}
function updateClosest(stateName) {
s.pickedState = stateName;
s.distance = 100;
s.neighbors = [];
const aLetters = stateName.split("").map((l) => l.toLowerCase());
Object.entries(s.names).forEach(([name, token]) => {
if (stateName !== name) {
let checkCount = 0;
const bLetters = name.split("").map((l) => l.toLowerCase());
aLetters.forEach((l) => {
if (!bLetters.includes(l)) {
checkCount += 1;
}
});
if (checkCount < s.distance) {
s.distance = checkCount;
s.neighbors = [name];
} else if (checkCount === s.distance) {
s.neighbors.push(name);
}
}
});
}
function missingLetters(a, b) {
const set = new Set();
const aLetters = a.split("");
const bLetters = b.split("").map((l) => l.toLowerCase());
aLetters.forEach((l) => {
if (!bLetters.includes(l.toLowerCase())) {
set.add(l.toUpperCase());
}
});
return [...set];
}
window.TheMap = class {
bittyInit() {
document.documentElement.style.setProperty("--page-visibility", "visible");
hoist = this;
}
async init(_event, _el) {
let response = await fetch("/data/states-albers-10m.json");
if (!response.ok) {
throw new Error("There was a problem getting the data");
} else {
s.data = await response.json();
this.makeMap();
}
}
pick(_event, el) {
el.innerHTML = `You picked: ${s.pickedState}<br />
The states with the most matching letters are:
`;
}
neighbors(_event, el) {
el.innerHTML = "";
s.neighbors.forEach((neighbor) => {
const li = document.createElement("li");
const spans1 = getSpans(s.pickedState, neighbor);
const spans2 = getSpans2(neighbor, s.pickedState);
const missing = missingLetters(s.pickedState, neighbor);
if (missing.length > 0) {
li.innerHTML = `${spans2} which is only missing the ${
formatText(missing)
} in ${spans1}`;
} else {
li.innerHTML = `${spans2} which has all the letters in ${spans1}`;
}
el.appendChild(li);
});
this.api.forward(null, "pick");
}
makeMap() {
const us = s.data;
const width = 975;
const height = 610;
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");
const path = d3.geoPath();
const g = svg.append("g");
const states = g.append("g")
.attr("fill", "#444")
.attr("cursor", "pointer")
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.join("path")
.on("click", clicked)
.attr("d", path);
states.append("title")
.text((d) => d.properties.name);
g.append("path")
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-linejoin", "round")
.attr("d", path(topojson.mesh(us, us.objects.states, (a, b) => a !== b)));
function clicked(event, d) {
const [[x0, y0], [x1, y1]] = path.bounds(d);
const stateName = event.target.__data__.properties.name;
updateClosest(stateName);
hoist.api.forward(event, "neighbors");
event.stopPropagation();
}
container.append(svg.node());
}
}
The CSS
[data-receive=neighbors] {
min-height: 4rem;
}
.hit-neighbor {
font-weight: 900;
text-decoration: underline;
}
.hit {
font-weight: 900;
}
.miss {
color: var(--warning);
text-decoration: underline;
}
.two-columns {
display: grid;
grid-template-columns: 1fr 3fr;
}
.reportList {
list-style: none;
padding-left: 0;
}