├── .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 |
--------------------------------------------------------------------------------