fjader 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. fjader-0.2.0/LICENSE +28 -0
  2. fjader-0.2.0/NOTICE +114 -0
  3. fjader-0.2.0/PKG-INFO +181 -0
  4. fjader-0.2.0/README.md +155 -0
  5. fjader-0.2.0/fjader/__init__.py +98 -0
  6. fjader-0.2.0/fjader/_accessor.py +74 -0
  7. fjader-0.2.0/fjader/_convert.py +36 -0
  8. fjader-0.2.0/fjader/_data.py +86 -0
  9. fjader-0.2.0/fjader/_display.py +97 -0
  10. fjader-0.2.0/fjader/_from_spec.py +131 -0
  11. fjader-0.2.0/fjader/_streamlit.py +137 -0
  12. fjader-0.2.0/fjader/_types.py +82 -0
  13. fjader-0.2.0/fjader/_version.py +1 -0
  14. fjader-0.2.0/fjader/_widget.py +59 -0
  15. fjader-0.2.0/fjader/data.py +12 -0
  16. fjader-0.2.0/fjader/fields.py +177 -0
  17. fjader-0.2.0/fjader/interactors.py +154 -0
  18. fjader-0.2.0/fjader/layout.py +163 -0
  19. fjader-0.2.0/fjader/marks/__init__.py +718 -0
  20. fjader-0.2.0/fjader/marks/_base.py +175 -0
  21. fjader-0.2.0/fjader/marks/area.py +54 -0
  22. fjader-0.2.0/fjader/marks/bar.py +47 -0
  23. fjader-0.2.0/fjader/marks/dot.py +24 -0
  24. fjader-0.2.0/fjader/marks/line.py +29 -0
  25. fjader-0.2.0/fjader/marks/rect.py +69 -0
  26. fjader-0.2.0/fjader/marks/text.py +30 -0
  27. fjader-0.2.0/fjader/pandas.py +20 -0
  28. fjader-0.2.0/fjader/plot.py +210 -0
  29. fjader-0.2.0/fjader/polars.py +20 -0
  30. fjader-0.2.0/fjader/static/THIRD_PARTY_LICENSES.txt +2919 -0
  31. fjader-0.2.0/fjader/static/widget.css +1 -0
  32. fjader-0.2.0/fjader/static/widget.js +40824 -0
  33. fjader-0.2.0/fjader/streamlit_frontend/THIRD_PARTY_LICENSES.txt +3313 -0
  34. fjader-0.2.0/fjader/streamlit_frontend/assets/index-BQspEPyb.css +1 -0
  35. fjader-0.2.0/fjader/streamlit_frontend/assets/index-CAUOm_8A.js +1887 -0
  36. fjader-0.2.0/fjader/streamlit_frontend/index.html +29 -0
  37. fjader-0.2.0/fjader/tests/__init__.py +0 -0
  38. fjader-0.2.0/fjader/tests/conftest.py +0 -0
  39. fjader-0.2.0/fjader/tests/test_accessor.py +93 -0
  40. fjader-0.2.0/fjader/tests/test_composition.py +101 -0
  41. fjader-0.2.0/fjader/tests/test_convert.py +69 -0
  42. fjader-0.2.0/fjader/tests/test_fields.py +137 -0
  43. fjader-0.2.0/fjader/tests/test_from_spec.py +152 -0
  44. fjader-0.2.0/fjader/tests/test_marks.py +215 -0
  45. fjader-0.2.0/fjader/tests/test_serialization.py +170 -0
  46. fjader-0.2.0/fjader/tests/test_transforms.py +64 -0
  47. fjader-0.2.0/fjader/transforms/__init__.py +56 -0
  48. fjader-0.2.0/fjader/transforms/_base.py +25 -0
  49. fjader-0.2.0/fjader/transforms/js.py +40 -0
  50. fjader-0.2.0/fjader/transforms/sql.py +35 -0
  51. fjader-0.2.0/pyproject.toml +39 -0
fjader-0.2.0/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2025-2026, the fjader authors
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
fjader-0.2.0/NOTICE ADDED
@@ -0,0 +1,114 @@
1
+ ================================================================================
2
+ THIRD-PARTY NOTICES
3
+ ================================================================================
4
+
5
+ The projects below shaped fjader's design, and parts of its code are adapted
6
+ from Mosaic. Their licenses follow in full.
7
+
8
+ --------------------------------------------------------------------------------
9
+ Mosaic, by the UW Interactive Data Lab
10
+ https://github.com/uwdata/mosaic
11
+ --------------------------------------------------------------------------------
12
+
13
+ fjader began as a Mosaic client, with @uwdata/mosaic-core as a dependency, and
14
+ its query layer is built on what Mosaic showed works. It now has its own query
15
+ layer, still builds every query with @uwdata/mosaic-sql, a direct dependency,
16
+ and kept Mosaic's ideas and, in places, the structure of Mosaic's code:
17
+
18
+ - Pre-aggregation with sufficient statistics. The preaggregation code in
19
+ src/lib/query-engine/ (sufficient-statistics.ts and preagg/) follows
20
+ mosaic-core's preagg module: aggregates rewritten into partial statistics,
21
+ FILTER clauses carried onto them, and preaggregated tables named by a hash
22
+ of their query.
23
+ - The crossfilter selection model, in which a selection clause filters every
24
+ mark except those its source excludes.
25
+ - vgplot's API style, its interactors (interval brushes, toggles) and its
26
+ JSON spec layout (hconcat, vconcat), without being compatible with vgplot.
27
+
28
+ Mosaic's license, reproduced in full, including its own notice for the parts
29
+ it derives from Observable Plot:
30
+
31
+ BSD 3-Clause License
32
+
33
+ Copyright (c) 2023-2025, UW Interactive Data Lab
34
+
35
+ Redistribution and use in source and binary forms, with or without
36
+ modification, are permitted provided that the following conditions are met:
37
+
38
+ 1. Redistributions of source code must retain the above copyright notice, this
39
+ list of conditions and the following disclaimer.
40
+
41
+ 2. Redistributions in binary form must reproduce the above copyright notice,
42
+ this list of conditions and the following disclaimer in the documentation
43
+ and/or other materials provided with the distribution.
44
+
45
+ 3. Neither the name of the copyright holder nor the names of its
46
+ contributors may be used to endorse or promote products derived from
47
+ this software without specific prior written permission.
48
+
49
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
50
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
51
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
52
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
53
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
54
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
55
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
56
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
57
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
58
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
59
+
60
+ ---
61
+
62
+ Portions of this software are derived from Observable Plot, which is released
63
+ under the ISC license.
64
+
65
+ Copyright 2020-2023 Observable, Inc.
66
+
67
+ Permission to use, copy, modify, and/or distribute this software for any purpose
68
+ with or without fee is hereby granted, provided that the above copyright notice
69
+ and this permission notice appear in all copies.
70
+
71
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
72
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
73
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
74
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
75
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
76
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
77
+ THIS SOFTWARE.
78
+
79
+ --------------------------------------------------------------------------------
80
+ Observable Plot, by Observable, Inc.
81
+ https://github.com/observablehq/plot
82
+ --------------------------------------------------------------------------------
83
+
84
+ fjader's mark API borrows from Observable Plot, though it is its own API and
85
+ not compatible with Plot's: mark names and channels, the shorthands that imply
86
+ stacking (x for x1/x2, y for y1/y2), the stackX/stackY conventions, z as the
87
+ series grouping channel, and many defaults.
88
+
89
+ fjader contains no code from Observable Plot; its license is included as a
90
+ courtesy. Observable Plot's license (ISC):
91
+
92
+ Copyright 2020-2025 Observable, Inc.
93
+
94
+ Permission to use, copy, modify, and/or distribute this software for any purpose
95
+ with or without fee is hereby granted, provided that the above copyright notice
96
+ and this permission notice appear in all copies.
97
+
98
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
99
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
100
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
101
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
102
+ OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
103
+ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
104
+ THIS SOFTWARE.
105
+
106
+ --------------------------------------------------------------------------------
107
+ Bundled dependencies
108
+ --------------------------------------------------------------------------------
109
+
110
+ The Python package ships prebuilt JavaScript bundles that include third-party
111
+ packages, among them Svelte, d3, Flechette, mosaic-sql, Apache Arrow and
112
+ DuckDB-WASM. Each bundle sits next to a THIRD_PARTY_LICENSES.txt listing every
113
+ package compiled into it, with its license in full. The npm package bundles
114
+ nothing: its dependencies install with their own licenses.
fjader-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: fjader
3
+ Version: 0.2.0
4
+ Summary: An experimental, declarative charting library for Svelte and Python that queries DuckDB and renders with WebGPU
5
+ License-Expression: BSD-3-Clause
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Framework :: Jupyter
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Visualization
13
+ Requires-Dist: narwhals>=1.0
14
+ Requires-Dist: anywidget>=0.9
15
+ Requires-Dist: pytest>=7.0 ; extra == 'dev'
16
+ Requires-Dist: mypy>=1.0 ; extra == 'dev'
17
+ Requires-Dist: pyarrow>=14.0 ; extra == 'pyarrow'
18
+ Requires-Dist: streamlit>=1.0 ; extra == 'streamlit'
19
+ Requires-Dist: pyarrow>=14.0 ; extra == 'streamlit'
20
+ Requires-Python: >=3.10
21
+ Project-URL: Repository, https://github.com/dlyngman/fjader
22
+ Provides-Extra: dev
23
+ Provides-Extra: pyarrow
24
+ Provides-Extra: streamlit
25
+ Description-Content-Type: text/markdown
26
+
27
+ # fjader
28
+
29
+ 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.
30
+
31
+ fjader builds on the ideas of [Mosaic](https://github.com/uwdata/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](https://observablehq.com/plot/), but the API is its own and not compatible with Plot's. Query results arrive as Arrow and are drawn with WebGPU.
32
+
33
+ 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.
34
+
35
+ The name comes from Swedish _fjäder_ (say "fyeh-der"), which means both spring and feather.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ npm install @fjader/svelte @uwdata/mosaic-sql # Svelte 5 components, and the SQL builder the examples use
41
+ pip install fjader # Python: marimo and Jupyter
42
+ pip install "fjader[streamlit]" # Python: Streamlit
43
+ ```
44
+
45
+ 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.
46
+
47
+ Three things to know before using it in a deployed app:
48
+
49
+ - **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.
50
+ - **DuckDB-WASM loads from jsDelivr at runtime.** The browser fetches it from the CDN, so locked-down networks that block it cannot render charts.
51
+ - **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.
52
+
53
+ ## Svelte
54
+
55
+ ```svelte
56
+ <script>
57
+ import {
58
+ initFjader,
59
+ ChartContainer,
60
+ WebGPUCanvas,
61
+ AxisLeft,
62
+ AxisBottom,
63
+ Grid,
64
+ BarMarkY,
65
+ stack
66
+ } from '@fjader/svelte';
67
+ import { count, loadParquet } from '@uwdata/mosaic-sql';
68
+ import { onMount } from 'svelte';
69
+
70
+ const { queryEngine } = initFjader();
71
+ let loaded = $state(false);
72
+
73
+ // DuckDB-WASM fetches only full URLs; a bare path means its own file system.
74
+ onMount(async () => {
75
+ const url = new URL('/data/penguins.parquet', location.href).href;
76
+ await queryEngine.exec([loadParquet('penguins', url).toString()]);
77
+ loaded = true;
78
+ });
79
+ </script>
80
+
81
+ {#if loaded}
82
+ <div style="height: 400px">
83
+ <ChartContainer xLabel="island" yLabel="count">
84
+ {#snippet background()}
85
+ <AxisLeft />
86
+ <AxisBottom />
87
+ {/snippet}
88
+ {#snippet canvas()}
89
+ <WebGPUCanvas>
90
+ {#snippet canvasComponents()}
91
+ <Grid />
92
+ <BarMarkY
93
+ table="penguins"
94
+ x="island"
95
+ y={count()}
96
+ fill="species"
97
+ transforms={[stack()]}
98
+ />
99
+ {/snippet}
100
+ </WebGPUCanvas>
101
+ {/snippet}
102
+ </ChartContainer>
103
+ </div>
104
+ {/if}
105
+ ```
106
+
107
+ 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.
108
+
109
+ ## Python
110
+
111
+ ```python
112
+ import polars as pl
113
+ import fjader as fj
114
+
115
+ weather = pl.read_parquet("seattle-weather.parquet")
116
+
117
+ fj.plot(
118
+ fj.dot(data=weather, x="date", y="temp_max", fill="weather", r="precipitation"),
119
+ x_label="Date",
120
+ y_label="Max temperature (°F)",
121
+ )
122
+ ```
123
+
124
+ 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.
125
+
126
+ ## How it works
127
+
128
+ **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.
129
+
130
+ **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.
131
+
132
+ **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.
133
+
134
+ **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.
135
+
136
+ ## Project structure
137
+
138
+ ```
139
+ src/lib/
140
+ components/ Marks, charts, axes, legends, interaction overlays
141
+ query-engine/ DuckDB connection, crossfilter, pre-aggregation
142
+ transforms/ Transform pipeline (SQL first, then JS)
143
+ scales/ Scale requirements, resolution and shared scale state
144
+ gpu/ GPUCoordinator, pipelines, shader libraries
145
+ animation/ Spring physics and the fixed-timestep loop
146
+ lod/ Level-of-detail rungs
147
+ interactors/ Brushes, toggles, tooltips, pan and zoom
148
+ spec/ JSON spec renderer and export
149
+ python/fjader/ Python API, Jupyter widget, Streamlit component
150
+ ```
151
+
152
+ ## Development
153
+
154
+ ```bash
155
+ npm install
156
+ npm run fetch-data # Example datasets, from their upstream sources
157
+ npm run dev # Dev server with examples and test routes
158
+ npm run build # Production build
159
+ npm run lint # ESLint + Prettier
160
+ npx vitest # JavaScript tests
161
+ npm run build:python # Widget and Streamlit bundles, then the Python wheel
162
+ ```
163
+
164
+ 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.
165
+
166
+ 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.
167
+
168
+ ## Acknowledgements
169
+
170
+ fjader takes ideas from two projects, and in places code from Mosaic:
171
+
172
+ - **[Mosaic](https://github.com/uwdata/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`.
173
+ - **[Observable Plot](https://github.com/observablehq/plot)**, by Observable. Mark names, channel shorthands, stacking conventions and many defaults are modeled on Plot's.
174
+
175
+ Mosaic is described in Jeffrey Heer and Dominik Moritz, [_Mosaic: An Architecture for Scalable & Interoperable Data Views_](https://idl.uw.edu/papers/mosaic), IEEE Transactions on Visualization and Computer Graphics 30 (Proc. VIS 2023), 2024, [doi:10.1109/TVCG.2023.3327189](https://doi.org/10.1109/TVCG.2023.3327189).
176
+
177
+ It also rests on [DuckDB](https://duckdb.org) and [DuckDB-WASM](https://github.com/duckdb/duckdb-wasm), [Flechette](https://github.com/uwdata/flechette), [d3](https://d3js.org) and [Svelte](https://svelte.dev).
178
+
179
+ ## License
180
+
181
+ BSD 3-Clause; see [LICENSE](https://github.com/dlyngman/fjader/blob/main/LICENSE). [NOTICE](https://github.com/dlyngman/fjader/blob/main/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.
fjader-0.2.0/README.md ADDED
@@ -0,0 +1,155 @@
1
+ # fjader
2
+
3
+ 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.
4
+
5
+ fjader builds on the ideas of [Mosaic](https://github.com/uwdata/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](https://observablehq.com/plot/), but the API is its own and not compatible with Plot's. Query results arrive as Arrow and are drawn with WebGPU.
6
+
7
+ 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.
8
+
9
+ The name comes from Swedish _fjäder_ (say "fyeh-der"), which means both spring and feather.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @fjader/svelte @uwdata/mosaic-sql # Svelte 5 components, and the SQL builder the examples use
15
+ pip install fjader # Python: marimo and Jupyter
16
+ pip install "fjader[streamlit]" # Python: Streamlit
17
+ ```
18
+
19
+ 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.
20
+
21
+ Three things to know before using it in a deployed app:
22
+
23
+ - **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.
24
+ - **DuckDB-WASM loads from jsDelivr at runtime.** The browser fetches it from the CDN, so locked-down networks that block it cannot render charts.
25
+ - **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.
26
+
27
+ ## Svelte
28
+
29
+ ```svelte
30
+ <script>
31
+ import {
32
+ initFjader,
33
+ ChartContainer,
34
+ WebGPUCanvas,
35
+ AxisLeft,
36
+ AxisBottom,
37
+ Grid,
38
+ BarMarkY,
39
+ stack
40
+ } from '@fjader/svelte';
41
+ import { count, loadParquet } from '@uwdata/mosaic-sql';
42
+ import { onMount } from 'svelte';
43
+
44
+ const { queryEngine } = initFjader();
45
+ let loaded = $state(false);
46
+
47
+ // DuckDB-WASM fetches only full URLs; a bare path means its own file system.
48
+ onMount(async () => {
49
+ const url = new URL('/data/penguins.parquet', location.href).href;
50
+ await queryEngine.exec([loadParquet('penguins', url).toString()]);
51
+ loaded = true;
52
+ });
53
+ </script>
54
+
55
+ {#if loaded}
56
+ <div style="height: 400px">
57
+ <ChartContainer xLabel="island" yLabel="count">
58
+ {#snippet background()}
59
+ <AxisLeft />
60
+ <AxisBottom />
61
+ {/snippet}
62
+ {#snippet canvas()}
63
+ <WebGPUCanvas>
64
+ {#snippet canvasComponents()}
65
+ <Grid />
66
+ <BarMarkY
67
+ table="penguins"
68
+ x="island"
69
+ y={count()}
70
+ fill="species"
71
+ transforms={[stack()]}
72
+ />
73
+ {/snippet}
74
+ </WebGPUCanvas>
75
+ {/snippet}
76
+ </ChartContainer>
77
+ </div>
78
+ {/if}
79
+ ```
80
+
81
+ 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.
82
+
83
+ ## Python
84
+
85
+ ```python
86
+ import polars as pl
87
+ import fjader as fj
88
+
89
+ weather = pl.read_parquet("seattle-weather.parquet")
90
+
91
+ fj.plot(
92
+ fj.dot(data=weather, x="date", y="temp_max", fill="weather", r="precipitation"),
93
+ x_label="Date",
94
+ y_label="Max temperature (°F)",
95
+ )
96
+ ```
97
+
98
+ 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.
99
+
100
+ ## How it works
101
+
102
+ **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.
103
+
104
+ **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.
105
+
106
+ **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.
107
+
108
+ **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.
109
+
110
+ ## Project structure
111
+
112
+ ```
113
+ src/lib/
114
+ components/ Marks, charts, axes, legends, interaction overlays
115
+ query-engine/ DuckDB connection, crossfilter, pre-aggregation
116
+ transforms/ Transform pipeline (SQL first, then JS)
117
+ scales/ Scale requirements, resolution and shared scale state
118
+ gpu/ GPUCoordinator, pipelines, shader libraries
119
+ animation/ Spring physics and the fixed-timestep loop
120
+ lod/ Level-of-detail rungs
121
+ interactors/ Brushes, toggles, tooltips, pan and zoom
122
+ spec/ JSON spec renderer and export
123
+ python/fjader/ Python API, Jupyter widget, Streamlit component
124
+ ```
125
+
126
+ ## Development
127
+
128
+ ```bash
129
+ npm install
130
+ npm run fetch-data # Example datasets, from their upstream sources
131
+ npm run dev # Dev server with examples and test routes
132
+ npm run build # Production build
133
+ npm run lint # ESLint + Prettier
134
+ npx vitest # JavaScript tests
135
+ npm run build:python # Widget and Streamlit bundles, then the Python wheel
136
+ ```
137
+
138
+ 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.
139
+
140
+ 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.
141
+
142
+ ## Acknowledgements
143
+
144
+ fjader takes ideas from two projects, and in places code from Mosaic:
145
+
146
+ - **[Mosaic](https://github.com/uwdata/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`.
147
+ - **[Observable Plot](https://github.com/observablehq/plot)**, by Observable. Mark names, channel shorthands, stacking conventions and many defaults are modeled on Plot's.
148
+
149
+ Mosaic is described in Jeffrey Heer and Dominik Moritz, [_Mosaic: An Architecture for Scalable & Interoperable Data Views_](https://idl.uw.edu/papers/mosaic), IEEE Transactions on Visualization and Computer Graphics 30 (Proc. VIS 2023), 2024, [doi:10.1109/TVCG.2023.3327189](https://doi.org/10.1109/TVCG.2023.3327189).
150
+
151
+ It also rests on [DuckDB](https://duckdb.org) and [DuckDB-WASM](https://github.com/duckdb/duckdb-wasm), [Flechette](https://github.com/uwdata/flechette), [d3](https://d3js.org) and [Svelte](https://svelte.dev).
152
+
153
+ ## License
154
+
155
+ BSD 3-Clause; see [LICENSE](https://github.com/dlyngman/fjader/blob/main/LICENSE). [NOTICE](https://github.com/dlyngman/fjader/blob/main/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.
@@ -0,0 +1,98 @@
1
+ """Fjader — GPU-accelerated data visualization for Python.
2
+
3
+ Phase 1: Spec generation. Produces JSON specs consumed by the Fjader
4
+ JavaScript renderer.
5
+
6
+ Usage:
7
+ import fjader as fj
8
+
9
+ chart = fj.dot(table='penguins', x='bill_length', y='bill_depth', fill='species')
10
+ chart.to_dict()
11
+
12
+ # Overlay (two marks, one chart)
13
+ overlay = fj.dot(table='t', x='a', y='b') * fj.line(table='t', x='a', y='b')
14
+
15
+ # Layout (two charts side by side)
16
+ layout = fj.dot(table='t', x='a', y='b') + fj.bar_y(table='t', x='species', y='count()')
17
+
18
+ # Chart-level config
19
+ fj.plot(fj.dot(table='t', x='a', y='b'), width=600, x_label='X')
20
+ """
21
+
22
+ from ._version import __version__
23
+
24
+ # Mark factory functions
25
+ from .marks import dot, bar_x, bar_y, line, area_y, area_x, rect, rect_x, rect_y, text
26
+
27
+ # Chart container
28
+ from .plot import plot
29
+
30
+ # Interactors
31
+ from .interactors import interval_x, interval_y, interval_2d, toggle, derived_interval
32
+
33
+ # Layout helpers
34
+ from .layout import hconcat, vconcat
35
+
36
+ # Transforms
37
+ from .transforms import (
38
+ filter,
39
+ having,
40
+ limit,
41
+ orderby,
42
+ topN,
43
+ stack_y,
44
+ stack_x,
45
+ normalize,
46
+ dodge,
47
+ )
48
+
49
+ # Field spec helpers
50
+ from .fields import count, sum, avg, min_, max_, median, bin, window, sql_expr
51
+
52
+ # Data helpers
53
+ from .data import sql
54
+
55
+ # Display helpers
56
+ from ._display import show
57
+
58
+ # Spec import
59
+ from ._from_spec import from_spec
60
+
61
+ # Lazy submodule access — import only when accessed (avoids hard dep on streamlit)
62
+ import importlib as _importlib
63
+
64
+ def __getattr__(name: str):
65
+ if name == "streamlit":
66
+ return _importlib.import_module("._streamlit", __name__)
67
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
68
+
69
+ # Types (for type annotations in user code)
70
+ from ._types import ColorScheme, CurveType, ScaleType, SortConfig, TextAnchor, LineAnchor
71
+ from .fields import FieldSpec
72
+
73
+ __all__ = [
74
+ "__version__",
75
+ # Marks
76
+ "dot", "bar_x", "bar_y", "line", "area_y", "area_x",
77
+ "rect", "rect_x", "rect_y", "text",
78
+ # Chart
79
+ "plot",
80
+ # Interactors
81
+ "interval_x", "interval_y", "interval_2d", "toggle", "derived_interval",
82
+ # Layout
83
+ "hconcat", "vconcat",
84
+ # Transforms
85
+ "filter", "having", "limit", "orderby",
86
+ "topN", "stack_y", "stack_x", "normalize", "dodge",
87
+ # Fields
88
+ "count", "sum", "avg", "min_", "max_", "median", "bin", "window", "sql_expr",
89
+ # Data
90
+ "sql",
91
+ # Display
92
+ "show",
93
+ # Spec import
94
+ "from_spec",
95
+ # Types
96
+ "FieldSpec", "ColorScheme", "CurveType", "ScaleType",
97
+ "SortConfig", "TextAnchor", "LineAnchor",
98
+ ]
@@ -0,0 +1,74 @@
1
+ """Shared DataFrame accessor for Fjader.
2
+
3
+ Provides ``df.fj.dot(x='a', y='b')`` syntax for quick exploration.
4
+ Returns anywidget instances that auto-display in Jupyter and Marimo,
5
+ and support composition via ``*`` (overlay), ``+`` (layout), and ``.opts()``.
6
+
7
+ Registration is done by importing the appropriate module::
8
+
9
+ import fjader.pandas as fj # registers .fj on pandas DataFrames
10
+ import fjader.polars as fj # registers .fj on polars DataFrames
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any
16
+
17
+ from . import marks as _marks
18
+ from ._display import show
19
+
20
+
21
+ class FjaderAccessor:
22
+ """DataFrame accessor — ``df.fj.dot(x='a', y='b', fill='species')``."""
23
+
24
+ def __init__(self, df: Any) -> None:
25
+ self._df = df
26
+
27
+ def _call(self, factory: Any, **kwargs: Any) -> Any:
28
+ for key in ("data", "table"):
29
+ if key in kwargs:
30
+ raise TypeError(
31
+ f"Cannot pass '{key}' through the .fj accessor — "
32
+ f"data is implicitly the DataFrame itself."
33
+ )
34
+ return show(factory(data=self._df, **kwargs))
35
+
36
+ def dot(self, **kwargs: Any) -> Any:
37
+ """Scatter/dot mark. See ``fj.dot()`` for parameters."""
38
+ return self._call(_marks.dot, **kwargs)
39
+
40
+ def bar_x(self, **kwargs: Any) -> Any:
41
+ """Horizontal bar mark. See ``fj.bar_x()`` for parameters."""
42
+ return self._call(_marks.bar_x, **kwargs)
43
+
44
+ def bar_y(self, **kwargs: Any) -> Any:
45
+ """Vertical bar mark. See ``fj.bar_y()`` for parameters."""
46
+ return self._call(_marks.bar_y, **kwargs)
47
+
48
+ def line(self, **kwargs: Any) -> Any:
49
+ """Line mark. See ``fj.line()`` for parameters."""
50
+ return self._call(_marks.line, **kwargs)
51
+
52
+ def area_y(self, **kwargs: Any) -> Any:
53
+ """Vertical area mark. See ``fj.area_y()`` for parameters."""
54
+ return self._call(_marks.area_y, **kwargs)
55
+
56
+ def area_x(self, **kwargs: Any) -> Any:
57
+ """Horizontal area mark. See ``fj.area_x()`` for parameters."""
58
+ return self._call(_marks.area_x, **kwargs)
59
+
60
+ def rect(self, **kwargs: Any) -> Any:
61
+ """Rectangle mark. See ``fj.rect()`` for parameters."""
62
+ return self._call(_marks.rect, **kwargs)
63
+
64
+ def rect_x(self, **kwargs: Any) -> Any:
65
+ """Horizontal rect mark. See ``fj.rect_x()`` for parameters."""
66
+ return self._call(_marks.rect_x, **kwargs)
67
+
68
+ def rect_y(self, **kwargs: Any) -> Any:
69
+ """Vertical rect mark. See ``fj.rect_y()`` for parameters."""
70
+ return self._call(_marks.rect_y, **kwargs)
71
+
72
+ def text(self, **kwargs: Any) -> Any:
73
+ """Text mark. See ``fj.text()`` for parameters."""
74
+ return self._call(_marks.text, **kwargs)