Skip to main content

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-core as 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 replaced mosaic-core with 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)

Source distribution for fjader 0.2.2
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)

Table of built distributions (wheels) for fjader 0.2.2
File Interpreter ABI Platform Actions
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page