<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: leaflet</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/leaflet.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-03-16T20:12:32+00:00</updated><author><name>Simon Willison</name></author><entry><title>Coding agents for data analysis</title><link href="https://simonwillison.net/2026/Mar/16/coding-agents-for-data-analysis/#atom-tag" rel="alternate"/><published>2026-03-16T20:12:32+00:00</published><updated>2026-03-16T20:12:32+00:00</updated><id>https://simonwillison.net/2026/Mar/16/coding-agents-for-data-analysis/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/"&gt;Coding agents for data analysis&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Here's the handout I prepared for my NICAR 2026 workshop "Coding agents for data analysis" - a three hour session aimed at data journalists demonstrating ways that tools like Claude Code and OpenAI Codex can be used to explore, analyze and clean data.&lt;/p&gt;
&lt;p&gt;Here's the table of contents:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/coding-agents.html"&gt;Coding agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/warmup.html"&gt;Warmup: ChatGPT and Claude&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/setup.html"&gt;Setup Claude Code and Codex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/asking-questions.html"&gt;Asking questions against a database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/exploring-data.html"&gt;Exploring data with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/cleaning-trees.html"&gt;Cleaning data: decoding neighborhood codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/visualizations.html"&gt;Creating visualizations with agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonw.github.io/nicar-2026-coding-agents/scraping.html"&gt;Scraping data with agents&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;I ran the workshop using GitHub Codespaces and OpenAI Codex, since it was easy (and inexpensive) to distribute a budget-restricted API key for Codex that attendees could use during the class. Participants ended up burning $23 of Codex tokens.&lt;/p&gt;
&lt;p&gt;The exercises all used Python and SQLite and some of them used Datasette.&lt;/p&gt;
&lt;p&gt;One highlight of the workshop was when we started &lt;a href="https://simonw.github.io/nicar-2026-coding-agents/visualizations.html#javascript-visualizations"&gt;running Datasette&lt;/a&gt; such that it served static content from a &lt;code&gt;viz/&lt;/code&gt; folder, then had Claude Code start vibe coding new interactive visualizations directly in that folder. Here's a heat map it created for my trees database using Leaflet and &lt;a href="https://github.com/Leaflet/Leaflet.heat"&gt;Leaflet.heat&lt;/a&gt;, &lt;a href="https://gist.github.com/simonw/985ae2a6a3cd3df3fd375eb58dabea0f"&gt;source code here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of a &amp;quot;Trees SQL Map&amp;quot; web application with the heading &amp;quot;Trees SQL Map&amp;quot; and subheading &amp;quot;Run a query and render all returned points as a heat map. The default query targets roughly 200,000 trees.&amp;quot; Below is an input field containing &amp;quot;/trees/-/query.json&amp;quot;, a &amp;quot;Run Query&amp;quot; button, and a SQL query editor with the text &amp;quot;SELECT cast(Latitude AS float) AS latitude, cast(Longitude AS float) AS longitude, CASE WHEN DBH IS NULL OR DBH = '' THEN 0.3 WHEN cast(DBH AS float) &amp;lt;= 0 THEN 0.3 WHEN cast(DBH AS float) &amp;gt;= 80 THEN 1.0&amp;quot; (query is truncated). A status message reads &amp;quot;Loaded 1,000 rows and plotted 1,000 points as heat map.&amp;quot; Below is a Leaflet/OpenStreetMap interactive map of San Francisco showing a heat map overlay of tree locations, with blue/green clusters concentrated in areas like the Richmond District, Sunset District, and other neighborhoods. Map includes zoom controls and a &amp;quot;Leaflet | © OpenStreetMap contributors&amp;quot; attribution." src="https://static.simonwillison.net/static/2026/tree-sql-map.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;I designed the handout to also be useful for people who weren't able to attend the session in person. As is usually the case, material aimed at data journalists is equally applicable to anyone else with data to explore.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/speaking"&gt;speaking&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-codespaces"&gt;github-codespaces&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nicar"&gt;nicar&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-code"&gt;claude-code&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/codex"&gt;codex&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;



</summary><category term="data-journalism"/><category term="geospatial"/><category term="python"/><category term="speaking"/><category term="sqlite"/><category term="ai"/><category term="datasette"/><category term="generative-ai"/><category term="llms"/><category term="github-codespaces"/><category term="nicar"/><category term="coding-agents"/><category term="claude-code"/><category term="codex"/><category term="leaflet"/></entry><entry><title>wolf-h3-viewer.glitch.me</title><link href="https://simonwillison.net/2025/Mar/9/h3-viewer/#atom-tag" rel="alternate"/><published>2025-03-09T14:51:55+00:00</published><updated>2025-03-09T14:51:55+00:00</updated><id>https://simonwillison.net/2025/Mar/9/h3-viewer/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://wolf-h3-viewer.glitch.me/"&gt;wolf-h3-viewer.glitch.me&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Neat interactive visualization of Uber's &lt;a href="https://h3geo.org/"&gt;H3&lt;/a&gt; hexagonal geographical indexing mechanism.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Map showing H3 geospatial index hexagons overlaid on the Minneapolis-Saint Paul metropolitan area. Various H3 cell IDs are displayed including &amp;quot;852621b3fffffff&amp;quot;, &amp;quot;852621a7fffffff&amp;quot;, &amp;quot;8527526fffffff&amp;quot;, &amp;quot;85262cd3fffffff&amp;quot;, and &amp;quot;85262c83fffffff&amp;quot;. A sidebar shows input fields for &amp;quot;lat,lon&amp;quot; with a &amp;quot;Go&amp;quot; button and &amp;quot;valid H3 id&amp;quot; with a &amp;quot;Find&amp;quot; button. Text indicates &amp;quot;Current H3 resolution: 5&amp;quot; and &amp;quot;Tip: Clicking an H3 cell will copy its id to the clipboard.&amp;quot; Map attribution shows &amp;quot;Leaflet | © OpenStreetMap contributors&amp;quot;." src="https://static.simonwillison.net/static/2025/h3-map.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://github.com/clupasq/h3-viewer"&gt;the source code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Why does H3 use hexagons? Because &lt;a href="https://www.youtube.com/watch?v=thOifuHs6eY"&gt;Hexagons are the Bestagons&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When hexagons come together, they form three-sided joints 120 degrees apart. This, for the least material, is the most mechanically stable arrangement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Only triangles, squares, and hexagons can tile a plane without gaps, and of those three shapes hexagons offer the best ratio of perimeter to area.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=43305920#43307944"&gt;Hacker News comment&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="javascript"/><category term="leaflet"/></entry><entry><title>Visualizing local election results with Datasette, Observable and MapLibre GL</title><link href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#atom-tag" rel="alternate"/><published>2024-11-09T23:32:06+00:00</published><updated>2024-11-09T23:32:06+00:00</updated><id>https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#atom-tag</id><summary type="html">
    &lt;p&gt;Alex Garcia and myself hosted the first &lt;a href="https://simonwillison.net/2024/Nov/7/datasette-public-office-hours/"&gt;Datasette Open Office Hours&lt;/a&gt; on Friday - a live-streamed video session where we hacked on a project together and took questions and tips from community members on Discord.&lt;/p&gt;
&lt;p&gt;We didn't record this one (surprisingly not a feature that Discord offers) but we hope to do more of these and record them in the future.&lt;/p&gt;
&lt;p&gt;This post is a detailed write-up of what we built during the session.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#san-mateo-county-election-results"&gt;San Mateo County election results&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#importing-csv-data-into-datasette"&gt;Importing CSV data into Datasette&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#modifying-the-schema"&gt;Modifying the schema&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#faceting-and-filtering-the-table"&gt;Faceting and filtering the table&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#importing-geospatial-precinct-shapes"&gt;Importing geospatial precinct shapes&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#enriching-that-data-to-extract-the-precinct-ids"&gt;Enriching that data to extract the precinct IDs&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#running-a-join"&gt;Running a join&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#creating-an-api-token-to-access-the-data"&gt;Creating an API token to access the data&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#getting-cors-working"&gt;Getting CORS working&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#working-with-datasette-in-observable"&gt;Working with Datasette in Observable&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#visualizing-those-with-maplibre-gl"&gt;Visualizing those with MapLibre GL&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#observable-plot"&gt;Observable Plot&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#bringing-it-all-together"&gt;Bringing it all together&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/9/visualizing-local-election-results/#we-ll-be-doing-this-again"&gt;We'll be doing this again&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h4 id="san-mateo-county-election-results"&gt;San Mateo County election results&lt;/h4&gt;
&lt;p&gt;I live in El Granada, a tiny town just north of Half Moon Bay in San Mateo County, California.&lt;/p&gt;
&lt;p&gt;Every county appears to handle counting and publishing election results differently. For San Mateo County the results are published &lt;a href="https://smcacre.gov/elections/november-5-2024-election-results"&gt;on this page&lt;/a&gt;, and detailed per-precinct and per-candidate breakdowns are made available as a CSV file.&lt;/p&gt;
&lt;p&gt;(I optimistically set up a &lt;a href="https://simonwillison.net/2020/Oct/9/git-scraping/"&gt;Git scraper&lt;/a&gt; for these results in &lt;a href="https://github.com/simonw/scrape-san-mateo-county-election-results-2024"&gt;simonw/scrape-san-mateo-county-election-results-2024&lt;/a&gt; only to learn that the CSV is updated just once a day, not continually as the ballots are counted.)&lt;/p&gt;
&lt;p&gt;I'm particularly invested in the results of the &lt;a href="http://granada.ca.gov/"&gt;Granada Community Services District&lt;/a&gt; board member elections. Our little town of El Granada is in "unincorporated San Mateo County" which means we don't have a mayor or any local officials, so the closest we get to hyper-local government is the officials that run our local sewage and parks organization! My partner Natalie ran &lt;a href="https://til.simonwillison.net/youtube/livestreaming"&gt;the candidate forum event&lt;/a&gt; (effectively the debate) featuring three of the four candidates running for the two open places on the board.&lt;/p&gt;
&lt;p&gt;Let's explore the data for that race using Datasette.&lt;/p&gt;
&lt;h4 id="importing-csv-data-into-datasette"&gt;Importing CSV data into Datasette&lt;/h4&gt;
&lt;p&gt;I ran my part of the demo using &lt;a href="https://www.datasette.cloud/"&gt;Datasette Cloud&lt;/a&gt;, the beta of my new hosted Datasette service.&lt;/p&gt;
&lt;p&gt;I started by using the pre-configured &lt;a href="https://github.com/datasette/datasette-import"&gt;datasette-import&lt;/a&gt; plugin to import the data from the CSV file into a fresh table:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-import-loop.gif" alt="Paste data to create a table - I drag and drop on a CSV file, which produces a preview of the first 100 of 15,589 rows. I click to Upload and a progress bar runs before redirecting me to the resulting table." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="modifying-the-schema"&gt;Modifying the schema&lt;/h4&gt;
&lt;p&gt;The table imported cleanly, but all of the columns from the CSV were still being treated as text. I used the &lt;a href=""&gt;datasette-edit-schema&lt;/a&gt; plugin to switch the relevant columns to integers so that we could run sums and sorts against them.&lt;/p&gt;
&lt;p&gt;(I also noted that I really should add a "detect column types" feature to that plugin!)&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-edit-schema.jpg" alt="Edit table data/san_mateo_election_results - an option to rename table and then one to change existing columns, where each column is listed in turn and some have their type select box set to integer instead of the default of text" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The resulting 15,589 rows represent counts from individual precincts around the county for each of the races and measures on the ballot, with a row per precinct per candidate/choice per race.&lt;/p&gt;
&lt;h4 id="faceting-and-filtering-the-table"&gt;Faceting and filtering the table&lt;/h4&gt;
&lt;p&gt;Since I'm interested in the Granada Community Services District election, I applied a facet on "Contest_title" and then used that to select that specific race.&lt;/p&gt;
&lt;p&gt;I applied additional facets on "candidate_name" and "Precinct name".&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/facet-candidates.jpg" alt="28 rows where Contest_title = Granada Community Services District Members, Board of Directors. Facets are precinct name (7 choices), candidate name (IRIS GRANT, JANET BRAYER, NANCY MARSH, WANDA BOWLES) and Contest_title" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This looks right to me: we have 7 precincts and 4 candidates for 28 rows in total.&lt;/p&gt;
&lt;h4 id="importing-geospatial-precinct-shapes"&gt;Importing geospatial precinct shapes&lt;/h4&gt;
&lt;p&gt;Those precinct names are pretty non-descriptive! What does 33001 mean?&lt;/p&gt;
&lt;p&gt;To answer that question, I added a new table.&lt;/p&gt;
&lt;p&gt;San Mateo County offers &lt;a href="https://smcacre.gov/elections/precinct-maps-pdf"&gt;precinct maps&lt;/a&gt; in the form of 23 PDF files. Our precincts are in the "Unincorporated Coastside" file:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/precinct-map-from-pdf.jpg" alt="Screenshot from a PDF - label is Unincorporated Coastside, it shows the area north of Half Moon Bay with a bunch of polygons with numeric identifiers." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Thankfully the county &lt;em&gt;also&lt;/em&gt; makes that data available as &lt;a href="https://data.smcgov.org/Government/Election-Precincts/g5sj-6zp8/about_data"&gt;geospatial data&lt;/a&gt;, hosted using Socrata with an option to export as GeoJSON.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/precincts-socrata.jpg" alt="Socrata interface, Election Precincts updated March 7 2022 - 533 views, 72 downloads, and export dataset modal shows a GeoJSON option to export 783 rows." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;datasette-import&lt;/code&gt; plugin can handle JSON files... and if a JSON file contains a top-level object with a key that is an array of objects, it will import those objects as a table.&lt;/p&gt;
&lt;p&gt;Dragging that file into Datasette is enough to import it as a table with a &lt;code&gt;properties&lt;/code&gt; JSON column containing properties and a &lt;code&gt;geometry&lt;/code&gt; JSON columnn with the GeoJSON geometry.&lt;/p&gt;
&lt;p&gt;Here's where another plugin kicks in: &lt;a href="https://datasette.io/plugins/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt; looks for columns that contain valid GeoJSON geometries and... draws them on a map!&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/precincts-in-datasette.jpg" alt="Datasette precincts table with 783 rows. The properties column contains JSON keys lastupdate, creationda, prencitid, notes and active - the geometry column renders maps with polygons showing the shape of the precinct." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So now we can see the shape of the individual geometries.&lt;/p&gt;
&lt;h4 id="enriching-that-data-to-extract-the-precinct-ids"&gt;Enriching that data to extract the precinct IDs&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;precinctid&lt;/code&gt; is present in the data, but it's tucked away in a JSON object in that &lt;code&gt;properties&lt;/code&gt; JSON blob. It would be more convenient if it was a top-level column.&lt;/p&gt;
&lt;p&gt;Datasette's &lt;a href="https://simonwillison.net/2023/Dec/1/datasette-enrichments/"&gt;enrichments feature&lt;/a&gt; provides tools for running operations against every row in a table and adding new columns based on the results.&lt;/p&gt;
&lt;p&gt;My Datasette Cloud instance was missing the &lt;a href="https://github.com/datasette/datasette-enrichments-quickjs"&gt;datasette-enrichments-quickjs plugin&lt;/a&gt; that would let me run JavaScript code against the data. I used my privileged access on Datasette Cloud to add that plugin to my requirements and restarted the instance to install it.&lt;/p&gt;
&lt;p&gt;I used that to run this JavaScript code against every row in the table and saved the output in a new &lt;code&gt;precinct_id&lt;/code&gt; column:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;enrich&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;row&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;row&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;properties&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;precinctid&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/enrich-precincts.jpg" alt="Enrich data in precincts. 783 rows selected. JavaScript. Enrich data with a custom JavaScript function. JavaScript function: function enrich(row) { return JSON.stringify(row) + &amp;quot; enriched&amp;quot;; } - Define an enrich(row) JavaScript function taking an object and returning a value. Row keys: properties, geometry. Output mode: store the function result in a single column. Output clumn name: precinct_id. The column to store the output in - will be created if it does not exist. Output column type: text." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This took less than a second to run, adding and populating a new &lt;code&gt;precinct_id&lt;/code&gt; column for the table.&lt;/p&gt;
&lt;h4 id="running-a-join"&gt;Running a join&lt;/h4&gt;
&lt;p&gt;I demonstrated how to run a join between the election results and the precincts table using the Datasette SQL query editor.&lt;/p&gt;
&lt;p&gt;I tried a few different things, but the most interesting query was this one:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  Precinct_name,
  &lt;span class="pl-c1"&gt;precincts&lt;/span&gt;.&lt;span class="pl-c1"&gt;geometry&lt;/span&gt;,
  total_ballots,
  json_group_object(
    candidate_name,
    total_votes
  ) &lt;span class="pl-k"&gt;as&lt;/span&gt; votes_by_candidate
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  election_results 
  &lt;span class="pl-k"&gt;join&lt;/span&gt; precincts &lt;span class="pl-k"&gt;on&lt;/span&gt; &lt;span class="pl-c1"&gt;election_results&lt;/span&gt;.&lt;span class="pl-c1"&gt;Precinct_name&lt;/span&gt; &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;precincts&lt;/span&gt;.&lt;span class="pl-c1"&gt;precinct_id&lt;/span&gt;
&lt;span class="pl-k"&gt;where&lt;/span&gt; 
  Contest_title &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;Granada Community Services District Members, Board of Directors&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;group by&lt;/span&gt; 
  Precinct_name,
  &lt;span class="pl-c1"&gt;precincts&lt;/span&gt;.&lt;span class="pl-c1"&gt;geometry&lt;/span&gt;,
  total_ballots;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/joined-precincts.jpg" alt="The SQL query returned four columns: Precinct_name, geometry with a map of the precinct, total_ballots with a number and votes_by_candidate with a JSON object mapping each candidate name to their number of votes." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="creating-an-api-token-to-access-the-data"&gt;Creating an API token to access the data&lt;/h4&gt;
&lt;p&gt;I was nearly ready to hand over to Alex for the second half of our demo, where he would use Observable Notebooks to build some custom visualizations on top of the data.&lt;/p&gt;
&lt;p&gt;A great pattern for this is to host the data in Datasette and then fetch it into Observable via the Datasette JSON API.&lt;/p&gt;
&lt;p&gt;Since Datasette Cloud instances are private by default we would need to create an API token that could do this.&lt;/p&gt;
&lt;p&gt;I used this interface (from the &lt;a href="https://github.com/simonw/datasette-auth-tokens"&gt;datasette-auth-tokens plugin&lt;/a&gt;) to create a new token with read-only access to all databases and tables in the instance:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/create-api-token.jpg" alt="Create an API token interface. This token will allow API access with the same abilities as your current user, swillison .Token will be restricted to: all databases and tables: view-database, all databases and tables: view-table, all databases and tables: execute-sql - token is set to read-only and never expires, a list of possible permissions with checkboxes is listed below." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Since we're running a dedicated instance just for Datasette Public Office Hours there's no reason not to distribute that read-only token in publically accessible code.&lt;/p&gt;
&lt;h4 id="getting-cors-working"&gt;Getting CORS working&lt;/h4&gt;
&lt;p&gt;Embarrassingly, I had forgotten that we would need CORS headers in order to access the data from an Observable notebook. Thankfully we have another plugin for that: &lt;a href="https://datasette.io/plugins/datasette-cors"&gt;datasette-cors&lt;/a&gt;. I installed that quickly and we confirmed that it granted access to the API from Observable as intended.&lt;/p&gt;
&lt;p&gt;I handed over to Alex for the next section of the demo.&lt;/p&gt;
&lt;h4 id="working-with-datasette-in-observable"&gt;Working with Datasette in Observable&lt;/h4&gt;
&lt;p&gt;Alex started by running a SQL query from client-side JavaScript to pull in the joined data for our specific El Granada race:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;sql&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;`&lt;/span&gt;
&lt;span class="pl-s"&gt;select&lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Split_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Reporting_flag,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Update_count,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Pct_Id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Pct_seq_nbr,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Reg_voters,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Turn_Out,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_Id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_seq_nbr,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_title,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Contest_party_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Selectable_Options,&lt;/span&gt;
&lt;span class="pl-s"&gt;  candidate_id,&lt;/span&gt;
&lt;span class="pl-s"&gt;  candidate_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Candidate_Type,&lt;/span&gt;
&lt;span class="pl-s"&gt;  cand_seq_nbr,&lt;/span&gt;
&lt;span class="pl-s"&gt;  Party_Code,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_votes,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_under_votes,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_over_votes,&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_ballots],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_under_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote Centers_over_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_ballots],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_under_votes],&lt;/span&gt;
&lt;span class="pl-s"&gt;  [Vote by Mail_over_votes]&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  election_results join precincts on election_results.Precinct_name = precincts.precinct_id&lt;/span&gt;
&lt;span class="pl-s"&gt;where "Contest_title" = "Granada Community Services District Members, Board of Directors"&lt;/span&gt;
&lt;span class="pl-s"&gt;limit 101;`&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And in the next cell:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;raw_data&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s"&gt;`https://datasette-public-office-hours.datasette.cloud/data/-/query.json?_shape=array&amp;amp;sql=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;    &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;  &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;Authorization&lt;/span&gt;: &lt;span class="pl-s"&gt;`Bearer &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;secret&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note the &lt;code&gt;?_shape=array&lt;/code&gt; parameter there, which causes Datasette to output the results directly as a JSON array of objects.&lt;/p&gt;
&lt;p&gt;That's all it takes to get the data into Observable. Adding another cell like this confirms that the data is now available:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;table&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;raw_data&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/inputs-table-raw-data.jpg" alt="An Observable cell running Inputs.table(raw_data) and displaying a table of Precinct_name and geometry columns, with GeoJSON" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="visualizing-those-with-maplibre-gl"&gt;Visualizing those with MapLibre GL&lt;/h4&gt;
&lt;p&gt;There are plenty of good options for visualizing GeoJSON data using JavaScript in an Observable notebook.&lt;/p&gt;
&lt;p&gt;Alex started with &lt;a href="https://maplibre.org/maplibre-gl-js/docs/"&gt;MapLibre GL&lt;/a&gt;, using the excellent &lt;a href="https://simonwillison.net/2024/Sep/28/openfreemap/"&gt;OpenFreeMap 3D tiles&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;viewof&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-s1"&gt;const&lt;/span&gt; container &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;html&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;height:800px;&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;yield&lt;/span&gt; &lt;span class="pl-s1"&gt;container&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;container&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;value&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-s1"&gt;maplibregl&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    container&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;zoom&lt;/span&gt;: &lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;//style: "https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json",&lt;/span&gt;
    &lt;span class="pl-c1"&gt;style&lt;/span&gt;: &lt;span class="pl-s"&gt;"https://tiles.openfreemap.org/styles/liberty"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;scrollZoom&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;yield&lt;/span&gt; &lt;span class="pl-s1"&gt;container&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

  &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;on&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"load"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;fitBounds&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d3&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;geoBounds&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;duration&lt;/span&gt;: &lt;span class="pl-c1"&gt;0&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addSource&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"geojson"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;data&lt;/span&gt;: &lt;span class="pl-s1"&gt;data&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addLayer&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;id&lt;/span&gt;: &lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"fill"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;source&lt;/span&gt;: &lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;paint&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-s"&gt;"fill-opacity"&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.4&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
        &lt;span class="pl-s"&gt;"fill-color"&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
          &lt;span class="pl-s"&gt;"case"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"=="&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"get"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"ratio"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#000000"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
          &lt;span class="pl-kos"&gt;[&lt;/span&gt;
            &lt;span class="pl-s"&gt;"interpolate"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"linear"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s"&gt;"get"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"ratio"&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;0.0&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#0000ff"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;0.5&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#d3d3d3"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
            &lt;span class="pl-c1"&gt;1.0&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"#ff0000"&lt;/span&gt;
          &lt;span class="pl-kos"&gt;]&lt;/span&gt;
        &lt;span class="pl-kos"&gt;]&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;on&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"click"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"precincts"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; precinct&lt;span class="pl-kos"&gt;,&lt;/span&gt; ratio &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;features&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;properties&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;description&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;

      &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-s1"&gt;maplibregl&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Popup&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
        &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setLngLat&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;e&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;lngLat&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
        &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setHTML&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;description&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
        &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-s1"&gt;invalidation&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;remove&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/maplibre-gl.jpg" alt="An Observable cell showing a map of El Granada - a black shape shows the outlines of the precincts." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;(This is just one of several iterations, I didn't capture detailed notes of every change Alex made to the code.)&lt;/p&gt;
&lt;h4 id="observable-plot"&gt;Observable Plot&lt;/h4&gt;
&lt;p&gt;Observable notebooks come pre-loaded with the excellent Observable Plot charting library - Mike Bostock's high-level charting tool built on top of D3.&lt;/p&gt;
&lt;p&gt;Alex used that to first render the shapes of the precincts directly, without even needing a tiled basemap:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;plot&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  width&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;: &lt;span class="pl-c1"&gt;600&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;legend&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;projection&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"conic-conformal"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;parallels&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;37&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;4&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;60&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;38&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;26&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;60&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;rotate&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;120&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;30&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;60&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;domain&lt;/span&gt;: &lt;span class="pl-s1"&gt;data&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;marks&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
    &lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;geo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;strokeOpacity&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.1&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;fill&lt;/span&gt;: &lt;span class="pl-s"&gt;"total_votes"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-en"&gt;title&lt;/span&gt;: &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;stringify&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;properties&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;tip&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;]&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;parallels&lt;/code&gt; and &lt;code&gt;rotate&lt;/code&gt; options there come from the handy &lt;a href="https://github.com/veltman/d3-stateplane?tab=readme-ov-file#nad83--california-zone-3-epsg26943"&gt;veltman/d3-stateplane&lt;/a&gt; repo, which lists recommended settings for the &lt;a href="https://en.wikipedia.org/wiki/State_Plane_Coordinate_System"&gt;State Plane Coordinate System&lt;/a&gt; used with projections in D3. Those values are for &lt;a href="https://www.conservation.ca.gov/cgs/rgm/state-plane-coordinate-system"&gt;California Zone 3&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/color-precincts.jpg" alt="An Observable cell shows six five distinct colored polygons, each for a different precinct. The shape of El Granada is clearly visible despite no other map tiles or labels." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;h4 id="bringing-it-all-together"&gt;Bringing it all together&lt;/h4&gt;
&lt;p&gt;For the grand finale, Alex combined everything learned so far to build an interactive map allowing a user to select any of the 110 races on the ballot and see a heatmap of results for any selected candidate and option:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/select-map.gif" alt="Animated demo. Choose a contest select - picking different contests updates the map at the bottom. For each contest the candidates or options are shown as radio buttons, and selecting those updates the map to show a heatmap of votes for that candidate in different precincts." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;You can try this out in &lt;a href="https://observablehq.com/d/2ed2ad2443d7bbb5"&gt;Alex's notebook&lt;/a&gt;. Here's the relevant code (Observable cells are divided by &lt;code&gt;// ---&lt;/code&gt; comments). Note that Observable notebooks are reactive and allow variables to be referenced out of order.&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-c"&gt;// Select the contest&lt;/span&gt;
&lt;span class="pl-s1"&gt;viewof&lt;/span&gt; &lt;span class="pl-s1"&gt;contest&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;select&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;contests&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;label&lt;/span&gt;: &lt;span class="pl-s"&gt;"Choose a contest"&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// And the candidate&lt;/span&gt;
&lt;span class="pl-s1"&gt;viewof&lt;/span&gt;&lt;span class="pl-kos"&gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;candidate&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Inputs&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;radio&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s1"&gt;candidates&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;label&lt;/span&gt;: &lt;span class="pl-s"&gt;"Choose a candidate"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;value&lt;/span&gt;: &lt;span class="pl-s1"&gt;candidates&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// Show the map itself&lt;/span&gt;
&lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;plot&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  width&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;: &lt;span class="pl-c1"&gt;600&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;legend&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;color&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;scheme&lt;/span&gt;: &lt;span class="pl-s"&gt;"blues"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;legend&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;projection&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"mercator"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;domain&lt;/span&gt;: &lt;span class="pl-s1"&gt;data2&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;marks&lt;/span&gt;: &lt;span class="pl-kos"&gt;[&lt;/span&gt;
    &lt;span class="pl-v"&gt;Plot&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;geo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;data2&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;strokeOpacity&lt;/span&gt;: &lt;span class="pl-c1"&gt;0.1&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;fill&lt;/span&gt;: &lt;span class="pl-s"&gt;"ratio"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;tip&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;]&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
# &lt;span class="pl-c1"&gt;--&lt;/span&gt;&lt;span class="pl-c1"&gt;-&lt;/span&gt;
&lt;span class="pl-s1"&gt;data2&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"FeatureCollection"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;features&lt;/span&gt;: &lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;type&lt;/span&gt;: &lt;span class="pl-s"&gt;"Feature"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;properties&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;precinct&lt;/span&gt;: &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Precinct_name&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;total_ballots&lt;/span&gt;: &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;total_ballots&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-c1"&gt;ratio&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;votes_by_candidate&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;candidate&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;total_ballots&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;geometry&lt;/span&gt;: &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;geometry&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s"&gt;`select&lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots,&lt;/span&gt;
&lt;span class="pl-s"&gt;  json_grop_object(&lt;/span&gt;
&lt;span class="pl-s"&gt;    candidate_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;    total_votes&lt;/span&gt;
&lt;span class="pl-s"&gt;  ) as votes_by_candidate&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  election_results &lt;/span&gt;
&lt;span class="pl-s"&gt;  join precincts on election_results.Precinct_name = precincts.precinct_id&lt;/span&gt;
&lt;span class="pl-s"&gt;where Contest_title = :contest&lt;/span&gt;
&lt;span class="pl-s"&gt;group by &lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt; contest &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-s"&gt;`select&lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots,&lt;/span&gt;
&lt;span class="pl-s"&gt;  json_group_object(&lt;/span&gt;
&lt;span class="pl-s"&gt;    candidate_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;    total_votes&lt;/span&gt;
&lt;span class="pl-s"&gt;  ) as votes_by_candidate&lt;/span&gt;
&lt;span class="pl-s"&gt;from&lt;/span&gt;
&lt;span class="pl-s"&gt;  election_results &lt;/span&gt;
&lt;span class="pl-s"&gt;  join precincts on election_results.Precinct_name = precincts.precinct_id&lt;/span&gt;
&lt;span class="pl-s"&gt;where Contest_title = :contest&lt;/span&gt;
&lt;span class="pl-s"&gt;group by &lt;/span&gt;
&lt;span class="pl-s"&gt;  Precinct_name,&lt;/span&gt;
&lt;span class="pl-s"&gt;  precincts.geometry,&lt;/span&gt;
&lt;span class="pl-s"&gt;  total_ballots;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt; contest &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// Fetch the available contests&lt;/span&gt;
&lt;span class="pl-s1"&gt;contests&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"select distinct Contest_title from election_results"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;d&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;Contest_title&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-c"&gt;// Extract available candidates for selected contest&lt;/span&gt;

&lt;span class="pl-s1"&gt;candidates&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Object&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;keys&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-c1"&gt;JSON&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;parse&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;raw_data2&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;votes_by_candidate&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;

&lt;span class="pl-c"&gt;// ---&lt;/span&gt;

&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;query&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s"&gt;`https://datasette-public-office-hours.datasette.cloud/data/-/query.json?&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;      &lt;span class="pl-kos"&gt;{&lt;/span&gt; sql&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;_shape&lt;/span&gt;: &lt;span class="pl-s"&gt;"array"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; ...&lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;    &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-c1"&gt;headers&lt;/span&gt;: &lt;span class="pl-kos"&gt;{&lt;/span&gt;
        &lt;span class="pl-c1"&gt;Authorization&lt;/span&gt;: &lt;span class="pl-s"&gt;`Bearer &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;secret&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;
      &lt;span class="pl-kos"&gt;}&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;then&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;r&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="we-ll-be-doing-this-again"&gt;We'll be doing this again&lt;/h4&gt;
&lt;p&gt;This was our first time trying something like this and I think it worked &lt;em&gt;really&lt;/em&gt; well. We're already thinking about ways to improve it next time:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I want to record these sessions and make them available on YouTube for people who couldn't be there live&lt;/li&gt;
&lt;li&gt;It would be fun to mix up the format. I'm particularly keen on getting more people involved giving demos - maybe having 5-10 minute lightning demo slots so we can see what other people are working on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep an eye on this blog or on the &lt;a href="https://datasette.io/discord"&gt;Datasette Discord&lt;/a&gt; for news about future sessions.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/politics"&gt;politics&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-cloud"&gt;datasette-cloud&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette-public-office-hours"&gt;datasette-public-office-hours&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="mapping"/><category term="politics"/><category term="projects"/><category term="datasette"/><category term="datasette-cloud"/><category term="alex-garcia"/><category term="datasette-public-office-hours"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-geojson 0.8.2</title><link href="https://simonwillison.net/2024/Apr/25/datasette-leaflet-geojson/#atom-tag" rel="alternate"/><published>2024-04-25T05:04:35+00:00</published><updated>2024-04-25T05:04:35+00:00</updated><id>https://simonwillison.net/2024/Apr/25/datasette-leaflet-geojson/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-geojson/releases/tag/0.8.2"&gt;datasette-leaflet-geojson 0.8.2&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>tiny-world-map</title><link href="https://simonwillison.net/2024/Apr/21/tiny-world-map/#atom-tag" rel="alternate"/><published>2024-04-21T22:11:14+00:00</published><updated>2024-04-21T22:11:14+00:00</updated><id>https://simonwillison.net/2024/Apr/21/tiny-world-map/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tinyworldmap/tiny-world-map"&gt;tiny-world-map&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I love this project. It’s a JavaScript file (694K uncompressed, 283KB compressed) which can be used with the Leaflet mapping library and provides a SVG base map of the world with country borders and labels for every world city with a population more than 48,000—10,000 cities total.&lt;/p&gt;

&lt;p&gt;This means you can bundle an offline map of the world as part of any application that doesn’t need a higher level of detail. A lot of smaller island nations are missing entirely though, so this may not be right for every project.&lt;/p&gt;

&lt;p&gt;It even includes a service worker to help implement offline mapping support, plus several variants of the map with less cities that are even smaller.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=40104774"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/svg"&gt;svg&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/serviceworkers"&gt;serviceworkers&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;



</summary><category term="geospatial"/><category term="javascript"/><category term="mapping"/><category term="svg"/><category term="serviceworkers"/><category term="leaflet"/></entry><entry><title>Claude and ChatGPT for ad-hoc sidequests</title><link href="https://simonwillison.net/2024/Mar/22/claude-and-chatgpt-case-study/#atom-tag" rel="alternate"/><published>2024-03-22T19:44:12+00:00</published><updated>2024-03-22T19:44:12+00:00</updated><id>https://simonwillison.net/2024/Mar/22/claude-and-chatgpt-case-study/#atom-tag</id><summary type="html">
    &lt;p&gt;Here is a short, illustrative example of one of the ways in which I use Claude and ChatGPT on a daily basis.&lt;/p&gt;
&lt;p&gt;I recently learned that the &lt;a href="https://en.wikipedia.org/wiki/Adirondack_Park"&gt;Adirondack Park&lt;/a&gt; is the single largest park in the contiguous United States, taking up a fifth of the state of New York.&lt;/p&gt;
&lt;p&gt;Naturally, my first thought was that it would be neat to have a GeoJSON file representing the boundary of the park.&lt;/p&gt;
&lt;p&gt;A quick search landed me on the &lt;a href="https://apa.ny.gov/gis/ApaData.html"&gt;Adirondack Park Agency GIS data page&lt;/a&gt;, which offered me a shapefile of the "Outer boundary of the New York State Adirondack Park as described in Section 9-0101 of the New York Environmental Conservation Law". Sounds good!&lt;/p&gt;
&lt;p&gt;I knew there were tools for converting shapefiles to GeoJSON, but I couldn't remember what they were. Since I had a terminal window open already, I typed the following:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre style="white-space: pre-wrap"&gt;llm -m opus -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;give me options on macOS for CLI tools to turn a shapefile into GeoJSON&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here I am using my &lt;a href="https://llm.datasette.io/"&gt;LLM tool&lt;/a&gt; (and &lt;a href="https://github.com/simonw/llm-claude-3"&gt;llm-claude-3&lt;/a&gt; plugin) to run a prompt through the new &lt;a href="https://www.anthropic.com/news/claude-3-family"&gt;Claude 3 Opus&lt;/a&gt;, my current favorite language model.&lt;/p&gt;
&lt;p&gt;It &lt;a href="https://gist.github.com/simonw/331918e46f33e27e997afb4e7c62fc74"&gt;replied with a couple of options&lt;/a&gt;, but the first was this:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;ogr2ogr -f GeoJSON output.geojson input.shp&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So I ran that against the shapefile, and then pasted &lt;a href="https://gist.github.com/simonw/c941f3454cdec7e10f500dc5a752b614"&gt;the resulting GeoJSON&lt;/a&gt; into &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt; to check if it worked... and nothing displayed. Then I looked at the GeoJSON and spotted this:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;"coordinates": [ [ -8358911.527799999341369, 5379193.197800002992153 ] ...&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;That didn't look right. Those co-ordinates aren't the correct scale for latitude and longitude values.&lt;/p&gt;
&lt;p&gt;So I sent a follow-up prompt to the model (the &lt;code&gt;-c&lt;/code&gt; option means "continue previous conversation"):&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre style="white-space: pre-wrap"&gt;llm -c &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;i tried using ogr2ogr but it gave me back GeoJSON with a weird coordinate system that was not lat/lon that i am used to&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It suggested this new command:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;ogr2ogr -f GeoJSON -t_srs EPSG:4326 output.geojson input.shp&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This time &lt;a href="https://gist.github.com/simonw/6c4cf102a8ea532dc365c2773f0eb6ea"&gt;it worked&lt;/a&gt;! The shapefile has now been converted to GeoJSON.&lt;/p&gt;
&lt;p&gt;Time elapsed so far: 2.5 minutes (I can tell from &lt;a href="https://llm.datasette.io/en/stable/logging.html"&gt;my LLM logs&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I pasted it into &lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; (with &lt;a href="https://github.com/datasette/datasette-paste"&gt;datasette-paste&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt;) to take a look at it more closely, and got this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/datasette-adirondack-boundaries.jpg" alt="A Datasette table with 106 rows. The first two are shown - both have properties and a geometry, and the geometry is a single line on a map. The first one has a ECL_Text of thence southerly along the westerly line of lots 223, 241, 259, 276, 293, 309, 325 and 340 to the southwesterly corner of lot number 340 in the Brantingham Tract and the second has thence westerly along the northern line of lots 204 and 203 to the midpoint of the northern line of lot 203" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;That's not a single polygon! That's 106 line segments... and they are fascinating. Look at those descriptions:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;thence westerly along the northern line of lots 204 and 203 to the midpoint of the northern line of lot 203&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is utterly delightful. The shapefile description did say "as described in Section 9-0101 of the New York Environmental Conservation Law", so I guess this is how you write geographically boundaries into law!&lt;/p&gt;
&lt;p&gt;But it's not what I wanted. I want a single polygon of the whole park, not 106 separate lines.&lt;/p&gt;
&lt;p&gt;I decided to switch models. ChatGPT has access to Code Interpreter, and I happen to know that Code Interpreter is quite effective at processing GeoJSON.&lt;/p&gt;
&lt;p&gt;I opened a new ChatGPT (with GPT-4) browser tab, uploaded my GeoJSON file and prompted it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This GeoJSON file is full of line segments. Use them to create me a single shape that is a Polygon&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-1.jpg" alt="ChatGPT screenshot - it shows some Python code with a result of &amp;lt;shapely.geometry.polygon.Polygon at 0x7eba83f9fca0 /&amp;gt;, then says: I've created a polygon from the line segments in the GeoJSON file. You can now use this polygon for further analysis or visualization. If you have specific requirements for the polygon or need it in a particular format, please let me know! ​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;OK, so it wrote some Python code and ran it. But did it work?&lt;/p&gt;
&lt;p&gt;I happen to know that Code Interpreter can save files to disk and provide links to download them, so I told it to do that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Save it to a GeoJSON file for me to download&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-2.jpg" alt="ChatGPT screenshot - this time it writes more Python code to define a GeoJSON polygon, then saves that to a file called /mnt/data/polygon.geojson and gives me a link to download it.​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I pasted &lt;a href="https://gist.github.com/simonw/c1002dbf5249de7addd0b65cb774d3e9"&gt;that&lt;/a&gt; into &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt;, and it was clearly wrong:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-bad-map.jpg" alt="geojson.io screenshot - a triangle shape sits on top of an area of upstate New York, clearly not in the shape of the park" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So I told it to try again. I didn't think very hard about this prompt, I basically went with a version of "do better":&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;that doesn't look right to me, check that it has all of the lines in it&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-3.jpg" alt="ChatGPT screenshot - it writes more Python code and outputs a link to complete_polygon.geojson​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;It gave me a new file, optimistically named &lt;code&gt;complete_polygon.geojson&lt;/code&gt;. Here's what that one looked like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-almost.jpg" alt="ChatGPT screenshot - it writes more Python code and outputs a link to complete_polygon.geojson​​" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This is getting a lot closer! Note how the right hand boundary of the park looks correct, but the rest of the image is scrambled.&lt;/p&gt;
&lt;p&gt;I had a hunch about the fix. I pasted in a screenshot of where we were so far and added my hunch about the solution:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That almost works but you need to sort the line segments first, it looked like this:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Honestly, pasting in the screenshot probably wasn't necessary here, but it amused me.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-4.jpg" alt="That almost works but you need to sort the line segments first, it looked like this: an a screenshot of a map" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;... and ChatGPT churned away again ...&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-chatgpt-5.jpg" alt="More Python code - link to the full transcript is below" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://gist.github.com/simonw/b9e4325b76e4a3813ff5482aa278c342"&gt;sorted_polygon.geojson&lt;/a&gt; is spot on! Here's what it looks like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/shapely-good-map.jpg" alt="A shaded polygon showing the exact shape of the boundary of Adirondack Park, overlayed on a map of the area" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Total time spent in ChatGPT: 3 minutes and 35 seconds. Plus 2.5 minutes with Claude 3 earlier, so an overall total of just over 6 minutes.&lt;/p&gt;
&lt;p&gt;Here's &lt;a href="https://gist.github.com/simonw/0343cdd3568bbe28cad15d1097b1b1c7"&gt;the full Claude transcript&lt;/a&gt; and the &lt;a href="https://gist.github.com/simonw/3eb845823c5ad4c48d2b4eb7586f1533"&gt;full transcript from ChatGPT&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="not-notable"&gt;This isn't notable&lt;/h4&gt;
&lt;p&gt;The most notable thing about this example is how completely &lt;em&gt;not&lt;/em&gt; notable it is.&lt;/p&gt;
&lt;p&gt;I get results like this from these tools several times a day. I'm not at all surprised that this worked, in fact, I would've been mildly surprised if it had not.&lt;/p&gt;
&lt;p&gt;Could I have done this without LLM assistance? Yes, but not nearly as quickly. And this was not a task on my critical path for the day - it was a sidequest at best and honestly more of a distraction.&lt;/p&gt;
&lt;p&gt;So, without LLM tools, I would likely have given this one up at the first hurdle.&lt;/p&gt;
&lt;p&gt;A year ago I wrote about how &lt;a href="https://simonwillison.net/2023/Mar/27/ai-enhanced-development/"&gt;AI-enhanced development makes me more ambitious with my projects&lt;/a&gt;. They are now so firmly baked into my daily work that they influence not just side projects but tiny sidequests like this one as well.&lt;/p&gt;
&lt;h4 id="not-simple"&gt;This certainly wasn't simple&lt;/h4&gt;
&lt;p&gt;Something else I like about this example is that it illustrates quite how much depth there is to getting great results out of these systems.&lt;/p&gt;
&lt;p&gt;In those few minutes I used two different interfaces to call two different models. I sent multiple follow-up prompts. I triggered Code Interpreter, took advantage of GPT-4 Vision and mixed in external tools like &lt;a href="https://geojson.io/"&gt;geojson.io&lt;/a&gt; and Datasette as well.&lt;/p&gt;
&lt;p&gt;I leaned a lot on my existing knowledge and experience:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I knew that tools existed for commandline processing of shapefiles and GeoJSON&lt;/li&gt;
&lt;li&gt;I instinctively knew that Claude 3 Opus was likely to correctly answer my initial prompt&lt;/li&gt;
&lt;li&gt;I knew the capabilities of Code Interpreter, including that it has libraries that can process geometries, what to say to get it to kick into action and how to get it to give me files to download&lt;/li&gt;
&lt;li&gt;My limited GIS knowledge was strong enough to spot a likely coordinate system problem, and I guessed the fix for the jumbled lines&lt;/li&gt;
&lt;li&gt;My prompting intuition is developed to the point that I didn't have to think very hard about what to say to get the best results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you have the right combination of domain knowledge and hard-won experience driving LLMs, you can &lt;em&gt;fly&lt;/em&gt; with these things.&lt;/p&gt;
&lt;h4 id="a-bit-trivial"&gt;Isn't this a bit trivial?&lt;/h4&gt;
&lt;p&gt;Yes it is, and that's the point. This was a five minute sidequest. Writing about it here took ten times longer than the exercise itself.&lt;/p&gt;
&lt;p&gt;I take on LLM-assisted sidequests like this one dozens of times a week. Many of them are substantially larger and more useful. They are having a very material impact on my work: I can get more done and solve much more interesting problems, because I'm not wasting valuable cycles figuring out &lt;code&gt;ogr2ogr&lt;/code&gt; invocations or mucking around with polygon libraries.&lt;/p&gt;
&lt;p&gt;Not to mention that I find working this way &lt;em&gt;fun&lt;/em&gt;! It feels like science fiction every time I do it. Our AI-assisted future is here right now and I'm still finding it weird, fascinating and deeply entertaining.&lt;/p&gt;
&lt;h4 id="llms-are-useful"&gt;LLMs are useful&lt;/h4&gt;
&lt;p&gt;There are many legitimate criticisms of LLMs. The copyright issues involved in their training, their enormous power consumption and the risks of people trusting them when they shouldn't (considering both accuracy and bias) are three that I think about a lot.&lt;/p&gt;
&lt;p&gt;The one criticism I wont accept is that they aren't &lt;em&gt;useful&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;One of the greatest misconceptions concerning LLMs is the idea that they are easy to use. They really aren't: getting great results out of them requires a great deal of experience and hard-fought intuition, combined with deep domain knowledge of the problem you are applying them to.&lt;/p&gt;
&lt;p&gt;I use these things every day. They help me take on much more interesting and ambitious problems than I could otherwise. I would miss them terribly if they were no longer available to me.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shapefiles"&gt;shapefiles&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/geojson"&gt;geojson&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openai"&gt;openai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/generative-ai"&gt;generative-ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/chatgpt"&gt;chatgpt&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/anthropic"&gt;anthropic&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude"&gt;claude&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/code-interpreter"&gt;code-interpreter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/coding-agents"&gt;coding-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="shapefiles"/><category term="geojson"/><category term="ai"/><category term="openai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/><category term="anthropic"/><category term="claude"/><category term="code-interpreter"/><category term="coding-agents"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-geojson 0.8.1</title><link href="https://simonwillison.net/2023/Dec/7/datasette-leaflet-geojson/#atom-tag" rel="alternate"/><published>2023-12-07T20:54:37+00:00</published><updated>2023-12-07T20:54:37+00:00</updated><id>https://simonwillison.net/2023/Dec/7/datasette-leaflet-geojson/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-geojson/releases/tag/0.8.1"&gt;datasette-leaflet-geojson 0.8.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>Exploration de données avec Datasette</title><link href="https://simonwillison.net/2023/May/27/exploration-de-donnees-avec-datasette/#atom-tag" rel="alternate"/><published>2023-05-27T00:36:52+00:00</published><updated>2023-05-27T00:36:52+00:00</updated><id>https://simonwillison.net/2023/May/27/exploration-de-donnees-avec-datasette/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://meetup-python-grenoble.github.io/datasette-workshop/"&gt;Exploration de données avec Datasette&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
One of the great delights of open source development is seeing people run workshops on your project, even more so when they’re in a language other than English! Romain Clement presented this French workshop for the Python Grenoble meetup on 25th May 2023, using GitHub Codespaces as the environment. It’s pretty comprehensive, including a 300,000+ row example table which illustrates Datasette plugins such as datasette-cluster-map and datasette-leaflet-geojson.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/tutorials"&gt;tutorials&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-codespaces"&gt;github-codespaces&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;



</summary><category term="tutorials"/><category term="datasette"/><category term="github-codespaces"/><category term="leaflet"/></entry><entry><title>A tiny web app to create images from OpenStreetMap maps</title><link href="https://simonwillison.net/2022/Jun/12/url-map/#atom-tag" rel="alternate"/><published>2022-06-12T05:49:35+00:00</published><updated>2022-06-12T05:49:35+00:00</updated><id>https://simonwillison.net/2022/Jun/12/url-map/#atom-tag</id><summary type="html">
    &lt;p&gt;Earlier today I found myself wanting to programmatically generate some images of maps.&lt;/p&gt;
&lt;p&gt;I wanted to create a map centered around a location, at a specific zoom level, and with a marker in a specific place.&lt;/p&gt;
&lt;p&gt;Some cursory searches failed to turn up exactly what I wanted, so I decided to build a tiny project to solve the problem, taking advantage of my &lt;a href="https://shot-scraper.datasette.io/"&gt;shot-scraper tool&lt;/a&gt; for automating screenshots of web pages.&lt;/p&gt;
&lt;p&gt;The result is &lt;a href="https://map.simonwillison.net/"&gt;map.simonwillison.net&lt;/a&gt; - hosted on GitHub Pages from my &lt;a href="https://github.com/simonw/url-map"&gt;simonw/url-map&lt;/a&gt; repository.&lt;/p&gt;
&lt;p&gt;Here's how to generate a map image of Washington DC:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shot-scraper 'https://map.simonwillison.net/?q=washington+dc' \
  --retina --width 600 --height 400 --wait 3000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That command generates a PNG 1200x800 image that's a retina screenshot of the map displayed at &lt;a href="https://map.simonwillison.net/?q=washington+dc"&gt;https://map.simonwillison.net/?q=washington+dc&lt;/a&gt; - after waiting three seconds to esure all of the tiles have fully loaded.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2022/map-simonwillison-net.jpg" alt="A map of Washington DC, with a Leaflet / OpenStreetMap attribution in the bottom right" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The website itself is &lt;a href="https://github.com/simonw/url-map/blob/main/README.md"&gt;documented here&lt;/a&gt;. It displays a map with no visible controls, though you can use gestures to zoom in and pan around - and the URL bar will update to reflect your navigation, so you can bookmark or share the URL once you've got it to the right spot.&lt;/p&gt;
&lt;p&gt;You can also use query string parameters to specify the map that should be initially displayed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8"&gt;https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8&lt;/a&gt; displays a map at zoom level 8 centered on the specified latitude, longitude coordinate pair.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?q=islington+london"&gt;https://map.simonwillison.net/?q=islington+london&lt;/a&gt; geocodes the &lt;code&gt;?q=&lt;/code&gt; text using &lt;a href="https://nominatim.openstreetmap.org/ui/search.html"&gt;OpenStreetMap Nominatim&lt;/a&gt; and zooms to the level that best fits the bounding box of the first returned result.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?q=islington+london&amp;amp;zoom=12"&gt;https://map.simonwillison.net/?q=islington+london&amp;amp;zoom=12&lt;/a&gt; does that but zooms to level 12 instead of using the best fit for the bounding box&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8&amp;amp;marker=51.49,0&amp;amp;marker=51.3,0.2"&gt;https://map.simonwillison.net/?center=51.49,0&amp;amp;zoom=8&amp;amp;marker=51.49,0&amp;amp;marker=51.3,0.2&lt;/a&gt; adds two blue markers to the specified map. You can pass &lt;code&gt;&amp;amp;marker=lat,lon&lt;/code&gt; as many times as you like to add multiple markers.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Annotated source code&lt;/h4&gt;
&lt;p&gt;The entire mapping application is contained in a single 68 line &lt;code&gt;index.html&lt;/code&gt; file that mixes HTML and JavaScript. It's built using the fantastic &lt;a href="https://leafletjs.com/"&gt;Leaflet&lt;/a&gt; open source mapping library.&lt;/p&gt;
&lt;p&gt;Since the code is so short, I'll enclude the entire thing here with some additional annotating comments.&lt;/p&gt;
&lt;p&gt;It started out as a copy of the first example in &lt;a href="https://leafletjs.com/examples/quick-start/"&gt;the Leaflet quick start guide&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-text-html-basic"&gt;&lt;pre&gt;&lt;span class="pl-c1"&gt;&amp;lt;!DOCTYPE html&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- Regular HTML boilerplate --&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;html&lt;/span&gt; &lt;span class="pl-c1"&gt;lang&lt;/span&gt;="&lt;span class="pl-s"&gt;en&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;head&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;meta&lt;/span&gt; &lt;span class="pl-c1"&gt;charset&lt;/span&gt;="&lt;span class="pl-s"&gt;utf-8&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;meta&lt;/span&gt; &lt;span class="pl-c1"&gt;name&lt;/span&gt;="&lt;span class="pl-s"&gt;viewport&lt;/span&gt;" &lt;span class="pl-c1"&gt;content&lt;/span&gt;="&lt;span class="pl-s"&gt;width=device-width, initial-scale=1&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;map.simonwillison.net&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;title&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!--&lt;/span&gt;
&lt;span class="pl-c"&gt;  Leaflet's CSS and JS are loaded from the unpgk.com CDN, with the&lt;/span&gt;
&lt;span class="pl-c"&gt;  Subresource Integrity (SRI) integrity="sha512..." attribute to ensure&lt;/span&gt;
&lt;span class="pl-c"&gt;  that the exact expected code is served by the CDN.&lt;/span&gt;
&lt;span class="pl-c"&gt;--&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;link&lt;/span&gt; &lt;span class="pl-c1"&gt;rel&lt;/span&gt;="&lt;span class="pl-s"&gt;stylesheet&lt;/span&gt;" &lt;span class="pl-c1"&gt;href&lt;/span&gt;="&lt;span class="pl-s"&gt;https://unpkg.com/leaflet@1.8.0/dist/leaflet.css&lt;/span&gt;" &lt;span class="pl-c1"&gt;integrity&lt;/span&gt;="&lt;span class="pl-s"&gt;sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ==&lt;/span&gt;" &lt;span class="pl-c1"&gt;crossorigin&lt;/span&gt;=""/&amp;gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt; &lt;span class="pl-c1"&gt;src&lt;/span&gt;="&lt;span class="pl-s"&gt;https://unpkg.com/leaflet@1.8.0/dist/leaflet.js&lt;/span&gt;" &lt;span class="pl-c1"&gt;integrity&lt;/span&gt;="&lt;span class="pl-s"&gt;sha512-BB3hKbKWOc9Ez/TAwyWxNXeoV9c1v6FIeYiBieIWkpLjauysF18NzgR1MBNBXf8/KABdlkX68nAhlwcDFLGPCQ==&lt;/span&gt;" &lt;span class="pl-c1"&gt;crossorigin&lt;/span&gt;=""&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- I want the map to occupy the entire browser window with no margins --&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-ent"&gt;html&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-ent"&gt;body&lt;/span&gt; {
  &lt;span class="pl-c1"&gt;height&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;100&lt;span class="pl-smi"&gt;%&lt;/span&gt;&lt;/span&gt;;
  &lt;span class="pl-c1"&gt;margin&lt;/span&gt;&lt;span class="pl-kos"&gt;:&lt;/span&gt; &lt;span class="pl-c1"&gt;0&lt;/span&gt;;
}
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;style&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;head&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- The Leaflet map renders in this 100% high/wide div --&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt; &lt;span class="pl-c1"&gt;id&lt;/span&gt;="&lt;span class="pl-s"&gt;map&lt;/span&gt;" &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;width: 100%; height: 100%;&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;div&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;toPoint&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// Convert "51.5,2.1" into [51.5, 2.1]&lt;/span&gt;
  &lt;span class="pl-k"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;split&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;","&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;parseFloat&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-c"&gt;// An async function so we can 'await fetch(...)' later on&lt;/span&gt;
&lt;span class="pl-k"&gt;async&lt;/span&gt; &lt;span class="pl-k"&gt;function&lt;/span&gt; &lt;span class="pl-en"&gt;load&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c"&gt;// URLSearchParams is a fantastic browser API - it makes it easy to both read&lt;/span&gt;
  &lt;span class="pl-c"&gt;// query string parameters from the URL and later to generate new ones&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;location&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;search&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// If the starting URL is /?center=51,32&amp;amp;zoom=3 this will pull those values out&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;center&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'center'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;'0,0'&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'zoom'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;zoom&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-en"&gt;parseInt&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt; &lt;span class="pl-c1"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;'2'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;10&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;q&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'q'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// .getAll() turns &amp;amp;marker=51.49,0&amp;amp;marker=51.3,0.2 into ['51.49,0', '51.3,0.2']&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;markers&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'marker'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// zoomControl: false turns off the visible +/- zoom buttons in Leaflet&lt;/span&gt;
  &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;L&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'map'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-c1"&gt;zoomControl&lt;/span&gt;: &lt;span class="pl-c1"&gt;false&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setView&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;toPoint&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;center&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;zoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-v"&gt;L&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;tileLayer&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c1"&gt;maxZoom&lt;/span&gt;: &lt;span class="pl-c1"&gt;19&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c1"&gt;attribution&lt;/span&gt;: &lt;span class="pl-s"&gt;'&amp;amp;copy; &amp;lt;a href="http://www.openstreetmap.org/copyright"&amp;gt;OpenStreetMap&amp;lt;/a&amp;gt;'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This option means retina-capable devices will get double-resolution tiles:&lt;/span&gt;
    &lt;span class="pl-c1"&gt;detectRetina&lt;/span&gt;: &lt;span class="pl-c1"&gt;true&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// We only pay attention to ?q= if ?center= was not provided:&lt;/span&gt;
  &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;q&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="pl-c1"&gt;!&lt;/span&gt;&lt;span class="pl-s1"&gt;params&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;get&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'center'&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// We use fetch to pass ?q= to the Nominatim API and get back JSON&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
      &lt;span class="pl-s"&gt;`https://nominatim.openstreetmap.org/search.php?q=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;q&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;format=jsonv2`&lt;/span&gt;
    &lt;span class="pl-kos"&gt;)&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;data&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;response&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// data[0] is the first result - it has a boundingbox array of four floats&lt;/span&gt;
    &lt;span class="pl-c"&gt;// which we can convert into a Leaflet-compatible bounding box like this:&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;bounds&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;[&lt;/span&gt;
      &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;2&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
      &lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;1&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;&lt;span class="pl-s1"&gt;data&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;0&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;boundingbox&lt;/span&gt;&lt;span class="pl-kos"&gt;[&lt;/span&gt;&lt;span class="pl-c1"&gt;3&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;]&lt;/span&gt;
    &lt;span class="pl-kos"&gt;]&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This sets both the map center and zooms to the correct level for the bbox:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;fitBounds&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;bounds&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// User-provided zoom over-rides this&lt;/span&gt;
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;setZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;parseInt&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;initialZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This is the code that updates the URL as the user pans or zooms around.&lt;/span&gt;
  &lt;span class="pl-c"&gt;// You can subscribe to both the moveend and zoomend Leaflet events in one go:&lt;/span&gt;
  &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;on&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'moveend zoomend'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Update URL bar with current location&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;newZoom&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;center&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getCenter&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// This time we use URLSearchParams to construct a center...=&amp;amp;zoom=... URL&lt;/span&gt;
    &lt;span class="pl-k"&gt;let&lt;/span&gt; &lt;span class="pl-s1"&gt;u&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-k"&gt;new&lt;/span&gt; &lt;span class="pl-v"&gt;URLSearchParams&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// Copy across ?marker=x&amp;amp;marker=y from existing URL, if they were set:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;markers&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'marker'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'center'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;`&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;center&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;lat&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;,&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;center&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;lng&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;'zoom'&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;newZoom&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-c"&gt;// replaceState() is a weird API - the third argument is the one we care about:&lt;/span&gt;
    &lt;span class="pl-s1"&gt;history&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;replaceState&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;null&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;'?'&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;u&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;toString&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-c"&gt;// This bit adds Leaflet markers to the map for ?marker= query string arguments:&lt;/span&gt;
  &lt;span class="pl-s1"&gt;markers&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;forEach&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-v"&gt;L&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;marker&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-en"&gt;toPoint&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;s&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addTo&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;map&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-en"&gt;load&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;script&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-kos"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="pl-ent"&gt;html&lt;/span&gt;&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="pl-c"&gt;&amp;lt;!-- See https://github.com/simonw/url-map for documentation --&amp;gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/shot-scraper"&gt;shot-scraper&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="maps"/><category term="openstreetmap"/><category term="projects"/><category term="shot-scraper"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-freedraw 0.3.1</title><link href="https://simonwillison.net/2022/Feb/3/datasette-leaflet-freedraw/#atom-tag" rel="alternate"/><published>2022-02-03T20:24:38+00:00</published><updated>2022-02-03T20:24:38+00:00</updated><id>https://simonwillison.net/2022/Feb/3/datasette-leaflet-freedraw/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.3.1"&gt;datasette-leaflet-freedraw 0.3.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-freedraw 0.3</title><link href="https://simonwillison.net/2022/Jan/14/datasette-leaflet-freedraw/#atom-tag" rel="alternate"/><published>2022-01-14T02:04:12+00:00</published><updated>2022-01-14T02:04:12+00:00</updated><id>https://simonwillison.net/2022/Jan/14/datasette-leaflet-freedraw/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.3"&gt;datasette-leaflet-freedraw 0.3&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>Creating a minimal SpatiaLite database with Python</title><link href="https://simonwillison.net/2021/Dec/17/minimal-spatialite-database-in-python/#atom-tag" rel="alternate"/><published>2021-12-17T23:57:29+00:00</published><updated>2021-12-17T23:57:29+00:00</updated><id>https://simonwillison.net/2021/Dec/17/minimal-spatialite-database-in-python/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; &lt;a href="https://til.simonwillison.net/spatialite/minimal-spatialite-database-in-python"&gt;Creating a minimal SpatiaLite database with Python&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="leaflet"/></entry><entry><title>Weeknotes: Mostly messing around with map tiles</title><link href="https://simonwillison.net/2021/Feb/7/weeknotes/#atom-tag" rel="alternate"/><published>2021-02-07T05:53:19+00:00</published><updated>2021-02-07T05:53:19+00:00</updated><id>https://simonwillison.net/2021/Feb/7/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;Most of what I worked on this week was covered in &lt;a href="https://simonwillison.net/2021/Feb/4/datasette-tiles/"&gt;Serving map tiles from SQLite with MBTiles and datasette-tiles&lt;/a&gt;. I built two new plugins: &lt;a href="https://datasette.io/plugins/datasette-tiles"&gt;datasette-tiles&lt;/a&gt; for serving map tiles, and &lt;a href="https://datasette.io/plugins/datasette-basemap"&gt;datasette-basemap&lt;/a&gt; which bundles map tiles for zoom levels 0-6 of OpenStreetMap. I also released &lt;a href="https://datasette.io/tools/download-tiles"&gt;download-tiles&lt;/a&gt; for downloading tiles and bundling them into an MBTiles database.&lt;/p&gt;
&lt;h4&gt;sqlite-utils 3.4.1&lt;/h4&gt;
&lt;p&gt;I added one new feature to &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt;: the &lt;code&gt;sqlite-utils import&lt;/code&gt; command can now be configured to read CSV files using alternative delimiters, by passing the &lt;code&gt;--delimiter&lt;/code&gt; option or the &lt;code&gt;--quotechar&lt;/code&gt; option.&lt;/p&gt;
&lt;p&gt;This is &lt;a href="https://sqlite-utils.datasette.io/en/stable/cli.html#cli-insert-csv-tsv-delimiter"&gt;covered in the documentation&lt;/a&gt;, which provides the following example:&lt;/p&gt;
&lt;pre lang="csv"&gt;&lt;code&gt;name;description
Cleo;|Very fine; a friendly dog|
Pancakes;A local corgi
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Imported using:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;sqlite-utils insert dogs.db dogs dogs.csv \
  --delimiter=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;;&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; --quotechar=&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;|&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Datasette 0.54.1&lt;/h4&gt;
&lt;p&gt;I spotted a subtle but nasty regression in Datasette: a change I made to how hidden form fields worked on the table page meant that clearing the &lt;code&gt;_search&lt;/code&gt; search input and re-submitting the form didn't take effect, and the search would persist. &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-54-1"&gt;Datasette 0.54.1&lt;/a&gt; fixes that bug.&lt;/p&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-jellyfish"&gt;datasette-jellyfish&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-jellyfish/releases/tag/1.0.1"&gt;1.0.1&lt;/a&gt; - 2021-02-06
&lt;br /&gt;Datasette plugin adding SQL functions for fuzzy text matching powered by Jellyfish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.4.1"&gt;3.4.1&lt;/a&gt; - 2021-02-06
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-tiles"&gt;datasette-tiles&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-tiles/releases/tag/0.5"&gt;0.5&lt;/a&gt; - 2021-02-04
&lt;br /&gt;Mapping tile server for Datasette, serving tiles from MBTiles packages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/download-tiles"&gt;download-tiles&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/download-tiles/releases/tag/0.4"&gt;0.4&lt;/a&gt; - 2021-02-03
&lt;br /&gt;Download map tiles and store them in an MBTiles database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-basemap"&gt;datasette-basemap&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-basemap/releases/tag/0.2"&gt;0.2&lt;/a&gt; - 2021-02-02
&lt;br /&gt;A basemap for Datasette and datasette-leaflet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.54.1"&gt;0.54.1&lt;/a&gt; - 2021-02-02
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-cluster-map/releases/tag/0.17.1"&gt;0.17.1&lt;/a&gt; - 2021-02-01
&lt;br /&gt;Datasette plugin that shows a map for any data with latitude/longitude columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-leaflet"&gt;datasette-leaflet&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-leaflet/releases/tag/0.2.2"&gt;0.2.2&lt;/a&gt; - 2021-02-01
&lt;br /&gt;Datasette plugin adding the Leaflet JavaScript library&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/sqlite_splitting-commas-sqlite.md"&gt;Splitting on commas in SQLite&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/datasette_serving-mbtiles.md"&gt;Serving MBTiles with datasette-media&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/gis_mapzen-elevation-tiles.md"&gt;Downloading MapZen elevation tiles&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/csv"&gt;csv&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csv"/><category term="openstreetmap"/><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="sqlite-utils"/><category term="leaflet"/></entry><entry><title>Serving map tiles from SQLite with MBTiles and datasette-tiles</title><link href="https://simonwillison.net/2021/Feb/4/datasette-tiles/#atom-tag" rel="alternate"/><published>2021-02-04T01:09:30+00:00</published><updated>2021-02-04T01:09:30+00:00</updated><id>https://simonwillison.net/2021/Feb/4/datasette-tiles/#atom-tag</id><summary type="html">
    &lt;p&gt;Working on &lt;a href="https://simonwillison.net/2021/Jan/31/weeknotes/"&gt;datasette-leaflet&lt;/a&gt; last week re-kindled my interest in using Datasette as a GIS (Geographic Information System) platform. SQLite already has strong GIS functionality in the form of &lt;a href="https://docs.datasette.io/en/stable/spatialite.html"&gt;SpatiaLite&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; is currently the &lt;a href="https://datasette.io/plugins?sort=downloads-this-week"&gt;most downloaded&lt;/a&gt; plugin. Most importantly, maps are fun!&lt;/p&gt;
&lt;h4&gt;MBTiles&lt;/h4&gt;
&lt;p&gt;I was talking to &lt;a href="https://macwright.com/"&gt;Tom MacWright&lt;/a&gt; on Monday and I mentioned that I'd been thinking about how SQLite might make a good mechanism for distributing tile images for use with libraries like Leaflet. "I might be able to save you some time there" he said... and he showed me &lt;a href="https://github.com/mapbox/mbtiles-spec"&gt;MBTiles&lt;/a&gt;, a specification he started developing ten years ago at Mapbox which does exactly that - bundles tile images up in SQLite databases.&lt;/p&gt;
&lt;p&gt;(My best guess is I read about MBTiles a while ago, then managed to forget about the spec entirely while the idea of using SQLite for tile distribution wedged itself in my head somewhere.)&lt;/p&gt;
&lt;h4&gt;The new datasette-tiles plugin&lt;/h4&gt;
&lt;p&gt;I found some example MBTiles files on the internet and started playing around with them. My first prototype used the &lt;a href="https://datasette.io/plugins/datasette-media"&gt;datasette-media&lt;/a&gt; plugin, described here previously in &lt;a href="https://simonwillison.net/2020/Jul/30/fun-binary-data-and-sqlite/"&gt;Fun with binary data and SQLite&lt;/a&gt;. I used some convoluted SQL to teach it that hits to &lt;code&gt;/-/media/tiles/{z},{x},{y}&lt;/code&gt; should serve up content from the &lt;code&gt;tiles&lt;/code&gt; table in my MBTiles database - you can see details of that prototype in &lt;a href="https://til.simonwillison.net/datasette/serving-mbtiles"&gt;this TIL: Serving MBTiles with datasette-media&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The obvious next step was to write a dedicated plugin: &lt;a href="https://datasette.io/plugins/datasette-tiles"&gt;datasette-tiles&lt;/a&gt;. Install it and run Datasette against any MBTiles database file and the plugin will set up a &lt;code&gt;/-/tiles/db-name/z/x/y.png&lt;/code&gt; endpoint that serves the specified tiles.&lt;/p&gt;
&lt;p&gt;It also adds a tile explorer view with a pre-configured Leaflet map. Here's &lt;a href="https://datasette-tiles-demo.datasette.io/-/tiles/japan-toner"&gt;a live demo&lt;/a&gt; serving up a subset of Stamen's toner map - just zoom levels 6 and 7 for the country of Japan.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette-tiles-demo.datasette.io/-/tiles/japan-toner"&gt;&lt;img alt="The tile explorer showing a toner map for Japan" src="https://static.simonwillison.net/static/2021/datasette-tiles-japan-toner-demo.png" style="max-width:100%;" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here's how to run this on your own computer:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Install Datasette
brew install datasette
# Install the plugin
datasette install datasette-tiles
# Download the japan-toner.db database
curl -O https://datasette-tiles-demo.datasette.io/japan-toner.db
# Launch Datasette and open a browser
datasette japan-toner.db -o
# Use the cog menu to access the tile explorer
# Or visit http://127.0.0.1:8001/-/tiles/japan-toner
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;Creating MBTiles files with my download-tiles tool&lt;/h4&gt;
&lt;p&gt;A sticking point when I started playing with MBTiles was finding example files to work with.&lt;/p&gt;
&lt;p&gt;After some digging, I came across the amazing &lt;a href="https://export.hotosm.org/en/v3/"&gt;HOT Export Tool&lt;/a&gt;. It's a project by the &lt;a href="https://www.hotosm.org/"&gt;Humanitarian OpenStreetMap Team&lt;/a&gt; that allows anyone to export subsets of data from OpenStreetMap in a wide variety of formats, including MBTiles.&lt;/p&gt;
&lt;p&gt;I filed &lt;a href="https://github.com/hotosm/osm-export-tool/issues/371"&gt;a minor bug report&lt;/a&gt; against it, and in doing so took a look at the source code (it's all open source)... and found &lt;a href="https://github.com/hotosm/osm-export-tool-python/blob/8e4165a454303abbea2bd18cf5ffcdd5b9d0370d/osm_export_tool/nontabular.py#L103-L108"&gt;the code that assembles MBTiles files&lt;/a&gt;. It uses another open source library called &lt;a href="https://github.com/makinacorpus/landez"&gt;Landez&lt;/a&gt;, which provides functions for downloading tiles from existing providers and bundling those up as an MBTiles SQLite file.&lt;/p&gt;
&lt;p&gt;I prefer command-line tools for this kind of thing over using Python libraries directly, so I fired up my &lt;a href="https://github.com/simonw/click-app"&gt;click-app cookiecutter template&lt;/a&gt; and built a thin command-line interface over the top of the library.&lt;/p&gt;
&lt;p&gt;The new tool is called &lt;a href="https://datasette.io/tools/download-tiles"&gt;download-tiles&lt;/a&gt; and it does exactly that: downloads tiles from a tile server and creates an MBTiles SQLite database on disk containing those tiles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please use this tool responsibly&lt;/strong&gt;. Downloading large numbers of tiles is bad manners. Be sure to familiarize yourself with the &lt;a href="https://operations.osmfoundation.org/policies/tiles/"&gt;OpenStreetMap Tile Usage Policy&lt;/a&gt;, and use the tool politely when pointing it at other tile servers.&lt;/p&gt;
&lt;p&gt;Basic usage is as follows:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;download-tiles world.mbtiles
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default the tool pulls tiles from OpenStreetMap. The above command will fetch zoom levels 0-3 of the entire world - 85 tiles total, well within acceptable usage limits.&lt;/p&gt;
&lt;p&gt;Various options (described in &lt;a href="https://datasette.io/tools/download-tiles"&gt;the README&lt;/a&gt;) can be used to customize the tiles that are downloaded. Here's how I created the &lt;a href="https://datasette-tiles-demo.datasette.io/japan-toner"&gt;japan-toner.db&lt;/a&gt; demo database, linked to above:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;download-tiles japan-toner.mbtiles \ 
    --zoom-levels 6-7 \
    --country Japan \
    --tiles-url "http://{s}.tile.stamen.com/toner/{z}/{x}/{y}.png" \
    --tiles-subdomains "a,b,c,d" \
    --attribution 'Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA.'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--country Japan&lt;/code&gt; option here looks up the bounding box for Japan &lt;a href="https://nominatim.openstreetmap.org/ui/search.html?country=japan"&gt;using Nominatim&lt;/a&gt;. &lt;code&gt;--zoom-levels 6-7&lt;/code&gt; fetches zoom levels 6 and 7 (in this case that makes for 193 tiles total). &lt;code&gt;--tiles-url&lt;/code&gt; and &lt;code&gt;--tiles-subdomain&lt;/code&gt; configure the tile server to fetch them from. The &lt;code&gt;--attribution&lt;/code&gt; option bakes that string into the &lt;a href="https://datasette-tiles-demo.datasette.io/japan-toner/metadata"&gt;metadata table&lt;/a&gt; for the database - which is then used to display it correctly in the tile explorer (and eventually in other Datasette plugins).&lt;/p&gt;
&lt;h4&gt;datasette-basemap&lt;/h4&gt;
&lt;p&gt;Out of the box, Datasette's current Leaflet plugins (&lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt; and so on) serve tiles directly from the OpenStreetMap tile server.&lt;/p&gt;
&lt;p&gt;I've never felt particularly comfortable about this. Users can configure the plugins to run against other tile servers, but pointing to OpenStreetMap as a default was the easiest way to ensure these plugins would work for people who just wanted to try them out.&lt;/p&gt;
&lt;p&gt;Now that I have the tooling for bundling map subsets, maybe I can do better.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/plugins/datasette-basemap"&gt;datasette-basemap&lt;/a&gt; offers an alternative: it's a plugin that bundles a 22.7MB SQLite file containing zoom levels 0-6 of OpenStreetMap - &lt;a href="https://datasette-tiles-demo.datasette.io/basemap/tiles?_facet=zoom_level"&gt;5,461 tiles total&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Running &lt;code&gt;pip install datasette-basemap&lt;/code&gt; (or &lt;code&gt;datasette install datasette-basemap&lt;/code&gt;) will install the plugin, complete with that database - and register it with Datasette.&lt;/p&gt;
&lt;p&gt;Start Datasette with the plugin installed and &lt;code&gt;/basemap&lt;/code&gt; will expose &lt;a href="https://datasette-tiles-demo.datasette.io/basemap"&gt;the bundled database&lt;/a&gt;. Install &lt;code&gt;datasette-tiles&lt;/code&gt; and you'll be able to browse it as a tile server: &lt;a href="https://datasette-tiles-demo.datasette.io/-/tiles/basemap"&gt;here's a demo&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;(I recommend also installing &lt;a href="https://datasette.io/plugins/datasette-render-images"&gt;datasette-render-images&lt;/a&gt; so you can see the tile images themselves in the regular table view, &lt;a href="https://datasette-tiles-demo.datasette.io/basemap/tiles"&gt;like this&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Zoom level 6 is close enough that major cities and the roads between them are visible, for all of the countries in the world. Not bad for 22.7MB!&lt;/p&gt;
&lt;p&gt;This is the first time I've built a Datasette plugin that bundles a full SQLite database as part of the Python package. The pattern seems to work well - I'm excited to explore it further with other projects.&lt;/p&gt;
&lt;h4&gt;Bonus feature: tile stacks&lt;/h4&gt;
&lt;p&gt;I added one last feature to &lt;code&gt;datasette-tiles&lt;/code&gt; before writing everything up for my blog. I'm calling this feature &lt;strong&gt;tile stacks&lt;/strong&gt; - it lets you serve tiles from multiple MBTiles files, falling back to other files if a tile is missing.&lt;/p&gt;
&lt;p&gt;Imagine you had a low-zoom-level world map (similar to &lt;code&gt;datasette-basemap&lt;/code&gt;) and a number of other databases providing packages of tiles for specific countries or cities. You could run Datasette like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette basemap.mbtiles japan.mbtiles london.mbtiles tokyo.mbtiles
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hitting &lt;code&gt;/-/tiles-stack/1/1/1.png&lt;/code&gt; would seek out the specified tile in the &lt;code&gt;tokyo.mbtiles&lt;/code&gt; file, then fall back to &lt;code&gt;london.mbtiles&lt;/code&gt; and then &lt;code&gt;japan.mbtiles&lt;/code&gt; and finally &lt;code&gt;basemap.mbtiles&lt;/code&gt; if it couldn't find it.&lt;/p&gt;
&lt;p&gt;For a demo, visit &lt;a href="https://datasette-tiles-demo.datasette.io/-/tiles-stack"&gt;https://datasette-tiles-demo.datasette.io/-/tiles-stack&lt;/a&gt; and zoom in on Japan. It should start to display the Stamen toner map once you get to zoom levels 6 and 7.&lt;/p&gt;
&lt;h4&gt;Next steps&lt;/h4&gt;
&lt;p&gt;I've been having a lot of fun exploring MBTiles - it's such a natural fit for Datasette, and it's exciting to be able to build new things on top of nearly a decade of innovation by other geo-hackers.&lt;/p&gt;
&lt;p&gt;There are plenty of features missing from &lt;code&gt;datasette-tiles&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It currently only handles &lt;code&gt;.png&lt;/code&gt; image data, but the &lt;a href="https://github.com/mapbox/mbtiles-spec/blob/master/1.3/spec.md"&gt;MBTiles 1.3 specification&lt;/a&gt; also defines &lt;code&gt;.jpg&lt;/code&gt; and &lt;code&gt;.webp&lt;/code&gt; tiles, plus vector tiles using Mapbox's &lt;code&gt;.pbf&lt;/code&gt; gzip-compressed protocol buffers.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/mapbox/utfgrid-spec"&gt;UTFGrid&lt;/a&gt; is a related specification for including "rasterized interaction data" in MBTiles databases - it helps efficiently provide maps &lt;a href="https://blog.mapbox.com/how-interactivity-works-with-utfgrid-3b7d437f9ca9"&gt;with millions of embedded objects&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As a newcomer to the MBTiles world I'd love to hear suggestions for new features and feedback on how I can improve what I've got so far in the &lt;a href="https://github.com/simonw/datasette-tiles/issues"&gt;datasette-tiles issues&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Being able to serve your own map tiles like this feels very much in the spirit of the OpenStreetMap project. I'm looking forward to using my own tile subsets for any future projects that fit within a sensible tile subset.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mapping"&gt;mapping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tom-macwright"&gt;tom-macwright&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="mapping"/><category term="openstreetmap"/><category term="projects"/><category term="sqlite"/><category term="datasette"/><category term="tom-macwright"/><category term="leaflet"/></entry><entry><title>Serving MBTiles with datasette-media</title><link href="https://simonwillison.net/2021/Feb/3/serving-mbtiles/#atom-tag" rel="alternate"/><published>2021-02-03T23:12:05+00:00</published><updated>2021-02-03T23:12:05+00:00</updated><id>https://simonwillison.net/2021/Feb/3/serving-mbtiles/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;TIL:&lt;/strong&gt; &lt;a href="https://til.simonwillison.net/datasette/serving-mbtiles"&gt;Serving MBTiles with datasette-media&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="leaflet"/></entry><entry><title>datasette-basemap 0.2</title><link href="https://simonwillison.net/2021/Feb/2/datasette-basemap/#atom-tag" rel="alternate"/><published>2021-02-02T23:33:57+00:00</published><updated>2021-02-02T23:33:57+00:00</updated><id>https://simonwillison.net/2021/Feb/2/datasette-basemap/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-basemap/releases/tag/0.2"&gt;datasette-basemap 0.2&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-basemap 0.1</title><link href="https://simonwillison.net/2021/Feb/2/datasette-basemap-2/#atom-tag" rel="alternate"/><published>2021-02-02T07:07:43+00:00</published><updated>2021-02-02T07:07:43+00:00</updated><id>https://simonwillison.net/2021/Feb/2/datasette-basemap-2/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-basemap/releases/tag/0.1"&gt;datasette-basemap 0.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-basemap 0.1a1</title><link href="https://simonwillison.net/2021/Feb/2/datasette-basemap-3/#atom-tag" rel="alternate"/><published>2021-02-02T02:02:51+00:00</published><updated>2021-02-02T02:02:51+00:00</updated><id>https://simonwillison.net/2021/Feb/2/datasette-basemap-3/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-basemap/releases/tag/0.1a1"&gt;datasette-basemap 0.1a1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-basemap 0.1a</title><link href="https://simonwillison.net/2021/Feb/2/datasette-basemap-4/#atom-tag" rel="alternate"/><published>2021-02-02T01:59:05+00:00</published><updated>2021-02-02T01:59:05+00:00</updated><id>https://simonwillison.net/2021/Feb/2/datasette-basemap-4/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-basemap/releases/tag/0.1a"&gt;datasette-basemap 0.1a&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet 0.2.2</title><link href="https://simonwillison.net/2021/Feb/1/datasette-leaflet/#atom-tag" rel="alternate"/><published>2021-02-01T22:20:28+00:00</published><updated>2021-02-01T22:20:28+00:00</updated><id>https://simonwillison.net/2021/Feb/1/datasette-leaflet/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet/releases/tag/0.2.2"&gt;datasette-leaflet 0.2.2&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>Weeknotes: datasette-leaflet, datasette-plugin cookiecutter upgrades</title><link href="https://simonwillison.net/2021/Jan/31/weeknotes/#atom-tag" rel="alternate"/><published>2021-01-31T07:39:10+00:00</published><updated>2021-01-31T07:39:10+00:00</updated><id>https://simonwillison.net/2021/Jan/31/weeknotes/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I &lt;a href="https://simonwillison.net/2021/Jan/25/datasette/"&gt;shipped Datasette 0.54&lt;/a&gt;, sent out &lt;a href="https://datasette.substack.com/p/datasette-054-and-querying-a-database"&gt;the latest Datasette Newsletter&lt;/a&gt; and then mostly worked on follow-up projects.&lt;/p&gt;
&lt;h4&gt;datasette-leaflet&lt;/h4&gt;
&lt;p&gt;Datasette 0.54 introduced support for JavaScript modules. &lt;a href="https://datasette.io/plugins/datasette-leaflet"&gt;datasette-leaflet&lt;/a&gt; aims to solve a growing problem: there are now three Datasette plugins that use the &lt;a href="https://leafletjs.com/"&gt;Leaflet&lt;/a&gt; JavaScript mapping library, and all three were loading it in different ways and using different versions.&lt;/p&gt;
&lt;p&gt;Those three plugins - &lt;a href="https://datasette.io/plugins/datasette-leaflet-freedraw"&gt;datasette-leaflet-freedraw&lt;/a&gt;, &lt;a href="https://datasette.io/plugins/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt; and &lt;a href="https://datasette.io/plugins/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; - now all depend on &lt;code&gt;datasette-leaflet&lt;/code&gt;. This should mean that even if multiple plugins are installed they will all load and use the same copy of the Leaflet library.&lt;/p&gt;
&lt;p&gt;There's just one problem: &lt;code&gt;datasette-leaflet-freedraw&lt;/code&gt; uses the &lt;a href="https://github.com/Wildhoney/Leaflet.FreeDraw"&gt;Leaflet.FreeDraw&lt;/a&gt; module, which it turns out already bundles its own copy of Leaflet. I still need to figure out how to build a version of that package which can take advantage of the shared copy of Leaflet - see open &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/issues/5"&gt;issue #5&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;datasette-plugin and cookiecutter tests&lt;/h4&gt;
&lt;p&gt;One of the tricker aspects of building a Datasette plugin is bundling static assets such as JavaScript and CSS in the plugin to be distributed via PyPI. I know this is hard because I frequently mess up the &lt;code&gt;package_data=&lt;/code&gt; section of &lt;code&gt;setup.py&lt;/code&gt; and have to ship another release to fix my mistake!&lt;/p&gt;
&lt;p&gt;I've upgraded the &lt;a href="https://github.com/simonw/datasette-plugin"&gt;datasette-plugin&lt;/a&gt; cookiecutter template to help with this. The template now asks you if you want to create &lt;code&gt;static/&lt;/code&gt; or &lt;code&gt;template/&lt;/code&gt; directories as part of the interactive configuration - if you say yes to either of those it will create the directories for you and configure &lt;code&gt;package_data=&lt;/code&gt; to correctly bundle any files contained within them.&lt;/p&gt;
&lt;p&gt;This lead to a couple of new TILs: &lt;a href="https://til.simonwillison.net/cookiecutter/pytest-for-cookiecutter"&gt;Testing cookiecutter templates with pytest&lt;/a&gt; and &lt;a href="https://til.simonwillison.net/cookiecutter/conditionally-creating-directories"&gt;Conditionally creating directories in cookiecutter&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;datasette-export-notebook improvements&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://datasette.io/plugins/datasette-export-notebook"&gt;datasette-export-notebook&lt;/a&gt; is a plugin that adds copy-and-paste instructions for exporting data from Datasette to Jupyter or Observable notebooks.&lt;/p&gt;
&lt;p&gt;The "stream all rows" option for Jupyter uses Datasette's CSV streaming export, which has the downside of losing any type information.&lt;/p&gt;
&lt;p&gt;As of &lt;a href="https://github.com/simonw/datasette-export-notebook/issues/8"&gt;issue #8&lt;/a&gt; the copy-and-paste code for Jupyter now uses the &lt;code&gt;dtype=&lt;/code&gt; option to specify the correct types.&lt;/p&gt;
&lt;p&gt;You can see it in action on &lt;a href="https://covid-19.datasettes.com/covid/us_census_county_populations_2019.Notebook"&gt;this page&lt;/a&gt;, where the Jupyter example code now looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-s1"&gt;df&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pandas&lt;/span&gt;.&lt;span class="pl-en"&gt;read_csv&lt;/span&gt;(
    &lt;span class="pl-s"&gt;"https://covid-19.datasettes.com/covid/us_census_county_populations_2019.csv?_stream=on"&lt;/span&gt;,
    &lt;span class="pl-s1"&gt;dtype&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;{
        &lt;span class="pl-s"&gt;"fips"&lt;/span&gt;: &lt;span class="pl-s1"&gt;int&lt;/span&gt;,
        &lt;span class="pl-s"&gt;"population"&lt;/span&gt;: &lt;span class="pl-s1"&gt;int&lt;/span&gt;,
    }
)&lt;/pre&gt;
&lt;h4&gt;Releases this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-export-notebook"&gt;datasette-export-notebook&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-export-notebook/releases/tag/0.3"&gt;0.3&lt;/a&gt; - 2021-01-29
&lt;br /&gt;Datasette plugin providing instructions for exporting data to Jupyter or Observable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-debug-asgi"&gt;datasette-debug-asgi&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-debug-asgi/releases/tag/1.0"&gt;1.0&lt;/a&gt; - 2021-01-29
&lt;br /&gt;Datasette plugin for dumping out the ASGI scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-template-sql"&gt;datasette-template-sql&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-template-sql/releases/tag/1.0.2"&gt;1.0.2&lt;/a&gt; - 2021-01-29
&lt;br /&gt;Datasette plugin for executing SQL queries from templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-cluster-map/releases/tag/0.17"&gt;0.17&lt;/a&gt; -  2021-01-29
&lt;br /&gt;Datasette plugin that shows a map for any data with latitude/longitude columns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-leaflet-geojson/releases/tag/0.8"&gt;0.8&lt;/a&gt; - 2021-01-26
&lt;br /&gt;Datasette plugin that replaces any GeoJSON column values with a Leaflet map.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-leaflet-freedraw"&gt;datasette-leaflet-freedraw&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.2.1"&gt;0.2.1&lt;/a&gt; - 2021-01-25
&lt;br /&gt;Draw polygons on maps in Datasette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-leaflet"&gt;datasette-leaflet&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-leaflet/releases/tag/0.2"&gt;0.2&lt;/a&gt; - 2021-01-25
&lt;br /&gt;Datasette plugin adding the Leaflet JavaScript library&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.54"&gt;0.54&lt;/a&gt; -  2021-01-25
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/cookiecutter/pytest-for-cookiecutter"&gt;Testing cookiecutter templates with pytest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/cookiecutter/conditionally-creating-directories"&gt;Conditionally creating directories in cookiecutter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="projects"/><category term="datasette"/><category term="weeknotes"/><category term="leaflet"/></entry><entry><title>datasette-leaflet 0.2.1</title><link href="https://simonwillison.net/2021/Jan/31/datasette-leaflet/#atom-tag" rel="alternate"/><published>2021-01-31T07:23:01+00:00</published><updated>2021-01-31T07:23:01+00:00</updated><id>https://simonwillison.net/2021/Jan/31/datasette-leaflet/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet/releases/tag/0.2.1"&gt;datasette-leaflet 0.2.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-geojson 0.8</title><link href="https://simonwillison.net/2021/Jan/26/datasette-leaflet-geojson/#atom-tag" rel="alternate"/><published>2021-01-26T02:41:07+00:00</published><updated>2021-01-26T02:41:07+00:00</updated><id>https://simonwillison.net/2021/Jan/26/datasette-leaflet-geojson/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-geojson/releases/tag/0.8"&gt;datasette-leaflet-geojson 0.8&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-freedraw 0.2.1</title><link href="https://simonwillison.net/2021/Jan/25/datasette-leaflet-freedraw/#atom-tag" rel="alternate"/><published>2021-01-25T19:23:59+00:00</published><updated>2021-01-25T19:23:59+00:00</updated><id>https://simonwillison.net/2021/Jan/25/datasette-leaflet-freedraw/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.2.1"&gt;datasette-leaflet-freedraw 0.2.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-freedraw 0.2</title><link href="https://simonwillison.net/2021/Jan/25/datasette-leaflet-freedraw-2/#atom-tag" rel="alternate"/><published>2021-01-25T19:05:34+00:00</published><updated>2021-01-25T19:05:34+00:00</updated><id>https://simonwillison.net/2021/Jan/25/datasette-leaflet-freedraw-2/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.2"&gt;datasette-leaflet-freedraw 0.2&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet 0.2</title><link href="https://simonwillison.net/2021/Jan/25/datasette-leaflet/#atom-tag" rel="alternate"/><published>2021-01-25T19:03:10+00:00</published><updated>2021-01-25T19:03:10+00:00</updated><id>https://simonwillison.net/2021/Jan/25/datasette-leaflet/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet/releases/tag/0.2"&gt;datasette-leaflet 0.2&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet 0.1</title><link href="https://simonwillison.net/2021/Jan/24/datasette-leaflet/#atom-tag" rel="alternate"/><published>2021-01-24T18:19:32+00:00</published><updated>2021-01-24T18:19:32+00:00</updated><id>https://simonwillison.net/2021/Jan/24/datasette-leaflet/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet/releases/tag/0.1"&gt;datasette-leaflet 0.1&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>Drawing shapes on a map to query a SpatiaLite database (and other weeknotes)</title><link href="https://simonwillison.net/2021/Jan/24/drawing-shapes-spatialite/#atom-tag" rel="alternate"/><published>2021-01-24T06:50:29+00:00</published><updated>2021-01-24T06:50:29+00:00</updated><id>https://simonwillison.net/2021/Jan/24/drawing-shapes-spatialite/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I built a Datasette plugin that lets you query a database by drawing shapes on a map!&lt;/p&gt;
&lt;h4&gt;datasette-leaflet-freedraw&lt;/h4&gt;
&lt;p&gt;SpatiaLite is a SQLite extension that adds &lt;a href="http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.3.0.html"&gt;a plethora&lt;/a&gt; of geospatial querying features. Datasette &lt;a href="https://docs.datasette.io/en/stable/spatialite.html"&gt;works well with SpatiaLite&lt;/a&gt; already, but every now and then I dip in to a geospatial project to see if there are any neat ways I can extend Datasette's spatial querying support.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/plugins/datasette-leaflet-freedraw"&gt;datasette-leaflet-freedraw&lt;/a&gt; is my new plugin which brings the excellent &lt;a href="https://github.com/Wildhoney/Leaflet.FreeDraw"&gt;FreeDraw&lt;/a&gt; Leaflet module to Datasette.&lt;/p&gt;
&lt;p&gt;It's a pure-JavaScript plugin which looks for Datasette SQL parameters called &lt;code&gt;:freedraw&lt;/code&gt; and enhances them with an interactive map. You can then draw on the map to populate the form field with a GeoJSON MultiPolygon representing the shape that you drew. Submit that to the server to excute it as part of a query.&lt;/p&gt;
&lt;p&gt;&lt;img alt="A screenshot showing the plugin with a shape drawn around San Francisco" src="https://static.simonwillison.net/static/2021/datasette-leaflet-freedraw.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This is best illustrated with a demo. Here's a SQL query that let you draw a shape to find California protected areas (parks, nature reserves and suchlike) that intersect with the polygon that you draw:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  AsGeoJSON(geometry), &lt;span class="pl-k"&gt;*&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  CPAD_2020a_SuperUnits
&lt;span class="pl-k"&gt;where&lt;/span&gt;
  Intersects(GeomFromGeoJSON(:freedraw), geometry) &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;
  &lt;span class="pl-k"&gt;and&lt;/span&gt; &lt;span class="pl-c1"&gt;CPAD_2020a_SuperUnits&lt;/span&gt;.&lt;span class="pl-c1"&gt;rowid&lt;/span&gt; &lt;span class="pl-k"&gt;in&lt;/span&gt; (
    &lt;span class="pl-k"&gt;select&lt;/span&gt;
      rowid
    &lt;span class="pl-k"&gt;from&lt;/span&gt;
      SpatialIndex
    &lt;span class="pl-k"&gt;where&lt;/span&gt;
      f_table_name &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;CPAD_2020a_SuperUnits&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      &lt;span class="pl-k"&gt;and&lt;/span&gt; search_frame &lt;span class="pl-k"&gt;=&lt;/span&gt; GeomFromGeoJSON(:freedraw)
  )&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This uses the &lt;a href="https://www.calands.org/cpad/"&gt;California Protected Areas Database&lt;/a&gt; by GreenInfo Network, &lt;a href="https://simonwillison.net/2020/Aug/28/weeknotes-cpad/"&gt;discussed here previously&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://calands.datasettes.com/calands/draw_to_find?freedraw=%7B%22type%22%3A%22MultiPolygon%22%2C%22coordinates%22%3A%5B%5B%5B%5B-122.40142822265626%2C37.810868914073005%5D%2C%5B-122.38838195800783%2C37.807071480609274%5D%2C%5B-122.37121582031251%2C37.79784832917947%5D%2C%5B-122.35885620117189%2C37.794592824285104%5D%2C%5B-122.35679626464845%2C37.77722770873696%5D%2C%5B-122.35061645507814%2C37.76040136229719%5D%2C%5B-122.34786987304688%2C37.74574303801687%5D%2C%5B-122.34786987304688%2C37.73379707124429%5D%2C%5B-122.35061645507814%2C37.72456477660484%5D%2C%5B-122.35542297363283%2C37.72021976910832%5D%2C%5B-122.3870086669922%2C37.703923721150886%5D%2C%5B-122.40966796875001%2C37.69957750311587%5D%2C%5B-122.43576049804689%2C37.697404298539745%5D%2C%5B-122.47421264648439%2C37.69849090879089%5D%2C%5B-122.50099182128908%2C37.70229391925025%5D%2C%5B-122.50991821289064%2C37.7071832174446%5D%2C%5B-122.51678466796876%2C37.714244967649265%5D%2C%5B-122.51953125000001%2C37.72510788462094%5D%2C%5B-122.52639770507814%2C37.743571187449064%5D%2C%5B-122.52571105957033%2C37.77071473849611%5D%2C%5B-122.51953125000001%2C37.78211205989562%5D%2C%5B-122.5078582763672%2C37.795678008523424%5D%2C%5B-122.49549865722658%2C37.80490143094975%5D%2C%5B-122.47901916503908%2C37.811953859192705%5D%2C%5B-122.46322631835939%2C37.814123701604466%5D%2C%5B-122.40142822265626%2C37.810868914073005%5D%5D%5D%5D%7D"&gt;Try the SQL query here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;(That &lt;code&gt;from SpatialIndex&lt;/code&gt; sub-select at the end is a pattern for taking advantage of SpatiaLite spatial indexes.)&lt;/p&gt;
&lt;p&gt;The plugin itself is pretty simple: it scans for &lt;code&gt;freedraw&lt;/code&gt; input fields and enhances them with a map interface that writes GeoJSON back to the form field.&lt;/p&gt;
&lt;p&gt;You can then use SpatiaLite functions such as &lt;code&gt;GeomFromGeoJSON()&lt;/code&gt; to transform that user input into something that can be used in a query.&lt;/p&gt;
&lt;p&gt;Another fun query: let's add &lt;code&gt;WHERE PARK_NAME like '%mini%'&lt;/code&gt; to the query to filter for just the "mini parks" in San Francisco:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select&lt;/span&gt;
  AsGeoJSON(geometry), &lt;span class="pl-k"&gt;*&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt;
  CPAD_2020a_SuperUnits
&lt;span class="pl-k"&gt;where&lt;/span&gt;
  PARK_NAME &lt;span class="pl-k"&gt;like&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%mini%&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;and&lt;/span&gt;
  Intersects(GeomFromGeoJSON(:freedraw), geometry) &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;
  &lt;span class="pl-k"&gt;and&lt;/span&gt; &lt;span class="pl-c1"&gt;CPAD_2020a_SuperUnits&lt;/span&gt;.&lt;span class="pl-c1"&gt;rowid&lt;/span&gt; &lt;span class="pl-k"&gt;in&lt;/span&gt; (
    &lt;span class="pl-k"&gt;select&lt;/span&gt;
      rowid
    &lt;span class="pl-k"&gt;from&lt;/span&gt;
      SpatialIndex
    &lt;span class="pl-k"&gt;where&lt;/span&gt;
      f_table_name &lt;span class="pl-k"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;CPAD_2020a_SuperUnits&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;
      &lt;span class="pl-k"&gt;and&lt;/span&gt; search_frame &lt;span class="pl-k"&gt;=&lt;/span&gt; GeomFromGeoJSON(:freedraw)
  )&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here are the mini parks &lt;a href="https://calands.datasettes.com/calands/draw_to_find_mini_parks?freedraw=%7B%22type%22%3A%22MultiPolygon%22%2C%22coordinates%22%3A%5B%5B%5B%5B-122.42134094238283%2C37.82009043941308%5D%2C%5B-122.39662170410158%2C37.814123701604466%5D%2C%5B-122.3925018310547%2C37.811953859192705%5D%2C%5B-122.36572265625001%2C37.78645343442073%5D%2C%5B-122.35473632812501%2C37.77017196507861%5D%2C%5B-122.34512329101564%2C37.74954362315467%5D%2C%5B-122.34512329101564%2C37.72836644908416%5D%2C%5B-122.34855651855469%2C37.72021976910832%5D%2C%5B-122.36640930175783%2C37.71044257039148%5D%2C%5B-122.3815155029297%2C37.70772645289051%5D%2C%5B-122.42134094238283%2C37.709899354855125%5D%2C%5B-122.45429992675781%2C37.70609673460725%5D%2C%5B-122.49824523925783%2C37.708269684354526%5D%2C%5B-122.51953125000001%2C37.71315858834301%5D%2C%5B-122.52296447753908%2C37.71533133102705%5D%2C%5B-122.53395080566408%2C37.73108180994824%5D%2C%5B-122.53463745117189%2C37.751172385606196%5D%2C%5B-122.53120422363283%2C37.763115548102924%5D%2C%5B-122.51472473144533%2C37.78808138412046%5D%2C%5B-122.49412536621095%2C37.80761398306056%5D%2C%5B-122.47489929199219%2C37.81792077237497%5D%2C%5B-122.45910644531251%2C37.81737834565083%5D%2C%5B-122.44331359863283%2C37.82171764783966%5D%2C%5B-122.42134094238283%2C37.82009043941308%5D%5D%5D%5D%7D"&gt;for San Francisco&lt;/a&gt; and &lt;a href="https://calands.datasettes.com/calands/draw_to_find_mini_parks?freedraw=%7B%22type%22%3A%22MultiPolygon%22%2C%22coordinates%22%3A%5B%5B%5B%5B-122.20916748046876%2C37.85045908105496%5D%2C%5B-122.19509124755861%2C37.84612146910074%5D%2C%5B-122.19028472900392%2C37.83663205340172%5D%2C%5B-122.18994140625001%2C37.83120898199491%5D%2C%5B-122.18444824218751%2C37.819548028632376%5D%2C%5B-122.18273162841798%2C37.804358908571395%5D%2C%5B-122.17826843261719%2C37.78536811469731%5D%2C%5B-122.17998504638673%2C37.77397129533325%5D%2C%5B-122.18410491943361%2C37.769629187677%5D%2C%5B-122.19200134277345%2C37.76474401178006%5D%2C%5B-122.2053909301758%2C37.76447260365713%5D%2C%5B-122.21740722656251%2C37.76582963431185%5D%2C%5B-122.22839355468751%2C37.7690864062913%5D%2C%5B-122.23972320556642%2C37.77641361883315%5D%2C%5B-122.24899291992189%2C37.78075532805877%5D%2C%5B-122.26650238037111%2C37.78998061344339%5D%2C%5B-122.27851867675783%2C37.791337175930714%5D%2C%5B-122.28572845458986%2C37.791337175930714%5D%2C%5B-122.29122161865236%2C37.791879793952084%5D%2C%5B-122.30083465576173%2C37.794050226188425%5D%2C%5B-122.3056411743164%2C37.79649188624147%5D%2C%5B-122.31250762939455%2C37.801103690609615%5D%2C%5B-122.31903076171876%2C37.81303878836991%5D%2C%5B-122.31491088867189%2C37.82253123860035%5D%2C%5B-122.31044769287111%2C37.827412594823755%5D%2C%5B-122.30598449707031%2C37.82985315185641%5D%2C%5B-122.28126525878908%2C37.83446287267591%5D%2C%5B-122.2713088989258%2C37.84042796619096%5D%2C%5B-122.25036621093751%2C37.842868092687425%5D%2C%5B-122.22839355468751%2C37.85073017332982%5D%2C%5B-122.22564697265626%2C37.85100126460795%5D%2C%5B-122.20916748046876%2C37.85045908105496%5D%5D%5D%5D%7D"&gt;for Oakland&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This demo uses the user's shapes as part of a &lt;code&gt;SELECT&lt;/code&gt; query, but it's also possible to use Datasette's &lt;a href="https://docs.datasette.io/en/stable/sql_queries.html#writable-canned-queries"&gt;Writable canned queries&lt;/a&gt; to create forms that will save the shapes to a database using an &lt;code&gt;INSERT&lt;/code&gt; or &lt;code&gt;UPDATE&lt;/code&gt; query.&lt;/p&gt;
&lt;p&gt;If you want to try this plugin out note that it currently requires the &lt;a href="https://github.com/simonw/datasette/releases/tag/0.54a0"&gt;Datasette 0.54a alpha&lt;/a&gt;. This is because it takes advantage of the new &lt;code&gt;&amp;lt;script type="module"&amp;gt;&lt;/code&gt; support I &lt;a href="https://simonwillison.net/2021/Jan/17/weeknotes-still-pretty-distracted/#script-type-module"&gt;added last week&lt;/a&gt;. &lt;strong&gt;Update 25th January 2021:&lt;/strong&gt; Datasette 0.54 has now &lt;a href="https://simonwillison.net/2021/Jan/25/datasette/"&gt;been released&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Other releases this week&lt;/h4&gt;
&lt;p&gt;I recorded my talk for &lt;a href="https://research-fosdem.github.io/"&gt;FOSDEM&lt;/a&gt; - and in doing so I broke my unproductive streak and pushed out releases for a whole bunch of Datasette projects - partly through fixing bugs I spotted while trying to record the talk!&lt;/p&gt;
&lt;p&gt;Here's the full list, with some annotations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-auth-github"&gt;datasette-auth-github&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-auth-github/releases/tag/0.13"&gt;0.13&lt;/a&gt; - 2021-01-24
&lt;br /&gt;Datasette plugin that authenticates users against GitHub. This is a big release: I finally finished upgrading the plugin to work against Datasette's new-ish &lt;a href="https://docs.datasette.io/en/stable/authentication.html"&gt;authentication and permissions&lt;/a&gt; mechanism.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-leaflet-freedraw"&gt;datasette-leaflet-freedraw&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.1.4"&gt;0.1.4&lt;/a&gt; -  2021-01-24
&lt;br /&gt;Draw polygons on maps in Datasette. See above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-cluster-map/releases/tag/0.16"&gt;0.16&lt;/a&gt; - 2021-01-23
&lt;br /&gt;Datasette plugin that shows a map for any data with latitude/longitude columns. Fixed some bugs related to the display of marker popups that included foreign key references to other tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/asgi-csrf"&gt;asgi-csrf&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/asgi-csrf/releases/tag/0.8"&gt;0.8&lt;/a&gt; - 2021-01-23
&lt;br /&gt;ASGI middleware for protecting against CSRF attacks. I fixed a broken test caused by my use of an undocumented and unstable API in &lt;code&gt;httpx&lt;/code&gt; - &lt;a href="https://github.com/simonw/asgi-csrf/issues/18"&gt;details here&lt;/a&gt;. I also added a new &lt;code&gt;always_set_cookie=True&lt;/code&gt; option for cases where you always want to ensure a CSRF cookie has been set, see &lt;a href="https://github.com/simonw/asgi-csrf/issues/16"&gt;#16&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-leaflet-geojson"&gt;datasette-leaflet-geojson&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette-leaflet-geojson/releases/tag/0.7"&gt;0.7&lt;/a&gt; - 2021-01-20
&lt;br /&gt;Datasette plugin that replaces any GeoJSON column values with a Leaflet map. I fixed a long-standing point of concern: this plugin renders a map for every row on a page containing GeoJSON, which can potentially mean hundreds of maps for queries that return may results. The plugin now renders just the first ten maps and provides a grey clickable "Click to show map" widget for anything past the first ten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette"&gt;datasette&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/datasette/releases/tag/0.54a0"&gt;0.54a0&lt;/a&gt; - 2021-01-19
&lt;br /&gt;Lots of stuff in this one, see &lt;a href="https://github.com/simonw/datasette/releases/tag/0.54a0"&gt;the release notes&lt;/a&gt;. I pushed out an alpha so I could start using the JavaScript modules stuff in &lt;code&gt;datasette-leaflet-freedraw&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-transform"&gt;sqlite-transform&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-transform/releases/tag/0.3.1"&gt;0.3.1&lt;/a&gt; - 2021-01-18
&lt;br /&gt;Tool for running transformations on columns in a SQLite database. I improved the &lt;code&gt;--help&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/dogsheep/swarm-to-sqlite"&gt;swarm-to-sqlite&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/dogsheep/swarm-to-sqlite/releases/tag/0.3.3"&gt;0.3.3&lt;/a&gt; - 2021-01-18
&lt;br /&gt;Create a SQLite database containing your checkin history from Foursquare Swarm. I fixed a bug caused by new fields showing up in the Swarm JSON output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/simonw/sqlite-utils"&gt;sqlite-utils&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/sqlite-utils/releases/tag/3.3"&gt;3.3&lt;/a&gt; - 2021-01-18
&lt;br /&gt;Python CLI utility and library for manipulating SQLite databases. I added the &lt;code&gt;alter=True&lt;/code&gt; argument to the &lt;code&gt;.m2m()&lt;/code&gt; method, which I needed to fix the bug in &lt;code&gt;swarm-to-sqlite&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/github_bulk-repo-github-graphql.md"&gt;Bulk fetching repository details with the GitHub GraphQL API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/sqlite_fixing-column-encoding-with-ftfy-and-sqlite-transform.md"&gt;Fixing broken text encodings with sqlite-transform and ftfy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/github_syntax-highlighting-python-console.md"&gt;Syntax highlighting Python console examples with GFM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/jupyter_javascript-in-a-jupyter-notebook.md"&gt;Embedding JavaScript in a Jupyter notebook&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/spatialite"&gt;spatialite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="projects"/><category term="spatialite"/><category term="sqlite"/><category term="datasette"/><category term="weeknotes"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-freedraw 0.1.4</title><link href="https://simonwillison.net/2021/Jan/24/datasette-leaflet-freedraw/#atom-tag" rel="alternate"/><published>2021-01-24T06:46:58+00:00</published><updated>2021-01-24T06:46:58+00:00</updated><id>https://simonwillison.net/2021/Jan/24/datasette-leaflet-freedraw/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-freedraw/releases/tag/0.1.4"&gt;datasette-leaflet-freedraw 0.1.4&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry><entry><title>datasette-leaflet-geojson 0.7</title><link href="https://simonwillison.net/2021/Jan/20/datasette-leaflet-geojson/#atom-tag" rel="alternate"/><published>2021-01-20T20:21:25+00:00</published><updated>2021-01-20T20:21:25+00:00</updated><id>https://simonwillison.net/2021/Jan/20/datasette-leaflet-geojson/#atom-tag</id><summary type="html">
    
        &lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; &lt;a href="https://github.com/simonw/datasette-leaflet-geojson/releases/tag/0.7"&gt;datasette-leaflet-geojson 0.7&lt;/a&gt;&lt;/p&gt;
        
    
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="datasette"/><category term="leaflet"/></entry></feed>