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.0

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.0
File Size Uploaded Provenance Actions
fjader-0.2.0.tar.gz 737.0 kB Without provenance Details for fjader-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for fjader 0.2.0
File Interpreter ABI Platform Actions
fjader-0.2.0-py3-none-any.whl Python 3 none any Details for fjader-0.2.0-py3-none-any.whl

Total release size: 1.5 MB

Release files / fjader-0.2.0.tar.gz

Download URL fjader-0.2.0.tar.gz
Size 737.0 kB
Tags Source
SHA-256 checksum
How to use checksums
36993fadade4d861e66c197026a4a1d3b2f8a4f7864d2de6f94b7569b125fd79
BLAKE2b-256 checksum
How to use checksums
1d21b3a3904cd062f25295e4d8b768cba379997d036ed1222355382724dd443a
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.0-py3-none-any.whl

Download URL fjader-0.2.0-py3-none-any.whl
Size 743.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
887e5f7ea436b6e4c5acbb47ebdc0d305a9c44a4f4959dfb81a9cae5d8c72ea7
BLAKE2b-256 checksum
How to use checksums
f80ee43258b27f8df39ac94addbdd6581082461107141deeb405a779f20f55fc
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.0

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