fjader
An experimental charting library for Svelte and Python with a declarative, grammar-of-graphics API, that queries DuckDB and renders with WebGPU. It is not feature complete, and the API may still change.
fjader builds on the ideas of Mosaic: charts are described as marks whose data comes from SQL queries run in DuckDB, and linked charts filter each other through selections. Its marks and channels borrow from Observable Plot, but the API is its own and not compatible with Plot's. Query results arrive as Arrow and are drawn with WebGPU.
There are three ways to use it: Svelte components; a JSON spec, rendered from JavaScript with Fjader.render(element, spec); and a Python package that builds specs and renders them in Jupyter, marimo or Streamlit.
The name comes from Swedish fjäder (say "fyeh-der"), which means both spring and feather.
Install
npm install @fjader/svelte @uwdata/mosaic-sql # Svelte 5 components, and the SQL builder the examples use
pip install fjader # Python: marimo and Jupyter
pip install "fjader[streamlit]" # Python: Streamlit
fjader needs Chrome 149 or later, or another Chromium browser on that version. It relies on two recent features other browsers don't ship yet: WebGPU's immediate_address_space, through which every chart passes its scales to the shaders (so every zoom and pan uses it), and the Temporal API, used for calendar offsets. There is no fallback renderer.
Three things to know before using it in a deployed app:
- Data goes to the browser in full. The Python widget and the Streamlit component send every column of a DataFrame to the browser, where DuckDB-WASM queries it. Anyone viewing the app can extract it, so pass only the columns you mean to show. In Jupyter, saved widget state can also store the data in the notebook file.
- DuckDB-WASM loads from jsDelivr at runtime. The browser fetches it from the CDN, so locked-down networks that block it cannot render charts.
- Specs are trusted input. A spec's
{ "sql": "…" }field runs as raw SQL in the viewer's DuckDB-WASM. Don't build specs from untrusted input.
Svelte
<script>
import {
initFjader,
ChartContainer,
WebGPUCanvas,
AxisLeft,
AxisBottom,
Grid,
BarMarkY,
stack
} from '@fjader/svelte';
import { count, loadParquet } from '@uwdata/mosaic-sql';
import { onMount } from 'svelte';
const { queryEngine } = initFjader();
let loaded = $state(false);
// DuckDB-WASM fetches only full URLs; a bare path means its own file system.
onMount(async () => {
const url = new URL('/data/penguins.parquet', location.href).href;
await queryEngine.exec([loadParquet('penguins', url).toString()]);
loaded = true;
});
</script>
{#if loaded}
<div style="height: 400px">
<ChartContainer xLabel="island" yLabel="count">
{#snippet background()}
<AxisLeft />
<AxisBottom />
{/snippet}
{#snippet canvas()}
<WebGPUCanvas>
{#snippet canvasComponents()}
<Grid />
<BarMarkY
table="penguins"
x="island"
y={count()}
fill="species"
transforms={[stack()]}
/>
{/snippet}
</WebGPUCanvas>
{/snippet}
</ChartContainer>
</div>
{/if}
A chart is a container with layers: axes in the background, marks on the GPU canvas. It fills its parent, so give the parent a height; the Python widget and Streamlit set one for you (400 and 500 px). The same chart can be written as a JSON spec and rendered with Fjader.render(element, spec); the npm package ships Svelte source, so this needs a build that compiles Svelte, such as Vite with the Svelte plugin.
Python
import polars as pl
import fjader as fj
weather = pl.read_parquet("seattle-weather.parquet")
fj.plot(
fj.dot(data=weather, x="date", y="temp_max", fill="weather", r="precipitation"),
x_label="Date",
y_label="Max temperature (°F)",
)
Charts display on their own in marimo and Jupyter; in Streamlit, call fj.streamlit.render(chart). polars DataFrames need nothing more; pandas ones need pyarrow (pip install "fjader[pyarrow]"). Marks compose: * overlays two marks in one chart, + places charts side by side, and df.fj.dot(x=..., y=...) works straight from a pandas or polars DataFrame.
How it works
Queries. Aggregation, binning, filtering, top-N, stacking and normalization compile to SQL and run in DuckDB-WASM in the browser. What reaches JavaScript is sized by the chart, not by the data.
Selections. As in Mosaic, brushes crossfilter the other charts, and pre-aggregated tables binned at pixel resolution keep brushing fast. fjader builds them on the first hover over a chart.
Marks. Charts are built from marks (dots, bars, rects, lines, areas, rules, text, parallel coordinates). Marks declare the scales they need, and charts sharing a variable share its scale.
Rendering. WebGPU shaders draw the marks. A fixed-timestep spring simulation animates domain changes and transitions between states. Level-of-detail rungs move between dots, bins and smoothed density; shift+scroll moves through them.
Project structure
src/lib/
components/ Marks, charts, axes, legends, interaction overlays
query-engine/ DuckDB connection, crossfilter, pre-aggregation
transforms/ Transform pipeline (SQL first, then JS)
scales/ Scale requirements, resolution and shared scale state
gpu/ GPUCoordinator, pipelines, shader libraries
animation/ Spring physics and the fixed-timestep loop
lod/ Level-of-detail rungs
interactors/ Brushes, toggles, tooltips, pan and zoom
spec/ JSON spec renderer and export
python/fjader/ Python API, Jupyter widget, Streamlit component
Development
npm install
npm run fetch-data # Example datasets, from their upstream sources
npm run dev # Dev server with examples and test routes
npm run build # Production build
npm run lint # ESLint + Prettier
npx vitest # JavaScript tests
npm run build:python # Widget and Streamlit bundles, then the Python wheel
The Python package's widget and Streamlit bundles are build output and not in git. In a checkout, run npm run build:all before using the Python package, and again after changing JavaScript code.
The pages under src/routes/ are development and test pages for working on fjader, not documentation. They read the example data that npm run fetch-data downloads.
Acknowledgements
fjader takes ideas from two projects, and in places code from Mosaic:
- Mosaic, by the UW Interactive Data Lab. fjader began as a Mosaic client, with
@uwdata/mosaic-coreas a dependency, and its query layer still follows Mosaic's design: pre-aggregation with sufficient statistics and the crossfilter selection model. The API, interactors and JSON spec layout are modeled on vgplot's, without being compatible with it. As fjader moved to rendering directly in WebGPU, it needed its queries tied more closely to its marks: results shaped for GPU buffers, query timing coupled to interaction frames, and state that follows Svelte 5's reactivity rather than a callback protocol. So it replacedmosaic-corewith its own query layer, adapted from Mosaic's, and still builds every query with@uwdata/mosaic-sql. - Observable Plot, by Observable. Mark names, channel shorthands, stacking conventions and many defaults are modeled on Plot's.
Mosaic is described in Jeffrey Heer and Dominik Moritz, Mosaic: An Architecture for Scalable & Interoperable Data Views, IEEE Transactions on Visualization and Computer Graphics 30 (Proc. VIS 2023), 2024, doi:10.1109/TVCG.2023.3327189.
It also rests on DuckDB and DuckDB-WASM, Flechette, d3 and Svelte.
License
BSD 3-Clause; see LICENSE. NOTICE credits the work fjader adapts and reproduces the licenses of Mosaic and Observable Plot in full. The Python package's bundled JavaScript ships with a THIRD_PARTY_LICENSES.txt covering every package compiled into it.
Metadata
Release files for fjader 0.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| fjader-0.2.2.tar.gz | 738.5 kB | Without provenance | Details for fjader-0.2.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| fjader-0.2.2-py3-none-any.whl | Python 3 | none | any | Details for fjader-0.2.2-py3-none-any.whl |
Total release size: 1.5 MB
Release files / fjader-0.2.2.tar.gz
| Download URL | fjader-0.2.2.tar.gz |
|---|---|
| Size | 738.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22ca7baeaf86943546aead45d0ad93881c8ff529578db630840084f3304aae14
|
|
BLAKE2b-256 checksum How to use checksums |
6c4c123a1248844a797d78401e24389d2e1980ea5819024921e673791fd50051
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / fjader-0.2.2-py3-none-any.whl
| Download URL | fjader-0.2.2-py3-none-any.whl |
|---|---|
| Size | 744.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6af8542103bedaee773a4bf025470824f7b31f04956e4347af34fe5e2eae8077
|
|
BLAKE2b-256 checksum How to use checksums |
a85fa60b879b06b1cd4a3c1b7bf96b3c9977fac2e488674952c2ab3262fd2137
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.18 {"installer":{"name":"uv","version":"0.9.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Security for fjader 0.2.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.