<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: visualization</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/visualization.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2025-09-09T18:11:32+00:00</updated><author><name>Simon Willison</name></author><entry><title>My review of Claude's new Code Interpreter, released under a very confusing name</title><link href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#atom-tag" rel="alternate"/><published>2025-09-09T18:11:32+00:00</published><updated>2025-09-09T18:11:32+00:00</updated><id>https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#atom-tag</id><summary type="html">
    &lt;p&gt;Today on the Anthropic blog: &lt;strong&gt;&lt;a href="https://www.anthropic.com/news/create-files"&gt;Claude can now create and edit files&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Claude can now create and edit Excel spreadsheets, documents, PowerPoint slide decks, and PDFs directly in &lt;a href="https://claude.ai/"&gt;Claude.ai&lt;/a&gt; and the desktop app. [...]&lt;/p&gt;
&lt;p&gt;File creation is now available as a preview for Max, Team, and Enterprise plan users. Pro users will get access in the coming weeks.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then right at the &lt;em&gt;very end&lt;/em&gt; of their post:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This feature gives Claude internet access to create and analyze files, which may put your data at risk. Monitor chats closely when using this feature. &lt;a href="https://support.anthropic.com/en/articles/12111783-create-and-edit-files-with-claude"&gt;Learn more&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And tucked away half way down their &lt;a href="https://support.anthropic.com/en/articles/12111783-create-and-edit-files-with-claude"&gt;Create and edit files with Claude&lt;/a&gt; help article:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;With this feature, Claude can also do more advanced data analysis and data science work. Claude can create Python scripts for data analysis. Claude can create data visualizations in image files like PNG. You can also upload CSV, TSV, and other files for data analysis and visualization.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Talk about &lt;a href="https://www.merriam-webster.com/wordplay/bury-the-lede-versus-lead"&gt;burying the lede&lt;/a&gt;... this is their version of &lt;a href="https://simonwillison.net/tags/code-interpreter/"&gt;ChatGPT Code Interpreter&lt;/a&gt;, my all-time favorite feature of ChatGPT!&lt;/p&gt;

&lt;p&gt;Claude can now write and execute custom Python (and Node.js) code in a server-side sandbox and use it to process and analyze data.&lt;/p&gt;
&lt;p&gt;In a particularly egregious example of AI companies being terrible at naming features, the official name for this one really does appear to be &lt;strong&gt;Upgraded file creation and analysis&lt;/strong&gt;. Sigh.&lt;/p&gt;
&lt;p&gt;This is quite a confusing release, because Claude &lt;em&gt;already&lt;/em&gt; had a variant of this feature, &lt;a href="https://www.anthropic.com/news/analysis-tool"&gt;released in October 2024&lt;/a&gt; with the weak but more sensible name &lt;strong&gt;Analysis tool&lt;/strong&gt;. Here are &lt;a href="https://simonwillison.net/2024/Oct/24/claude-analysis-tool/"&gt;my notes from when that came out&lt;/a&gt;. That tool worked by generating and executing JavaScript in the user's own browser.&lt;/p&gt;
&lt;p&gt;The new tool works entirely differently. It's much closer in implementation to OpenAI's Code Interpreter: Claude now has access to a server-side container environment in which it can run shell commands and execute Python and Node.js code to manipulate data and both read and generate files.&lt;/p&gt;
&lt;p&gt;It's worth noting that Anthropic have a similar feature in their API called &lt;a href="https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/code-execution-tool"&gt;Code execution tool&lt;/a&gt;, but today is the first time end-users of Claude have been able to execute arbitrary code in a server-side container.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#switching-it-on-in-settings-features"&gt;Switching it on in settings/features&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#exploring-the-environment"&gt;Exploring the environment&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#starting-with-something-easy"&gt;Starting with something easy&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#something-much-harder-recreating-the-ai-adoption-chart"&gt;Something much harder: recreating the AI adoption chart&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#prompt-injection-risks"&gt;Prompt injection risks&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#my-verdict-on-claude-code-interpreter-so-far"&gt;My verdict on Claude Code Interpreter so far&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#ai-labs-find-explaining-this-feature-incredibly-difficult"&gt;AI labs find explaining this feature incredibly difficult&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="switching-it-on-in-settings-features"&gt;Switching it on in settings/features&lt;/h4&gt;
&lt;p&gt;I have a Pro Plan but found the setting to enable it on the &lt;a href="https://claude.ai/settings/features"&gt;claude.ai/settings/features&lt;/a&gt;. It's possible my account was granted early access without me realizing, since the Pro plan isn't supposed to have it yet:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-analysis-toggle.jpg" alt="Experimental. Preview and provide feedback on upcoming enhancements to our platform. Please note: experimental features might influence Claude’s behavior and some interactions may differ from the standard experience. Analysis tool:  Claude can write and run code to process data, run analysis, and produce data visualizations in real time. Upgraded file creation and analysis: Allow Claude to create and edit docs, spreadsheets, presentations, PDFs, and data reports on web and desktop. Does not support versioning or remixing of Artifacts. This feature gives Claude network access to create and analyze files, which has security risks. Monitor chats closely when using this feature. The two features each have a toggle - the toggle for the file creation tool is turned on." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Interestingly, toggling on the "Upgraded file creation and analysis" option toggles off the "Analysis tool" option, and vice versa. You can't have both at the same time.&lt;/p&gt;
&lt;p&gt;I guess this is to help reduce confusion? It surprised me though as Claude can sometimes use the existing analysis tool to help it in the creation of artifacts, as I &lt;a href="https://simonwillison.net/2025/Jun/25/ai-powered-apps-with-claude/"&gt;noted here&lt;/a&gt; when I wrote about the Claudeception feature they added in June.&lt;/p&gt;
&lt;h4 id="exploring-the-environment"&gt;Exploring the environment&lt;/h4&gt;
&lt;p&gt;I started by using the tool to explore the tool. Here's my &lt;a href="https://claude.ai/share/d796a596-380c-4db2-847d-2eeedc620bc0"&gt;first transcript&lt;/a&gt; where I simply prompted the following to see what it could figure out about itself:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Tell me everything you can about your shell and Python environments&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's part of the output:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-files-1.jpg" alt="My prompt, then Claude shows: I'll explore my shell and Python environments to give you a comprehensive overview. • Getting system information and OS details • Checking shell type and version • Getting shell info and bash version • Checking Python version and locations - this one is expandedd to show: bash python3 -version &amp;amp;&amp;amp; which python3 &amp;amp;&amp;amp; which python Output Python 3.12.3 /usr/bin/python3 /usr/bin/python" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;And the key details it returned, each of which I've verified as true by checking the displayed output of the relevant commands:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OS&lt;/strong&gt;: Ubuntu 24.04.2 LTS (Noble) running on Linux kernel 4.4.0-&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture&lt;/strong&gt;: x86_64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell&lt;/strong&gt;: GNU Bash 5.2.21(1)-release&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Current User&lt;/strong&gt;: root (with access to /home/claude directory)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Working Directory&lt;/strong&gt;: Currently in root (/), but /home/claude is the main workspace&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python Version&lt;/strong&gt;: Python 3.12.3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python Executable&lt;/strong&gt;: /usr/bin/python3&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Package Manager&lt;/strong&gt;: pip 24.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python Path&lt;/strong&gt;: Standard system locations including:
&lt;ul&gt;
&lt;li&gt;/usr/lib/python3.12 (standard library)&lt;/li&gt;
&lt;li&gt;/usr/local/lib/python3.12/dist-packages (local packages)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk Space&lt;/strong&gt;: 4.9GB total, with 4.6GB available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: 9.0GB RAM available, minimal usage currently&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: v18.19.1 available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: Configured with global packages in &lt;code&gt;/home/claude/.npm-global&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;So we have an Ubuntu 24.04.2 container with 9GB of RAM, ~5GB of disk, Python 3.12.3, and Node.js v18.19.1.&lt;/p&gt;
&lt;p&gt;How about network access? I tried the following:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Can you access the Internet? Can you run curl? How about using Python http to fetch https://latest.datasette.io/fixtures.json ?&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Again, you can see the &lt;a href="https://claude.ai/share/d796a596-380c-4db2-847d-2eeedc620bc0"&gt;detailed output in the transcript&lt;/a&gt;. Claude tried &lt;code&gt;https://latest.datasette.io/fixtures.json&lt;/code&gt; and then &lt;code&gt;https://httpbin.org/json&lt;/code&gt; and got a 403 forbidden error for both, then &lt;code&gt;https://google.com&lt;/code&gt; and got this curious result:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;curl -s -I https://google.com&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
date: Tue, 09 Sep 2025 16:02:17 GMT
server: envoy

HTTP/2 403 
content-length: 13
content-type: text/plain
date: Tue, 09 Sep 2025 16:02:17 GMT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Claude did note that it can still use the &lt;code&gt;web_fetch&lt;/code&gt; and &lt;code&gt;web_search&lt;/code&gt; containers independently of that container environment, so it should be able to fetch web content using tools running outside of the container and then write it to a file there.&lt;/p&gt;
&lt;p&gt;On a hunch I tried this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Run pip install sqlite-utils&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;... and it worked! Claude can &lt;code&gt;pip install&lt;/code&gt; additional packages from &lt;a href="https://pypi.org/"&gt;PyPI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A little more poking around revealed the following relevant environment variables:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTPS_PROXY=http://21.0.0.167:15001
no_proxy=localhost,127.0.0.1,169.254.169.254,metadata.google.internal,*.svc.cluster.local,*.local,*.googleapis.com,*.google.com
NO_PROXY=localhost,127.0.0.1,169.254.169.254,metadata.google.internal,*.svc.cluster.local,*.local,*.googleapis.com,*.google.com
https_proxy=http://21.0.0.167:15001
http_proxy=http://21.0.0.167:15001
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So based on an earlier HTTP header there's an &lt;a href="https://www.envoyproxy.io/"&gt;Envoy proxy&lt;/a&gt; running at an accessible port which apparently implements a strict allowlist.&lt;/p&gt;
&lt;p&gt;I later noticed that &lt;a href="https://support.anthropic.com/en/articles/12111783-create-and-edit-files-with-claude#h_0ee9d698a1"&gt;the help page&lt;/a&gt; includes a full description of what's on that allowlist:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Anthropic Services (Explicit)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;api.anthropic.com, statsig.anthropic.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Version Control&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;github.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Package Managers - JavaScript/Node&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NPM:&lt;/strong&gt; registry.npmjs.org, npmjs.com, npmjs.org&lt;br /&gt;
&lt;strong&gt;Yarn:&lt;/strong&gt; yarnpkg.com, registry.yarnpkg.com&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Package Managers - Python&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;pypi.org, files.pythonhosted.org, pythonhosted.org&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So it looks like we have a &lt;em&gt;very&lt;/em&gt; similar system to ChatGPT Code Interpreter. The key differences are that Claude's system can install additional Python packages and has Node.js pre-installed.&lt;/p&gt;
&lt;p&gt;One important limitation from the docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The maximum file size is 30MB per file for both uploads and downloads.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The ChatGPT &lt;a href="https://help.openai.com/en/articles/8555545-file-uploads-faq"&gt;limit here&lt;/a&gt; is 512MB. I've often uploaded 100MB+ SQLite database files to ChatGPT, so I'm a little disappointed by this lower limit for Claude.&lt;/p&gt;
&lt;h4 id="starting-with-something-easy"&gt;Starting with something easy&lt;/h4&gt;
&lt;p&gt;I grabbed a copy of the SQLite database behind &lt;a href="https://til.simonwillison.net/"&gt;my TILs website&lt;/a&gt; (21.9MB &lt;a href="https://s3.amazonaws.com/til.simonwillison.net/tils.db"&gt;from here&lt;/a&gt;) and uploaded it to Claude, then prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Use your Python environment to explore this SQLite database and generate a PDF file containing a join diagram of all the tables&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's &lt;a href="https://claude.ai/share/f91a95be-0fb0-4e14-b46c-792b47117a3d"&gt;that conversation&lt;/a&gt;. It did an OK job, producing both &lt;a href="https://static.simonwillison.net/static/2025/til_database_join_diagram.pdf"&gt;the PDF&lt;/a&gt; I asked for and a PNG equivalent which looks like this (since created files are not available in shared chats):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/til_database_join_diagram.jpg" alt="Each table gets a box with a name and columns. A set of lines is overlaid which doesn't quite seem to represent the joins in a useful fashion." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This isn't an ideal result - those join lines are difficult to follow - but I'm confident I could get from here to something I liked with only a little more prompting. The important thing is that the system clearly works, and can analyze data in uploaded SQLite files and use them to produce images and PDFs.&lt;/p&gt;
&lt;h4 id="something-much-harder-recreating-the-ai-adoption-chart"&gt;Something much harder: recreating the AI adoption chart&lt;/h4&gt;
&lt;p&gt;Thankfully I have a fresh example of a really challenging ChatGPT Code Interpreter task from just last night, which I described in great detail in &lt;a href="https://simonwillison.net/2025/Sep/9/apollo-ai-adoption/"&gt;Recreating the Apollo AI adoption rate chart with GPT-5, Python and Pyodide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Short version: I took &lt;a href="https://www.apolloacademy.com/ai-adoption-rate-trending-down-for-large-companies/"&gt;this chart&lt;/a&gt; from Apollo Global and asked ChatGPT to recreate it based on a screenshot and an uploaded XLSX file.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/apollo-ai-chart.jpg" alt="AI adoption rates starting to decline for larger firms. A chart of AI adoption rate by firm size. Includes lines for 250+, 100-249, 50-99, 20-49, 10-19, 5-8 and 1-4 sized organizations. Chart starts in November 2023 with percentages ranging from 3 to 5, then all groups grow through August 2025 albeit with the 250+ group having a higher score than the others. That 25+ group peaks in Jul5 2025 at around 14% and then appears to slope slightly downwards to 12% by August. Some of the other lines also start to tip down, though not as much." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;This time I skipped the bit where I had ChatGPT hunt down the original data and jumped straight to the "recreate this chart" step. I used the exact same prompt as I provided to ChatGPT:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Use this data to recreate this chart using python&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And uploaded the same two files - &lt;a href="https://static.simonwillison.net/static/cors-allow/2025/Employment-Size-Class-Sep-2025.xlsx"&gt;this XLSX file&lt;/a&gt; and the &lt;a href="https://static.simonwillison.net/static/2025/apollo-ai-chart.jpg"&gt;screenshot of the original chart&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-files-2.jpg" alt="Claude UI - prompt is &amp;quot;Use this data to recreate this chart using python&amp;quot; - I've selected Sonnet 4 and given it both the XLSX and the screenshot as attachments." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Claude wrote and ran a bunch of Python code and produced this:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-attempt-1.jpg" alt="The chart has jagged lines, not smooth ones, but they do appear to show the correct numbers. The title and axis are positioned in ways that overlap other text a bit." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;That's not a bad first attempt - we'll sort out the jagged lines in a moment. With ChatGPT I later told it to plot a six-survey rolling average (since that's what Apollo had done with theirs), so I ran the exact same prompt against Claude:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Plot it as a six survey rolling average&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-attempt-2.jpg" alt="The lines are more even now, and appear to match the numbers reported by Apollo. They still have a jagged look to them though." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So I told Claude:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Make those lines less jagged&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This did not do what I wanted:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-attempt-3.jpg" alt="The chart looks mostly the same. The lines are a tiny bit thicker." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So I gave it a bit of a hint:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;No not like that, I wanted the lines to be a smooth curve like in the original screenshot - you shouldn't have to use scipy for this or manipulate the data, I'm sure matplotlib can do this already&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A little bit of sycophancy:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You're absolutely right! Matplotlib has built-in curve smoothing. I was overcomplicating it. Let me create a version that uses matplotlib's natural curve smoothing capabilities. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This time it used the &lt;code&gt;antialiased=True&lt;/code&gt; and &lt;code&gt;solid_capstyle='round'&lt;/code&gt; options in matplotlib, which again wasn't what I was after. I tried one more thing:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;I did not mean smooth the rendering of the lines. I meant that I wanted curved and not straight lines between points.&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(Normally if I have to argue this much with a model I'll start a fresh session and try a different prompting strategy from the start.)&lt;/p&gt;
&lt;p&gt;But this did give me what I was looking for:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-attempt-4.jpg" alt="This time the chart finally has smooth curves between points." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Oddly enough my ChatGPT chart suffered from that same overlapping title glitch, so I gave Claude the same prompt I had given ChatGPT:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;fix the chart title, it looks like this:&lt;/code&gt; (with a screenshot)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/claude-attempt-5.jpg" alt="Now the chart shows a clear title at the top saying AI adoption rates starting to decline for larger firms" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;We got there in the end! I've shared &lt;a href="https://claude.ai/share/cc32d405-cb53-4e52-a1a0-9b4df4e528ac"&gt;the full transcript of the chat&lt;/a&gt;, although frustratingly the images and some of the code may not be visible. I &lt;a href="https://gist.github.com/simonw/806e1aa0e6c29ad64834037f779e0dc0"&gt;created this Gist&lt;/a&gt; with copies of the files that it let me download.&lt;/p&gt;
&lt;h4 id="prompt-injection-risks"&gt;Prompt injection risks&lt;/h4&gt;
&lt;p&gt;ChatGPT Code Interpreter has no access to the internet at all, which limits how much damage an attacker can do if they manage to sneak their own malicious instructions into the model's context.&lt;/p&gt;
&lt;p&gt;Since Claude Code Interpreter (I'm &lt;em&gt;not&lt;/em&gt; going to be calling it "Upgraded file creation and analysis"!) has a limited form of internet access, we need to worry about &lt;a href="https://simonwillison.net/2025/Jun/16/the-lethal-trifecta/"&gt;lethal trifecta&lt;/a&gt; and other prompt injection attacks.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://support.anthropic.com/en/articles/12111783-create-and-edit-files-with-claude#h_0ee9d698a1"&gt;help article&lt;/a&gt; actually covers this in some detail:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;It is possible for a bad actor to inconspicuously add instructions via external files or websites that trick Claude into:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Downloading and running untrusted code in the sandbox environment for malicious purposes.&lt;/li&gt;
&lt;li&gt;Reading sensitive data from a &lt;a href="http://claude.ai"&gt;claude.ai&lt;/a&gt; connected knowledge source (e.g., Remote MCP, projects) and using the sandbox environment to make an external network request to leak the data.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This means Claude can be tricked into sending information from its context (e.g., prompts, projects, data via MCP, Google integrations) to malicious third parties. To mitigate these risks, we recommend you monitor Claude while using the feature and stop it if you see it using or accessing data unexpectedly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;"We recommend you monitor Claude while using the feature" smells me to me like unfairly outsourcing the problem to Anthropic's users, but I'm not sure what more they can do!&lt;/p&gt;
&lt;p&gt;It's interesting that they still describe the external communication risk even though they've locked down a lot of network access. My best guess is that they know that allowlisting &lt;code&gt;github.com&lt;/code&gt; opens an &lt;em&gt;enormous&lt;/em&gt; array of potential exfiltration vectors.&lt;/p&gt;
&lt;p&gt;Anthropic also note:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We have performed red-teaming and security testing on the feature. We have a continuous process for ongoing security testing and red-teaming of this feature.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I plan to be cautious using this feature with any data that I very much don't want to be leaked to a third party, if there's even the slightest chance that a malicious instructions might sneak its way in.&lt;/p&gt;
&lt;h4 id="my-verdict-on-claude-code-interpreter-so-far"&gt;My verdict on Claude Code Interpreter so far&lt;/h4&gt;
&lt;p&gt;I'm generally very excited about this. Code Interpreter has been my most-valued LLM feature since it launched in early 2023, and the Claude version includes some upgrades on the original - package installation, Node.js support - that I expect will be very useful.&lt;/p&gt;
&lt;p&gt;I don't particularly mark it down for taking a little more prompting to recreate the Apollo chart than ChatGPT did. For one thing I was using Claude Sonnet 4 - I expect Claude Opus 4.1 would have done better. I also have a much stronger intuition for Code Interpreter prompts that work with GPT-5.&lt;/p&gt;
&lt;p&gt;I don't think my chart recreation exercise here should be taken as showing any meaningful differences between the two.&lt;/p&gt;
&lt;h4 id="ai-labs-find-explaining-this-feature-incredibly-difficult"&gt;AI labs find explaining this feature incredibly difficult&lt;/h4&gt;
&lt;p&gt;I find it &lt;em&gt;fascinating&lt;/em&gt; how difficult the AI labs find describing this feature to people! OpenAI went from "Code Interpreter" to "Advanced Data Analysis" and maybe back again? It's hard to even find their official landing page for that feature now. (I &lt;a href="https://chatgpt.com/share/68c070ff-fe9c-8006-91b5-cff799253836"&gt;got GPT-5 to look for it&lt;/a&gt; and it hunted for 37 seconds and settled on the help page for &lt;a href="https://help.openai.com/en/articles/8437071-data-analysis-with-chatgpt"&gt;Data analysis with ChatGPT&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Anthropic already used the bad name "Analysis tool" for a different implementation, and now have the somehow-worse name "Upgraded file creation and analysis". Their launch announcement avoids even talking about code execution, focusing exclusively on the tool's ability to generate spreadsheets and PDFs!&lt;/p&gt;
&lt;p&gt;I wonder if any of the AI labs will crack the code on how to name and explain this thing? I feel like it's still a very under-appreciated feature of LLMs, despite having been around for more than two years now.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/nodejs"&gt;nodejs&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&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/prompt-injection"&gt;prompt-injection&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/llm-tool-use"&gt;llm-tool-use&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="nodejs"/><category term="python"/><category term="visualization"/><category term="ai"/><category term="openai"/><category term="prompt-injection"/><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="llm-tool-use"/><category term="vibe-coding"/></entry><entry><title>Recreating the Apollo AI adoption rate chart with GPT-5, Python and Pyodide</title><link href="https://simonwillison.net/2025/Sep/9/apollo-ai-adoption/#atom-tag" rel="alternate"/><published>2025-09-09T06:47:49+00:00</published><updated>2025-09-09T06:47:49+00:00</updated><id>https://simonwillison.net/2025/Sep/9/apollo-ai-adoption/#atom-tag</id><summary type="html">
    &lt;p&gt;Apollo Global Management's "Chief Economist" Dr. Torsten Sløk released &lt;a href="https://www.apolloacademy.com/ai-adoption-rate-trending-down-for-large-companies/"&gt;this interesting chart&lt;/a&gt; which appears to show a slowdown in AI adoption rates among large (&amp;gt;250 employees) companies:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/apollo-ai-chart.jpg" alt="AI adoption rates starting to decline for larger firms. A chart of AI adoption rate by firm size. Includes lines for 250+, 100-249, 50-99, 20-49, 10-19, 5-8 and 1-4 sized organizations. Chart starts in November 2023 with percentages ranging from 3 to 5, then all groups grow through August 2025 albeit with the 250+ group having a higher score than the others. That 25+ group peaks in Jul5 2025 at around 14% and then appears to slope slightly downwards to 12% by August. Some of the other lines also start to tip down, though not as much." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Here's the full description that accompanied the chart:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The US Census Bureau conducts a biweekly survey of 1.2 million firms, and one question is whether a business has used AI tools such as machine learning, natural language processing, virtual agents or voice recognition to help produce goods or services in the past two weeks. Recent data by firm size shows that AI adoption has been declining among companies with more than 250 employees, see chart below.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(My first thought on seeing that chart is that I hope it represents the &lt;em&gt;peak of inflated expectations&lt;/em&gt; leading into the &lt;em&gt;trough of dissillusionment&lt;/em&gt; in the &lt;a href="https://en.wikipedia.org/wiki/Gartner_hype_cycle"&gt;Gartner Hype Cycle&lt;/a&gt; (which Wikipedia calls "largely disputed, with studies pointing to it being inconsistently true at best"), since that means we might be reaching the end of the initial hype phase and heading towards the &lt;em&gt;slope of enlightenment&lt;/em&gt;.)&lt;/p&gt;
&lt;h4 id="finding-the-us-census-data-with-gpt-5-search"&gt;Finding the US Census data with GPT-5 search&lt;/h4&gt;
&lt;p&gt;This is the first I'd heard of the US Census Bureau running a biweekly (that's once every two weeks) survey about AI!&lt;/p&gt;
&lt;p&gt;I decided to track down the numbers and see if I could recreate the chart myself. And since &lt;a href="https://simonwillison.net/2025/Sep/6/research-goblin/"&gt;GPT-5 is really good at search now&lt;/a&gt; I fed it the following prompt to see how well it could do:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;&amp;gt; The US Census Bureau conducts a biweekly survey of 1.2 million firms, and one question is whether a business has used AI tools such as machine learning, natural language processing, virtual agents or voice recognition to help produce goods or services in the past two weeks. Recent data by firm size shows that AI adoption has been declining among companies with more than 250 employees, see chart below.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Find me that original data&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It took 1m19s to &lt;a href="https://chatgpt.com/share/68bf48cf-0e70-8006-a045-96fa8e7ddfc1"&gt;find exactly what I needed&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Here’s the original U.S. Census Bureau source for that series:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BTOS “Employment size class” time series (XLSX)&lt;/strong&gt; — biweekly shares of firms answering &lt;strong&gt;Yes&lt;/strong&gt; to “In the last two weeks, did this business use Artificial Intelligence (AI) in producing goods or services?”, broken out by firm-size buckets (incl. &lt;strong&gt;250+ employees&lt;/strong&gt;). Coverage: &lt;strong&gt;Sep 11, 2023 → Aug 24, 2025&lt;/strong&gt;. (&lt;a href="https://www.census.gov/hfp/btos/data_downloads"&gt;Census.gov&lt;/a&gt;) [...]&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;That &lt;a href="https://www.census.gov/hfp/btos/data_downloads"&gt;Census page&lt;/a&gt; was not &lt;em&gt;at all&lt;/em&gt; obvious. Thankfully GPT-5 had tipped me off to the "Employment size class" file, this link here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/census-page.jpg" alt="US Census website. Business Trends and Outlook Survey, Updated August 28, 2025. Current Data has 6 visible XLSX files with names like WFH Supplement, WFH Questions 27-29, National, Sectur, Subsector and Emplomyent size class. A red arrow highlights that last one." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;So I downloaded that file, and confirmed that it was indeed a spreadsheet containing the data I wanted (in among all sorts of other survey questions). Here's &lt;a href="https://static.simonwillison.net/static/cors-allow/2025/Employment-Size-Class-Sep-2025.xlsx"&gt;a 374KB XLSX copy&lt;/a&gt; of the file I downloaded.&lt;/p&gt;
&lt;h4 id="recreating-the-chart-with-gpt-5-code-interpreter"&gt;Recreating the chart with GPT-5 code interpreter&lt;/h4&gt;
&lt;p&gt;So what should I do with it now? I decided to see if GPT-5 could turn the spreadsheet back into that original chart, using Python running in its &lt;a href="https://simonwillison.net/tags/code-interpreter/"&gt;code interpreter&lt;/a&gt; tool.&lt;/p&gt;
&lt;p&gt;So I uploaded the XLSX file back to ChatGPT, dropped in a screenshot of the Apollo chart and prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Use this data to recreate this chart using python&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/chart-prompt.jpg" alt="ChatGPT. I dropped in a screenshot of the chart, uploaded the spreadsheet which turned into an inline table browser UI and prompted it to recreate the chart using python." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I thought this was a pretty tall order, but it's always worth throwing big challenges at an LLM to learn from how well it does.&lt;/p&gt;
&lt;p&gt;It &lt;em&gt;really worked hard on this&lt;/em&gt;. I didn't time it exactly but it spent at least 7 minutes "reasoning" across 5 different thinking blocks, interspersed with over a dozen Python analysis sessions. It used &lt;code&gt;pandas&lt;/code&gt; and &lt;code&gt;numpy&lt;/code&gt; to explore the uploaded spreadsheet and find the right figures, then tried several attempts at plotting with &lt;code&gt;matplotlib&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;As far as I can tell GPT-5 in ChatGPT can now feed charts it creates back into its own vision model, because it appeared to render a broken (empty) chart and then keep on trying to get it working.&lt;/p&gt;
&lt;p&gt;It found a data dictionary in the last tab of the spreadsheet and used that to build a lookup table matching the letters &lt;code&gt;A&lt;/code&gt; through &lt;code&gt;G&lt;/code&gt; to the actual employee size buckets.&lt;/p&gt;
&lt;p&gt;At the end of the process it spat out this chart:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/recreated-chart-1.jpg" alt="matplotlib chart. The title is AI adoption rates starting to decline for larger firms, though there's a typography glitch in that title. It has a neat legend for the different size ranges, then a set of lines that look about right compared to the above graph - but they are more spiky and the numbers appear to trend up again at the end of the chart." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;At first glance I thought it had nailed it... but then I compared the chart more closely with the Apollo original and spotted some definite discrepancies. GPT-5's chart peaked at 14.5% but the highest value in Apollo's was more like 13.5%. The GPT-5 chart was spikier - and most interestingly it included a clear uptick in the last data point where Apollo's had trended downwards.&lt;/p&gt;
&lt;p&gt;I decided it was time to look at the actual data. I opened up the spreadsheet in Numbers, found the AI question columns and manually reviewed them. They seemed to match the GPT-5 chart results - so why the difference to Apollo's?&lt;/p&gt;
&lt;p&gt;Then I noticed a crucial detail in the Apollo chart that I had cropped out of my original screenshot!&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Data is six-survey moving average.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So I told ChatGPT:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Do the first question, plot it as a six survey rolling average&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I asked for the first question because it turned out there were two that were relevant in the survey spreadsheet.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In the last two weeks, did this business use Artificial Intelligence (AI) in producing goods or services? (Examples of AI: machine learning, natural language processing, virtual agents, voice recognition, etc.)&lt;/li&gt;
&lt;li&gt;During the next six months, do you think this business will be using Artificial Intelligence (AI) in producing goods or services? (Examples of AI: machine learning, natural language processing, virtual agents, voice recognition, etc.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It churned away for a little longer, added this code to the script:&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;# Compute 6-survey rolling average (biweekly cadence → ~12 weeks)&lt;/span&gt;
&lt;span class="pl-s1"&gt;rolled&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;wide&lt;/span&gt;.&lt;span class="pl-c1"&gt;rolling&lt;/span&gt;(&lt;span class="pl-s1"&gt;window&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;6&lt;/span&gt;, &lt;span class="pl-s1"&gt;min_periods&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;6&lt;/span&gt;).&lt;span class="pl-c1"&gt;mean&lt;/span&gt;()&lt;/pre&gt;
&lt;p&gt;And popped out this chart (after I told it to fix the glitch in the title):&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/recreated-chart-2.jpg" alt="Second chart. This time the lines are basically an exact match for the Apollo one." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I think it's done it! This is a very solid match for the Apollo original, recreated using &lt;code&gt;matplotlib&lt;/code&gt; and &lt;code&gt;pandas&lt;/code&gt; from the same underlying source data from the US Census.&lt;/p&gt;
&lt;p&gt;Here's the full Python code it wrote, which I think is quite readable (in as much as Pandas code can be):&lt;/p&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;pandas&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;
&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;matplotlib&lt;/span&gt;.&lt;span class="pl-s1"&gt;pyplot&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-s1"&gt;plt&lt;/span&gt;
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;matplotlib&lt;/span&gt;.&lt;span class="pl-s1"&gt;ticker&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-v"&gt;PercentFormatter&lt;/span&gt;

&lt;span class="pl-s1"&gt;path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"/mnt/data/Employment Size Class.xlsx"&lt;/span&gt;

&lt;span class="pl-s1"&gt;resp&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;read_excel&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;sheet_name&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Response Estimates"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;dates&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;read_excel&lt;/span&gt;(&lt;span class="pl-s1"&gt;path&lt;/span&gt;, &lt;span class="pl-s1"&gt;sheet_name&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Collection and Reference Dates"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;is_current&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;resp&lt;/span&gt;[&lt;span class="pl-s"&gt;"Question"&lt;/span&gt;].&lt;span class="pl-c1"&gt;astype&lt;/span&gt;(&lt;span class="pl-s1"&gt;str&lt;/span&gt;).&lt;span class="pl-c1"&gt;str&lt;/span&gt;.&lt;span class="pl-c1"&gt;strip&lt;/span&gt;().&lt;span class="pl-c1"&gt;str&lt;/span&gt;.&lt;span class="pl-c1"&gt;startswith&lt;/span&gt;(&lt;span class="pl-s"&gt;"In the last two weeks"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;ai_yes&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;resp&lt;/span&gt;[&lt;span class="pl-s1"&gt;is_current&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s1"&gt;resp&lt;/span&gt;[&lt;span class="pl-s"&gt;"Answer"&lt;/span&gt;].&lt;span class="pl-c1"&gt;astype&lt;/span&gt;(&lt;span class="pl-s1"&gt;str&lt;/span&gt;).&lt;span class="pl-c1"&gt;str&lt;/span&gt;.&lt;span class="pl-c1"&gt;strip&lt;/span&gt;().&lt;span class="pl-c1"&gt;str&lt;/span&gt;.&lt;span class="pl-c1"&gt;lower&lt;/span&gt;().&lt;span class="pl-c1"&gt;eq&lt;/span&gt;(&lt;span class="pl-s"&gt;"yes"&lt;/span&gt;)].&lt;span class="pl-c1"&gt;copy&lt;/span&gt;()

&lt;span class="pl-s1"&gt;code_to_bucket&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; {&lt;span class="pl-s"&gt;"A"&lt;/span&gt;:&lt;span class="pl-s"&gt;"1-4"&lt;/span&gt;,&lt;span class="pl-s"&gt;"B"&lt;/span&gt;:&lt;span class="pl-s"&gt;"5-9"&lt;/span&gt;,&lt;span class="pl-s"&gt;"C"&lt;/span&gt;:&lt;span class="pl-s"&gt;"10-19"&lt;/span&gt;,&lt;span class="pl-s"&gt;"D"&lt;/span&gt;:&lt;span class="pl-s"&gt;"20-49"&lt;/span&gt;,&lt;span class="pl-s"&gt;"E"&lt;/span&gt;:&lt;span class="pl-s"&gt;"50-99"&lt;/span&gt;,&lt;span class="pl-s"&gt;"F"&lt;/span&gt;:&lt;span class="pl-s"&gt;"100-249"&lt;/span&gt;,&lt;span class="pl-s"&gt;"G"&lt;/span&gt;:&lt;span class="pl-s"&gt;"250 or more employees"&lt;/span&gt;}
&lt;span class="pl-s1"&gt;ai_yes&lt;/span&gt;[&lt;span class="pl-s"&gt;"Bucket"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;ai_yes&lt;/span&gt;[&lt;span class="pl-s"&gt;"Empsize"&lt;/span&gt;].&lt;span class="pl-c1"&gt;map&lt;/span&gt;(&lt;span class="pl-s1"&gt;code_to_bucket&lt;/span&gt;)

&lt;span class="pl-s1"&gt;period_cols&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;ai_yes&lt;/span&gt;.&lt;span class="pl-c1"&gt;columns&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-en"&gt;str&lt;/span&gt;(&lt;span class="pl-s1"&gt;c&lt;/span&gt;).&lt;span class="pl-c1"&gt;isdigit&lt;/span&gt;() &lt;span class="pl-c1"&gt;and&lt;/span&gt; &lt;span class="pl-en"&gt;len&lt;/span&gt;(&lt;span class="pl-en"&gt;str&lt;/span&gt;(&lt;span class="pl-s1"&gt;c&lt;/span&gt;))&lt;span class="pl-c1"&gt;==&lt;/span&gt;&lt;span class="pl-c1"&gt;6&lt;/span&gt;]
&lt;span class="pl-s1"&gt;long&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;ai_yes&lt;/span&gt;.&lt;span class="pl-c1"&gt;melt&lt;/span&gt;(&lt;span class="pl-s1"&gt;id_vars&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;[&lt;span class="pl-s"&gt;"Bucket"&lt;/span&gt;], &lt;span class="pl-s1"&gt;value_vars&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;period_cols&lt;/span&gt;, &lt;span class="pl-s1"&gt;var_name&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;, &lt;span class="pl-s1"&gt;value_name&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"value"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;dates&lt;/span&gt;[&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;dates&lt;/span&gt;[&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;].&lt;span class="pl-c1"&gt;astype&lt;/span&gt;(&lt;span class="pl-s1"&gt;str&lt;/span&gt;)
&lt;span class="pl-s1"&gt;long&lt;/span&gt;[&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;long&lt;/span&gt;[&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;].&lt;span class="pl-c1"&gt;astype&lt;/span&gt;(&lt;span class="pl-s1"&gt;str&lt;/span&gt;)
&lt;span class="pl-s1"&gt;merged&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;long&lt;/span&gt;.&lt;span class="pl-c1"&gt;merge&lt;/span&gt;(&lt;span class="pl-s1"&gt;dates&lt;/span&gt;[[&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;,&lt;span class="pl-s"&gt;"Ref End"&lt;/span&gt;]], &lt;span class="pl-s1"&gt;on&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Smpdt"&lt;/span&gt;, &lt;span class="pl-s1"&gt;how&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"left"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;merged&lt;/span&gt;[&lt;span class="pl-s"&gt;"date"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;to_datetime&lt;/span&gt;(&lt;span class="pl-s1"&gt;merged&lt;/span&gt;[&lt;span class="pl-s"&gt;"Ref End"&lt;/span&gt;], &lt;span class="pl-s1"&gt;errors&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"coerce"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;merged&lt;/span&gt;[&lt;span class="pl-s"&gt;"value"&lt;/span&gt;] &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;to_numeric&lt;/span&gt;(&lt;span class="pl-s1"&gt;long&lt;/span&gt;[&lt;span class="pl-s"&gt;"value"&lt;/span&gt;].&lt;span class="pl-c1"&gt;astype&lt;/span&gt;(&lt;span class="pl-s1"&gt;str&lt;/span&gt;).&lt;span class="pl-c1"&gt;str&lt;/span&gt;.&lt;span class="pl-c1"&gt;replace&lt;/span&gt;(&lt;span class="pl-s"&gt;"%"&lt;/span&gt;,&lt;span class="pl-s"&gt;""&lt;/span&gt;,&lt;span class="pl-s1"&gt;regex&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;).&lt;span class="pl-c1"&gt;str&lt;/span&gt;.&lt;span class="pl-c1"&gt;strip&lt;/span&gt;(), &lt;span class="pl-s1"&gt;errors&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"coerce"&lt;/span&gt;)

&lt;span class="pl-s1"&gt;order&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; [&lt;span class="pl-s"&gt;"250 or more employees"&lt;/span&gt;,&lt;span class="pl-s"&gt;"100-249"&lt;/span&gt;,&lt;span class="pl-s"&gt;"50-99"&lt;/span&gt;,&lt;span class="pl-s"&gt;"20-49"&lt;/span&gt;,&lt;span class="pl-s"&gt;"10-19"&lt;/span&gt;,&lt;span class="pl-s"&gt;"5-9"&lt;/span&gt;,&lt;span class="pl-s"&gt;"1-4"&lt;/span&gt;]
&lt;span class="pl-s1"&gt;wide&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;merged&lt;/span&gt;.&lt;span class="pl-c1"&gt;pivot_table&lt;/span&gt;(&lt;span class="pl-s1"&gt;index&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"date"&lt;/span&gt;, &lt;span class="pl-s1"&gt;columns&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"Bucket"&lt;/span&gt;, &lt;span class="pl-s1"&gt;values&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"value"&lt;/span&gt;, &lt;span class="pl-s1"&gt;aggfunc&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"mean"&lt;/span&gt;).&lt;span class="pl-c1"&gt;sort_index&lt;/span&gt;()
&lt;span class="pl-s1"&gt;wide&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;wide&lt;/span&gt;[[&lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;order&lt;/span&gt; &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;c&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;wide&lt;/span&gt;.&lt;span class="pl-c1"&gt;columns&lt;/span&gt;]]
&lt;span class="pl-s1"&gt;rolled&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;wide&lt;/span&gt;.&lt;span class="pl-c1"&gt;rolling&lt;/span&gt;(&lt;span class="pl-s1"&gt;window&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;6&lt;/span&gt;, &lt;span class="pl-s1"&gt;min_periods&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;6&lt;/span&gt;).&lt;span class="pl-c1"&gt;mean&lt;/span&gt;()

&lt;span class="pl-s1"&gt;start&lt;/span&gt;, &lt;span class="pl-s1"&gt;end&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;Timestamp&lt;/span&gt;(&lt;span class="pl-s"&gt;"2023-11-01"&lt;/span&gt;), &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;Timestamp&lt;/span&gt;(&lt;span class="pl-s"&gt;"2025-08-31"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;rolled_win&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;rolled&lt;/span&gt;.&lt;span class="pl-c1"&gt;loc&lt;/span&gt;[(&lt;span class="pl-s1"&gt;rolled&lt;/span&gt;.&lt;span class="pl-c1"&gt;index&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;start&lt;/span&gt;) &lt;span class="pl-c1"&gt;&amp;amp;&lt;/span&gt; (&lt;span class="pl-s1"&gt;rolled&lt;/span&gt;.&lt;span class="pl-c1"&gt;index&lt;/span&gt; &lt;span class="pl-c1"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;end&lt;/span&gt;)]

&lt;span class="pl-s1"&gt;fig&lt;/span&gt;, &lt;span class="pl-s1"&gt;ax&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;plt&lt;/span&gt;.&lt;span class="pl-c1"&gt;subplots&lt;/span&gt;(&lt;span class="pl-s1"&gt;figsize&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;(&lt;span class="pl-c1"&gt;12&lt;/span&gt;, &lt;span class="pl-c1"&gt;6&lt;/span&gt;))
&lt;span class="pl-k"&gt;for&lt;/span&gt; &lt;span class="pl-s1"&gt;col&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;order&lt;/span&gt;:
    &lt;span class="pl-k"&gt;if&lt;/span&gt; &lt;span class="pl-s1"&gt;col&lt;/span&gt; &lt;span class="pl-c1"&gt;in&lt;/span&gt; &lt;span class="pl-s1"&gt;rolled_win&lt;/span&gt;.&lt;span class="pl-c1"&gt;columns&lt;/span&gt;:
        &lt;span class="pl-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;plot&lt;/span&gt;(&lt;span class="pl-s1"&gt;rolled_win&lt;/span&gt;.&lt;span class="pl-c1"&gt;index&lt;/span&gt;, &lt;span class="pl-s1"&gt;rolled_win&lt;/span&gt;[&lt;span class="pl-s1"&gt;col&lt;/span&gt;], &lt;span class="pl-s1"&gt;label&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s1"&gt;col&lt;/span&gt;, &lt;span class="pl-s1"&gt;linewidth&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;2&lt;/span&gt;)

&lt;span class="pl-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;set_title&lt;/span&gt;(&lt;span class="pl-s"&gt;"AI adoption (last two weeks) — 6‑survey rolling average"&lt;/span&gt;, &lt;span class="pl-s1"&gt;pad&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;16&lt;/span&gt;)
&lt;span class="pl-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;yaxis&lt;/span&gt;.&lt;span class="pl-c1"&gt;set_major_formatter&lt;/span&gt;(&lt;span class="pl-en"&gt;PercentFormatter&lt;/span&gt;(&lt;span class="pl-c1"&gt;100&lt;/span&gt;))
&lt;span class="pl-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;set_ylabel&lt;/span&gt;(&lt;span class="pl-s"&gt;"%"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;set_xlabel&lt;/span&gt;(&lt;span class="pl-s"&gt;""&lt;/span&gt;)
&lt;span class="pl-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;grid&lt;/span&gt;(&lt;span class="pl-c1"&gt;True&lt;/span&gt;, &lt;span class="pl-s1"&gt;alpha&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;0.25&lt;/span&gt;, &lt;span class="pl-s1"&gt;linestyle&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-s1"&gt;ax&lt;/span&gt;.&lt;span class="pl-c1"&gt;legend&lt;/span&gt;(&lt;span class="pl-s1"&gt;title&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;None&lt;/span&gt;, &lt;span class="pl-s1"&gt;loc&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"upper left"&lt;/span&gt;, &lt;span class="pl-s1"&gt;ncols&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;2&lt;/span&gt;, &lt;span class="pl-s1"&gt;frameon&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;False&lt;/span&gt;)
&lt;span class="pl-s1"&gt;plt&lt;/span&gt;.&lt;span class="pl-c1"&gt;tight_layout&lt;/span&gt;()

&lt;span class="pl-s1"&gt;png_path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"/mnt/data/ai_adoption_rolling6_by_firm_size.png"&lt;/span&gt;
&lt;span class="pl-s1"&gt;svg_path&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;"/mnt/data/ai_adoption_rolling6_by_firm_size.svg"&lt;/span&gt;
&lt;span class="pl-s1"&gt;plt&lt;/span&gt;.&lt;span class="pl-c1"&gt;savefig&lt;/span&gt;(&lt;span class="pl-s1"&gt;png_path&lt;/span&gt;, &lt;span class="pl-s1"&gt;dpi&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-c1"&gt;200&lt;/span&gt;, &lt;span class="pl-s1"&gt;bbox_inches&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"tight"&lt;/span&gt;)
&lt;span class="pl-s1"&gt;plt&lt;/span&gt;.&lt;span class="pl-c1"&gt;savefig&lt;/span&gt;(&lt;span class="pl-s1"&gt;svg_path&lt;/span&gt;, &lt;span class="pl-s1"&gt;bbox_inches&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;"tight"&lt;/span&gt;)&lt;/pre&gt;
&lt;p&gt;I like how it generated &lt;a href="https://static.simonwillison.net/static/2025/ai_adoption_rolling6_by_firm_size.svg"&gt;an SVG version&lt;/a&gt; of the chart without me even asking for it.&lt;/p&gt;
&lt;p&gt;You can access &lt;a href="https://chatgpt.com/share/68bf48cf-0e70-8006-a045-96fa8e7ddfc1"&gt;the ChatGPT transcript&lt;/a&gt; to see full details of everything it did.&lt;/p&gt;
&lt;h4 id="rendering-that-chart-client-side-using-pyodide"&gt;Rendering that chart client-side using Pyodide&lt;/h4&gt;
&lt;p&gt;I had one more challenge to try out. Could I render that same chart entirely in the browser using &lt;a href="https://pyodide.org/en/stable/"&gt;Pyodide&lt;/a&gt;, which can execute both Pandas and Matplotlib?&lt;/p&gt;
&lt;p&gt;I fired up a new ChatGPT GPT-5 session and prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Build a canvas that loads Pyodide and uses it to render an example bar chart with pandas and matplotlib and then displays that on the page&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My goal here was simply to see if I could get a proof of concept of a chart rendered, ideally using the Canvas feature of ChatGPT. Canvas is OpenAI's version of Claude Artifacts, which lets the model write and then execute HTML and JavaScript directly in the ChatGPT interface.&lt;/p&gt;
&lt;p&gt;It worked! Here's &lt;a href="https://chatgpt.com/c/68bf2993-ca94-832a-a95e-fb225911c0a6"&gt;the transcript&lt;/a&gt; and here's &lt;a href="https://tools.simonwillison.net/pyodide-bar-chart"&gt;what it built me&lt;/a&gt;, exported  to my &lt;a href="https://tools.simonwillison.net/"&gt;tools.simonwillison.net&lt;/a&gt; GitHub Pages site (&lt;a href="https://github.com/simonw/tools/blob/main/pyodide-bar-chart.html"&gt;source code here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/pyodide-matplotlib.jpg" alt="Screenshot of a web application demonstrating Pyodide integration. Header reads &amp;quot;Pyodide + pandas + matplotlib — Bar Chart&amp;quot; with subtitle &amp;quot;This page loads Pyodide in the browser, uses pandas to prep some data, renders a bar chart with matplotlib, and displays it below — all client-side.&amp;quot; Left panel shows terminal output: &amp;quot;Ready&amp;quot;, &amp;quot;# Python environment ready&amp;quot;, &amp;quot;• pandas 2.2.0&amp;quot;, &amp;quot;• numpy 1.26.4&amp;quot;, &amp;quot;• matplotlib 3.5.2&amp;quot;, &amp;quot;Running chart code...&amp;quot;, &amp;quot;Done. Chart updated.&amp;quot; with &amp;quot;Re-run demo&amp;quot; and &amp;quot;Show Python&amp;quot; buttons. Footer note: &amp;quot;CDN: pyodide, pandas, numpy, matplotlib are fetched on demand. First run may take a few seconds.&amp;quot; Right panel displays a bar chart titled &amp;quot;Example Bar Chart (pandas + matplotlib in Pyodide)&amp;quot; showing blue bars for months Jan through Jun with values approximately: Jan(125), Feb(130), Mar(80), Apr(85), May(85), Jun(120). Y-axis labeled &amp;quot;Streams&amp;quot; ranges 0-120, X-axis labeled &amp;quot;Month&amp;quot;." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I've now proven to myself that I can render those Python charts directly in the browser. Next step: recreate the Apollo chart.&lt;/p&gt;
&lt;p&gt;I knew it would need a way to load the spreadsheet that was CORS-enabled. I uploaded my copy to my &lt;code&gt;/static/cors-allow/2025/...&lt;/code&gt; directory (configured in Cloudflare to serve CORS headers), pasted in the finished plotting code from earlier and told ChatGPT:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Now update it to have less explanatory text and a less exciting design (black on white is fine) and run the equivalent of this:&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;(... pasted in Python code from earlier ...)&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Load the XLSX sheet from https://static.simonwillison.net/static/cors-allow/2025/Employment-Size-Class-Sep-2025.xlsx&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It didn't quite work - I got an error about &lt;code&gt;openpyxl&lt;/code&gt; which I manually researched the fix for and prompted:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Use await micropip.install("openpyxl") to install openpyxl - instead of using loadPackage&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I had to paste in another error message:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;zipfile.BadZipFile: File is not a zip file&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then one about a &lt;code&gt;SyntaxError: unmatched ')'&lt;/code&gt; and a &lt;code&gt;TypeError: Legend.__init__() got an unexpected keyword argument 'ncols'&lt;/code&gt; - copying and pasting error messages remains a frustrating but necessary part of the vibe-coding loop.&lt;/p&gt;
&lt;p&gt;... but with those fixes in place, the resulting code worked! Visit &lt;a href="https://tools.simonwillison.net/ai-adoption"&gt;tools.simonwillison.net/ai-adoption&lt;/a&gt; to see the final result:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/recreated-chart-pyodide.jpg" alt="Web page. Title is AI adoption - 6-survey rolling average. Has a Run, Downlaed PNG, Downlaod SVG button. Panel on the left says Loading Python... Fetcing packages numpy, pandas, matplotlib. Installing openpyxl via micropop... ready. Running. Done. Right hand panel shows the rendered chart." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;Here's the code for that page, &lt;a href="https://github.com/simonw/tools/blob/main/ai-adoption.html"&gt;170 lines&lt;/a&gt; all-in of HTML, CSS, JavaScript and Python.&lt;/p&gt;
&lt;h4 id="what-i-ve-learned-from-this"&gt;What I've learned from this&lt;/h4&gt;
&lt;p&gt;This was another of those curiosity-inspired investigations that turned into a whole set of useful lessons.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GPT-5 is great at tracking down US Census data, no matter how difficult their site is to understand if you don't work with their data often&lt;/li&gt;
&lt;li&gt;It can do a very good job of turning data + a screenshot of a chart into a recreation of that chart using code interpreter, Pandas and matplotlib&lt;/li&gt;
&lt;li&gt;Running Python + matplotlib in a browser via Pyodide is very easy and only takes a few dozen lines of code&lt;/li&gt;
&lt;li&gt;Fetching an XLSX sheet into Pyodide is only a small extra step using &lt;code&gt;pyfetch&lt;/code&gt; and &lt;code&gt;openpyxl&lt;/code&gt;:
&lt;pre style="margin-top: 0.5em"&gt;&lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;micropip&lt;/span&gt;
&lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-s1"&gt;micropip&lt;/span&gt;.&lt;span class="pl-c1"&gt;install&lt;/span&gt;(&lt;span class="pl-s"&gt;"openpyxl"&lt;/span&gt;)
&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;pyodide&lt;/span&gt;.&lt;span class="pl-s1"&gt;http&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;pyfetch&lt;/span&gt;
&lt;span class="pl-s1"&gt;resp_fetch&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;pyfetch&lt;/span&gt;(&lt;span class="pl-c1"&gt;URL&lt;/span&gt;)
&lt;span class="pl-s1"&gt;wb_bytes&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;resp_fetch&lt;/span&gt;.&lt;span class="pl-c1"&gt;bytes&lt;/span&gt;()
&lt;span class="pl-s1"&gt;xf&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;pd&lt;/span&gt;.&lt;span class="pl-c1"&gt;ExcelFile&lt;/span&gt;(&lt;span class="pl-s1"&gt;io&lt;/span&gt;.&lt;span class="pl-c1"&gt;BytesIO&lt;/span&gt;(&lt;span class="pl-s1"&gt;wb_bytes&lt;/span&gt;), &lt;span class="pl-s1"&gt;engine&lt;/span&gt;&lt;span class="pl-c1"&gt;=&lt;/span&gt;&lt;span class="pl-s"&gt;'openpyxl'&lt;/span&gt;)&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Another new-to-me pattern: you can render an image to the DOM from Pyodide code &lt;a href="https://github.com/simonw/tools/blob/cf26ed8a6f243159bdc90a3d88f818261732103f/ai-adoption.html#L124"&gt;like this&lt;/a&gt;:
&lt;pre style="margin-top: 0.5em"&gt;&lt;span class="pl-k"&gt;from&lt;/span&gt; &lt;span class="pl-s1"&gt;js&lt;/span&gt; &lt;span class="pl-k"&gt;import&lt;/span&gt; &lt;span class="pl-s1"&gt;document&lt;/span&gt;
&lt;span class="pl-s1"&gt;document&lt;/span&gt;.&lt;span class="pl-c1"&gt;getElementById&lt;/span&gt;(&lt;span class="pl-s"&gt;'plot'&lt;/span&gt;).&lt;span class="pl-c1"&gt;src&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s"&gt;'data:image/png;base64,'&lt;/span&gt; &lt;span class="pl-c1"&gt;+&lt;/span&gt; &lt;span class="pl-s1"&gt;img_b64&lt;/span&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I will most definitely be using these techniques again in future.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: Coincidentally Claude released their own upgraded equivalent to ChatGPT Code Interpreter later on the day that I published this story, so I &lt;a href="https://simonwillison.net/2025/Sep/9/claude-code-interpreter/#something-much-harder-recreating-the-ai-adoption-chart"&gt;ran the same chart recreation experiment&lt;/a&gt; against Claude Sonnet 4 to see how it compared.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/census"&gt;census&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/data-journalism"&gt;data-journalism&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/tools"&gt;tools&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&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/code-interpreter"&gt;code-interpreter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llm-reasoning"&gt;llm-reasoning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/vibe-coding"&gt;vibe-coding&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-search"&gt;ai-assisted-search&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gpt-5"&gt;gpt-5&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="census"/><category term="data-journalism"/><category term="javascript"/><category term="python"/><category term="tools"/><category term="visualization"/><category term="ai"/><category term="pyodide"/><category term="openai"/><category term="generative-ai"/><category term="chatgpt"/><category term="llms"/><category term="ai-assisted-programming"/><category term="code-interpreter"/><category term="llm-reasoning"/><category term="vibe-coding"/><category term="ai-assisted-search"/><category term="gpt-5"/></entry><entry><title>python-importtime-graph</title><link href="https://simonwillison.net/2025/Jun/20/python-importtime-graph/#atom-tag" rel="alternate"/><published>2025-06-20T19:31:45+00:00</published><updated>2025-06-20T19:31:45+00:00</updated><id>https://simonwillison.net/2025/Jun/20/python-importtime-graph/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kmichel/python-importtime-graph"&gt;python-importtime-graph&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I was exploring why a Python tool was taking over a second to start running and I learned about the &lt;code&gt;python -X importtime&lt;/code&gt; feature, &lt;a href="https://docs.python.org/3/using/cmdline.html#cmdoption-X"&gt;documented here&lt;/a&gt;. Adding that option causes Python to spit out a text tree showing the time spent importing every module.&lt;/p&gt;
&lt;p&gt;I tried that like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python -X importtime -m llm plugins
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's for &lt;a href="https://llm.datasette.io/"&gt;LLM&lt;/a&gt; running 41 different plugins. Here's &lt;a href="https://gist.github.com/simonw/5b7ee41b5ee324105f23ee695d4c0906"&gt;the full output&lt;/a&gt; from that command, which starts like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import time: self [us] | cumulative | imported package
import time:        77 |         77 |   _io
import time:        19 |         19 |   marshal
import time:       131 |        131 |   posix
import time:       363 |        590 | _frozen_importlib_external
import time:       450 |        450 |   time
import time:       110 |        559 | zipimport
import time:        64 |         64 |     _codecs
import time:       252 |        315 |   codecs
import time:       277 |        277 |   encodings.aliases
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Kevin Michel built this excellent tool for visualizing these traces as a treemap. It runs in a browser - visit &lt;a href="https://kmichel.github.io/python-importtime-graph/"&gt;kmichel.github.io/python-importtime-graph/&lt;/a&gt; and paste in the trace to get the visualization.&lt;/p&gt;
&lt;p&gt;Here's what I got for that LLM example trace:&lt;/p&gt;
&lt;p&gt;&lt;img alt="An illegibly dense treemap" src="https://static.simonwillison.net/static/2025/llm-importtime.jpg" /&gt;&lt;/p&gt;
&lt;p&gt;As you can see, it's pretty dense! Here's &lt;a href="https://static.simonwillison.net/static/2025/llm-importtime.svg"&gt;the SVG version&lt;/a&gt; which is a lot more readable, since you can zoom in to individual sections.&lt;/p&gt;
&lt;p&gt;Zooming in it looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Performance profiling treemap visualization showing execution times for Python libraries and modules, with color-coded rectangular blocks sized proportionally to their execution time, displaying &amp;quot;Total: 2845.828 ms&amp;quot; at the top with major components like &amp;quot;lim.cli: 2256.275 ms&amp;quot; and &amp;quot;openai: 150.043 ms&amp;quot;" src="https://static.simonwillison.net/static/2025/llm-importtime-zoom.jpg" /&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/performance"&gt;performance&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;&lt;/p&gt;



</summary><category term="performance"/><category term="python"/><category term="visualization"/></entry><entry><title>Observable Plot: Waffle mark</title><link href="https://simonwillison.net/2024/Aug/6/observable-plot-waffle-mark/#atom-tag" rel="alternate"/><published>2024-08-06T21:40:48+00:00</published><updated>2024-08-06T21:40:48+00:00</updated><id>https://simonwillison.net/2024/Aug/6/observable-plot-waffle-mark/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/plot/marks/waffle"&gt;Observable Plot: Waffle mark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
New feature in &lt;a href="https://github.com/observablehq/plot/blob/v0.6.16/CHANGELOG.md#0616"&gt;Observable Plot 0.6.16&lt;/a&gt;: the waffle mark! I really like this one. Here's an example showing the gender and weight of athletes in this year's Olympics:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Displays weight distribution data for males and females using small colored squares stacked to form columns, with blue representing females and orange representing males. The x-axis shows weight from 30 to 170, while the y-axis indicates frequency up to 2,800. The distribution forms a bell curve, with females peaking at lower weights than males, and clear differences visible between the genders, especially at weight extremes." src="https://static.simonwillison.net/static/2024/waffle.png" /&gt;

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mbostock/status/1820867956366065819"&gt;@mbostock&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-plot"&gt;observable-plot&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="visualization"/><category term="observable"/><category term="observable-plot"/></entry><entry><title>I’ve stopped using box plots. Should you?</title><link href="https://simonwillison.net/2024/Jun/19/box-plots/#atom-tag" rel="alternate"/><published>2024-06-19T00:22:55+00:00</published><updated>2024-06-19T00:22:55+00:00</updated><id>https://simonwillison.net/2024/Jun/19/box-plots/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://nightingaledvs.com/ive-stopped-using-box-plots-should-you/"&gt;I’ve stopped using box plots. Should you?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Nick Desbarats explains box plots (including with &lt;a href="https://www.youtube.com/watch?v=iBq23-eQhp8"&gt;this excellent short YouTube video&lt;/a&gt;) and then discusses why he thinks "typically less than 20 percent" of participants in his workshops already understand how to read them.&lt;/p&gt;
&lt;p&gt;A key problem is that they are unintuitive: a box plot has four sections, two thin lines (the top and bottom whisker segments) and two larger boxes, joined around the median. Each of these elements represents the same number of samples (one quartile each) but the thin lines v.s. thick boxes imply that the whiskers contain less samples than the boxes.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/io4aui/i_ve_stopped_using_box_plots_should_you"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="visualization"/></entry><entry><title>datasette-plot - a new Datasette Plugin for building data visualizations</title><link href="https://simonwillison.net/2023/Dec/31/datasette-plot/#atom-tag" rel="alternate"/><published>2023-12-31T05:04:19+00:00</published><updated>2023-12-31T05:04:19+00:00</updated><id>https://simonwillison.net/2023/Dec/31/datasette-plot/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.datasette.cloud/blog/2023/datasette-plot/"&gt;datasette-plot - a new Datasette Plugin for building data visualizations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I forgot to link to this here last week: Alex Garcia released the first version of datasette-plot, a brand new Datasette visualization plugin built on top of the Observable Plot charting library. We plan to use this as the new, updated alternative to my older datasette-vega plugin.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&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/alex-garcia"&gt;alex-garcia&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-plot"&gt;observable-plot&lt;/a&gt;&lt;/p&gt;



</summary><category term="plugins"/><category term="visualization"/><category term="datasette"/><category term="observable"/><category term="alex-garcia"/><category term="observable-plot"/></entry><entry><title>datasette-dashboards</title><link href="https://simonwillison.net/2022/Apr/7/datasette-dashboards/#atom-tag" rel="alternate"/><published>2022-04-07T18:36:45+00:00</published><updated>2022-04-07T18:36:45+00:00</updated><id>https://simonwillison.net/2022/Apr/7/datasette-dashboards/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/rclement/datasette-dashboards"&gt;datasette-dashboards&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Romain Clement’s datasette-dashboards plugin lets you configure dashboards for Datasette using YAML, combining markdown blocks, Vega graphs and single number metrics using a layout powered by CSS grids. This is a beautiful piece of software design, with a very compelling live demo.

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


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="plugins"/><category term="visualization"/><category term="datasette"/></entry><entry><title>Datasette table diagram using Mermaid</title><link href="https://simonwillison.net/2022/Feb/14/datasette-table-diagram-using-mermaid/#atom-tag" rel="alternate"/><published>2022-02-14T19:43:15+00:00</published><updated>2022-02-14T19:43:15+00:00</updated><id>https://simonwillison.net/2022/Feb/14/datasette-table-diagram-using-mermaid/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@simonw/datasette-table-diagram-using-mermaid"&gt;Datasette table diagram using Mermaid&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Mermaid is a DSL for generating diagrams from plain text, designed to be embedded in Markdown. GitHub just added support for Mermaid to their Markdown pipeline, which inspired me to try it out. Here’s an Observable Notebook I built which uses Mermaid to visualize the relationships between Datasette tables based on their foreign keys.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1493305519481626626"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/dsl"&gt;dsl&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&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/mermaid"&gt;mermaid&lt;/a&gt;&lt;/p&gt;



</summary><category term="dsl"/><category term="github"/><category term="visualization"/><category term="datasette"/><category term="observable"/><category term="mermaid"/></entry><entry><title>Observable Plot Cheatsheets</title><link href="https://simonwillison.net/2022/Jan/25/observable-plot-cheatsheets/#atom-tag" rel="alternate"/><published>2022-01-25T22:12:45+00:00</published><updated>2022-01-25T22:12:45+00:00</updated><id>https://simonwillison.net/2022/Jan/25/observable-plot-cheatsheets/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@observablehq/plot-cheatsheets"&gt;Observable Plot Cheatsheets&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Beautiful new set of cheatsheets by Mike Freeman for the Observable Plot charting library. This is really top notch documentation—the cheatsheets are available as printable PDFs but the real value here is in the interactive versions of them, which include Observable-powered sliders to tweak the different examples and copy out the resulting generated code.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mf_viz/status/1485705178925916175"&gt;@mf_viz&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-plot"&gt;observable-plot&lt;/a&gt;&lt;/p&gt;



</summary><category term="visualization"/><category term="observable"/><category term="observable-plot"/></entry><entry><title>Plot &amp; Vega-Lite</title><link href="https://simonwillison.net/2021/May/4/plot-vega-lite/#atom-tag" rel="alternate"/><published>2021-05-04T16:32:47+00:00</published><updated>2021-05-04T16:32:47+00:00</updated><id>https://simonwillison.net/2021/May/4/plot-vega-lite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@observablehq/plot-vega-lite"&gt;Plot &amp;amp; Vega-Lite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Useful documentation comparing the brand new Observable Plot to Vega-Lite, complete with examples of how to achieve the same thing in both libraries.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/d3"&gt;d3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-plot"&gt;observable-plot&lt;/a&gt;&lt;/p&gt;



</summary><category term="visualization"/><category term="d3"/><category term="observable"/><category term="observable-plot"/></entry><entry><title>Observable Plot</title><link href="https://simonwillison.net/2021/May/4/observable-plot/#atom-tag" rel="alternate"/><published>2021-05-04T16:28:34+00:00</published><updated>2021-05-04T16:28:34+00:00</updated><id>https://simonwillison.net/2021/May/4/observable-plot/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@observablehq/plot"&gt;Observable Plot&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is huge: a brand new high-level JavaScript visualization library from Mike Bostock, the author of D3—partially inspired by Vega-Lite which I’ve used enthusiastically in the past. First impressions are that this is a big step forward for quickly building high-quality visualizations. It’s released under the ISC license which is “functionally equivalent to the BSD 2-Clause and MIT licenses”.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/mbostock/status/1389611128339144704"&gt;@mbostock&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/d3"&gt;d3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/mike-bostock"&gt;mike-bostock&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable-plot"&gt;observable-plot&lt;/a&gt;&lt;/p&gt;



</summary><category term="open-source"/><category term="visualization"/><category term="d3"/><category term="observable"/><category term="mike-bostock"/><category term="observable-plot"/></entry><entry><title>Weeknotes: datasette-seaborn, fivethirtyeight-polls</title><link href="https://simonwillison.net/2020/Sep/18/weeknotes-datasette-seaborn/#atom-tag" rel="alternate"/><published>2020-09-18T19:49:02+00:00</published><updated>2020-09-18T19:49:02+00:00</updated><id>https://simonwillison.net/2020/Sep/18/weeknotes-datasette-seaborn/#atom-tag</id><summary type="html">
    &lt;p&gt;This week I &lt;a href="https://simonwillison.net/2020/Sep/15/datasette-0-49/"&gt;released Datasette 0.49&lt;/a&gt; and tinkered with datasette-seaborn, dogsheep-beta and polling data from FiveThirtyEight.&lt;/p&gt;
&lt;h4 id="weeknotes-18-sep-datasette-seaborn"&gt;datasette-seaborn&lt;/h4&gt;
&lt;p&gt;Datasette currently has two key plugins for data visualization: &lt;a href="https://github.com/simonw/datasette-vega"&gt;datasette-vega&lt;/a&gt; for line, bar and scatter charts (powered by &lt;a href="https://vega.github.io/vega-lite/"&gt;Vega-Lite&lt;/a&gt;) and &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datsette-cluster-map&lt;/a&gt; for creating clustered marker maps.&lt;/p&gt;
&lt;p&gt;I'm always looking for other interesting visualization opportunities. Seaborn 0.11 &lt;a href="https://seaborn.pydata.org/whatsnew.html"&gt;came out last week&lt;/a&gt; and is a very exciting piece of software.&lt;/p&gt;
&lt;p&gt;Seaborn focuses on statistical visualization tools - histograms, boxplots and the like - and represents 8 years of development lead by Michael Waskom. It's built on top of &lt;a href="https://matplotlib.org/"&gt;Matplotlib&lt;/a&gt; and exhibits meticulously good taste.&lt;/p&gt;
&lt;p&gt;So I've started building &lt;a href="https://github.com/simonw/datasette-seaborn"&gt;datasette-seaborn&lt;/a&gt;, a plugin which provides an HTTP interface for generating Seaborn charts using data stored in Datasette.&lt;/p&gt;
&lt;p&gt;It's still very alpha but early results are promising. I've chosen to implement it as a &lt;a href="https://docs.datasette.io/en/stable/plugin_hooks.html#register-output-renderer-datasette"&gt;custom output renderer&lt;/a&gt; - so adding &lt;code&gt;.seaborn&lt;/code&gt; to any Datasette table (plus some querystring parameters) will output a rendered PNG chart of that data, just like how &lt;code&gt;.json&lt;/code&gt; or &lt;code&gt;csv&lt;/code&gt; give you that data in different export formats.&lt;/p&gt;
&lt;p&gt;Here's an example chart generated by the plugin using the fabulous &lt;a href="https://allisonhorst.github.io/palmerpenguins/"&gt;palmerpenguins&lt;/a&gt; dataset (intended as a new alternative to &lt;a href="https://archive.ics.uci.edu/ml/datasets/iris"&gt;Iris&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2020/penguins.seaborn.png" style="max-width:100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I generated this image from the following URL:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;https://datasette-seaborn-demo.datasette.io/penguins/penguins.seaborn?_size=1000&amp;amp;_seaborn=kdeplot&amp;amp;_seaborn_x=flipper_length_mm&amp;amp;_seaborn_hue=species&amp;amp;_seaborn_multiple=stack&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This interface should be considered unstable and likely to change, but it illustrates the key idea behind the plugin: use &lt;code&gt;?_seaborn_x&lt;/code&gt; parameters to feed in options for the chart that you want to render.&lt;/p&gt;
&lt;p&gt;The two biggest issues with the plugin right now are that it &lt;a href="https://github.com/simonw/datasette-seaborn/issues/4"&gt;renders images on the main thread&lt;/a&gt;, potentially blocking the event loop, and it passes querystring arguments directly to seaborn &lt;a href="https://github.com/simonw/datasette-seaborn/issues/3"&gt;without first validating them&lt;/a&gt; which is almost certainly a security problem.&lt;/p&gt;
&lt;p&gt;So very much an alpha, but it's a promising start!&lt;/p&gt;
&lt;h4 id="weeknotes-18-sep-dogsheep-beta"&gt;dogsheep-beta&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/dogsheep/dogsheep-beta"&gt;Dogsheep Beta&lt;/a&gt; provides faceted search across my personal data from a whole variety of different sources.&lt;/p&gt;
&lt;p&gt;I demo'd it &lt;a href="https://www.youtube.com/watch?v=CPQCD3Qxxik"&gt;at PyCon AU&lt;/a&gt; a few weeks ago, and promised that a full write-up would follow. I still need to honour that promise! I'm figuring out how to provide a good interactive demo at the moment that doesn't expose my personal data.&lt;/p&gt;
&lt;p&gt;I added sort by date in addition to sort by relevance in &lt;a href="https://github.com/dogsheep/dogsheep-beta/releases/tag/0.7"&gt;version 0.7&lt;/a&gt; this week.&lt;/p&gt;
&lt;h4 id="weeknotes-18-sep-fivethirtyeight-polls"&gt;fivethirtyeight-polls&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://fivethirtyeight.com/"&gt;FiveThirtEight&lt;/a&gt; have long published the data behind their stories in their &lt;a href="https://github.com/fivethirtyeight/data"&gt;fivethirtyeight/data&lt;/a&gt; GitHub repository, and I've been using that data to power &lt;a href="https://fivethirtyeight.datasettes.com/"&gt;a Datasette demo&lt;/a&gt; ever since I first released the project.&lt;/p&gt;
&lt;p&gt;They run an index of their data projects at &lt;a href="https://data.fivethirtyeight.com/"&gt;data.fivethirtyeight.com&lt;/a&gt;, and this week I noticed that they list US election polling data there that wasn't being picked up by my &lt;a href="https://fivethirtyeight.datasettes.com/"&gt;fivethirtyeight.datasettes.com&lt;/a&gt; site.&lt;/p&gt;
&lt;p&gt;It turns out this is listed in the GitHub repository &lt;a href="https://github.com/fivethirtyeight/data/tree/master/polls"&gt;as a README.md file&lt;/a&gt; but without the actual CSV data. Instead, the README links to external CSV files with URLs like &lt;code&gt;https://projects.fivethirtyeight.com/polls-page/president_primary_polls.csv&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It looks to me like they're running their own custom web application which provides the CSV data as an export format, rather than keeping that CSV data directly in their GitHub repository.&lt;/p&gt;
&lt;p&gt;This makes sense - I imagine they run a lot of custom code to help them manage their data - but does mean that &lt;a href="https://github.com/simonw/fivethirtyeight-datasette"&gt;my Datasette publishing scripts&lt;/a&gt; weren't catching their polling data.&lt;/p&gt;
&lt;p&gt;Since they release their data as Creative Commons Attribution 4.0 International I decided to start archiving it on GitHub, where it would be easier to automate.&lt;/p&gt;
&lt;p&gt;I set up &lt;a href="https://github.com/simonw/fivethirtyeight-polls"&gt;simonw/fivethirtyeight-polls&lt;/a&gt; to do just that. It's a classic implementation of the &lt;a href="https://github.com/topics/git-scraping"&gt;git-scraping pattern&lt;/a&gt;: it runs &lt;a href="https://github.com/simonw/fivethirtyeight-polls/blob/main/.github/workflows/build.yml"&gt;a workflow script&lt;/a&gt; four times a day which fetches their latest CSV files and commits them to a repo. This means I now have a &lt;a href="https://github.com/simonw/fivethirtyeight-polls/commits/main"&gt;commit history&lt;/a&gt; of changes they have made to their polling data!&lt;/p&gt;
&lt;p&gt;I updated my FiveThirtyEight Datasette script to publish that data as a new &lt;code&gt;polls.db&lt;/code&gt; database, which is now being served at &lt;a href="https://fivethirtyeight.datasettes.com/polls"&gt;fivethirtyeight.datasettes.com/polls&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And since that Datasette instance runs the &lt;a href="https://github.com/simonw/datasette-graphql"&gt;datasette-graphql&lt;/a&gt; plugin, you can now use GraphQL to explore FiveThirtyEight's most recent polling data at &lt;a href="https://fivethirtyeight.datasettes.com/graphql/polls"&gt;https://fivethirtyeight.datasettes.com/graphql/polls&lt;/a&gt; - here's an &lt;a href="https://fivethirtyeight.datasettes.com/graphql/polls?query=%7B%0A%20%20generic_ballot_polls(filter%3A%7Bpollster_rating_name%3A%7Beq%3A%22YouGov%22%7D%7D)%20%7B%0A%20%20%20%20totalCount%0A%20%20%20%20nodes%20%7B%0A%20%20%20%20%20%20pollster%0A%20%20%20%20%20%20sponsor_ids%0A%20%20%20%20%20%20sponsors%0A%20%20%20%20%20%20partisan%0A%20%20%20%20%20%20display_name%0A%20%20%20%20%20%20pollster_rating_name%0A%20%20%20%20%20%20fte_grade%0A%20%20%20%20%20%20population%0A%20%20%20%20%20%20population_full%0A%20%20%20%20%20%20methodology%0A%20%20%20%20%20%20office_type%0A%20%20%20%20%20%20start_date%0A%20%20%20%20%20%20end_date%0A%20%20%20%20%20%20tracking%0A%20%20%20%20%20%20created_at%0A%20%20%20%20%20%20notes%0A%20%20%20%20%20%20url%0A%20%20%20%20%20%20stage%0A%20%20%20%20%20%20dem%0A%20%20%20%20%20%20rep%0A%20%20%20%20%20%20ind%0A%20%20%20%20%20%20question_id%0A%20%20%20%20%20%20poll_id%0A%20%20%20%20%20%20cycle%0A%20%20%20%20%20%20state%0A%20%20%20%20%20%20pollster_id%0A%20%20%20%20%20%20pollster_rating_id%0A%20%20%20%20%20%20sample_size%0A%20%20%20%20%20%20seat_number%0A%20%20%20%20%20%20sponsor_candidate%0A%20%20%20%20%20%20nationwide_batch%0A%20%20%20%20%20%20rowid%0A%20%20%20%20%7D%0A%20%20%7D%0A%7D%0A"&gt;example query&lt;/a&gt;.&lt;/p&gt;
&lt;h4 id="weeknotes-18-sep-github-to-sqlite-get"&gt;github-to-sqlite get&lt;/h4&gt;
&lt;p&gt;&lt;a href="https://github.com/dogsheep/github-to-sqlite"&gt;github-to-sqlite&lt;/a&gt; lets you fetch data from the GitHub API and write it into a SQLite database.&lt;/p&gt;
&lt;p&gt;This week I &lt;a href="https://github.com/dogsheep/github-to-sqlite/issues/50"&gt;added a sub-command&lt;/a&gt; for hitting the API directly and getting back data on the console, inspired by the &lt;code&gt;fetch&lt;/code&gt; subcommand in &lt;code&gt;twitter-to-sqlite&lt;/code&gt;. This is useful for trying out new APIs, since it both takes into account your GitHub authentication credentials (from an environment variable or an &lt;code&gt;auth.json&lt;/code&gt; file) and can handle &lt;code&gt;Link&lt;/code&gt; header pagination.&lt;/p&gt;
&lt;p&gt;This example fetches all of my repos, paginating across multiple API requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github-to-sqlite get /users/simonw/repos --paginate
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can use the &lt;code&gt;--nl&lt;/code&gt; option to get back the results as newline-delimited JSON. This makes it easy to pipe them directly into &lt;code&gt;sqlite-utils&lt;/code&gt; like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;github-to-sqlite get /users/simonw/repos --paginate --nl \
  | sqlite-utils insert simonw.db repos - --nl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;See &lt;a href="https://sqlite-utils.readthedocs.io/en/stable/cli.html#inserting-json-data"&gt;Inserting JSON data&lt;/a&gt; in the &lt;code&gt;sqlite-utils&lt;/code&gt; documentation for an explanation of what this is doing.&lt;/p&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-actions_debug-tmate.md"&gt;Open a debugging shell in GitHub Actions with tmate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://til.simonwillison.net/til/til/github-actions_service-containers-docker.md"&gt;Talking to a PostgreSQL service container from inside a Docker container&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;a href="https://github.com/dogsheep/dogsheep-beta/releases/tag/0.7.1"&gt;dogsheep-beta 0.7.1&lt;/a&gt; - 2020-09-17&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dogsheep/dogsheep-beta/releases/tag/0.7"&gt;dogsheep-beta 0.7&lt;/a&gt; - 2020-09-17&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/dogsheep/github-to-sqlite/releases/tag/2.6"&gt;github-to-sqlite 2.6&lt;/a&gt; - 2020-09-17&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette/releases/tag/0.49.1"&gt;datasette 0.49.1&lt;/a&gt; - 2020-09-15&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-ics/releases/tag/0.5"&gt;datasette-ics 0.5&lt;/a&gt; - 2020-09-14&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-copyable/releases/tag/0.3"&gt;datasette-copyable 0.3&lt;/a&gt; - 2020-09-14&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-atom/releases/tag/0.8"&gt;datasette-atom 0.8&lt;/a&gt; - 2020-09-14&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-yaml/releases/tag/0.1"&gt;datasette-yaml 0.1&lt;/a&gt; - 2020-09-14&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette/releases/tag/0.49"&gt;datasette 0.49&lt;/a&gt; - 2020-09-14&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette/releases/tag/0.49a1"&gt;datasette 0.49a1&lt;/a&gt; - 2020-09-14&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-seaborn/releases/tag/0.1a1"&gt;datasette-seaborn 0.1a1&lt;/a&gt; - 2020-09-11&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/simonw/datasette-seaborn/releases/tag/0.1a0"&gt;datasette-seaborn 0.1a0&lt;/a&gt; - 2020-09-11&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/visualization"&gt;visualization&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;/p&gt;
    

</summary><category term="projects"/><category term="visualization"/><category term="datasette"/><category term="weeknotes"/></entry><entry><title>Datasette table diagram, now with a DOT graph</title><link href="https://simonwillison.net/2020/May/8/datasette-table-diagram-now-dot-graph/#atom-tag" rel="alternate"/><published>2020-05-08T03:23:32+00:00</published><updated>2020-05-08T03:23:32+00:00</updated><id>https://simonwillison.net/2020/May/8/datasette-table-diagram-now-dot-graph/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://observablehq.com/@simonw/datasette-table-diagram"&gt;Datasette table diagram, now with a DOT graph&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Thomas Ballinger shared a huge improvement to my Observable notebook for rendering a diagram of a collection of Datasette tables. He showed how to use the DOT language to render a full schema digram with arrows joining together the different tables. I’ve applied his changes to my notebook.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/ballingt/status/1258592855787925504"&gt;@ballingt&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&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;/p&gt;



</summary><category term="visualization"/><category term="datasette"/><category term="observable"/></entry><entry><title>Weeknotes: Niche Museums, Kepler, Trees and Streaks</title><link href="https://simonwillison.net/2019/Oct/28/niche-museums-kepler/#atom-tag" rel="alternate"/><published>2019-10-28T22:42:10+00:00</published><updated>2019-10-28T22:42:10+00:00</updated><id>https://simonwillison.net/2019/Oct/28/niche-museums-kepler/#atom-tag</id><summary type="html">
    &lt;h3 id="Niche_Museums_4"&gt;Niche Museums&lt;/h3&gt;

&lt;p&gt;Every now and then someone will ask “so when are you going to build Museums Near Me then?”, based on &lt;a href="https://twitter.com/simonw/status/1171159213436997633"&gt;my obsession with niche museums&lt;/a&gt; and websites like &lt;a href="https://www.owlsnearme.com/"&gt;www.owlsnearme.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For my Strategic Communications course at Stanford last week I had to perform a midterm presentation - a six minute talk to convince my audience of something, accompanied by slides and a handout.&lt;/p&gt;
&lt;p&gt;I chose “you should seek out and explore tiny museums” as my topic, and used it as an excuse to finally start the website!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.niche-museums.com/"&gt;www.niche-museums.com&lt;/a&gt; is the result. It’s a small but growing collection of niche museums (17 so far, mostly in the San Francisco Bay Area) complete with the all important blue “Use my location” button to see museums near you.&lt;/p&gt;
&lt;p&gt;Naturally I built it on &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt;. I’ll be writing more about the implementation (and releasing the underlying code) soon. I also built a new plugin for it, &lt;a href="https://github.com/simonw/datasette-haversine "&gt;datasette-haversine&lt;/a&gt;.&lt;/p&gt;

&lt;h3 id="Mapping_museums_against_Starbucks_16"&gt;Mapping museums against Starbucks&lt;/h3&gt;

&lt;p&gt;I needed a way to emphasize quite how many tiny museums there are in the USA. I decided to do this with a visualization.&lt;/p&gt;
&lt;p&gt;It turns out there are 15,891 branches of Starbucks in the USA… and at least 30,132 museums!&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2019/starbucks.png" alt="15,891 Starbucks" style="max-width: 100%" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2019/museums.png" alt="At least 30.132 museums!" style="max-width: 100%" /&gt;&lt;/p&gt;

&lt;p&gt;I made these maps using a couple of sources.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.alltheplaces.xyz/"&gt;All The Places&lt;/a&gt; is a crowdsourced scraper project which aims to build scrapers for every company that has a “store locator” area of their website. Starbucks has &lt;a href="https://www.starbucks.com/store-locator"&gt;a store locator&lt;/a&gt; and All The Places have &lt;a href="https://github.com/alltheplaces/alltheplaces/blob/master/locations/spiders/starbucks.py"&gt;a scraper for it&lt;/a&gt;, so you can download GeoJSON of every Starbucks. I wrote a quick script to import that GeoJSON into Datasette using sqlite-utils.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://www.imls.gov/"&gt;Institute of Museum and Library Services&lt;/a&gt; is an independent agency of the federal government that supports museums and libraries across the country. They publish a &lt;a href="https://www.imls.gov/research-evaluation/data-collection/museum-data-files"&gt;dataset of Museums in the USA&lt;/a&gt; as a set of CSV files. I used &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; to load those into Datasette, than ran a union query to combine the three files together.&lt;/p&gt;
&lt;p&gt;So I have Datasette instances (with a CSV export feature) for both Starbucks and USA museums, with altitudes and longitudes for each.&lt;/p&gt;
&lt;p&gt;Now how to turn that into a map?&lt;/p&gt;
&lt;p&gt;I turned to my new favourite GIS tool, &lt;a href="https://kepler.gl/"&gt;Kepler&lt;/a&gt;. Kepler is an open source GIS visualization tool released by Uber, based on WebGL. It’s astonishingly powerful and can be used directly in your browser by clicking the “Get Started” button on their website (which I assumed would take you to installation instructions, but no, it loads up the entire tool in your browser).&lt;/p&gt;
&lt;p&gt;You can import millions of points of data into Kepler and it will visualize them for you directly. I used a Datasette query to export the CSVs, then loaded in my Starbucks CSV, exported an image, loaded in the Museums CSV as a separate colour and exported a second image. The whole project ended up taking about 15 minutes. Kepler is a great addition to the toolbelt!&lt;/p&gt;

&lt;h3 id="Animating_the_PGE_outages_40"&gt;Animating the PG&amp;amp;E outages&lt;/h3&gt;

&lt;p&gt;My &lt;a href="https://simonwillison.net/2019/Oct/10/pge-outages/"&gt;PG&amp;amp;E outages scraper&lt;/a&gt; continues to record a snapshot of the PG&amp;amp;E outage map JSON every ten minutes. I’m posting updates to &lt;a href="https://twitter.com/simonw/status/1182440312590848001"&gt;a thread on Twitter&lt;/a&gt;, but discovering Kepler inspired me to look at more sophisticated visualization options.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://medium.com/vis-gl/animating-40-years-of-california-earthquakes-e4ffcdd4a289"&gt;This tutorial&lt;/a&gt; by Giuseppe Macrì tipped me off the the fact that you can use Kepler to animate points against timestamps!&lt;/p&gt;
&lt;p&gt;Here’s the result: a video animation showing how PG&amp;amp;E’s outages have evolved since the 5th of October:&lt;/p&gt;

&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Here&amp;#39;s a video animation of PG&amp;amp;E&amp;#39;s outages from October 5th up until just a few minutes ago &lt;a href="https://t.co/50K3BrROZR"&gt;pic.twitter.com/50K3BrROZR&lt;/a&gt;&lt;/p&gt;- Simon Willison (@simonw) &lt;a href="https://twitter.com/simonw/status/1188612004572880896?ref_src=twsrc%5Etfw"&gt;October 28, 2019&lt;/a&gt;&lt;/blockquote&gt;

&lt;h3 id="Hayes_Valley_Trees_50"&gt;Hayes Valley Trees&lt;/h3&gt;

&lt;p&gt;The city announced plans to cut down 27 ficus trees in our neighborhood in San Francisco. I’ve been working with Natalie to help a small group of citizens organize an appeal, and this weekend I helped run a survey of the affected trees (recording their exact locations in a CSV file) and then built &lt;a href="https://www.hayes-valley-trees.com/"&gt;www.hayes-valley-trees.com&lt;/a&gt; (&lt;a href="https://github.com/simonw/hayes-valley-trees"&gt;source on GitHub&lt;/a&gt;) to link to from fliers attached to each affected tree.&lt;/p&gt;
&lt;p&gt;It started out as &lt;a href="https://glitch.com/~hayes-valley-trees"&gt;a Datasette&lt;/a&gt; (running on Glitch) but since it’s only 27 data points I ended up freezing the data in a static JSON file to avoid having to tolerate any cold start times. The site is deployed as static assets on Zeit Now using their handy &lt;a href="https://zeit.co/github"&gt;GitHub continuous deployment tool&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="Streaks_56"&gt;Streaks&lt;/h3&gt;
&lt;p&gt;It turns out I’m very motivated by streaks: I’m at 342 days for Duolingo Spanish and 603 days for an Apple Watch move streak. Could I apply this to other things in my life?&lt;/p&gt;
&lt;p&gt;I &lt;a href="https://twitter.com/simonw/status/1186824721280593920"&gt;asked on Twitter&lt;/a&gt; and was recommended the &lt;a href="https://streaks.app/"&gt;Streaks iOS app&lt;/a&gt;. It’s beautiful! I’m now tracking streaks for guitar practice, Duolingo, checking email, checking Slack, reading some books and adding a new museum to &lt;a href="http://www.niche-museums.com"&gt;www.niche-museums.com&lt;/a&gt; (if I add one a day I can get from 17 museums today to 382 in a year!)&lt;/p&gt;
&lt;p&gt;It seems to be working pretty well so far. I particularly like their iPhone widget.&lt;/p&gt;

&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2019/streaks-widget.jpg" alt="Streaks widget" style="max-width: 100%" /&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/museums"&gt;museums&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/productivity"&gt;productivity&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/weeknotes"&gt;weeknotes&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/baked-data"&gt;baked-data&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/streaks"&gt;streaks&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/duolingo"&gt;duolingo&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="museums"/><category term="productivity"/><category term="projects"/><category term="visualization"/><category term="weeknotes"/><category term="baked-data"/><category term="streaks"/><category term="duolingo"/></entry><entry><title>kepler.gl</title><link href="https://simonwillison.net/2019/Oct/25/keplergl/#atom-tag" rel="alternate"/><published>2019-10-25T04:16:07+00:00</published><updated>2019-10-25T04:16:07+00:00</updated><id>https://simonwillison.net/2019/Oct/25/keplergl/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://kepler.gl/"&gt;kepler.gl&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Uber built this open source geospatial analysis tool for large-scale data sets, and they offer it as a free hosted online tool—just click Get Started on the site. I uploaded two CSV files with 30,000+ latitude/longitude points in them just now and used Kepler to render them as images.


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



</summary><category term="geospatial"/><category term="visualization"/></entry><entry><title>Thematic map - GIS Wiki</title><link href="https://simonwillison.net/2019/Oct/21/thematic-map-gis-wiki/#atom-tag" rel="alternate"/><published>2019-10-21T02:25:17+00:00</published><updated>2019-10-21T02:25:17+00:00</updated><id>https://simonwillison.net/2019/Oct/21/thematic-map-gis-wiki/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://wiki.gis.com/wiki/index.php/Thematic_map"&gt;Thematic map - GIS Wiki&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is a really useful wiki full of GIS information, and the coverage of different types of thematic maps is particularly thorough.


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



</summary><category term="geospatial"/><category term="visualization"/><category term="wiki"/></entry><entry><title>Exploring Neural Networks with Activation Atlases</title><link href="https://simonwillison.net/2019/Apr/19/exploring-neural-networks-activation-atlases/#atom-tag" rel="alternate"/><published>2019-04-19T02:24:16+00:00</published><updated>2019-04-19T02:24:16+00:00</updated><id>https://simonwillison.net/2019/Apr/19/exploring-neural-networks-activation-atlases/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://distill.pub/2019/activation-atlas/"&gt;Exploring Neural Networks with Activation Atlases&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Another promising attempt at visualizing what’s going on inside a neural network.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/machine-learning"&gt;machine-learning&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;&lt;/p&gt;



</summary><category term="machine-learning"/><category term="visualization"/></entry><entry><title>List of Physical Visualizations</title><link href="https://simonwillison.net/2019/Mar/4/list-of-physical-visualizations/#atom-tag" rel="alternate"/><published>2019-03-04T02:45:52+00:00</published><updated>2019-03-04T02:45:52+00:00</updated><id>https://simonwillison.net/2019/Mar/4/list-of-physical-visualizations/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://dataphys.org/list/"&gt;List of Physical Visualizations&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“A chronological list of physical visualizations and related artifacts, maintained by Pierre Dragicevic and Yvonne Jansen”—327 and counting!

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/dvhdr/status/1102307310645460992"&gt;@dvhdr&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="visualization"/></entry><entry><title>datasette-vega</title><link href="https://simonwillison.net/2018/Jun/29/datasette-vega/#atom-tag" rel="alternate"/><published>2018-06-29T15:00:48+00:00</published><updated>2018-06-29T15:00:48+00:00</updated><id>https://simonwillison.net/2018/Jun/29/datasette-vega/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/datasette-vega"&gt;datasette-vega&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I wrote a visualization plugin for Datasette that uses the excellent Vega “visualization grammar” library to provide bar, line and scatter charts configurable against any Datasette table or SQL query.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1012693000022036482"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="plugins"/><category term="projects"/><category term="visualization"/><category term="datasette"/></entry><entry><title>Datasette plugins, and building a clustered map visualization</title><link href="https://simonwillison.net/2018/Apr/20/datasette-plugins/#atom-tag" rel="alternate"/><published>2018-04-20T15:41:11+00:00</published><updated>2018-04-20T15:41:11+00:00</updated><id>https://simonwillison.net/2018/Apr/20/datasette-plugins/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; now supports plugins!&lt;/p&gt;
&lt;p&gt;Last Saturday &lt;a href="https://twitter.com/simonw/status/985377670388105216"&gt;I asked Twitter&lt;/a&gt; for examples of Python projects with successful plugin ecosystems. &lt;a href="https://docs.pytest.org/"&gt;pytest&lt;/a&gt; was the clear winner: the &lt;a href="https://plugincompat.herokuapp.com/"&gt;pytest plugin compatibility table&lt;/a&gt; (an ingenious innovation that I would love to eventually copy for Datasette) lists 457 plugins, and even the core pytest system itself is built as a collection of default plugins that can be replaced or over-ridden.&lt;/p&gt;
&lt;p&gt;Best of all: pytest’s plugin mechanism is available as a separate package: &lt;a href="https://pluggy.readthedocs.io/"&gt;pluggy&lt;/a&gt;. And pluggy was exactly what I needed for Datasette.&lt;/p&gt;
&lt;p&gt;You can follow the ongoing development of the feature in issue &lt;a href="https://github.com/simonw/datasette/issues/14"&gt;#14&lt;/a&gt;. This morning I released &lt;a href="https://github.com/simonw/datasette/releases/tag/0.20"&gt;Datasette 0.20&lt;/a&gt; with support for a number of different plugin hooks: plugins can add custom template tags and SQL functions, and can also bundle their own static assets, JavaScript, CSS and templates. The hooks are described in some detail in the &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html"&gt;Datasette Plugins&lt;/a&gt; documentation.&lt;/p&gt;
&lt;h2&gt;&lt;a id="datasetteclustermap_10"&gt;&lt;/a&gt;datasette-cluster-map&lt;/h2&gt;
&lt;p&gt;I also released my first plugin: &lt;a href="https://pypi.org/project/datasette-cluster-map/"&gt;datasette-cluster-map&lt;/a&gt;. Once installed, it looks out for database tables that have a &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt; column. When it finds them, it draws all of the points on an interactive map using &lt;a href="http://leafletjs.com/"&gt;Leaflet&lt;/a&gt; and &lt;a href="https://github.com/Leaflet/Leaflet.markercluster"&gt;Leaflet.markercluster&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let’s &lt;a href="https://datasette-cluster-map-demo.now.sh/polar-bears-455fe3a/USGS_WC_eartags_output_files_2009-2011-Status"&gt;try it out on some polar bears&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2018/polar-bears-cluster-map.gif" alt="Polar Bears on a cluster map"/&gt;&lt;/p&gt;
&lt;p&gt;The USGS Alaska Science Center have released a delightful set of data entitled &lt;a href="https://alaska.usgs.gov/products/data.php?dataid=130"&gt;Sensor and Location data from Ear Tag PTTs Deployed on Polar Bears in the Southern Beaufort Sea 2009 to 2011&lt;/a&gt;. It’s a collection of CSV files, which means it’s &lt;a href="https://gist.github.com/simonw/9f8bf23b37a42d7628c4dcc4bba10253"&gt;trivial to convert it to SQLite&lt;/a&gt; using my &lt;a href="https://github.com/simonw/csvs-to-sqlite"&gt;csvs-to-sqlite&lt;/a&gt; tool.&lt;/p&gt;
&lt;p&gt;Having created the SQLite database, we can deploy it to a hosting account on &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt; alongside the new plugin like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Make sure we have the latest datasette
pip3 install datasette --upgrade
# Deploy polar-bears.db to now with an increased default page_size
datasette publish now \
    --install=datasette-cluster-map \
    --extra-options &amp;quot;--page_size=500&amp;quot; \
    polar-bears.db
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;--install&lt;/code&gt; option is new in Datasette 0.20 (it works for &lt;code&gt;datasette publish heroku&lt;/code&gt; as well) - it tells the publishing provider to &lt;code&gt;pip install&lt;/code&gt; the specified package. You can use it more than once to install multiple plugins, and it accepts a path to a zip file in addition to the name of a PyPI package.&lt;/p&gt;
&lt;p&gt;Explore the full demo at &lt;a href="https://datasette-cluster-map-demo.now.sh/polar-bears"&gt;https://datasette-cluster-map-demo.now.sh/polar-bears&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;&lt;a id="Visualize_any_query_on_a_map_35"&gt;&lt;/a&gt;Visualize any query on a map&lt;/h2&gt;
&lt;p&gt;Since the plugin inserts itself at the top of any Datasette table view with &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt; columns, there are all sorts of neat tricks you can do with it.&lt;/p&gt;
&lt;p&gt;I also loaded the San Francisco tree list (thanks, &lt;a href="https://data.sfgov.org/City-Infrastructure/Street-Tree-List/tkzw-k3nq"&gt;Department of Public Works&lt;/a&gt;) into the demo. Impressively, you can click “load all” &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List"&gt;on this page&lt;/a&gt; and &lt;code&gt;Leaflet.markercluster&lt;/code&gt; will load in all 189,144 points and display them on the same map… and it works fine on my laptop and my phone. Computers in 2018 are pretty good!&lt;/p&gt;
&lt;p&gt;But since it’s a Datasette table, we can filter it. Here’s a map of &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List?qSpecies=2"&gt;every New Zealand Xmas Tree&lt;/a&gt; in San Francisco (8,683 points). Here’s &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List?qCareAssistant=1"&gt;every tree where the Caretaker is Friends of the Urban Forest&lt;/a&gt;. Here’s &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1/Street_Tree_List?PlantDate__contains=1990&amp;amp;_search=palm&amp;amp;_sort=qLegalStatus"&gt;every palm tree planted in 1990&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2018/datasette-palm-trees-1990.png" alt="Palm trees planted in 1990" /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update:&lt;/strong&gt; This is an incorrect example: there are 21 matches on "palm avenue" because the FTS search index covers the address field - they're not actually palm trees. Here's a corrected query for &lt;a href="https://datasette-cluster-map-demo.now.sh/sf-trees-02c8ef1?sql=select+PlantDate%2C+Street_Tree_List.rowid%2C+latitude%2C+longitude%2C+qSpecies.value%0D%0Afrom+Street_Tree_List+join+qSpecies+on+Street_Tree_List.qSpecies+%3D+qSpecies.id%0D%0Awhere+qSpecies.value+like+%22%25palm%25%22+and+PlantDate+like+%22%251990%25%22"&gt;palm trees planted in 1990&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The plugin currently only works against columns called &lt;code&gt;latitude&lt;/code&gt; and &lt;code&gt;longitude&lt;/code&gt;… but if your columns are called something else, don’t worry: you can craft a custom SQL query that aliases your columns and everything will work as intended. Here’s an example &lt;a href="https://datasette-cluster-map-demo.now.sh/polar-bears-455fe3a?sql=select+*%2C+%22Capture+Latitude%22+as+latitude%2C+%22Capture+Longitude%22+as+longitude%0D%0Afrom+%5BUSGS_WC_eartag_deployments_2009-2011%5D"&gt;against some more polar bear data&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select *, &amp;quot;Capture Latitude&amp;quot; as latitude, &amp;quot;Capture Longitude&amp;quot; as longitude
from [USGS_WC_eartag_deployments_2009-2011]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;&lt;a id="Writing_your_own_plugins_50"&gt;&lt;/a&gt;Writing your own plugins&lt;/h2&gt;
&lt;p&gt;I’m really excited to see what people invent. If you want to have a go, your first stop should be the &lt;a href="https://datasette.readthedocs.io/en/latest/plugins.html"&gt;Plugins documentation&lt;/a&gt;. If you want an example of a simple plugin (including the all-important mechanism for packaging it up using &lt;code&gt;setup.py&lt;/code&gt;) take a look at &lt;a href="https://github.com/simonw/datasette-cluster-map"&gt;datasette-cluster-map&lt;/a&gt; on GitHub.&lt;/p&gt;
&lt;p&gt;And if you have any thoughts, ideas or suggestions on how the plugin mechanism can be further employed please join the conversation on &lt;a href="https://github.com/simonw/datasette/issues/14"&gt;issue #14&lt;/a&gt;. I’ve literally just got started with Datasette’s plugin hooks, and I’m very keen to hear about things people want to build that aren’t yet supported.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/maps"&gt;maps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pytest"&gt;pytest&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="maps"/><category term="plugins"/><category term="projects"/><category term="visualization"/><category term="datasette"/><category term="pytest"/><category term="leaflet"/></entry><entry><title>Visualizing disk IO activity using log-scale banded graphs</title><link href="https://simonwillison.net/2018/Apr/11/visualizing-disk-io-activity-using-log-scale-banded-graphs/#atom-tag" rel="alternate"/><published>2018-04-11T17:04:45+00:00</published><updated>2018-04-11T17:04:45+00:00</updated><id>https://simonwillison.net/2018/Apr/11/visualizing-disk-io-activity-using-log-scale-banded-graphs/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://bvckup2.com/wip/10042018"&gt;Visualizing disk IO activity using log-scale banded graphs&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
This is a neat data visualization trick: to display rates of disk I/O, it splits the rate into a GB, MB and KB section on a stacked chart. This means that if you are getting jitter in the order of KBs even while running at 400+MB/second you can see the jitter in the KB section.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=16812288"&gt;Show HN&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


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



</summary><category term="visualization"/></entry><entry><title>Vega-Lite</title><link href="https://simonwillison.net/2018/Mar/28/vega-lite/#atom-tag" rel="alternate"/><published>2018-03-28T17:22:05+00:00</published><updated>2018-03-28T17:22:05+00:00</updated><id>https://simonwillison.net/2018/Mar/28/vega-lite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://vega.github.io/vega-lite/"&gt;Vega-Lite&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A “high-level grammar of interactive graphics”. Part of the Vega project, which provides a mechanism for creating declarative visualizations by defining them using JSON. Vega-Lite is particularly interesting to me because it makes extremely tasteful decisions about how data should be visualized—give it some records, tell it which properties to plot on an axis and it will default to a display that makes sense for that data. The more I play with this the more impressed I am at the quality of its default settings.


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



</summary><category term="visualization"/></entry><entry><title>Observable: An Earthquake Globe in Ten Minutes</title><link href="https://simonwillison.net/2018/Jan/31/observable-earthquake-globe-ten-minutes/#atom-tag" rel="alternate"/><published>2018-01-31T17:01:16+00:00</published><updated>2018-01-31T17:01:16+00:00</updated><id>https://simonwillison.net/2018/Jan/31/observable-earthquake-globe-ten-minutes/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=uEmDwflQ3xE"&gt;Observable: An Earthquake Globe in Ten Minutes&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Well worth your time. Jeremy Ashkenas uses Observable to live-code an interactive visualization of recent earthquakes around the world, using USGS data (fetched as JSON), d3, topoJSON and an Observable notebook. I’m sold—this is truly ground-breaking new technology.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jeremy-ashkenas"&gt;jeremy-ashkenas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/explorables"&gt;explorables&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/d3"&gt;d3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="jeremy-ashkenas"/><category term="visualization"/><category term="explorables"/><category term="d3"/><category term="observable"/></entry><entry><title>Exploring Line Lengths in Python Packages</title><link href="https://simonwillison.net/2017/Nov/10/exploring-line-lengths-in-python-packages/#atom-tag" rel="alternate"/><published>2017-11-10T15:34:29+00:00</published><updated>2017-11-10T15:34:29+00:00</updated><id>https://simonwillison.net/2017/Nov/10/exploring-line-lengths-in-python-packages/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://jakevdp.github.io/blog/2017/11/09/exploring-line-lengths-in-python-packages/"&gt;Exploring Line Lengths in Python Packages&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Interesting exploration of the impact if the 79 character length limit rule of thumb on various Python packages—and a thoroughly useful guide to histogram plotting in Jupyter, pandas and matplotlib.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pandas"&gt;pandas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jupyter"&gt;jupyter&lt;/a&gt;&lt;/p&gt;



</summary><category term="pandas"/><category term="python"/><category term="visualization"/><category term="jupyter"/></entry><entry><title>Generating interactive HTML charts from Python?</title><link href="https://simonwillison.net/2016/Nov/25/generating-interactive-html-charts-from/#atom-tag" rel="alternate"/><published>2016-11-25T20:05:00+00:00</published><updated>2016-11-25T20:05:00+00:00</updated><id>https://simonwillison.net/2016/Nov/25/generating-interactive-html-charts-from/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="http://ask.metafilter.com/303041/Generating-interactive-HTML-charts-from-Python#4388982"&gt;Generating interactive HTML charts from Python?&lt;/a&gt; on Ask MetaFilter&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;D3 is absolutely amazing but the learning curve is a bit steep. Totally worth the effort to learn it in the long run, but it's not so useful if you want to get something done quickly.&lt;/p&gt;
&lt;p&gt;I've used &lt;a href="http://nvd3.org"&gt;NVD3&lt;/a&gt; successfully in the past - it's another high level library on top of D3. Much faster to get results than D3 on its own.&lt;/p&gt;
&lt;p&gt;From your description it sounds like you should also check out &lt;a href="http://square.github.io/crossfilter/"&gt;crossfilter&lt;/a&gt; which, for the right use-cases, is phenomenal.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/ask-metafilter"&gt;ask-metafilter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/bokeh"&gt;bokeh&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pandas"&gt;pandas&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plotly"&gt;plotly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datavis"&gt;datavis&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="ask-metafilter"/><category term="bokeh"/><category term="pandas"/><category term="python"/><category term="visualization"/><category term="plotly"/><category term="datavis"/></entry><entry><title>Visualization Strategies: Text &amp; Documents</title><link href="https://simonwillison.net/2008/Aug/22/visualization/#atom-tag" rel="alternate"/><published>2008-08-22T11:17:22+00:00</published><updated>2008-08-22T11:17:22+00:00</updated><id>https://simonwillison.net/2008/Aug/22/visualization/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.timshowers.com/2008/08/visualization-strategies-text-documents/"&gt;Visualization Strategies: Text &amp;amp; Documents&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“List of ...” style posts usually make me want to stab someone with a fork; this is how that kind of post should be done—well researched, carefully written and, most importantly doesn’t call itself a “Top X Ys that will Z your ZZ”!


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



</summary><category term="lists"/><category term="visualization"/></entry><entry><title>Designing Google Reader's trends</title><link href="https://simonwillison.net/2007/Jan/15/designing/#atom-tag" rel="alternate"/><published>2007-01-15T00:53:23+00:00</published><updated>2007-01-15T00:53:23+00:00</updated><id>https://simonwillison.net/2007/Jan/15/designing/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.veen.com/jeff/archives/000955.html"&gt;Designing Google Reader&amp;#x27;s trends&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
“But beyond the visualization, this serves as a good example of collecting and understanding the ambient information that flows through our digital lives.”


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/design"&gt;design&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google"&gt;google&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/google-reader"&gt;google-reader&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jeffrey-veen"&gt;jeffrey-veen&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/visualization"&gt;visualization&lt;/a&gt;&lt;/p&gt;



</summary><category term="design"/><category term="google"/><category term="google-reader"/><category term="jeffrey-veen"/><category term="visualization"/></entry></feed>