<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: vaccines</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/vaccines.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2021-04-26T01:02:22+00:00</updated><author><name>Simon Willison</name></author><entry><title>Weeknotes: Vaccinate The States, and how I learned that returning dozens of MB of JSON works just fine these days</title><link href="https://simonwillison.net/2021/Apr/26/vaccinate-the-states/#atom-tag" rel="alternate"/><published>2021-04-26T01:02:22+00:00</published><updated>2021-04-26T01:02:22+00:00</updated><id>https://simonwillison.net/2021/Apr/26/vaccinate-the-states/#atom-tag</id><summary type="html">
    &lt;p&gt;On Friday &lt;a href="https://www.vaccinateca.com/"&gt;VaccinateCA&lt;/a&gt; grew in scope, a lot: we launched a new website called &lt;a href="https://www.vaccinatethestates.com/"&gt;Vaccinate The States&lt;/a&gt;. Patrick McKenzie wrote &lt;a href="https://www.kalzumeus.com/2021/04/23/vaccinate-the-states/"&gt;more about the project here&lt;/a&gt; - the short version is that we're building the most comprehensive possible dataset of vaccine availability in the USA, using a combination of data collation, online research and continuing to make a huge number of phone calls.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of Vaccinate The States, showing a map with a LOT of markers on it" src="https://static.simonwillison.net/static/2021/vaccinate-the-states.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;VIAL, the Django application I've been working on &lt;a href="https://simonwillison.net/tags/vaccinateca/"&gt;since late February&lt;/a&gt;, had to go through some extensive upgrades to help support this effort!&lt;/p&gt;
&lt;p&gt;VIAL has a number of responsibilities. It acts as our central point of truth for the vaccination locations that we are tracking, powers the app used by our callers to serve up locations to call and record the results, and as-of this week it's also a central point for our efforts to combine data from multiple other providers and scrapers.&lt;/p&gt;
&lt;p&gt;The data ingestion work is happening in a public repository, &lt;a href="https://github.com/CAVaccineInventory/vaccine-feed-ingest"&gt;CAVaccineInventory/vaccine-feed-ingest&lt;/a&gt;. I have yet to write a single line of code there (and I thoroughly enjoy working on that kind of code) because I've been heads down working on VIAL itself to ensure it can support the ingestion efforts.&lt;/p&gt;
&lt;h4&gt;Matching and concordances&lt;/h4&gt;
&lt;p&gt;If you're combining data about vaccination locations from a range of different sources, one of the biggest challenges is de-duplicating the data: it's important the same location doesn't show up multiple times (potentially with slightly differing details) due to appearing in multiple sources.&lt;/p&gt;
&lt;p&gt;Our first step towards handling this involved the addition of "concordance identifiers" to VIAL.&lt;/p&gt;
&lt;p&gt;I first encountered the term "concordance" being used for this &lt;a href="https://whosonfirst.org/docs/concordances/"&gt;in the Who's On First project&lt;/a&gt;, which is building a gazetteer of every city/state/country/county/etc on earth.&lt;/p&gt;
&lt;p&gt;A concordance is an identifier in another system. Our location ID for RITE AID PHARMACY 05976 in Santa Clara is &lt;code&gt;receu5biMhfN8wH7P&lt;/code&gt; - which is &lt;code&gt;e3dfcda1-093f-479a-8bbb-14b80000184c&lt;/code&gt; in &lt;a href="https://vaccinefinder.org/"&gt;VaccineFinder&lt;/a&gt; and &lt;code&gt;7537904&lt;/code&gt; in &lt;a href="https://www.vaccinespotter.org/"&gt;Vaccine Spotter&lt;/a&gt; and &lt;code&gt;ChIJZaiURRPKj4ARz5nAXcWosUs&lt;/code&gt; in Google Places.&lt;/p&gt;
&lt;p&gt;We're storing them in a Django table called &lt;code&gt;ConcordanceIdentifier&lt;/code&gt;: each record has an &lt;code&gt;authority&lt;/code&gt; (e.g. &lt;code&gt;vaccinespotter_org&lt;/code&gt;) and an identifier (&lt;code&gt;7537904&lt;/code&gt;) and a many-to-many relationship to our &lt;code&gt;Location&lt;/code&gt; model.&lt;/p&gt;
&lt;p&gt;Why many-to-many? Surely we only want a single location for any one of these identifiers?&lt;/p&gt;
&lt;p&gt;Exactly! That's why it's many-to-many: because if we import the same location twice, then assign concordance identifiers to it, we can instantly spot that it's a duplicate and needs to be merged.&lt;/p&gt;
&lt;h4&gt;Raw data from scrapers&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;ConcordanceIdentifier&lt;/code&gt; also has a many-to-many relationship with a new table, called &lt;code&gt;SourceLocation&lt;/code&gt;. This table is essentially a PostgreSQL JSON column with a few other columns (including &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt;) into which our scrapers and ingesters can dump raw data. This means we can use PostgreSQL queries to perform all kinds of analysis on the unprocessed data before it gets cleaned up, de-duplicated and loaded into our point-of-truth &lt;code&gt;Location&lt;/code&gt; table.&lt;/p&gt;
&lt;h4&gt;How to dedupe and match locations?&lt;/h4&gt;
&lt;p&gt;Initially I thought we would do the deduping and matching inside of VIAL itself, using the raw data that had been ingested into the &lt;code&gt;SourceLocation&lt;/code&gt; table.&lt;/p&gt;
&lt;p&gt;Since we were on a tight internal deadline it proved more practical for people to start experimenting with matching code outside of VIAL. But that meant they needed the raw data - 40,000+ location records (and growing rapidly).&lt;/p&gt;
&lt;p&gt;A few weeks ago I built a CSV export feature for the VIAL admin screens, using Django's &lt;a href="https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.StreamingHttpResponse"&gt;StreamingHttpResponse&lt;/a&gt; class combined with keyset pagination for bulk export without sucking the entire table into web server memory - &lt;a href="https://til.simonwillison.net/django/export-csv-from-django-admin"&gt;details in this TIL&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Our data ingestion team wanted a GeoJSON export - specifically newline-delimited GeoJSON - which they could then load into &lt;a href="https://geopandas.org/"&gt;GeoPandas&lt;/a&gt; to help run matching operations.&lt;/p&gt;
&lt;p&gt;So I built a simple "search API" which defaults to returning 20 results at a time, but also has an option to "give me everything" - using the same technique I used for the CSV export: keyset pagination combined with a &lt;code&gt;StreamingHttpResponse&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And it worked! It turns out that if you're running on modern infrastructure (Cloud Run and Cloud SQL in our case) in 2021 getting Django to return 50+MB of JSON in a streaming response works just fine.&lt;/p&gt;
&lt;p&gt;Some of these exports are taking 20+ seconds, but for a small audience of trusted clients that's completely fine.&lt;/p&gt;
&lt;p&gt;While working on this I realized that my idea of what size of data is appropriate for a dynamic web application to return more or less formed back in 2005. I still think it's rude to serve multiple MBs of JavaScript up to an inexpensive mobile phone on an expensive connection, but for server-to-server or server-to-automation-script situations serving up 50+ MB of JSON in one go turns out to be a perfectly cromulent way of doing things.&lt;/p&gt;
&lt;h4&gt;Export full results from django-sql-dashboard&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/simonw/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt; is my Datasette-inspired library for adding read-only arbitrary SQL queries to any Django+PostgreSQL application.&lt;/p&gt;
&lt;p&gt;I built the first version &lt;a href="https://simonwillison.net/2021/Mar/14/weeknotes/"&gt;last month&lt;/a&gt; to help compensate for switching VaccinateCA away from Airtable - one of the many benefits of Airtable is that it allows all kinds of arbitrary reporting, and Datasette has shown me that bookmarkable SQL queries can provide a huge amount of that value with very little written code, especially within organizations where SQL is already widely understood.&lt;/p&gt;
&lt;p&gt;While it allows people to run any SQL they like (against a read-only PostgreSQL connection with a time limit) it restricts viewing to the first 1,000 records to be returned - because building robust, performant pagination against arbitrary SQL queries is a hard problem to solve.&lt;/p&gt;
&lt;p&gt;Today I released &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/0.10a0"&gt;django-sql-dashboard 0.10a0&lt;/a&gt; with the ability to export all results for a query as a downloadable CSV or TSV file, using the same &lt;code&gt;StreamingHttpResponse&lt;/code&gt; technique as my Django admin CSV export and all-results-at-once search endpoint.&lt;/p&gt;
&lt;p&gt;I expect it to be pretty useful! It means I can run any SQL query I like against a Django project and get back the full results - often dozens of MBs - in a form I can import into other tools (including Datasette).&lt;/p&gt;
&lt;p&gt;&lt;img alt="Screenshot of the SQL Dashboard interface, showing the new 'Export as CSV/TSV' buttons which trigger a file download dialog" src="https://static.simonwillison.net/static/2021/export-csv-dashboard.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/django/django-admin-horizontal-scroll.md"&gt;Usable horizontal scrollbars in the Django admin for mouse users&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/django/filter-by-comma-separated-values.md"&gt;Filter by comma-separated values in the Django admin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/postgresql/constructing-geojson-in-postgresql.md"&gt;Constructing GeoJSON in PostgreSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/django/export-csv-from-django-admin.md"&gt;Django Admin action for exporting selected rows as CSV&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/django-sql-dashboard/releases/tag/0.10a1"&gt;0.10a1&lt;/a&gt; - (&lt;a href="https://github.com/simonw/django-sql-dashboard/releases"&gt;21 total releases&lt;/a&gt;) - 2021-04-25
&lt;br /&gt;Django app for building dashboards using raw SQL queries&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/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccines"&gt;vaccines&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccinate-ca"&gt;vaccinate-ca&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-sql-dashboard"&gt;django-sql-dashboard&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="csv"/><category term="django"/><category term="django-admin"/><category term="postgresql"/><category term="projects"/><category term="vaccines"/><category term="weeknotes"/><category term="vaccinate-ca"/><category term="django-sql-dashboard"/></entry><entry><title>Animated choropleth of vaccinations by US county</title><link href="https://simonwillison.net/2021/Apr/4/animated-choropleth-of-vaccinations-by-us-county/#atom-tag" rel="alternate"/><published>2021-04-04T05:37:24+00:00</published><updated>2021-04-04T05:37:24+00:00</updated><id>https://simonwillison.net/2021/Apr/4/animated-choropleth-of-vaccinations-by-us-county/#atom-tag</id><summary type="html">
    &lt;p&gt;Last week &lt;a href="https://simonwillison.net/2021/Mar/28/weeknotes/#cdc-vaccination-datasette"&gt;I mentioned&lt;/a&gt; that I've recently started scraping and storing the CDC's per-county vaccination numbers in my &lt;a href="https://github.com/simonw/cdc-vaccination-history"&gt;cdc-vaccination-history&lt;/a&gt; GitHub repository. This week I used &lt;a href="https://observablehq.com/@simonw/us-county-vaccinations-choropleth-map"&gt;an Observable notebook&lt;/a&gt; and d3's TopoJSON support to render those numbers on an animated choropleth map.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Animated map of choropleth county vaccinations" src="https://static.simonwillison.net/static/2021/animated-choropleth.gif" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;The full code is available at &lt;a href="https://observablehq.com/@simonw/us-county-vaccinations-choropleth-map"&gt;https://observablehq.com/@simonw/us-county-vaccinations-choropleth-map&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;From scraper to Datasette&lt;/h4&gt;
&lt;p&gt;My scraper for this data is &lt;a href="https://github.com/simonw/cdc-vaccination-history/blob/f89e0c8166b25645f8e92ebc148f9cb9db119554/.github/workflows/scrape.yml#L20"&gt;a single line&lt;/a&gt; in a GitHub Actions workflow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl https://covid.cdc.gov/covid-data-tracker/COVIDData/getAjaxData?id=vaccination_county_condensed_data \
  | jq . &amp;gt; counties.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I pipe the data through &lt;code&gt;jq&lt;/code&gt; to pretty-print it, just to get nicer diffs.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/cdc-vaccination-history/blob/f89e0c8166b25645f8e92ebc148f9cb9db119554/build_database.py"&gt;build_database.py&lt;/a&gt; script then iterates over the accumulated git history of that &lt;code&gt;counties.json&lt;/code&gt; file and uses &lt;a href="https://sqlite-utils.datasette.io/"&gt;sqlite-utils&lt;/a&gt; to build a SQLite table:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;i&lt;/span&gt;, (&lt;span class="pl-s1"&gt;when&lt;/span&gt;, &lt;span class="pl-s1"&gt;hash&lt;/span&gt;, &lt;span class="pl-s1"&gt;content&lt;/span&gt;) &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-en"&gt;enumerate&lt;/span&gt;(
    &lt;span class="pl-en"&gt;iterate_file_versions&lt;/span&gt;(&lt;span class="pl-s"&gt;"."&lt;/span&gt;, (&lt;span class="pl-s"&gt;"counties.json"&lt;/span&gt;,))
):
    &lt;span class="pl-k"&gt;try&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;counties&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;json&lt;/span&gt;.&lt;span class="pl-en"&gt;loads&lt;/span&gt;(
            &lt;span class="pl-s1"&gt;content&lt;/span&gt;
        )[&lt;span class="pl-s"&gt;"vaccination_county_condensed_data"&lt;/span&gt;]
    &lt;span class="pl-k"&gt;except&lt;/span&gt; &lt;span class="pl-v"&gt;ValueError&lt;/span&gt;:
        &lt;span class="pl-c"&gt;# Bad JSON&lt;/span&gt;
        &lt;span class="pl-k"&gt;continue&lt;/span&gt;
    &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;county&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;counties&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;id&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;county&lt;/span&gt;[&lt;span class="pl-s"&gt;"FIPS"&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-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;county&lt;/span&gt;[&lt;span class="pl-s"&gt;"Date"&lt;/span&gt;]
        &lt;span class="pl-s1"&gt;db&lt;/span&gt;[
            &lt;span class="pl-s"&gt;"daily_reports_counties"&lt;/span&gt;
        ].&lt;span class="pl-en"&gt;insert&lt;/span&gt;(
            &lt;span class="pl-en"&gt;dict&lt;/span&gt;(&lt;span class="pl-s1"&gt;county&lt;/span&gt;, &lt;span class="pl-s1"&gt;id&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;id&lt;/span&gt;), &lt;span class="pl-s1"&gt;pk&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"id"&lt;/span&gt;,
            &lt;span class="pl-s1"&gt;alter&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-s1"&gt;replace&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;True&lt;/span&gt;
        )&lt;/pre&gt;
&lt;p&gt;The resulting table can be seen at &lt;a href="https://cdc-vaccination-history.datasette.io/cdc/daily_reports_counties"&gt;cdc/daily_reports_counties&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;From Datasette to Observable&lt;/h4&gt;
&lt;p&gt;Observable notebooks are my absolute favourite tool for prototyping new visualizations. There are examples of pretty much anything you could possibly want to create, and the Observable ecosystem actively encourages forking and sharing new patterns.&lt;/p&gt;
&lt;p&gt;Loading data from Datasette into Observable is easy, using Datasette's various HTTP APIs. For this visualization I needed to pull two separate things from Datasette.&lt;/p&gt;
&lt;p&gt;Firstly, for any given date I need the full per-county vaccination data. Here's the full table &lt;a href="https://cdc-vaccination-history.datasette.io/cdc/daily_reports_counties?Date=2021-04-02"&gt;filtered for April 2nd&lt;/a&gt; for example.&lt;/p&gt;
&lt;p&gt;Since that's 3,221 rows Datasette's JSON export would need to be paginated... but Datasette's CSV export can stream all 3,000+ rows in a single request. So I'm using that, fetched using the &lt;code&gt;d3.csv()&lt;/code&gt; function:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;county_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;d3&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;csv&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-s"&gt;`https://cdc-vaccination-history.datasette.io/cdc/daily_reports_counties.csv?_stream=on&amp;amp;Date=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;county_date&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;_size=max`&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;In order to animate the different dates, I need a list of available dates. I can get those with a SQL query:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;&lt;span class="pl-k"&gt;select distinct&lt;/span&gt; &lt;span class="pl-k"&gt;Date&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; daily_reports_counties
&lt;span class="pl-k"&gt;order by&lt;/span&gt; &lt;span class="pl-k"&gt;Date&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Datasette's JSON API has a &lt;code&gt;?_shape=arrayfirst&lt;/code&gt; option which will return a single JSON array of the first values in each row, which means I can do this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://cdc-vaccination-history.datasette.io/cdc.json?sql=select%20distinct%20Date%20from%20daily_reports_counties%20order%20by%20Date&amp;amp;_shape=arrayfirst"&gt;https://cdc-vaccination-history.datasette.io/cdc.json?sql=select%20distinct%20Date%20from%20daily_reports_counties%20order%20by%20Date&amp;amp;_shape=arrayfirst&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And get back just the dates as an array:&lt;/p&gt;
&lt;div class="highlight highlight-source-json"&gt;&lt;pre&gt;[
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-03-26&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-03-27&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-03-28&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-03-29&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-03-30&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-03-31&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-04-01&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-04-02&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;,
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;2021-04-03&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;
]&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Mike Bostock has a handy &lt;a href="https://observablehq.com/@mbostock/scrubber"&gt;Scrubber&lt;/a&gt; implementation which can provide a slider with the ability to play and stop iterating through values. In the notebook that can be used like so:&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;county_date&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-v"&gt;Scrubber&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;county_dates&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;delay&lt;/span&gt;: &lt;span class="pl-c1"&gt;500&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-c1"&gt;autoplay&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-s1"&gt;county_dates&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-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://cdc-vaccination-history.datasette.io/cdc.json?sql=select%20distinct%20Date%20from%20daily_reports_counties%20order%20by%20Date&amp;amp;_shape=arrayfirst"&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;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-k"&gt;import&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt; &lt;span class="pl-v"&gt;Scrubber&lt;/span&gt; &lt;span class="pl-kos"&gt;}&lt;/span&gt; &lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s"&gt;"@mbostock/scrubber"&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;Drawing the map&lt;/h4&gt;
&lt;p&gt;The map itself is rendered using &lt;a href="https://github.com/topojson/topojson"&gt;TopoJSON&lt;/a&gt;, an extension to GeoJSON that efficiently encodes topology.&lt;/p&gt;
&lt;p&gt;Consider the map of 3,200 counties in the USA: since counties border each other, most of those border polygons end up duplicating each other to a certain extent.&lt;/p&gt;
&lt;p&gt;TopoJSON only stores each shared boundary once, but still knows how they relate to each other which means the data can be used to draw shapes filled with colours.&lt;/p&gt;
&lt;p&gt;I'm using the &lt;code&gt;https://d3js.org/us-10m.v1.json&lt;/code&gt; TopoJSON file built and published with d3. Here's my JavaScript for rendering that into an SVG map:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&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;svg&lt;/span&gt; &lt;span class="pl-c1"&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;create&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"svg"&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;attr&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"viewBox"&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&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-s1"&gt;width&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-c1"&gt;700&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;style&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"width"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"100%"&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;style&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"height"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"auto"&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;svg&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;"g"&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;selectAll&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"path"&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;data&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
      &lt;span class="pl-s1"&gt;topojson&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;feature&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;topojson_data&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;topojson_data&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;objects&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;counties&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;features&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;enter&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;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"path"&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;attr&lt;/span&gt;&lt;span class="pl-kos"&gt;(&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-k"&gt;function&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-kos"&gt;{&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-c1"&gt;!&lt;/span&gt;&lt;span class="pl-s1"&gt;county_data&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;id&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-s"&gt;'white'&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;v&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;county_data&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;id&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;Series_Complete_65PlusPop_Pct&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-s1"&gt;d3&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;interpolate&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"white"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s"&gt;"green"&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;v&lt;/span&gt; &lt;span class="pl-c1"&gt;/&lt;/span&gt; &lt;span class="pl-c1"&gt;100&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;attr&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"d"&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt; &lt;span class="pl-s1"&gt;path&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;append&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"title"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c"&gt;// Tooltip&lt;/span&gt;
    &lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;text&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-s1"&gt;d&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;if&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;county_data&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;id&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-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-k"&gt;return&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&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;        &lt;span class="pl-s1"&gt;county_data&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;id&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;Series_Complete_65PlusPop_Pct&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&gt;% of the 65+ population in &lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;county_data&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;id&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;County&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;county_data&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;id&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;StateAbbr&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;trim&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; have had the complete vaccination`&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-s1"&gt;svg&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;node&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&gt;Next step: a plugin&lt;/h4&gt;
&lt;p&gt;Now that I have a working map, my next goal is to package this up as a Datasette plugin. I'm hoping to create a generic choropleth plugin which bundles TopoJSON for some common maps - probably world countries, US states and US counties to start off with - but also allows custom maps to be supported as easily as possible.&lt;/p&gt;
&lt;h4&gt;Datasette 0.56&lt;/h4&gt;
&lt;p&gt;Also this week, I shipped &lt;a href="https://docs.datasette.io/en/stable/changelog.html#v0-56"&gt;Datasette 0.56&lt;/a&gt;. It's a relatively small release - mostly documentation improvements and bug fixes, but I've alse bundled SpatiaLite 5 with the official Datasette Docker image.&lt;/p&gt;
&lt;h4&gt;TIL this week&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/simonw/til/blob/main/markdown/markdown-extensions-python.md"&gt;Useful Markdown extensions in Python&lt;/a&gt;&lt;/li&gt;
&lt;/ul&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/airtable-export"&gt;airtable-export&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://github.com/simonw/airtable-export/releases/tag/0.6"&gt;0.6&lt;/a&gt; - (&lt;a href="https://github.com/simonw/airtable-export/releases"&gt;8 total releases&lt;/a&gt;) - 2021-04-02
&lt;br /&gt;Export Airtable data to YAML, JSON or SQLite files on disk&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.56"&gt;0.56&lt;/a&gt; - (&lt;a href="https://github.com/simonw/datasette/releases"&gt;85 total releases&lt;/a&gt;) - 2021-03-29
&lt;br /&gt;An open source multi-tool for exploring and publishing data&lt;/li&gt;
&lt;/ul&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/choropleths"&gt;choropleths&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccines"&gt;vaccines&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/d3"&gt;d3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="choropleths"/><category term="vaccines"/><category term="d3"/><category term="datasette"/><category term="observable"/><category term="weeknotes"/></entry><entry><title>The Worst Ideas of the Decade: Vaccine scares</title><link href="https://simonwillison.net/2009/Dec/22/worst/#atom-tag" rel="alternate"/><published>2009-12-22T21:17:04+00:00</published><updated>2009-12-22T21:17:04+00:00</updated><id>https://simonwillison.net/2009/Dec/22/worst/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.washingtonpost.com/wp-srv/special/opinions/outlook/worst-ideas/vaccines-and-autism.html"&gt;The Worst Ideas of the Decade: Vaccine scares&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“The movement blaming vaccines for causing autism emerged in the early 2000s, and it was one of the most catastrophically horrible ideas of the decade.”


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/science"&gt;science&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vaccines"&gt;vaccines&lt;/a&gt;&lt;/p&gt;



</summary><category term="science"/><category term="vaccines"/></entry></feed>