Making an Interactive US Map with D3js

in #programming2 months ago

usmap.jpg

The image above was made with stable diffusion using the prompt 'A blocky abstracted colorful map of the united states.'

I recently sketched out an interactive map to look at both destructive and constructive events brought about by people who were pushed too far. I modified the Tilegrams NPR 1-to-1 json file to use as a basis and made the page with d3js. The page is very simple. It's just a title, the map, a side panel that opens when a relevant state is clicked, a legend, a text area, and a footer.

Here are d3 script imports

<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>

Here's my styling

        body {
            display: flex;
            flex-direction: column;         
            justify-content: center;
            align-items: center;
            margin: 0;
            background-color: #fff;
        }
        
        a:link {
            color: red;
            text-decoration: none;          
        }

        a:visited {
            color: cyan;        
        }

        a:hover {
            color: white;
            background-color: black;            
        }       
        
        #header {
            width: 100%; /* Full width of the page */
            text-align: center; 
            padding: 10px 0; /* Add space above and below the text */
            position: fixed; /* Keeps it at the top of the flow */
            margin-bottom: 10px;            
            top: 0;
            z-index: 1000;
            pointer-events: none; /* Allows clicks to pass through */           
            transition: transform 0.3s ease-in-out; /* Smooth hide/show transition */           
        }

        #header.hidden {
            transform: translateY(-100%);
        }       
        
        #header h1 {
            font-family: "Pathway Gothic One", sans-serif;          
            font-size: 48px;
            font-weight: bold;
            color: red;         
        }
        
        #text-container {
            width: 60%; /* Adjust width to fit within the page layout */
            margin: 20px auto; /* Center horizontally and add spacing */
            padding: 10px;
            padding-bottom: 60px;           
            background-color: #f9f9f9; /* Optional: Background color for visibility */
            font-family: "Noto Serif", serif;
            font-size: 16px;
            line-height: 1.5;
            color: #333;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); /* Optional: Add a shadow for depth */
        }       

        #footer {
            color: cyan;
            background-color: black;            
            font-family: courier;
            font-weight: bold;
            text-align: center;
            padding: 8px;
            width: 100%;
            margin-top: 20px;           
            position: fixed; /* Keeps it at the bottom of the flow */
            bottom: 0;          
        }       

        #map-container {
            width: 100%; /* Full width */
            height: 88vh;           
            position: relative;         
            margin-top: 80px;
            margin-bottom: 60px;            
            margin-left: 50px;          
        }

        svg {
            display: block;     
            width: 100%; /* Responsive scaling */
            height: 100%; /* Maintain aspect ratio */
            z-index: 1000;          
        }

        .state {
            fill: #ccc;
            stroke: #fff;
            stroke-width: 8px;
            transition: transform 0.1s ease-in-out;         
        }
        
        .state:active {
            transform: scale(1.03); /* Slightly enlarge */
        }       

        .state:hover {
            fill: red !important;
        }
        
        #info-panel {
            position: fixed;
            top: 0;
            left: -360px;
            width: 360px;
            height: 100%;
            background: rgba(255, 255, 255, 0.8);
            font-family: "Noto Serif", serif;           
            overflow-y: auto;
            transition: left 0.2s ease-in-out;
            padding: 20px;
            z-index: 10;            
        }

        #info-panel.open {
            left: 0;
        }

        #info-panel h2 {
            margin-top: 0;
            font-family: "Pathway Gothic One", sans-serif;          
        }

        #close-panel {
            position: absolute;
            bottom: 80px; /* Distance from the bottom of the panel */
            right: 20px;  /* Distance from the right edge of the panel */
            background: #fff;
            color: black;
            letter-spacing: 2px;            
            border: 2px solid gray;
            padding: 10px 15px;
            font-family: "Pathway Gothic One", sans-serif;          
            font-size: 14px;
            font-weight: bold;
            cursor: pointer;
            z-index: 1000; /* Ensure it's above other elements */
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); /* Optional: Adds a shadow for depth */
            opacity: 0; /* Hidden by default */
            pointer-events: none; /* Prevent interaction when hidden */         
        }
        
        #info-panel.open #close-panel {
            opacity: 1; /* Show the button when the panel is open */
            pointer-events: auto; /* Enable interaction */
        }       

        #close-panel:hover {
            background: black; /* Darker red when hovered */
            color: white;           
        }
        
        #legend {
            position: absolute;
            bottom: 20px;
            right: 20px;
            background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent background */
            padding: 10px;
            font-family: "Pathway Gothic One", sans-serif;
            font-size: 14px;
            line-height: 1.5;           
        }
        .legend-item {
            display: flex;
            align-items: center;
            margin-bottom: 5px;
        }
        .legend-item:last-child {
            margin-bottom: 0;
        }
        .legend-color {
            width: 20px;
            height: 20px;
            margin-right: 10px;
        }
        
        .label {
            font-size: 16px;
            font-weight: bold;
            font-family: "Pathway Gothic One", sans-serif;          
            text-anchor: middle; /* Center the text */
            pointer-events: none; /* Prevent interaction with labels */
        }

        .state.destructive {
            fill: black;
        }
        .state.constructive {
            fill: cyan;
        }
        .state.both {
            fill: green;
        }

        .label.gray {
            fill: gray;
        }

Here's the relevant html

    <div id="header"><h1>Mapping the Discontent</h1></div>
    <div id="map-container">
        <svg id="map" viewBox="-200 -100 1200 800" preserveAspectRatio="xMidYMid slice"></svg>
        <div id="legend">
            <div class="legend-item">
                <div class="legend-color" style="background-color: black;"></div>
                <span>Destructive Event</span>
            </div>
            <div class="legend-item">
                <div class="legend-color" style="background-color: cyan;"></div>
                <span>Constructive Event</span>
            </div>
            <div class="legend-item">
                <div class="legend-color" style="background-color: green;"></div>
                <span>Constructive & Destructive</span>
            </div>
        </div>  
    </div>      
    <div id="info-panel">
        <h2 id="state-name"></h2>
        <p id="state-details">Details about the state will go here.</p>
        <button id="close-panel">CLOSE</button>         
    </div>      
    <div id="text-container">
        <p>This is an interactive map to look at both destructive and constructive events brought about by people who were pushed too far. Eventually, hidden connections between events may be revealed.
        </p>
    </div>  
    <div id="footer">Page by <a href="https://freemindgazette.substack.com/">Mark Bailey</a></div>

And here's the javascript

        const mapData = {"type": "Topology", "objects": {"tiles": {"type": "GeometryCollection", "geometries": [{"type": "Polygon", "id": "10", "properties": {"name": "DELAWARE", "tilegramValue": 1, "abbr": "DE"}, "arcs": [[0, 1, 2, 3, 4]]}, {"type": "Polygon", "id": "11", "properties": {"name": "DISTRICT OF COLUMBIA", "tilegramValue": 1, "abbr": "DC"}, "arcs": [[5, -1, 6, 7]]}, {"type": "Polygon", "id": "12", "properties": {"name": "FLORIDA", "tilegramValue": 1, "abbr": "FL"}, "arcs": [[8, 9, 10]]}, {"type": "Polygon", "id": "13", "properties": {"name": "GEORGIA", "tilegramValue": 1, "abbr": "GA"}, "arcs": [[-9, 11, 12, 13, 14]]}, {"type": "Polygon", "id": "15", "properties": {"name": "HAWAII", "tilegramValue": 1, "abbr": "HI"}, "arcs": [[15]]}, {"type": "Polygon", "id": "16", "properties": {"name": "IDAHO", "tilegramValue": 1, "abbr": "ID"}, "arcs": [[16, 17, 18, 19, 20, 21]]}, {"type": "Polygon", "id": "17", "properties": {"name": "ILLINOIS", "tilegramValue": 1, "abbr": "IL"}, "arcs": [[22, 23, 24, 25, 26, 27]]}, {"type": "Polygon", "id": "18", "properties": {"name": "INDIANA", "tilegramValue": 1, "abbr": "IN"}, "arcs": [[28, 29, 30, 31, 32, -25]]}, {"type": "Polygon", "id": "19", "properties": {"name": "IOWA", "tilegramValue": 1, "abbr": "IA"}, "arcs": [[33, 34, -28, 35, 36, 37]]}, {"type": "Polygon", "id": "20", "properties": {"name": "KANSAS", "tilegramValue": 1, "abbr": "KS"}, "arcs": [[38, 39, 40, 41, 42, 43]]}, {"type": "Polygon", "id": "21", "properties": {"name": "KENTUCKY", "tilegramValue": 1, "abbr": "KY"}, "arcs": [[44, 45, 46, -29, -24, 47]]}, {"type": "Polygon", "id": "22", "properties": {"name": "LOUISIANA", "tilegramValue": 1, "abbr": "LA"}, "arcs": [[48, 49, 50, 51, -40, 52]]}, {"type": "Polygon", "id": "23", "properties": {"name": "MAINE", "tilegramValue": 1, "abbr": "ME"}, "arcs": [[53, 54]]}, {"type": "Polygon", "id": "24", "properties": {"name": "MARYLAND", "tilegramValue": 1, "abbr": "MD"}, "arcs": [[55, -7, -5, 56, 57, 58]]}, {"type": "Polygon", "id": "25", "properties": {"name": "MASSACHUSETTS", "tilegramValue": 1, "abbr": "MA"}, "arcs": [[59, 60, 61, 62, 63, 64]]}, {"type": "Polygon", "id": "26", "properties": {"name": "MICHIGAN", "tilegramValue": 1, "abbr": "MI"}, "arcs": [[-32, 65, 66]]}, {"type": "Polygon", "id": "27", "properties": {"name": "MINNESOTA", "tilegramValue": 1, "abbr": "MN"}, "arcs": [[67, -37, 68, 69, 70]]}, {"type": "Polygon", "id": "28", "properties": {"name": "MISSISSIPPI", "tilegramValue": 1, "abbr": "MS"}, "arcs": [[71, 72, 73, 74, -51]]}, {"type": "Polygon", "id": "29", "properties": {"name": "MISSOURI", "tilegramValue": 1, "abbr": "MO"}, "arcs": [[-42, 75, -48, -23, -35, 76]]}, {"type": "Polygon", "id": "30", "properties": {"name": "MONTANA", "tilegramValue": 1, "abbr": "MT"}, "arcs": [[-20, 77, 78, 79, 80]]}, {"type": "Polygon", "id": "31", "properties": {"name": "NEBRASKA", "tilegramValue": 1, "abbr": "NE"}, "arcs": [[81, -43, -77, -34, 82, 83]]}, {"type": "Polygon", "id": "32", "properties": {"name": "NEVADA", "tilegramValue": 1, "abbr": "NV"}, "arcs": [[84, 85, 86, 87, -18, 88]]}, {"type": "Polygon", "id": "33", "properties": {"name": "NEW HAMPSHIRE", "tilegramValue": 1, "abbr": "NH"}, "arcs": [[-63, 89, 90, -54, 91, 92]]}, {"type": "Polygon", "id": "34", "properties": {"name": "NEW JERSEY", "tilegramValue": 1, "abbr": "NJ"}, "arcs": [[-57, -4, 93, -60, 94, 95]]}, {"type": "Polygon", "id": "35", "properties": {"name": "NEW MEXICO", "tilegramValue": 1, "abbr": "NM"}, "arcs": [[96, 97, -44, -82, 98, 99]]}, {"type": "Polygon", "id": "36", "properties": {"name": "NEW YORK", "tilegramValue": 1, "abbr": "NY"}, "arcs": [[100, -95, -65, 101, 102]]}, {"type": "Polygon", "id": "37", "properties": {"name": "NORTH CAROLINA", "tilegramValue": 1, "abbr": "NC"}, "arcs": [[103, -14, 104, 105, 106, 107]]}, {"type": "Polygon", "id": "38", "properties": {"name": "NORTH DAKOTA", "tilegramValue": 1, "abbr": "ND"}, "arcs": [[108, 109, -71, 110, -79]]}, {"type": "Polygon", "id": "39", "properties": {"name": "OHIO", "tilegramValue": 1, "abbr": "OH"}, "arcs": [[111, 112, 113, 114, -66, -31]]}, {"type": "Polygon", "id": "40", "properties": {"name": "OKLAHOMA", "tilegramValue": 1, "abbr": "OK"}, "arcs": [[115, 116, -53, -39, -98, 117]]}, {"type": "Polygon", "id": "41", "properties": {"name": "OREGON", "tilegramValue": 1, "abbr": "OR"}, "arcs": [[118, -89, -17, 119]]}, {"type": "Polygon", "id": "42", "properties": {"name": "PENNSYLVANIA", "tilegramValue": 1, "abbr": "PA"}, "arcs": [[120, -58, -96, -101, 121, -114]]}, {"type": "Polygon", "id": "44", "properties": {"name": "RHODE ISLAND", "tilegramValue": 1, "abbr": "RI"}, "arcs": [[122, 123, -90, -62]]}, {"type": "Polygon", "id": "45", "properties": {"name": "SOUTH CAROLINA", "tilegramValue": 1, "abbr": "SC"}, "arcs": [[-13, 124, -8, -56, 125, -105]]}, {"type": "Polygon", "id": "46", "properties": {"name": "SOUTH DAKOTA", "tilegramValue": 1, "abbr": "SD"}, "arcs": [[126, -83, -38, -68, -110, 127]]}, {"type": "Polygon", "id": "47", "properties": {"name": "TENNESSEE", "tilegramValue": 1, "abbr": "TN"}, "arcs": [[-74, 128, -108, 129, -46, 130]]}, {"type": "Polygon", "id": "48", "properties": {"name": "TEXAS", "tilegramValue": 1, "abbr": "TX"}, "arcs": [[-49, -117, 131]]}, {"type": "Polygon", "id": "49", "properties": {"name": "UTAH", "tilegramValue": 1, "abbr": "UT"}, "arcs": [[132, 133, -100, 134, -86, 135]]}, {"type": "Polygon", "id": "50", "properties": {"name": "VERMONT", "tilegramValue": 1, "abbr": "VT"}, "arcs": [[-102, -64, -93, 136]]}, {"type": "Polygon", "id": "51", "properties": {"name": "VIRGINIA", "tilegramValue": 1, "abbr": "VA"}, "arcs": [[-106, -126, -59, -121, -113, 137]]}, {"type": "Polygon", "id": "53", "properties": {"name": "WASHINGTON", "tilegramValue": 1, "abbr": "WA"}, "arcs": [[-21, -81, 138]]}, {"type": "Polygon", "id": "54", "properties": {"name": "WEST VIRGINIA", "tilegramValue": 1, "abbr": "WV"}, "arcs": [[-130, -107, -138, -112, -30, -47]]}, {"type": "Polygon", "id": "55", "properties": {"name": "WISCONSIN", "tilegramValue": 1, "abbr": "WI"}, "arcs": [[-36, -27, 139, -69]]}, {"type": "Polygon", "id": "56", "properties": {"name": "WYOMING", "tilegramValue": 1, "abbr": "WY"}, "arcs": [[-88, 140, -128, -109, -78, -19]]}, {"type": "Polygon", "id": "02", "properties": {"name": "ALASKA", "tilegramValue": 1, "abbr": "AK"}, "arcs": [[141]]}, {"type": "Polygon", "id": "06", "properties": {"name": "CALIFORNIA", "tilegramValue": 1, "abbr": "CA"}, "arcs": [[-136, -85, -119, 142]]}, {"type": "Polygon", "id": "08", "properties": {"name": "COLORADO", "tilegramValue": 1, "abbr": "CO"}, "arcs": [[-135, -99, -84, -127, -141, -87]]}, {"type": "Polygon", "id": "04", "properties": {"name": "ARIZONA", "tilegramValue": 1, "abbr": "AZ"}, "arcs": [[-118, -97, -134, 143]]}, {"type": "Polygon", "id": "05", "properties": {"name": "ARKANSAS", "tilegramValue": 1, "abbr": "AR"}, "arcs": [[-52, -75, -131, -45, -76, -41]]}, {"type": "Polygon", "id": "01", "properties": {"name": "ALABAMA", "tilegramValue": 1, "abbr": "AL"}, "arcs": [[144, -10, -15, -104, -129, -73]]}, {"type": "Polygon", "id": "09", "properties": {"name": "CONNECTICUT", "tilegramValue": 1, "abbr": "CT"}, "arcs": [[-3, 145, -123, -61, -94]]}]}}, "arcs": [[[8399999999, 4000000000], [400000000, -400000000]], [[8799999999, 3600000000], [400000000, 400000000], [0, 800000000]], [[9199999999, 4800000000], [-400000000, 399999999]], [[8799999999, 5199999999], [-400000000, -399999999]], [[8399999999, 4800000000], [0, -800000000]], [[7999999999, 2800000000], [400000000, -400000000], [400000000, 400000000], [0, 800000000]], [[8399999999, 4000000000], [-400000000, -400000000]], [[7999999999, 3600000000], [0, -800000000]], [[7199999999, 1200000000], [-400000000, 400000000]], [[6799999999, 1600000000], [-400000000, -400000000]], [[6399999999, 1200000000], [0, -800000000], [400000000, -400000000], [400000000, 400000000], [0, 800000000]], [[7199999999, 1200000000], [400000000, 400000000], [0, 800000000]], [[7599999999, 2400000000], [-400000000, 400000000]], [[7199999999, 2800000000], [-400000000, -400000000]], [[6799999999, 2400000000], [0, -800000000]], [[0, 400000000], [400000000, -400000000], [400000000, 400000000], [0, 800000000], [-400000000, 400000000], [-400000000, -400000000], [0, -800000000]], [[1600000000, 5199999999], [400000000, -399999999]], [[2000000000, 4800000000], [400000000, 399999999]], [[2400000000, 5199999999], [0, 800000000]], [[2400000000, 5999999999], [-400000000, 400000000]], [[2000000000, 6399999999], [-400000000, -400000000]], [[1600000000, 5999999999], [0, -800000000]], [[4800000000, 5199999999], [399999999, -399999999]], [[5199999999, 4800000000], [400000000, 399999999]], [[5599999999, 5199999999], [0, 800000000]], [[5599999999, 5999999999], [-400000000, 400000000]], [[5199999999, 6399999999], [-399999999, -400000000]], [[4800000000, 5999999999], [0, -800000000]], [[5599999999, 5199999999], [400000000, -399999999]], [[5999999999, 4800000000], [400000000, 399999999]], [[6399999999, 5199999999], [0, 800000000]], [[6399999999, 5999999999], [-400000000, 400000000]], [[5999999999, 6399999999], [-400000000, -400000000]], [[4000000000, 5199999999], [400000000, -399999999]], [[4400000000, 4800000000], [400000000, 399999999]], [[4800000000, 5999999999], [-400000000, 400000000]], [[4400000000, 6399999999], [-400000000, -400000000]], [[4000000000, 5999999999], [0, -800000000]], [[4000000000, 2800000000], [400000000, -400000000]], [[4400000000, 2400000000], [400000000, 400000000]], [[4800000000, 2800000000], [0, 800000000]], [[4800000000, 3600000000], [-400000000, 400000000]], [[4400000000, 4000000000], [-400000000, -400000000]], [[4000000000, 3600000000], [0, -800000000]], [[5199999999, 4000000000], [400000000, -400000000]], [[5599999999, 3600000000], [400000000, 400000000]], [[5999999999, 4000000000], [0, 800000000]], [[5199999999, 4800000000], [0, -800000000]], [[4400000000, 1600000000], [400000000, -400000000]], [[4800000000, 1200000000], [399999999, 400000000]], [[5199999999, 1600000000], [0, 800000000]], [[5199999999, 2400000000], [-399999999, 400000000]], [[4400000000, 2400000000], [0, -800000000]], [[9199999999, 8799999999], [400000000, -400000000]], [[9599999999, 8399999999], [400000000, 400000000], [0, 800000000], [-400000000, 400000000], [-400000000, -400000000], [0, -800000000]], [[7599999999, 4000000000], [400000000, -400000000]], [[8399999999, 4800000000], [-400000000, 399999999]], [[7999999999, 5199999999], [-400000000, -399999999]], [[7599999999, 4800000000], [0, -800000000]], [[8399999999, 6399999999], [400000000, -400000000]], [[8799999999, 5999999999], [400000000, 400000000]], [[9199999999, 6399999999], [0, 800000000]], [[9199999999, 7199999999], [-400000000, 400000000]], [[8799999999, 7599999999], [-400000000, -400000000]], [[8399999999, 7199999999], [0, -800000000]], [[6399999999, 5999999999], [400000000, 400000000]], [[6799999999, 6399999999], [0, 800000000], [-400000000, 400000000], [-400000000, -400000000], [0, -800000000]], [[3600000000, 6399999999], [400000000, -400000000]], [[4400000000, 6399999999], [0, 800000000]], [[4400000000, 7199999999], [-400000000, 400000000], [-400000000, -400000000]], [[3600000000, 7199999999], [0, -800000000]], [[5199999999, 1600000000], [400000000, -400000000], [400000000, 400000000]], [[5999999999, 1600000000], [0, 800000000]], [[5999999999, 2400000000], [-400000000, 400000000]], [[5599999999, 2800000000], [-400000000, -400000000]], [[4800000000, 3600000000], [399999999, 400000000]], [[4400000000, 4800000000], [0, -800000000]], [[2400000000, 5999999999], [400000000, 400000000]], [[2800000000, 6399999999], [0, 800000000]], [[2800000000, 7199999999], [-400000000, 400000000], [-400000000, -400000000]], [[2000000000, 7199999999], [0, -800000000]], [[3600000000, 4000000000], [400000000, -400000000]], [[4000000000, 5199999999], [-400000000, -399999999]], [[3600000000, 4800000000], [0, -800000000]], [[2000000000, 4000000000], [400000000, -400000000]], [[2400000000, 3600000000], [400000000, 400000000]], [[2800000000, 4000000000], [0, 800000000]], [[2800000000, 4800000000], [-400000000, 399999999]], [[2000000000, 4800000000], [0, -800000000]], [[9199999999, 7199999999], [400000000, 400000000]], [[9599999999, 7599999999], [0, 800000000]], [[9199999999, 8799999999], [-400000000, -400000000]], [[8799999999, 8399999999], [0, -800000000]], [[8799999999, 5199999999], [0, 800000000]], [[8399999999, 6399999999], [-400000000, -400000000]], [[7999999999, 5999999999], [0, -800000000]], [[3200000000, 2800000000], [400000000, -400000000]], [[3600000000, 2400000000], [400000000, 400000000]], [[3600000000, 4000000000], [-400000000, -400000000]], [[3200000000, 3600000000], [0, -800000000]], [[7599999999, 6399999999], [400000000, -400000000]], [[8399999999, 7199999999], [-400000000, 400000000]], [[7999999999, 7599999999], [-400000000, -400000000], [0, -800000000]], [[6399999999, 2800000000], [400000000, -400000000]], [[7199999999, 2800000000], [0, 800000000]], [[7199999999, 3600000000], [-400000000, 400000000]], [[6799999999, 4000000000], [-400000000, -400000000]], [[6399999999, 3600000000], [0, -800000000]], [[2800000000, 6399999999], [400000000, -400000000]], [[3200000000, 5999999999], [400000000, 400000000]], [[3600000000, 7199999999], [-400000000, 400000000], [-400000000, -400000000]], [[6399999999, 5199999999], [400000000, -399999999]], [[6799999999, 4800000000], [400000000, 399999999]], [[7199999999, 5199999999], [0, 800000000]], [[7199999999, 5999999999], [-400000000, 400000000]], [[3600000000, 1600000000], [400000000, -400000000]], [[4000000000, 1200000000], [400000000, 400000000]], [[3600000000, 2400000000], [0, -800000000]], [[1600000000, 3600000000], [400000000, 400000000]], [[1600000000, 5199999999], [-400000000, -399999999], [0, -800000000], [400000000, -400000000]], [[7199999999, 5199999999], [400000000, -399999999]], [[7599999999, 6399999999], [-400000000, -400000000]], [[9199999999, 6399999999], [400000000, -400000000]], [[9599999999, 5999999999], [400000000, 400000000], [0, 800000000], [-400000000, 400000000]], [[7599999999, 2400000000], [400000000, 400000000]], [[7599999999, 4000000000], [-400000000, -400000000]], [[3200000000, 5199999999], [400000000, -399999999]], [[3200000000, 5999999999], [0, -800000000]], [[5999999999, 2400000000], [400000000, 400000000]], [[6399999999, 3600000000], [-400000000, 400000000]], [[5599999999, 3600000000], [0, -800000000]], [[4000000000, 1200000000], [0, -800000000], [400000000, -400000000], [400000000, 400000000], [0, 800000000]], [[2400000000, 2800000000], [400000000, -400000000]], [[2800000000, 2400000000], [400000000, 400000000]], [[3200000000, 3600000000], [-400000000, 400000000]], [[2400000000, 3600000000], [0, -800000000]], [[8799999999, 8399999999], [-400000000, 400000000], [-400000000, -400000000], [0, -800000000]], [[6799999999, 4800000000], [0, -800000000]], [[2000000000, 7199999999], [-400000000, 400000000], [-400000000, -400000000], [0, -800000000], [400000000, -400000000]], [[5199999999, 6399999999], [0, 800000000], [-399999999, 400000000], [-400000000, -400000000]], [[2800000000, 4800000000], [400000000, 399999999]], [[400000000, 8799999999], [400000000, -400000000], [400000000, 400000000], [0, 800000000], [-400000000, 400000000], [-400000000, -400000000], [0, -800000000]], [[1600000000, 3600000000], [0, -800000000], [400000000, -400000000], [400000000, 400000000]], [[2800000000, 2400000000], [0, -800000000], [400000000, -400000000], [400000000, 400000000]], [[5999999999, 1600000000], [400000000, -400000000]], [[9199999999, 4800000000], [400000000, 399999999], [0, 800000000]]], "transform": {"scale": [6.585401508602708e-08, 3.802083333713542e-08], "translate": [263.4160603177667, 15.208333333333332]}, "bbox": [263.4160603177667, 15.208333333333332, 921.9562111121836, 395.4166666666667], "properties": {"tilegramMetricPerTile": 1, "tilegramVersion": "1.2.0", "tilegramTileSize": {"width": 52.68321206355335, "height": 60.833333333333336}, "tilegramGeography": "United States"}};       

        const projection = d3.geoIdentity()
            .reflectY(true)
            .fitSize([800, 600], topojson.feature(mapData, mapData.objects.tiles));

        const path = d3.geoPath().projection(projection);

        const svg = d3.select("#map");

        const geojson = topojson.feature(mapData, mapData.objects.tiles);
        
        const eventsData = [
            { state: "NY", city: "Manhattan", date: "2024-12-04", type: "destructive", summary: "<p>Luigi Mangione kills the CEO of UnitedHealthcare Brian Thompson in midtown Manhattan.</p><p>The assasination was carefully planned.</p>" },
            { state: "LA", city: "New Orleans", date: "2025-01-01", type: "destructive", summary: "US Army veteran Shamsud-Din Jabbar drove into a crowd and then died in a shootout with police, killing 15 and injuring 30 more." },
            { state: "NV", city: "Las Vegas", date: "2025-01-01", type: "destructive", summary: "Matthew Livelsberger blew up a Tesla Cybertruck outside the Trump International Las Vegas Hotel." },
            { state: "WI", city: "Madison", date: "2024-12-16", type: "destructive", summary: "15-year-old Natalie Rupnow opens fire at her religious school, killing a teacher and a classmate and wounding several others." },
            { state: "MN", city: "Minneapolis", date: "2025-01-01", type: "constructive", summary: "We went ice skating." }         
        ];

        // Group events by state
        const stateEvents = {};
        eventsData.forEach(event => {
            if (!stateEvents[event.state]) {
                stateEvents[event.state] = { destructive: [], constructive: [] };
            }
            stateEvents[event.state][event.type].push(event);
        });     

        // Draw the map
        svg.selectAll("path")
            .data(geojson.features)
            .enter()
            .append("path")
            .attr("class", "state")
            .attr("d", path)
            .on("click", function (event, d) {
                const panel = document.getElementById("info-panel");
                const stateName = document.getElementById("state-name");
                const stateDetails = document.getElementById("state-details");

                // Update panel content
                stateName.textContent = d.properties.name;
                stateDetails.textContent = `Details about ${d.properties.name} will go here.`;

                // Toggle panel
                if (!panel.classList.contains("open") || stateName.textContent !== d.properties.name) {
                    panel.classList.add("open");
                } else {
                    panel.classList.remove("open");
                }
            });
        document.getElementById("close-panel").addEventListener("click", () => {
            document.getElementById("info-panel").classList.remove("open");
        });
        // Add state labels
        svg.selectAll("text")
            .data(geojson.features)
            .enter()
            .append("text")
            .attr("class", "label")
            .attr("x", d => path.centroid(d)[0]) // X-coordinate of centroid
            .attr("y", d => path.centroid(d)[1]) // Y-coordinate of centroid
            .text(d => d.properties.abbr); // Display abbreviation

        // Update state colors based on events
        svg.selectAll("path")
            .data(geojson.features)
            .attr("class", "state")
            .style("fill", d => {
                const events = stateEvents[d.properties.abbr];
                if (!events) return "#ccc"; // Default color
                const hasDestructive = events.destructive.length > 0;
                const hasConstructive = events.constructive.length > 0;
                if (hasDestructive && hasConstructive) return "green";
                if (hasDestructive) return "black";
                if (hasConstructive) return "cyan";
                return "#ccc";
            })
            .on("click", function (event, d) {
                const events = stateEvents[d.properties.abbr];
                if (!events) return;

                // Update the panel with event summaries
                const panel = document.getElementById("info-panel");
                const stateName = document.getElementById("state-name");
                const stateDetails = document.getElementById("state-details");

                stateName.textContent = d.properties.name;
                stateDetails.innerHTML = "";

                ["destructive", "constructive"].forEach(type => {
                    events[type].forEach(evt => {
                        const title = `<h4>${evt.city} (${evt.date})</h4>`;
                        const content = `<p>${evt.summary}</p>`;
                        const eventHTML = `<div class="event ${type}">${title}${content}</div>`;

                        stateDetails.innerHTML += eventHTML;
                    });
                });

                panel.classList.add("open");
            });
            
        // Style labels based on events
        svg.selectAll("text")
            .data(geojson.features)
            .attr("class", "label")
            .style("fill", d => {
                const events = stateEvents[d.properties.abbr];
                if (!events) return "#333"; // Default color
                if (events.destructive.length > 0) return "gray";
                return "#333";
            });

        let lastScrollTop = 0; // Keeps track of the last scroll position
        const header = document.getElementById("header");

        window.addEventListener("scroll", () => {
            const currentScroll = window.pageYOffset || document.documentElement.scrollTop;

            if (currentScroll > lastScrollTop) {
            // Scrolling down
                header.classList.add("hidden");
            } else {
            // Scrolling up
                header.classList.remove("hidden");
            }

            lastScrollTop = currentScroll <= 0 ? 0 : currentScroll; // Ensure it doesn't go negative
        });

Notes

I've been wanting to get into d3 for a long time and this project finally gave me an excuse to start playing around. Eventually, I want to create nice interactive graphs with the library and this is a stepping stone towards that. The only stumbling block I ran into was with the svg display. After trying several things and enlisting the help of gpt, I was finally able to get it to display mostly correctly using <svg id="map" viewBox="-200 -100 1200 800" preserveAspectRatio="xMidYMid slice"></svg> The display still isn't perfect, but it's probably good enough for now.

Along with the svg json, I set up the events data as a separate json input, with the idea to eventually have this stored remotely in a bin that can be added to/updated by users. The structure of this data hasn't yet been finalized, so I'll be tweaking it until I get it right before taking the next step.


Read Free Mind Gazette on Substack

Read my novels:

See my NFTs:

  • Small Gods of Time Travel is a 41 piece Tezos NFT collection on Objkt that goes with my book by the same name.
  • History and the Machine is a 20 piece Tezos NFT collection on Objkt based on my series of oil paintings of interesting people from history.
  • Artifacts of Mind Control is a 15 piece Tezos NFT collection on Objkt based on declassified CIA documents from the MKULTRA program.
Sort:  

Went ice skating!!! :D

Needed a positive placeholder : )