Maps

    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;
    }