├── .gitignore ├── docs ├── data │ ├── observable-latency.parquet.sh │ ├── flights-200k.parquet │ ├── seattle-weather.parquet │ ├── gaia.parquet.sh │ └── nyc-taxi.parquet.sh ├── components │ └── mosaic.js ├── style.css ├── nyc-taxi-rides.md ├── gaia-star-catalog.md ├── mosaic-duckdb-wasm.md ├── index.md ├── data-loading.md ├── observable-latency.md └── flight-delays.md ├── package.json ├── README.md ├── observablehq.config.ts ├── LICENSE └── .github └── workflows └── deploy.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | docs/.observablehq/cache/ 4 | node_modules/ 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /docs/data/observable-latency.parquet.sh: -------------------------------------------------------------------------------- 1 | curl https://idl.uw.edu/mosaic-datasets/data/observable-latency.parquet 2 | -------------------------------------------------------------------------------- /docs/data/flights-200k.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/mosaic-framework-example/main/docs/data/flights-200k.parquet -------------------------------------------------------------------------------- /docs/data/seattle-weather.parquet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uwdata/mosaic-framework-example/main/docs/data/seattle-weather.parquet -------------------------------------------------------------------------------- /docs/components/mosaic.js: -------------------------------------------------------------------------------- 1 | import * as vg from "npm:@uwdata/vgplot"; 2 | 3 | export async function vgplot(queries) { 4 | const mc = vg.coordinator(); 5 | const api = vg.createAPIContext({ coordinator: mc }); 6 | mc.databaseConnector(vg.wasmConnector()); 7 | if (queries) { 8 | await mc.exec(queries(api)); 9 | } 10 | return api; 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mosaic-framework-example", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "clean": "rm -rf docs/.observablehq/cache", 7 | "build": "rm -rf dist && observable build", 8 | "dev": "observable preview", 9 | "deploy": "observable deploy", 10 | "observable": "observable" 11 | }, 12 | "dependencies": { 13 | "@observablehq/framework": "^1.9.0" 14 | }, 15 | "engines": { 16 | "node": ">=18" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mosaic + Framework Examples 2 | 3 | This site shares examples of integrating [Mosaic](https://idl.uw.edu/mosaic) and DuckDB into [Observable Framework](https://observablehq.com/framework). 4 | 5 | **[View the deployed examples](https://idl.uw.edu/mosaic-framework-example)** 6 | 7 | The examples demonstrate: 8 | 9 | - Visualization and real-time interaction with massive data sets 10 | - Using Mosaic and DuckDB-WASM within Framework pages 11 | - Using DuckDB within a data loader and configuring GitHub Actions 12 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | @import url("observablehq:default.css"); 2 | @import url("observablehq:theme-air.css"); 3 | 4 | #observablehq-header .banner { 5 | display: flex; 6 | align-items: center; 7 | justify-content: end; 8 | gap: 0.5rem; 9 | height: 2.2rem; 10 | margin: -1.5rem -2rem 2rem -2rem; 11 | padding: 0.5rem 2rem; 12 | border-bottom: solid 1px var(--theme-foreground-faintest); 13 | font: 500 14px var(--sans-serif); 14 | } 15 | 16 | #observablehq-header a[href] { 17 | color: inherit; 18 | } 19 | 20 | #observablehq-header a[target="_blank"]:hover span { 21 | text-decoration: underline; 22 | } 23 | 24 | #observablehq-header a[target="_blank"]:not(:hover, :focus)::after { 25 | color: var(--theme-foreground-muted); 26 | } 27 | 28 | .input label { 29 | margin-right: 0.5em; 30 | } 31 | -------------------------------------------------------------------------------- /docs/data/gaia.parquet.sh: -------------------------------------------------------------------------------- 1 | # The DuckDB executable must be on your environment path! 2 | # Write to a named file as portable file descriptors such as 3 | # (/dev/stdout) appear to be unavailable in GitHub actions. 4 | duckdb :memory: << EOF 5 | -- Compute u and v coordinates via natural earth projection 6 | CREATE TABLE gaia AS 7 | WITH prep AS ( 8 | SELECT 9 | radians((-l + 540) % 360 - 180) AS lambda, 10 | radians(b) AS phi, 11 | asin(sqrt(3)/2 * sin(phi)) AS t, 12 | t^2 AS t2, 13 | t2^3 AS t6, 14 | * 15 | FROM 'https://idl.uw.edu/mosaic-datasets/data/gaia-5m.parquet' 16 | ) 17 | SELECT 18 | ( 19 | (1.340264 * lambda * cos(t)) / 20 | (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) 21 | )::FLOAT AS u, 22 | (t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))))::FLOAT AS v, 23 | bp_rp::FLOAT AS bp_rp, 24 | phot_g_mean_mag::FLOAT AS phot_g_mean_mag, 25 | parallax::FLOAT AS parallax 26 | FROM prep 27 | WHERE parallax BETWEEN -5 AND 20; 28 | 29 | -- Write output parquet file 30 | COPY gaia TO 'gaia.parquet' WITH (FORMAT PARQUET); 31 | EOF 32 | 33 | cat gaia.parquet >&1 # Write output to stdout 34 | rm gaia.parquet # Clean up 35 | -------------------------------------------------------------------------------- /docs/data/nyc-taxi.parquet.sh: -------------------------------------------------------------------------------- 1 | # The DuckDB executable must be on your environment path! 2 | # Use DuckDB version 0.9.2 or later 3 | # Write to a named file as portable file descriptors such as 4 | # (/dev/stdout) appear to be unavailable in GitHub actions. 5 | duckdb :memory: << EOF 6 | -- Load spatial extension 7 | INSTALL spatial; LOAD spatial; 8 | 9 | -- Project, following the example at https://github.com/duckdb/duckdb_spatial 10 | CREATE TEMP TABLE rides AS SELECT 11 | pickup_datetime::TIMESTAMP AS datetime, 12 | ST_Transform(ST_Point(pickup_latitude, pickup_longitude), 'EPSG:4326', 'ESRI:102718') AS pick, 13 | ST_Transform(ST_Point(dropoff_latitude, dropoff_longitude), 'EPSG:4326', 'ESRI:102718') AS drop 14 | FROM 'https://idl.uw.edu/mosaic-datasets/data/nyc-rides-2010.parquet'; 15 | 16 | -- Write output parquet file 17 | COPY (SELECT 18 | HOUR(datetime) + MINUTE(datetime) / 60 AS time, 19 | ST_X(pick)::INTEGER AS px, -- extract pickup x-coord 20 | ST_Y(pick)::INTEGER AS py, -- extract pickup y-coord 21 | ST_X(drop)::INTEGER AS dx, -- extract dropff x-coord 22 | ST_Y(drop)::INTEGER AS dy -- extract dropff y-coord 23 | FROM rides) TO 'trips.parquet' WITH (FORMAT PARQUET); 24 | EOF 25 | 26 | cat trips.parquet >&1 # Write output to stdout 27 | rm trips.parquet # Clean up 28 | -------------------------------------------------------------------------------- /observablehq.config.ts: -------------------------------------------------------------------------------- 1 | // See https://observablehq.com/framework/config for documentation. 2 | export default { 3 | // The project’s title; used in the sidebar and webpage titles. 4 | title: "Mosaic + Framework", 5 | 6 | // The pages and sections in the sidebar. If you don’t specify this option, 7 | // all pages will be listed in alphabetical order. Listing pages explicitly 8 | // lets you organize them into sections and have unlisted pages. 9 | pages: [ 10 | { 11 | name: "Example Articles", 12 | pages: [ 13 | {name: "Flight Delays", path: "/flight-delays"}, 14 | {name: "NYC Taxi Rides", path: "/nyc-taxi-rides"}, 15 | {name: "Gaia Star Catalog", path: "/gaia-star-catalog"}, 16 | {name: "Observable Latency", path: "/observable-latency"}, 17 | ] 18 | }, 19 | { 20 | name: "Implementation Notes", 21 | pages: [ 22 | {name: "Data Loading with DuckDB", path: "/data-loading"}, 23 | {name: "Mosaic & DuckDB-WASM", path: "/mosaic-duckdb-wasm"} 24 | ] 25 | } 26 | ], 27 | 28 | // Some additional configuration options and their defaults: 29 | // theme: "default", // try "light", "dark", "slate", etc. 30 | style: "style.css", 31 | footer: `Interactive Data Lab, University of Washington`, 32 | toc: false, // whether to show the table of contents 33 | pager: true, // whether to show previous & next links in the footer 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, UW Interactive Data Lab 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: "pages" 16 | cancel-in-progress: false 17 | 18 | jobs: 19 | deploy: 20 | runs-on: ubuntu-latest 21 | 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | 26 | steps: 27 | - name: Install DuckDB CLI 28 | run: | 29 | wget https://github.com/duckdb/duckdb/releases/download/v0.10.0/duckdb_cli-linux-amd64.zip 30 | unzip duckdb_cli-linux-amd64.zip 31 | mkdir /opt/duckdb && mv duckdb /opt/duckdb && chmod +x /opt/duckdb/duckdb && sudo ln -s /opt/duckdb/duckdb /usr/bin/duckdb 32 | rm duckdb_cli-linux-amd64.zip 33 | 34 | - uses: actions/checkout@v4 35 | with: 36 | fetch-depth: 0 37 | 38 | - uses: actions/setup-node@v4 39 | with: 40 | node-version: "20.x" 41 | cache: "npm" 42 | 43 | - name: Install Node dependencies 44 | run: npm ci 45 | 46 | - id: date 47 | run: echo "date=$(TZ=America/Los_Angeles date +'%Y-%m-%d')" >> $GITHUB_OUTPUT 48 | 49 | - id: cache-data 50 | uses: actions/cache@v4 51 | with: 52 | path: | 53 | docs/.observablehq/cache 54 | key: data-${{ hashFiles('docs/data/*') }}-${{ steps.date.outputs.date }} 55 | 56 | - if: steps.cache-data.outputs.cache-hit == 'true' 57 | run: find docs/.observablehq/cache -type f -exec touch {} + 58 | 59 | - name: Build 60 | run: npm run build 61 | 62 | - uses: actions/configure-pages@v4 63 | - uses: actions/upload-pages-artifact@v3 64 | with: 65 | path: dist 66 | - name: Deploy 67 | id: deployment 68 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /docs/nyc-taxi-rides.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NYC Taxi Rides 3 | header: | 4 |
7 | sql: 8 | trips: data/nyc-taxi.parquet 9 | --- 10 | 11 | # NYC Taxi Rides 12 | ## Pickup and dropoff points for 1M NYC taxi rides on Jan 1-3, 2010. 13 | 14 | Using a data loader, we ingest a remote file into DuckDB and project [_longitude_, _latitude_] coordinates (in the database!) to spatial positions with units of feet (1 foot = 12 inches). 15 | We then load the prepared data to visualize taxi pickup and dropoff locations, as well as the volume of rides by the time of day. 16 | 17 | _Please wait a few seconds for the dataset to load._ 18 | 19 | ```js 20 | const $filter = vg.Selection.crossfilter(); 21 | 22 | const defaultAttributes = [ 23 | vg.width(335), 24 | vg.height(550), 25 | vg.margin(0), 26 | vg.xAxis(null), 27 | vg.yAxis(null), 28 | vg.xDomain([975000, 1005000]), 29 | vg.yDomain([190000, 240000]), 30 | vg.colorScale("symlog") 31 | ]; 32 | ``` 33 | 34 | ```js 35 | vg.hconcat( 36 | vg.plot( 37 | vg.raster( 38 | vg.from("trips", { filterBy: $filter }), 39 | { x: "px", y: "py", imageRendering: "pixelated" } 40 | ), 41 | vg.intervalXY({ as: $filter }), 42 | vg.text( 43 | [{label: "Taxi Pickups"}], 44 | { 45 | dx: 10, 46 | dy: 10, 47 | text: "label", 48 | fill: "black", 49 | fontSize: "1.2em", 50 | frameAnchor: "top-left" 51 | } 52 | ), 53 | ...defaultAttributes, 54 | vg.colorScheme("turbo") 55 | ), 56 | vg.hspace(10), 57 | vg.plot( 58 | vg.raster( 59 | vg.from("trips", { filterBy: $filter }), 60 | { x: "dx", y: "dy", imageRendering: "pixelated" } 61 | ), 62 | vg.intervalXY({ as: $filter }), 63 | vg.text( 64 | [{label: "Taxi Dropoffs"}], 65 | { 66 | dx: 10, 67 | dy: 10, 68 | text: "label", 69 | fill: "black", 70 | fontSize: "1.2em", 71 | frameAnchor: "top-left" 72 | } 73 | ), 74 | ...defaultAttributes, 75 | vg.colorScheme("turbo") 76 | ) 77 | ) 78 | ``` 79 | 80 | ```js 81 | vg.plot( 82 | vg.rectY( 83 | vg.from("trips"), 84 | { x: vg.bin("time"), y: vg.count(), inset: 0.5 } 85 | ), 86 | vg.intervalX({ as: $filter }), 87 | vg.yTickFormat("s"), 88 | vg.xLabel("Pickup Hour"), 89 | vg.yLabel("Number of Rides"), 90 | vg.width(680), 91 | vg.height(100) 92 | ) 93 | ``` 94 | 95 | Select an interval in a plot to filter the maps. 96 | _What spatial patterns can you find?_ 97 | -------------------------------------------------------------------------------- /docs/gaia-star-catalog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Gaia Star Catalog 3 | header: | 4 | 7 | sql: 8 | gaia: data/gaia.parquet 9 | --- 10 | 11 | # Gaia Star Catalog 12 | ## Explore a 5M record sample of the 1.8B star catalog 13 | 14 | [Gaia](https://gea.esac.esa.int/archive/) is a European space mission providing astrometry, photometry, and spectroscopy of nearly 2000 million stars in the Milky Way as well as significant samples of extragalactic and solar system objects. 15 | 16 | Here we visualize a 5M star sample. 17 | A raster sky map reveals our Milky Way galaxy. 18 | Select higher parallax (≥ 6) stars in the histogram to reveal a [Hertzsprung-Russel diagram](https://en.wikipedia.org/wiki/Hertzsprung%E2%80%93Russell_diagram) in the plot of stellar color vs. magnitude on the right. 19 | 20 | ```js 21 | const $brush = vg.Selection.crossfilter(); 22 | ``` 23 | 24 | ```js 25 | vg.hconcat( 26 | vg.vconcat( 27 | vg.plot( 28 | vg.raster( 29 | vg.from("gaia", {filterBy: $brush}), 30 | { x: "u", y: "v", fill: "density", pixelSize: 2 } 31 | ), 32 | vg.intervalXY({pixelSize: 2, as: $brush}), 33 | vg.xyDomain(vg.Fixed), 34 | vg.colorScale("sqrt"), 35 | vg.colorScheme("viridis"), 36 | vg.xAxis(null), 37 | vg.yAxis(null), 38 | vg.width(560), 39 | vg.height(320), 40 | vg.margins({ top: 20, bottom: 10, left: 5, right: 5 }) 41 | ), 42 | vg.hconcat( 43 | vg.plot( 44 | vg.rectY( 45 | vg.from("gaia", {filterBy: $brush}), 46 | { 47 | x: vg.bin("phot_g_mean_mag"), 48 | y: vg.count(), 49 | fill: "steelblue", 50 | inset: 0.5 51 | } 52 | ), 53 | vg.intervalX({as: $brush}), 54 | vg.xDomain(vg.Fixed), 55 | vg.xTicks(5), 56 | vg.yScale("sqrt"), 57 | vg.yGrid(true), 58 | vg.width(280), 59 | vg.height(180), 60 | vg.marginLeft(65) 61 | ), 62 | vg.plot( 63 | vg.rectY( 64 | vg.from("gaia", {filterBy: $brush}), 65 | {x: vg.bin("parallax"), y: vg.count(), fill: "steelblue", inset: 0.5} 66 | ), 67 | vg.intervalX({as: $brush}), 68 | vg.xDomain(vg.Fixed), 69 | vg.xTicks(5), 70 | vg.yScale("sqrt"), 71 | vg.yGrid(true), 72 | vg.width(280), 73 | vg.height(180), 74 | vg.marginLeft(65) 75 | ) 76 | ) 77 | ), 78 | vg.hspace(10), 79 | vg.plot( 80 | vg.raster( 81 | vg.from("gaia", {filterBy: $brush}), 82 | { x: "bp_rp", y: "phot_g_mean_mag", fill: "density", pixelSize: 2 } 83 | ), 84 | vg.intervalXY({pixelSize: 2, as: $brush}), 85 | vg.xyDomain(vg.Fixed), 86 | vg.colorScale("sqrt"), 87 | vg.colorScheme("viridis"), 88 | vg.xTicks(5), 89 | vg.yReverse(true), 90 | vg.width(320), 91 | vg.height(500), 92 | vg.marginLeft(25), 93 | vg.marginTop(20), 94 | vg.marginRight(1) 95 | ) 96 | ) 97 | ``` 98 | -------------------------------------------------------------------------------- /docs/mosaic-duckdb-wasm.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using Mosaic & DuckDB-WASM 3 | header: | 4 | 7 | --- 8 | 9 | # Using Mosaic & DuckDB-WASM 10 | 11 | Behind the scenes, a number of steps are needed for Mosaic and DuckDB-WASM to "play nice" with Observable's reactive runtime. 12 | Unlike standard JavaScript, the Observable runtime will happily run JavaScript "out-of-order". 13 | Observable uses dependencies among code blocks, rather than the order within the file, to determine what to run and when to run it. 14 | This reactivity can cause problems for code that depends on "side effects" that are not tracked by Observable's runtime. 15 | 16 | In the past, we had to carefully work our way around these side effects when manually loading data and initializing Mosaic. 17 | Fortunately, as of version 1.3.0 onward, Observable Framework includes built-in [DuckDB data loading](https://observablehq.com/framework/sql) and [Mosaic initialization](https://observablehq.com/framework/lib/mosaic) support to handle this for us. 18 | 19 | ## Loading Data into DuckDB-WASM 20 | 21 | Observable supports loading files by simply listing them in a page's YAML front matter under the `sql` key. The following example loads 200,000 flights records into DuckDB-WASM from a backing parquet file: 22 | 23 | ```yaml 24 | --- 25 | sql: 26 | flights: data/flights-200k.parquet 27 | --- 28 | ``` 29 | 30 | Observable ensures `sql` data loading is performed prior to downstream code execution, preventing out-of-order issues. If the data file is produced using a [data loader](https://observablehq.com/framework/loaders), the loader will be invoked, akin to using an Observable `FileAttachment`. 31 | 32 | ## Mosaic vgplot Initialization 33 | 34 | Observable Framework includes [Mosaic vgplot](https://idl.uw.edu/mosaic/what-is-mosaic/) as a "built-in" standard library component. If Observable sees the `vg` variable referenced but not otherwise defined, it automatically imports vgplot and includes it as a dependency. 35 | 36 | Observable Framework will instantiate a new API instance (bound to the `vg` variable) and configure it to use the built-in [DuckDBClient](https://observablehq.com/framework/lib/duckdb) in the Mosaic coordinator's [database connector](https://idl.uw.edu/mosaic/core/#data-source). 37 | 38 | Here's what the internal vgplot initialization looks like: 39 | 40 | ```js run=false 41 | import * as vgplot from "npm:@uwdata/vgplot"; 42 | import {getDefaultClient} from "observablehq:stdlib/duckdb"; 43 | 44 | export default async function vg() { 45 | const coordinator = new vgplot.Coordinator(); 46 | const api = vgplot.createAPIContext({coordinator}); 47 | const duckdb = (await getDefaultClient())._db; 48 | coordinator.databaseConnector(vgplot.wasmConnector({duckdb})); 49 | return api; 50 | } 51 | ``` 52 | 53 | This code first instantiates a new central coordinator, which manages all queries. 54 | It then creates a new API context, which is what ultimately is returned. 55 | 56 | Next, the code configures Mosaic to use DuckDB-WASM as an in-browser database. 57 | Normally the `wasmConnector()` method creates a new database instance in a worker thread, but here we instead pass in Observable's own DuckDB client. 58 | 59 | Once that completes, we're ready to use the API! 60 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mosaic + Framework Examples 3 | header: | 4 | 7 | sql: 8 | weather: data/seattle-weather.parquet 9 | --- 10 | 11 | # Mosaic + Framework Examples 12 | ## Using Mosaic and DuckDB in Observable Framework 13 | 14 | [Mosaic](https://idl.uw.edu/mosaic) is a system for linking data visualizations, tables, and input widgets, all leveraging a database ([DuckDB](https://duckdb.org/)) for scalable processing. With Mosaic, you can interactively visualize and explore millions and even billions of data points. 15 | 16 | This site shows how to publish Mosaic and DuckDB-powered interactive dashboards and data-driven articles using [Observable Framework](https://observablehq.com/framework/). The examples illustrate: 17 | 18 | - Visualization and real-time interaction with massive data sets 19 | - Using Mosaic and DuckDB-WASM within Framework pages 20 | - Using DuckDB in a data loader and in GitHub Actions 21 | 22 | All source markup and code is available at