Graphic Walker
Client Side
Use Duckdb Wasm as Computation

Use Duckdb-wasm for computation in GraphicWalker

You can use duckdb-wasm to boost the computation speed for graphic walker.

We provided a package to help you use duckdb-wasm in GraphicWalker.

You can also use any database as a computation engine, by using the gw-dsl-parser to transform our queries to sql.

yarn add @kanaries/gw-dsl-parser

Usage

This Example is in Vite.

computation.ts

import initWasm, { parser_dsl_with_table } from '@kanaries/gw-dsl-parser';
import dslWasm from '@kanaries/gw-dsl-parser/gw_dsl_parser_bg.wasm?url';
 
import * as duckdb from '@duckdb/duckdb-wasm';
import duckdb_wasm from '@duckdb/duckdb-wasm/dist/duckdb-mvp.wasm?url';
import mvp_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-mvp.worker.js?url';
import duckdb_wasm_eh from '@duckdb/duckdb-wasm/dist/duckdb-eh.wasm?url';
import eh_worker from '@duckdb/duckdb-wasm/dist/duckdb-browser-eh.worker.js?url';
 
import { Table, Vector } from 'apache-arrow';
import { bigNumToString } from 'apache-arrow/util/bn';
 
const MANUAL_BUNDLES: duckdb.DuckDBBundles = {
    mvp: {
        mainModule: duckdb_wasm,
        mainWorker: mvp_worker,
    },
    eh: {
        mainModule: duckdb_wasm_eh,
        mainWorker: eh_worker,
    },
};
 
let db: duckdb.AsyncDuckDB;
let inited = false;
 
export async function init() {
    if (inited) return;
    inited = true;
    // Select a bundle based on browser checks
    const bundle = await duckdb.selectBundle(MANUAL_BUNDLES);
 
    const worker_url = URL.createObjectURL(
        new Blob([`importScripts("${bundle.mainWorker!}");`], {
            type: 'text/javascript',
        })
    );
 
    // Instantiate the asynchronus version of DuckDB-Wasm
    const worker = new Worker(worker_url);
    const logger = new duckdb.ConsoleLogger();
    db = new duckdb.AsyncDuckDB(logger, worker);
    await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
    URL.revokeObjectURL(worker_url);
    await initWasm(dslWasm);
}
 
const ArrowToJSON = (v: any): any => {
    if (typeof v === 'object') {
        if (v instanceof Vector) {
            return Array.from(v).map(ArrowToJSON);
        } else {
            return parseInt(bigNumToString(v as any));
        }
    }
    if (typeof v === 'bigint') {
        return Number(v);
    }
    return v;
};
 
const transformData = (table: Table) => {
    return table.toArray().map((r) => Object.fromEntries(Object.entries(r.toJSON()).map(([k, v]) => [k, ArrowToJSON(v)])));
};
 
export async function getComputation(data: any[]) {
    if (data.length === 0) {
        return {
            close: async () => {},
            computation: async () => [],
        };
    }
    const tableName = nanoid().replace('-', '');
    const fileName = `${tableName}.json`;
    const conn = await db.connect();
    await db.registerFileText(fileName, JSON.stringify(data));
    await conn.insertJSONFromPath(fileName, { name: tableName });
    return {
        close: async () => {
            await conn.close();
            await db.dropFile(fileName);
        },
        computation: async (query: any) => {
            const sql = parser_dsl_with_table(tableName, JSON.stringify(query));
            const res = await conn.query(sql).then(transformData);
            return res;
        },
    };
}

app.tsx

import { useMemo, useState, useEffect } from 'react';
import { GraphicWalker } from '@kanaries/graphic-walker';
import { transData } from '@kanaries/graphic-walker/dataSource/utils';
import data from './data.json';
import { init, getComputation } from './computation';
 
export default function App() {
    const { dataSource, fields } = useMemo(() => transData(data), [data]);
    const [computation, setComputation] = useState();
 
    useEffect(() => {
        init().then(() => {
            getComputation(dataSource).then(setComputation);
        });
    }, [dataSource]);
 
    if (!computation) return null;
    return (
        <GraphicWalker computation={computation} fields={fields} />
    );
}

pureRenderer.tsx

import { useMemo, useState, useEffect } from 'react';
import { PureRenderer } from '@kanaries/graphic-walker';
import { transData } from '@kanaries/graphic-walker/dataSource/utils';
import data from './data.json';
import spec from './spec.json';
import { init, getComputation } from './computation';
 
export default function PureRenderer() {
    const { dataSource } = useMemo(() => transData(data), [data]);
    const [computation, setComputation] = useState();
 
    useEffect(() => {
        init().then(() => {
            getComputation(dataSource).then(setComputation);
        });
    }, [dataSource]);
 
    if (!computation) return null;
    return (
        <PureRenderer type="remote" computation={computation} visualConfig={spec[0].config} visualState={spec[0].encodings} visualLayout={spec[0].layout} />
    );
}