├── .gitignore ├── vite.config.js ├── index.html ├── package.json ├── README.md └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | token.js 3 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | 3 | export default defineConfig({ 4 | server: { 5 | headers: { 6 | "Cross-Origin-Opener-Policy": "same-origin", 7 | "Cross-Origin-Embedder-Policy": "require-corp", 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mosaic + Motherduck Demo 7 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mosaic-motherduck", 3 | "version": "0.0.1", 4 | "description": "A demo of Mosaic + Motherduck", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "author": "Dominik Moritz", 11 | "license": "BSD-3-Clause", 12 | "dependencies": { 13 | "@motherduck/wasm-client": "^0.4.0", 14 | "@uwdata/vgplot": "^0.7.0" 15 | }, 16 | "devDependencies": { 17 | "vite": "^5.1.6" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mosaic + MotherDuck Demo 2 | 3 | A quick demo application without any additional frameworks of how [Mosaic](https://uwdata.github.io/mosaic/) can work with [MotherDuck](https://motherduck.com) (specifically, the [WebAssembly client library](https://github.com/motherduckdb/wasm-client)). 4 | I built this in an evening which shows how easy it is. It's not super polished but that hopefully makes it easier to understand. 5 | 6 | To run the example, install the dependencies with `npm i`. You will need to create a `token.js` file that exports your MotherDuck API token as `token`. 7 | 8 | ```js 9 | export const token = "..." 10 | ``` 11 | 12 | Then run the server with `npm run dev`. 13 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import { MDConnection } from "@motherduck/wasm-client"; 2 | import * as vg from "@uwdata/vgplot"; 3 | import { token } from "./token.js"; 4 | import * as arrow from "apache-arrow"; 5 | 6 | async function arrowTableFromResult(result) { 7 | if (result.type === "streaming") { 8 | const batches = await result.arrowStream.readAll(); 9 | return new arrow.Table(batches); 10 | } 11 | throw Error("expected streaming result"); 12 | } 13 | 14 | function mdConnector(token) { 15 | const connection = MDConnection.create({ 16 | mdToken: token, 17 | }); 18 | return { 19 | query: async (query) => { 20 | const { sql, type } = query; 21 | const result = await connection.evaluateStreamingQuery(sql); 22 | switch (type) { 23 | case "arrow": 24 | return arrowTableFromResult(result); 25 | case "json": 26 | return Array.from(await arrowTableFromResult(result)); 27 | default: 28 | case "exec": 29 | return undefined; 30 | } 31 | }, 32 | }; 33 | } 34 | 35 | const connector = mdConnector(token); 36 | 37 | const app = document.querySelector("#app"); 38 | 39 | vg.coordinator().databaseConnector(connector); 40 | 41 | const table = "s.main.gaia_sample_1_percent_projected" 42 | 43 | const size = await connector.query({ sql: `SELECT COUNT(*) as cnt FROM ${table.split(".").map(s => `"${s}"`).join(".")}`, type: "arrow" }) 44 | 45 | const count = document.createElement("div") 46 | count.innerHTML = `Number of rows: ${new Intl.NumberFormat('en-US', { maximumSignificantDigits: 3 }).format(size.get(0).cnt)}` 47 | 48 | app.appendChild(count); 49 | 50 | const $brush = vg.Selection.crossfilter(); 51 | const $bandwidth = vg.Param.value(0); 52 | const $pixelSize = vg.Param.value(2); 53 | const $scaleType = vg.Param.value("sqrt"); 54 | 55 | const chart = vg.hconcat( 56 | vg.vconcat( 57 | vg.plot( 58 | vg.raster(vg.from(table, { filterBy: $brush }), { 59 | x: "u", 60 | y: "v", 61 | fill: "density", 62 | bandwidth: $bandwidth, 63 | pixelSize: $pixelSize, 64 | }), 65 | vg.intervalXY({ pixelSize: 2, as: $brush }), 66 | vg.xyDomain(vg.Fixed), 67 | vg.colorScale($scaleType), 68 | vg.colorScheme("viridis"), 69 | vg.width(880), 70 | vg.height(500), 71 | vg.marginLeft(25), 72 | vg.marginTop(20), 73 | vg.marginRight(1) 74 | ), 75 | vg.hconcat( 76 | vg.plot( 77 | vg.rectY(vg.from(table, { filterBy: $brush }), { 78 | x: vg.bin("phot_g_mean_mag"), 79 | y: vg.count(), 80 | fill: "steelblue", 81 | inset: 0.5, 82 | }), 83 | vg.intervalX({ as: $brush }), 84 | vg.xDomain(vg.Fixed), 85 | vg.yScale($scaleType), 86 | vg.yGrid(true), 87 | vg.width(440), 88 | vg.height(240), 89 | vg.marginLeft(65) 90 | ), 91 | vg.plot( 92 | vg.rectY(vg.from(table, { filterBy: $brush }), { 93 | x: vg.bin("parallax"), 94 | y: vg.count(), 95 | fill: "steelblue", 96 | inset: 0.5, 97 | }), 98 | vg.intervalX({ as: $brush }), 99 | vg.xDomain(vg.Fixed), 100 | vg.yScale($scaleType), 101 | vg.yGrid(true), 102 | vg.width(440), 103 | vg.height(240), 104 | vg.marginLeft(65) 105 | ) 106 | ) 107 | ), 108 | vg.hspace(10), 109 | vg.plot( 110 | vg.raster(vg.from(table, { filterBy: $brush }), { 111 | x: "bp_rp", 112 | y: "phot_g_mean_mag", 113 | fill: "density", 114 | bandwidth: $bandwidth, 115 | pixelSize: $pixelSize, 116 | }), 117 | vg.intervalXY({ pixelSize: 2, as: $brush }), 118 | vg.xyDomain(vg.Fixed), 119 | vg.colorScale($scaleType), 120 | vg.colorScheme("viridis"), 121 | vg.yReverse(true), 122 | vg.width(460), 123 | vg.height(740), 124 | vg.marginLeft(25), 125 | vg.marginTop(20), 126 | vg.marginRight(1) 127 | ) 128 | ); 129 | 130 | app.appendChild(chart); 131 | --------------------------------------------------------------------------------