figfoundry 0.1.0__tar.gz → 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.
- figfoundry-0.2.0/PKG-INFO +261 -0
- figfoundry-0.2.0/README.md +227 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/static/canvas.css +8 -1
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/static/canvas.html +3 -3
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/static/canvas.js +49 -7
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/static/style.css +12 -1
- {figfoundry-0.1.0 → figfoundry-0.2.0}/pyproject.toml +4 -3
- figfoundry-0.1.0/PKG-INFO +0 -125
- figfoundry-0.1.0/README.md +0 -91
- {figfoundry-0.1.0 → figfoundry-0.2.0}/.gitattributes +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/.gitignore +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/LICENSE +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/__init__.py +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/__main__.py +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/app.py +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/canvas.py +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/cli.py +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/data_io.py +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/datasets/sample.npz +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/figfoundry/static/index.html +0 -0
- {figfoundry-0.1.0 → figfoundry-0.2.0}/uv.lock +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: figfoundry
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: WYSIWYG matplotlib figure composer: load data (.npz/.pkl), drag signals onto a canvas to build plots, and export clean reproducible code.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ductai199x/figfoundry
|
|
6
|
+
Project-URL: Repository, https://github.com/ductai199x/figfoundry
|
|
7
|
+
Project-URL: Issues, https://github.com/ductai199x/figfoundry/issues
|
|
8
|
+
Author-email: Tai Duc Nguyen <taiducnguyen.drexel@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: data-visualization,figures,matplotlib,plotting,scientific-figures,wysiwyg
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Web Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: fastapi>=0.110
|
|
25
|
+
Requires-Dist: matplotlib>=3.9
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Requires-Dist: pydantic>=2.0
|
|
28
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
29
|
+
Requires-Dist: uvicorn>=0.29
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: httpx2>=2.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
<p align="center">
|
|
36
|
+
<img src="media/logo.svg" width="88" alt="Fig Foundry">
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<h1 align="center">Fig Foundry</h1>
|
|
40
|
+
|
|
41
|
+
<p align="center">
|
|
42
|
+
<b>A WYSIWYG figure studio for matplotlib.</b><br>
|
|
43
|
+
Drag your data onto a free canvas, compose publication figures, and export the
|
|
44
|
+
exact Python that draws them. The preview is the generated code, executed — so
|
|
45
|
+
what you see is what the <code>.py</code> produces.
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<a href="https://pypi.org/project/figfoundry/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/figfoundry?color=2f6690"></a>
|
|
50
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-2f6690"></a>
|
|
51
|
+
<img alt="Python 3.10 and newer" src="https://img.shields.io/badge/python-3.10+-2f6690">
|
|
52
|
+
<img alt="Runs on Windows, macOS and Linux" src="https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-2f6690">
|
|
53
|
+
</p>
|
|
54
|
+
|
|
55
|
+
<p align="center">
|
|
56
|
+
<a href="#install"><b>Install</b></a> ·
|
|
57
|
+
<a href="#a-quick-tour">Tour</a> ·
|
|
58
|
+
<a href="#measured-not-claimed">Measured</a> ·
|
|
59
|
+
<a href="#how-it-works">How it works</a> ·
|
|
60
|
+
<a href="#command-line">Command line</a>
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
<p align="center">
|
|
64
|
+
<img src="media/hero.png" width="880" alt="The canvas: a nine-panel autocorrelation figure composed from the signals of one dataset, with the signal library on the left and a per-plot inspector on the right.">
|
|
65
|
+
</p>
|
|
66
|
+
<p align="center">
|
|
67
|
+
<sub>A nine-panel figure composed on the canvas from one dataset's signals. Every panel
|
|
68
|
+
is a real matplotlib <code>Axes</code> placed by explicit coordinates, so it can sit
|
|
69
|
+
anywhere, at any size.</sub>
|
|
70
|
+
</p>
|
|
71
|
+
|
|
72
|
+
You have the data; Fig Foundry is the table you lay it out on. Load a dataset, drag a
|
|
73
|
+
signal onto the canvas and it becomes a plot. Move and resize plots with snapping, nudge
|
|
74
|
+
and style each one in the inspector, add the second series or the σ-band, and when it is
|
|
75
|
+
right, export the code. Because the preview is rendered by running that very code, there is
|
|
76
|
+
no "close enough" — the figure you arrange and the script you ship are the same drawing.
|
|
77
|
+
|
|
78
|
+
<table>
|
|
79
|
+
<tr>
|
|
80
|
+
<td width="50%" valign="top">
|
|
81
|
+
<b>WYSIWYG by construction</b><br>
|
|
82
|
+
One drawing path. The browser preview executes the generated <code>.py</code>, so the
|
|
83
|
+
export is byte-for-byte what you see. There is no second renderer to drift.
|
|
84
|
+
</td>
|
|
85
|
+
<td width="50%" valign="top">
|
|
86
|
+
<b>The atom is data</b><br>
|
|
87
|
+
A figure is a scene of plots over named signals. Drag a signal to make a plot; drop it
|
|
88
|
+
on one to overlay a line, a σ-band, or error bars. Nothing is hand-drawn.
|
|
89
|
+
</td>
|
|
90
|
+
</tr>
|
|
91
|
+
<tr>
|
|
92
|
+
<td width="50%" valign="top">
|
|
93
|
+
<b>Twenty-three plot types</b><br>
|
|
94
|
+
Line, step, scatter, grouped scatter, bar, box, violin, histograms, hexbin, image,
|
|
95
|
+
contour, pcolormesh, vector fields, streamplot, decision boundary, pie, polar, and
|
|
96
|
+
three 3-D types.
|
|
97
|
+
</td>
|
|
98
|
+
<td width="50%" valign="top">
|
|
99
|
+
<b>Private, on your machine</b><br>
|
|
100
|
+
A local web app. Your datasets, figures and exports live in <code>~/.figfoundry</code>;
|
|
101
|
+
nothing you load is sent anywhere. Pickle files load through a whitelist, not a
|
|
102
|
+
deserializer.
|
|
103
|
+
</td>
|
|
104
|
+
</tr>
|
|
105
|
+
</table>
|
|
106
|
+
|
|
107
|
+
## A quick tour
|
|
108
|
+
|
|
109
|
+
<p align="center">
|
|
110
|
+
<img src="media/dark.png" width="880" alt="The same nine-panel figure with the interface in dark mode: dark chrome around a white artboard.">
|
|
111
|
+
</p>
|
|
112
|
+
<p align="center"><b>Light, dark, or your system's.</b> The interface re-skins; the figure
|
|
113
|
+
itself stays on a white artboard, so the artwork is publication-clean in either theme.</p>
|
|
114
|
+
|
|
115
|
+
<p align="center">
|
|
116
|
+
<img src="media/viewfinder.png" width="620" alt="The 3-D viewfinder: a surface plot with elevation, azimuth and zoom controls, and invert toggles for rotate and zoom.">
|
|
117
|
+
</p>
|
|
118
|
+
<p align="center"><b>3-D, to your angle.</b> Double-click a 3-D plot to open the viewfinder:
|
|
119
|
+
drag to rotate, scroll to zoom, or use the sliders. Accept writes the camera back to the
|
|
120
|
+
figure and, with it, into the exported code.</p>
|
|
121
|
+
|
|
122
|
+
<table>
|
|
123
|
+
<tr>
|
|
124
|
+
<td width="50%" valign="top">
|
|
125
|
+
<img src="media/export.png" width="420" alt="The export dialog: the generated Python, with PNG, SVG and PDF figure buttons and a download.">
|
|
126
|
+
<p><b>Export the code and the figure.</b> The standalone <code>.py</code>, plus the
|
|
127
|
+
figure as PNG, SVG or PDF — SVG and PDF are vector, for the paper.</p>
|
|
128
|
+
</td>
|
|
129
|
+
<td width="50%" valign="top">
|
|
130
|
+
<img src="media/picker.png" width="420" alt="The picker: a New figure card and your saved figures, each with a live thumbnail.">
|
|
131
|
+
<p><b>Your figures.</b> Every canvas is saved with a live thumbnail; open, duplicate
|
|
132
|
+
or delete from here. Each one is tied to the dataset it was built from.</p>
|
|
133
|
+
</td>
|
|
134
|
+
</tr>
|
|
135
|
+
</table>
|
|
136
|
+
|
|
137
|
+
## Measured, not claimed
|
|
138
|
+
|
|
139
|
+
Every figure below is reproducible from the repository: the test suite, the CI runs, and the
|
|
140
|
+
security probes.
|
|
141
|
+
|
|
142
|
+
| | |
|
|
143
|
+
|---|---|
|
|
144
|
+
| Plot types, each verified preview-identical to its export | **23 of 23** ([tests](tests/test_plot_types.py)) |
|
|
145
|
+
| Tests, passing on Linux and Windows, Python 3.10 and 3.12 | **152** ([CI](https://github.com/ductai199x/figfoundry/actions)) |
|
|
146
|
+
| Malicious pickle payloads the whitelist refuses to load | **18**, including `np.fromfile`, `np.load`, `__import__`, `eval`, `exec` ([tests](tests/test_data_app.py)) |
|
|
147
|
+
| Separate renderers the export can drift from | **0** — the preview executes the export |
|
|
148
|
+
|
|
149
|
+
## What it does
|
|
150
|
+
|
|
151
|
+
- **Composes on a free canvas.** Plots are explicit rectangles, not a fixed grid, so they
|
|
152
|
+
go anywhere and at any size. Move, resize with snapping, nudge with the arrow keys, and
|
|
153
|
+
multi-select (Shift+click) to move, nudge or delete a group at once.
|
|
154
|
+
- **Twenty-three plot types.** Line, step, scatter, grouped scatter (color-by-class), bar and
|
|
155
|
+
horizontal bar, histogram, box plot, violin plot, 2-D histogram, hexbin, image, contour
|
|
156
|
+
(filled and lines), pcolormesh, vector field, streamplot, decision boundary (a surface with
|
|
157
|
+
the points on top), pie, polar, 3-D scatter, 3-D surface and 3-D wireframe.
|
|
158
|
+
- **Styles every element.** Per plot: the data, plot type, color, line and marker, σ-band and
|
|
159
|
+
error bars, ticks and tick labels, limits and scale, spines, title, colormap and colorbar,
|
|
160
|
+
and artists — vertical and horizontal lines, text, annotations, arrows, boxes, circles,
|
|
161
|
+
ellipses, polygons and legends. Figure-level axis labels, legend, suptitle and free text.
|
|
162
|
+
- **Suggests accessible color.** One click applies the colorblind-safe Okabe–Ito palette to a
|
|
163
|
+
plot's series; a palette menu (Okabe–Ito, Bright, Viridis, Plasma, Cividis) is available
|
|
164
|
+
through the API.
|
|
165
|
+
- **Speaks LaTeX.** Labels, titles and text take mathtext, so `$x_0$` and `$\alpha$` render.
|
|
166
|
+
- **Lays out for you.** Arrange in a grid, align and distribute a selection, set margins and
|
|
167
|
+
gaps, and sync axis ranges across every plot at once.
|
|
168
|
+
- **Exports clean code.** The figure as a standalone <code>.py</code> that references the
|
|
169
|
+
dataset, plus the rendered figure as PNG, SVG or PDF. The 3-D camera, the colors, the
|
|
170
|
+
ticks — all of it is in the code.
|
|
171
|
+
- **Remembers your setup.** Themes, and the 3-D rotate/zoom direction, persist; figures,
|
|
172
|
+
datasets and exports live in <code>~/.figfoundry</code>.
|
|
173
|
+
|
|
174
|
+
## How it works
|
|
175
|
+
|
|
176
|
+
```mermaid
|
|
177
|
+
flowchart LR
|
|
178
|
+
data(["Your data<br/>.npz / .pkl / .csv"]) --> spec["The spec<br/>a scene: canvas,<br/>plots, artists"]
|
|
179
|
+
spec --> code["codegen<br/>a standalone .py"]
|
|
180
|
+
code --> run["execute the .py"]
|
|
181
|
+
run --> view(["Preview<br/>(what you see)"])
|
|
182
|
+
code --> out(["Export<br/>.py + PNG / SVG / PDF"])
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **One path, both ends.** `codegen_canvas` turns the spec into a plain <code>.py</code> — a
|
|
186
|
+
list of <code>fig.add_axes([...])</code> calls over the dataset. The preview is that file,
|
|
187
|
+
run; the export is that file, saved. They cannot disagree, because they are the same code.
|
|
188
|
+
- **The spec is the source of truth.** A figure is a JSON scene (canvas, plots, artists). The
|
|
189
|
+
browser edits it; the backend renders it by executing the generated code. Save it and it is
|
|
190
|
+
a canvas you can reopen, duplicate and drive over the API.
|
|
191
|
+
- **Data stays data.** Signals are arrays, referenced by name. The exported code loads the
|
|
192
|
+
dataset and indexes it — the numbers in the figure are the numbers in your file, unedited.
|
|
193
|
+
|
|
194
|
+
## Data
|
|
195
|
+
|
|
196
|
+
A *dataset* is a named set of signals (arrays), 1-D and 2-D. Load one with **Load data…**
|
|
197
|
+
(pick an existing dataset, or upload a file):
|
|
198
|
+
|
|
199
|
+
- **`.npz`** — a numpy zip of named arrays. Safe by construction.
|
|
200
|
+
- **`.pkl`** — a Python pickle, loaded through a **whitelist unpickler** that permits only
|
|
201
|
+
numpy arrays and plain containers. A pickle that tries to run code — `os`, `subprocess`,
|
|
202
|
+
`eval`, `np.fromfile` and the rest — is refused, not executed.
|
|
203
|
+
- **`.csv`** — a table with a header row; each numeric column becomes a signal.
|
|
204
|
+
|
|
205
|
+
## Command line
|
|
206
|
+
|
|
207
|
+
The same engines run from the shell, for scripting and automation:
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
figfoundry # start the web app (default)
|
|
211
|
+
figfoundry serve # ...with flags: --port, --host, --no-browser
|
|
212
|
+
figfoundry list # list saved figures
|
|
213
|
+
figfoundry new # create a blank figure, print its slug
|
|
214
|
+
figfoundry export SLUG [--out PATH] # write a figure's .py
|
|
215
|
+
figfoundry render SLUG [--format png|svg|pdf] [--out PATH]
|
|
216
|
+
figfoundry datasets # list available datasets
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Install
|
|
220
|
+
|
|
221
|
+
From [PyPI](https://pypi.org/project/figfoundry/), with [uv](https://docs.astral.sh/uv/):
|
|
222
|
+
|
|
223
|
+
```sh
|
|
224
|
+
uv tool install figfoundry # puts the `figfoundry` command on your PATH
|
|
225
|
+
figfoundry # → opens http://127.0.0.1:8037
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Or:
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
pip install figfoundry
|
|
232
|
+
# or from this repository:
|
|
233
|
+
uv tool install figfoundry --from "git+https://github.com/ductai199x/figfoundry.git"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Python 3.10 or newer. It runs wherever Python and a browser do.
|
|
237
|
+
|
|
238
|
+
<details>
|
|
239
|
+
<summary>Development</summary>
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
uv sync --extra dev
|
|
243
|
+
uv run --extra dev pytest -q # 152 tests
|
|
244
|
+
uv run --with ruff ruff check . # lint
|
|
245
|
+
uv run figfoundry serve # the app
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
The suite covers the WYSIWYG invariant for every plot type, the pickle whitelist, dataset I/O
|
|
249
|
+
(npz/pkl/csv), vector export, the CLI, and the API through a test client. CI runs it, plus the
|
|
250
|
+
build and an artifact check, on Linux and Windows for Python 3.10 and 3.12
|
|
251
|
+
([.github/workflows/ci.yml](.github/workflows/ci.yml)).
|
|
252
|
+
|
|
253
|
+
</details>
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
Copyright (C) 2026 Tai Duc Nguyen.
|
|
258
|
+
|
|
259
|
+
Fig Foundry is licensed under the [MIT License](LICENSE): use it, study it, change it and
|
|
260
|
+
share it. The one sample dataset it ships is synthetic; your own data never leaves your
|
|
261
|
+
machine.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="media/logo.svg" width="88" alt="Fig Foundry">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Fig Foundry</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>A WYSIWYG figure studio for matplotlib.</b><br>
|
|
9
|
+
Drag your data onto a free canvas, compose publication figures, and export the
|
|
10
|
+
exact Python that draws them. The preview is the generated code, executed — so
|
|
11
|
+
what you see is what the <code>.py</code> produces.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://pypi.org/project/figfoundry/"><img alt="PyPI version" src="https://img.shields.io/pypi/v/figfoundry?color=2f6690"></a>
|
|
16
|
+
<a href="LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/license-MIT-2f6690"></a>
|
|
17
|
+
<img alt="Python 3.10 and newer" src="https://img.shields.io/badge/python-3.10+-2f6690">
|
|
18
|
+
<img alt="Runs on Windows, macOS and Linux" src="https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-2f6690">
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="#install"><b>Install</b></a> ·
|
|
23
|
+
<a href="#a-quick-tour">Tour</a> ·
|
|
24
|
+
<a href="#measured-not-claimed">Measured</a> ·
|
|
25
|
+
<a href="#how-it-works">How it works</a> ·
|
|
26
|
+
<a href="#command-line">Command line</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
<p align="center">
|
|
30
|
+
<img src="media/hero.png" width="880" alt="The canvas: a nine-panel autocorrelation figure composed from the signals of one dataset, with the signal library on the left and a per-plot inspector on the right.">
|
|
31
|
+
</p>
|
|
32
|
+
<p align="center">
|
|
33
|
+
<sub>A nine-panel figure composed on the canvas from one dataset's signals. Every panel
|
|
34
|
+
is a real matplotlib <code>Axes</code> placed by explicit coordinates, so it can sit
|
|
35
|
+
anywhere, at any size.</sub>
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
You have the data; Fig Foundry is the table you lay it out on. Load a dataset, drag a
|
|
39
|
+
signal onto the canvas and it becomes a plot. Move and resize plots with snapping, nudge
|
|
40
|
+
and style each one in the inspector, add the second series or the σ-band, and when it is
|
|
41
|
+
right, export the code. Because the preview is rendered by running that very code, there is
|
|
42
|
+
no "close enough" — the figure you arrange and the script you ship are the same drawing.
|
|
43
|
+
|
|
44
|
+
<table>
|
|
45
|
+
<tr>
|
|
46
|
+
<td width="50%" valign="top">
|
|
47
|
+
<b>WYSIWYG by construction</b><br>
|
|
48
|
+
One drawing path. The browser preview executes the generated <code>.py</code>, so the
|
|
49
|
+
export is byte-for-byte what you see. There is no second renderer to drift.
|
|
50
|
+
</td>
|
|
51
|
+
<td width="50%" valign="top">
|
|
52
|
+
<b>The atom is data</b><br>
|
|
53
|
+
A figure is a scene of plots over named signals. Drag a signal to make a plot; drop it
|
|
54
|
+
on one to overlay a line, a σ-band, or error bars. Nothing is hand-drawn.
|
|
55
|
+
</td>
|
|
56
|
+
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<td width="50%" valign="top">
|
|
59
|
+
<b>Twenty-three plot types</b><br>
|
|
60
|
+
Line, step, scatter, grouped scatter, bar, box, violin, histograms, hexbin, image,
|
|
61
|
+
contour, pcolormesh, vector fields, streamplot, decision boundary, pie, polar, and
|
|
62
|
+
three 3-D types.
|
|
63
|
+
</td>
|
|
64
|
+
<td width="50%" valign="top">
|
|
65
|
+
<b>Private, on your machine</b><br>
|
|
66
|
+
A local web app. Your datasets, figures and exports live in <code>~/.figfoundry</code>;
|
|
67
|
+
nothing you load is sent anywhere. Pickle files load through a whitelist, not a
|
|
68
|
+
deserializer.
|
|
69
|
+
</td>
|
|
70
|
+
</tr>
|
|
71
|
+
</table>
|
|
72
|
+
|
|
73
|
+
## A quick tour
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="media/dark.png" width="880" alt="The same nine-panel figure with the interface in dark mode: dark chrome around a white artboard.">
|
|
77
|
+
</p>
|
|
78
|
+
<p align="center"><b>Light, dark, or your system's.</b> The interface re-skins; the figure
|
|
79
|
+
itself stays on a white artboard, so the artwork is publication-clean in either theme.</p>
|
|
80
|
+
|
|
81
|
+
<p align="center">
|
|
82
|
+
<img src="media/viewfinder.png" width="620" alt="The 3-D viewfinder: a surface plot with elevation, azimuth and zoom controls, and invert toggles for rotate and zoom.">
|
|
83
|
+
</p>
|
|
84
|
+
<p align="center"><b>3-D, to your angle.</b> Double-click a 3-D plot to open the viewfinder:
|
|
85
|
+
drag to rotate, scroll to zoom, or use the sliders. Accept writes the camera back to the
|
|
86
|
+
figure and, with it, into the exported code.</p>
|
|
87
|
+
|
|
88
|
+
<table>
|
|
89
|
+
<tr>
|
|
90
|
+
<td width="50%" valign="top">
|
|
91
|
+
<img src="media/export.png" width="420" alt="The export dialog: the generated Python, with PNG, SVG and PDF figure buttons and a download.">
|
|
92
|
+
<p><b>Export the code and the figure.</b> The standalone <code>.py</code>, plus the
|
|
93
|
+
figure as PNG, SVG or PDF — SVG and PDF are vector, for the paper.</p>
|
|
94
|
+
</td>
|
|
95
|
+
<td width="50%" valign="top">
|
|
96
|
+
<img src="media/picker.png" width="420" alt="The picker: a New figure card and your saved figures, each with a live thumbnail.">
|
|
97
|
+
<p><b>Your figures.</b> Every canvas is saved with a live thumbnail; open, duplicate
|
|
98
|
+
or delete from here. Each one is tied to the dataset it was built from.</p>
|
|
99
|
+
</td>
|
|
100
|
+
</tr>
|
|
101
|
+
</table>
|
|
102
|
+
|
|
103
|
+
## Measured, not claimed
|
|
104
|
+
|
|
105
|
+
Every figure below is reproducible from the repository: the test suite, the CI runs, and the
|
|
106
|
+
security probes.
|
|
107
|
+
|
|
108
|
+
| | |
|
|
109
|
+
|---|---|
|
|
110
|
+
| Plot types, each verified preview-identical to its export | **23 of 23** ([tests](tests/test_plot_types.py)) |
|
|
111
|
+
| Tests, passing on Linux and Windows, Python 3.10 and 3.12 | **152** ([CI](https://github.com/ductai199x/figfoundry/actions)) |
|
|
112
|
+
| Malicious pickle payloads the whitelist refuses to load | **18**, including `np.fromfile`, `np.load`, `__import__`, `eval`, `exec` ([tests](tests/test_data_app.py)) |
|
|
113
|
+
| Separate renderers the export can drift from | **0** — the preview executes the export |
|
|
114
|
+
|
|
115
|
+
## What it does
|
|
116
|
+
|
|
117
|
+
- **Composes on a free canvas.** Plots are explicit rectangles, not a fixed grid, so they
|
|
118
|
+
go anywhere and at any size. Move, resize with snapping, nudge with the arrow keys, and
|
|
119
|
+
multi-select (Shift+click) to move, nudge or delete a group at once.
|
|
120
|
+
- **Twenty-three plot types.** Line, step, scatter, grouped scatter (color-by-class), bar and
|
|
121
|
+
horizontal bar, histogram, box plot, violin plot, 2-D histogram, hexbin, image, contour
|
|
122
|
+
(filled and lines), pcolormesh, vector field, streamplot, decision boundary (a surface with
|
|
123
|
+
the points on top), pie, polar, 3-D scatter, 3-D surface and 3-D wireframe.
|
|
124
|
+
- **Styles every element.** Per plot: the data, plot type, color, line and marker, σ-band and
|
|
125
|
+
error bars, ticks and tick labels, limits and scale, spines, title, colormap and colorbar,
|
|
126
|
+
and artists — vertical and horizontal lines, text, annotations, arrows, boxes, circles,
|
|
127
|
+
ellipses, polygons and legends. Figure-level axis labels, legend, suptitle and free text.
|
|
128
|
+
- **Suggests accessible color.** One click applies the colorblind-safe Okabe–Ito palette to a
|
|
129
|
+
plot's series; a palette menu (Okabe–Ito, Bright, Viridis, Plasma, Cividis) is available
|
|
130
|
+
through the API.
|
|
131
|
+
- **Speaks LaTeX.** Labels, titles and text take mathtext, so `$x_0$` and `$\alpha$` render.
|
|
132
|
+
- **Lays out for you.** Arrange in a grid, align and distribute a selection, set margins and
|
|
133
|
+
gaps, and sync axis ranges across every plot at once.
|
|
134
|
+
- **Exports clean code.** The figure as a standalone <code>.py</code> that references the
|
|
135
|
+
dataset, plus the rendered figure as PNG, SVG or PDF. The 3-D camera, the colors, the
|
|
136
|
+
ticks — all of it is in the code.
|
|
137
|
+
- **Remembers your setup.** Themes, and the 3-D rotate/zoom direction, persist; figures,
|
|
138
|
+
datasets and exports live in <code>~/.figfoundry</code>.
|
|
139
|
+
|
|
140
|
+
## How it works
|
|
141
|
+
|
|
142
|
+
```mermaid
|
|
143
|
+
flowchart LR
|
|
144
|
+
data(["Your data<br/>.npz / .pkl / .csv"]) --> spec["The spec<br/>a scene: canvas,<br/>plots, artists"]
|
|
145
|
+
spec --> code["codegen<br/>a standalone .py"]
|
|
146
|
+
code --> run["execute the .py"]
|
|
147
|
+
run --> view(["Preview<br/>(what you see)"])
|
|
148
|
+
code --> out(["Export<br/>.py + PNG / SVG / PDF"])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- **One path, both ends.** `codegen_canvas` turns the spec into a plain <code>.py</code> — a
|
|
152
|
+
list of <code>fig.add_axes([...])</code> calls over the dataset. The preview is that file,
|
|
153
|
+
run; the export is that file, saved. They cannot disagree, because they are the same code.
|
|
154
|
+
- **The spec is the source of truth.** A figure is a JSON scene (canvas, plots, artists). The
|
|
155
|
+
browser edits it; the backend renders it by executing the generated code. Save it and it is
|
|
156
|
+
a canvas you can reopen, duplicate and drive over the API.
|
|
157
|
+
- **Data stays data.** Signals are arrays, referenced by name. The exported code loads the
|
|
158
|
+
dataset and indexes it — the numbers in the figure are the numbers in your file, unedited.
|
|
159
|
+
|
|
160
|
+
## Data
|
|
161
|
+
|
|
162
|
+
A *dataset* is a named set of signals (arrays), 1-D and 2-D. Load one with **Load data…**
|
|
163
|
+
(pick an existing dataset, or upload a file):
|
|
164
|
+
|
|
165
|
+
- **`.npz`** — a numpy zip of named arrays. Safe by construction.
|
|
166
|
+
- **`.pkl`** — a Python pickle, loaded through a **whitelist unpickler** that permits only
|
|
167
|
+
numpy arrays and plain containers. A pickle that tries to run code — `os`, `subprocess`,
|
|
168
|
+
`eval`, `np.fromfile` and the rest — is refused, not executed.
|
|
169
|
+
- **`.csv`** — a table with a header row; each numeric column becomes a signal.
|
|
170
|
+
|
|
171
|
+
## Command line
|
|
172
|
+
|
|
173
|
+
The same engines run from the shell, for scripting and automation:
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
figfoundry # start the web app (default)
|
|
177
|
+
figfoundry serve # ...with flags: --port, --host, --no-browser
|
|
178
|
+
figfoundry list # list saved figures
|
|
179
|
+
figfoundry new # create a blank figure, print its slug
|
|
180
|
+
figfoundry export SLUG [--out PATH] # write a figure's .py
|
|
181
|
+
figfoundry render SLUG [--format png|svg|pdf] [--out PATH]
|
|
182
|
+
figfoundry datasets # list available datasets
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Install
|
|
186
|
+
|
|
187
|
+
From [PyPI](https://pypi.org/project/figfoundry/), with [uv](https://docs.astral.sh/uv/):
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
uv tool install figfoundry # puts the `figfoundry` command on your PATH
|
|
191
|
+
figfoundry # → opens http://127.0.0.1:8037
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Or:
|
|
195
|
+
|
|
196
|
+
```sh
|
|
197
|
+
pip install figfoundry
|
|
198
|
+
# or from this repository:
|
|
199
|
+
uv tool install figfoundry --from "git+https://github.com/ductai199x/figfoundry.git"
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Python 3.10 or newer. It runs wherever Python and a browser do.
|
|
203
|
+
|
|
204
|
+
<details>
|
|
205
|
+
<summary>Development</summary>
|
|
206
|
+
|
|
207
|
+
```sh
|
|
208
|
+
uv sync --extra dev
|
|
209
|
+
uv run --extra dev pytest -q # 152 tests
|
|
210
|
+
uv run --with ruff ruff check . # lint
|
|
211
|
+
uv run figfoundry serve # the app
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The suite covers the WYSIWYG invariant for every plot type, the pickle whitelist, dataset I/O
|
|
215
|
+
(npz/pkl/csv), vector export, the CLI, and the API through a test client. CI runs it, plus the
|
|
216
|
+
build and an artifact check, on Linux and Windows for Python 3.10 and 3.12
|
|
217
|
+
([.github/workflows/ci.yml](.github/workflows/ci.yml)).
|
|
218
|
+
|
|
219
|
+
</details>
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
Copyright (C) 2026 Tai Duc Nguyen.
|
|
224
|
+
|
|
225
|
+
Fig Foundry is licensed under the [MIT License](LICENSE): use it, study it, change it and
|
|
226
|
+
share it. The one sample dataset it ships is synthetic; your own data never leaves your
|
|
227
|
+
machine.
|
|
@@ -105,7 +105,14 @@ select.ctl-sel:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--
|
|
|
105
105
|
.v3-row { display: grid; grid-template-columns: 78px 1fr 46px; align-items: center; gap: 12px; }
|
|
106
106
|
.v3-lab { font-size: 12.5px; font-weight: 600; color: var(--ink); }
|
|
107
107
|
.v3-range { width: 100%; cursor: pointer; }
|
|
108
|
-
.v3-val {
|
|
108
|
+
.v3-val {
|
|
109
|
+
font-size: 12px; color: var(--ink); font-variant-numeric: tabular-nums; text-align: right; font-family: inherit; font-weight: 600;
|
|
110
|
+
width: 100%; background: transparent; border: 1px solid transparent; border-radius: 5px;
|
|
111
|
+
padding: 1px 5px; margin: -1px -5px;
|
|
112
|
+
cursor: default; transition: background .12s ease, border-color .12s ease, box-shadow .12s ease;
|
|
113
|
+
}
|
|
114
|
+
.v3-val:hover { background: var(--hover); cursor: text; }
|
|
115
|
+
.v3-val:focus { background: var(--input-bg); border-color: var(--accent); cursor: text; outline: none; box-shadow: 0 0 0 3px var(--accent-weak); }
|
|
109
116
|
.v3-toggles { display: flex; gap: 22px; margin-top: 3px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
110
117
|
.v3-check { display: flex; align-items: center; gap: 7px; font-size: 12.5px; color: var(--ink); cursor: pointer; user-select: none; }
|
|
111
118
|
.v3-check input { accent-color: var(--accent); cursor: pointer; width: 15px; height: 15px; }
|
|
@@ -111,17 +111,17 @@
|
|
|
111
111
|
<div class="v3-row">
|
|
112
112
|
<label class="v3-lab" for="v3e">Elevation</label>
|
|
113
113
|
<input type="range" id="v3e" class="v3-range" min="-90" max="90" step="1">
|
|
114
|
-
<
|
|
114
|
+
<input type="text" class="v3-val num-in" id="v3eVal" value="30" inputmode="decimal" spellcheck="false" aria-label="Elevation value">
|
|
115
115
|
</div>
|
|
116
116
|
<div class="v3-row">
|
|
117
117
|
<label class="v3-lab" for="v3a">Azimuth</label>
|
|
118
118
|
<input type="range" id="v3a" class="v3-range" min="-180" max="180" step="1">
|
|
119
|
-
<
|
|
119
|
+
<input type="text" class="v3-val num-in" id="v3aVal" value="-60" inputmode="decimal" spellcheck="false" aria-label="Azimuth value">
|
|
120
120
|
</div>
|
|
121
121
|
<div class="v3-row">
|
|
122
122
|
<label class="v3-lab" for="v3d">Zoom</label>
|
|
123
123
|
<input type="range" id="v3d" class="v3-range" min="3" max="22" step="0.5">
|
|
124
|
-
<
|
|
124
|
+
<input type="text" class="v3-val num-in" id="v3dVal" value="10" inputmode="decimal" spellcheck="false" aria-label="Zoom value">
|
|
125
125
|
</div>
|
|
126
126
|
<div class="v3-toggles">
|
|
127
127
|
<label class="v3-check"><input type="checkbox" id="v3invR"> Invert rotate</label>
|
|
@@ -1063,11 +1063,38 @@ function hint(t) { const h = document.createElement("div"); h.className = "hint"
|
|
|
1063
1063
|
function slider(label, path, val, min, max, step, afterChange) {
|
|
1064
1064
|
const w = document.createElement("div"); w.className = "ctl";
|
|
1065
1065
|
const head = document.createElement("div"); head.className = "ctl-head";
|
|
1066
|
-
const lab = document.createElement("label"); lab.textContent = label;
|
|
1066
|
+
const lab = document.createElement("label"); lab.textContent = label;
|
|
1067
|
+
// The value is a click-to-edit number field: plain text by default, an input
|
|
1068
|
+
// box on focus. Type a value and commit with Enter or blur (click outside);
|
|
1069
|
+
// Esc reverts. This is the only way to enter an exact number (the slider is
|
|
1070
|
+
// for coarse adjustment), so it must stay in sync with the range input.
|
|
1071
|
+
const vv = document.createElement("input"); vv.type = "text"; vv.className = "val num-in";
|
|
1072
|
+
vv.inputMode = "decimal"; vv.spellcheck = false; vv.setAttribute("aria-label", label + " value");
|
|
1067
1073
|
head.append(lab, vv); w.appendChild(head);
|
|
1068
|
-
const inp = document.createElement("input"); inp.type = "range"; inp.min = min; inp.max = max; inp.step = step;
|
|
1069
|
-
|
|
1074
|
+
const inp = document.createElement("input"); inp.type = "range"; inp.min = min; inp.max = max; inp.step = step;
|
|
1075
|
+
let cur = val;
|
|
1076
|
+
const paint = () => { if (document.activeElement !== vv) vv.value = fmt(cur, step); inp.value = cur; };
|
|
1077
|
+
paint();
|
|
1078
|
+
function live(v) {
|
|
1079
|
+
if (isNaN(v)) return;
|
|
1080
|
+
if (state.sliderSnapshot === null) state.sliderSnapshot = clone(state.spec);
|
|
1081
|
+
cur = v; setPath(state.spec, path, v); paint(); scheduleDraft();
|
|
1082
|
+
}
|
|
1083
|
+
inp.addEventListener("input", () => live(parseFloat(inp.value)));
|
|
1070
1084
|
inp.addEventListener("change", () => { commitSnapshot(); if (afterChange) afterChange(); commit(); });
|
|
1085
|
+
function commitVal() {
|
|
1086
|
+
const v = parseFloat(vv.value);
|
|
1087
|
+
if (isNaN(v)) { paint(); return; } // not a number: keep the last good value
|
|
1088
|
+
const cv = Math.max(min, Math.min(max, v)); // clamp to the slider's range
|
|
1089
|
+
if (state.sliderSnapshot === null) state.sliderSnapshot = clone(state.spec);
|
|
1090
|
+
cur = cv; setPath(state.spec, path, cv); inp.value = cv; vv.value = fmt(cv, step);
|
|
1091
|
+
commitSnapshot(); if (afterChange) afterChange(); commit();
|
|
1092
|
+
}
|
|
1093
|
+
vv.addEventListener("change", commitVal); // commit on blur (click outside)
|
|
1094
|
+
vv.addEventListener("keydown", (e) => {
|
|
1095
|
+
if (e.key === "Enter") { e.preventDefault(); vv.blur(); } // commit on Enter
|
|
1096
|
+
else if (e.key === "Escape") paint(); // discard on Esc
|
|
1097
|
+
});
|
|
1071
1098
|
w.appendChild(inp); return w;
|
|
1072
1099
|
}
|
|
1073
1100
|
function text(label, path, val) {
|
|
@@ -1178,10 +1205,11 @@ const v3dPref = (k) => { try { return localStorage.getItem("figfoundry.invert3d.
|
|
|
1178
1205
|
const v3dSetPref = (k, v) => { try { localStorage.setItem("figfoundry.invert3d." + k, v ? "1" : "0"); } catch (e) {} };
|
|
1179
1206
|
function v3dVals() {
|
|
1180
1207
|
if (!v3d) return;
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1208
|
+
// The value fields are editable inputs; don't clobber the one being typed into.
|
|
1209
|
+
const set = (id, v) => { const el = $(id); if (el && document.activeElement !== el) el.value = v; };
|
|
1210
|
+
set("#v3eVal", Math.round(v3d.elev));
|
|
1211
|
+
set("#v3aVal", Math.round(v3d.azim));
|
|
1212
|
+
set("#v3dVal", Math.round(v3d.dist * 10) / 10);
|
|
1185
1213
|
}
|
|
1186
1214
|
|
|
1187
1215
|
async function openView3D(pid) {
|
|
@@ -1214,6 +1242,20 @@ function v3dWire() {
|
|
|
1214
1242
|
$("#v3e").oninput = (e) => { v3d.elev = +e.target.value; v3dVals(); v3dRender(); };
|
|
1215
1243
|
$("#v3a").oninput = (e) => { v3d.azim = +e.target.value; v3dVals(); v3dRender(); };
|
|
1216
1244
|
$("#v3d").oninput = (e) => { v3d.dist = +e.target.value; v3dVals(); v3dRender(); };
|
|
1245
|
+
// The value fields are click-to-edit: type an exact number, commit on Enter or
|
|
1246
|
+
// blur, discard on Esc. Keeps the slider + 3-D preview in sync with the typed value.
|
|
1247
|
+
const commit3 = (id, key, lo, hi, sl) => {
|
|
1248
|
+
const el = $(id);
|
|
1249
|
+
el.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); el.blur(); } else if (e.key === "Escape") v3dVals(); });
|
|
1250
|
+
el.addEventListener("change", () => {
|
|
1251
|
+
let v = parseFloat(el.value);
|
|
1252
|
+
if (isNaN(v)) { v3dVals(); return; }
|
|
1253
|
+
v = clamp(v, lo, hi); v3d[key] = v; $(sl).value = v; v3dVals(); v3dRender();
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
commit3("#v3eVal", "elev", -90, 90, "#v3e");
|
|
1257
|
+
commit3("#v3aVal", "azim", -180, 180, "#v3a");
|
|
1258
|
+
commit3("#v3dVal", "dist", 3, 22, "#v3d");
|
|
1217
1259
|
$("#v3invR").onchange = (e) => { if (v3d) v3d.invertR = e.target.checked; v3dSetPref("rotate", e.target.checked); };
|
|
1218
1260
|
$("#v3invZ").onchange = (e) => { if (v3d) v3d.invertZ = e.target.checked; v3dSetPref("zoom", e.target.checked); };
|
|
1219
1261
|
const img = $("#view3dImg"); let drag = null;
|
|
@@ -254,7 +254,18 @@ a.card { text-decoration: none; color: inherit; display: block; }
|
|
|
254
254
|
.ctl { margin-bottom: 13px; }
|
|
255
255
|
.ctl .ctl-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
|
|
256
256
|
.ctl label { font-size: 12.5px; color: var(--muted); }
|
|
257
|
-
.ctl .val {
|
|
257
|
+
.ctl .val {
|
|
258
|
+
font-size: 12.5px; color: var(--ink); font-variant-numeric: tabular-nums; font-weight: 600; font-family: inherit;
|
|
259
|
+
width: 68px; text-align: right;
|
|
260
|
+
background: transparent; border: 1px solid transparent; border-radius: 5px;
|
|
261
|
+
padding: 1px 6px; margin: -1px -6px; /* padding + negative margin: the box appears on focus without shifting the number */
|
|
262
|
+
cursor: default; transition: background .12s ease, border-color .12s ease, box-shadow .12s ease;
|
|
263
|
+
}
|
|
264
|
+
.ctl .val:hover { background: var(--hover); cursor: text; }
|
|
265
|
+
.ctl .val:focus {
|
|
266
|
+
background: var(--input-bg); border-color: var(--accent); cursor: text; outline: none;
|
|
267
|
+
box-shadow: 0 0 0 3px var(--accent-weak);
|
|
268
|
+
}
|
|
258
269
|
|
|
259
270
|
input[type="range"] {
|
|
260
271
|
width: 100%; height: 4px; -webkit-appearance: none; appearance: none;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "figfoundry"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "WYSIWYG matplotlib figure composer: load data (.npz/.pkl), drag signals onto a canvas to build plots, and export clean reproducible code."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -48,8 +48,9 @@ build-backend = "hatchling.build"
|
|
|
48
48
|
packages = ["figfoundry"]
|
|
49
49
|
|
|
50
50
|
[tool.hatch.build.targets.sdist]
|
|
51
|
-
# tests/ is dev-only;
|
|
52
|
-
|
|
51
|
+
# tests/ is dev-only; media/ (README screenshots) and .github/ (CI) are not
|
|
52
|
+
# needed to install the package, so keep them out of the source distribution.
|
|
53
|
+
exclude = ["tests", "media", ".github", "dist"]
|
|
53
54
|
|
|
54
55
|
[tool.ruff]
|
|
55
56
|
line-length = 110
|
figfoundry-0.1.0/PKG-INFO
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: figfoundry
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: WYSIWYG matplotlib figure composer: load data (.npz/.pkl), drag signals onto a canvas to build plots, and export clean reproducible code.
|
|
5
|
-
Project-URL: Homepage, https://github.com/ductai199x/figfoundry
|
|
6
|
-
Project-URL: Repository, https://github.com/ductai199x/figfoundry
|
|
7
|
-
Project-URL: Issues, https://github.com/ductai199x/figfoundry/issues
|
|
8
|
-
Author-email: Tai Duc Nguyen <taiducnguyen.drexel@gmail.com>
|
|
9
|
-
License-Expression: MIT
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Keywords: data-visualization,figures,matplotlib,plotting,scientific-figures,wysiwyg
|
|
12
|
-
Classifier: Development Status :: 4 - Beta
|
|
13
|
-
Classifier: Environment :: Web Environment
|
|
14
|
-
Classifier: Intended Audience :: Developers
|
|
15
|
-
Classifier: Intended Audience :: Science/Research
|
|
16
|
-
Classifier: Operating System :: OS Independent
|
|
17
|
-
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Topic :: Multimedia :: Graphics
|
|
22
|
-
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
-
Requires-Python: >=3.10
|
|
24
|
-
Requires-Dist: fastapi>=0.110
|
|
25
|
-
Requires-Dist: matplotlib>=3.9
|
|
26
|
-
Requires-Dist: numpy>=1.24
|
|
27
|
-
Requires-Dist: pydantic>=2.0
|
|
28
|
-
Requires-Dist: python-multipart>=0.0.9
|
|
29
|
-
Requires-Dist: uvicorn>=0.29
|
|
30
|
-
Provides-Extra: dev
|
|
31
|
-
Requires-Dist: httpx2>=2.0; extra == 'dev'
|
|
32
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
-
Description-Content-Type: text/markdown
|
|
34
|
-
|
|
35
|
-
# Fig Foundry
|
|
36
|
-
|
|
37
|
-
A WYSIWYG matplotlib figure composer. Load your data (`.npz` / `.pkl` / `.csv`),
|
|
38
|
-
drag signals onto a free canvas to build plots, arrange and style them, and export
|
|
39
|
-
clean, reproducible Python.
|
|
40
|
-
|
|
41
|
-
**The preview renders by executing the generated code**, so what you see is
|
|
42
|
-
byte-for-byte what the exported `.py` produces. There is no second renderer to
|
|
43
|
-
drift.
|
|
44
|
-
|
|
45
|
-
Fig Foundry is a standalone local app with no dependency on any particular
|
|
46
|
-
project's data. It ships with a small neutral `sample.npz` (1-D signals plus a
|
|
47
|
-
2-D field) so a fresh install has something to open; load your own dataset to work
|
|
48
|
-
with real data.
|
|
49
|
-
|
|
50
|
-
## Install
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
uv tool install ./path/to/fig_editor # installs a `figfoundry` command
|
|
54
|
-
# or, from a git repo:
|
|
55
|
-
uv tool install figfoundry --from "git+https://github.com/<you>/<repo>.git"
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
Then run:
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
figfoundry # starts the web app on http://127.0.0.1:8037 and opens the browser
|
|
62
|
-
figfoundry serve --port 9000 --no-browser
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## Command line
|
|
66
|
-
|
|
67
|
-
The same engines are available as subcommands for scripting:
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
figfoundry list # list saved figures
|
|
71
|
-
figfoundry new --name my-fig # create a blank figure, print its slug
|
|
72
|
-
figfoundry export SLUG [--out PATH] # write a figure's generated .py
|
|
73
|
-
figfoundry render SLUG --format svg|pdf|png # render a figure to a file
|
|
74
|
-
figfoundry datasets # list available datasets
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Data
|
|
78
|
-
|
|
79
|
-
A *dataset* is a named collection of signals (arrays). Supported formats:
|
|
80
|
-
|
|
81
|
-
- **`.npz`** — numpy zip of named arrays. Safe by construction.
|
|
82
|
-
- **`.pkl`** — Python pickle, loaded through a **whitelist unpickler** that only
|
|
83
|
-
permits numpy arrays and plain containers (blocks `os`, `subprocess`, `socket`,
|
|
84
|
-
`ctypes`, …). A malicious pickle cannot execute code.
|
|
85
|
-
- **`.csv`** — a table with a header row; each numeric column becomes a signal.
|
|
86
|
-
|
|
87
|
-
1-D and 2-D arrays are both supported (2-D for image/contour/vector plots). Load a
|
|
88
|
-
dataset with the **Load data…** button in the toolbar; each canvas is tied to one
|
|
89
|
-
dataset, and its signals populate the library.
|
|
90
|
-
|
|
91
|
-
Your figures, datasets, and exports live in `~/.figfoundry/`.
|
|
92
|
-
|
|
93
|
-
## What you can do
|
|
94
|
-
|
|
95
|
-
- Drag a signal onto the canvas to make a plot (or onto an existing plot to
|
|
96
|
-
overlay a line / σ-band / error bars / replace).
|
|
97
|
-
- **Plot types (23):** line, step, scatter, grouped scatter (clustering), bar,
|
|
98
|
-
bar (horizontal), histogram, box plot, violin plot, 2-D histogram, hexbin, image,
|
|
99
|
-
contour (filled / lines), pcolormesh, vector field, streamplot, decision boundary,
|
|
100
|
-
pie, polar, 3-D scatter, surface (3-D), wireframe (3-D).
|
|
101
|
-
- Move, resize (with snapping), and nudge (arrow keys) plots and elements.
|
|
102
|
-
- Multi-select (Shift+click) to nudge / move / delete several at once.
|
|
103
|
-
- Per-plot: data, color, line/marker, σ-band, error bars, ticks, labels, limits,
|
|
104
|
-
spines, title, colormap + colorbar, and artists (vline, hline, text, annotate,
|
|
105
|
-
arrow, box, circle, ellipse, polygon, span, legend).
|
|
106
|
-
- **Suggest accessible colors** — one click applies the colorblind-safe Okabe-Ito
|
|
107
|
-
palette to a plot's series.
|
|
108
|
-
- **LaTeX mathtext** in labels, titles, and text (e.g. `$x_0$`).
|
|
109
|
-
- Layout: grid, align, distribute, margins, sync axis ranges.
|
|
110
|
-
- Figure elements: axis labels, legend, suptitle, free text.
|
|
111
|
-
- Name / duplicate / delete figures. Export the `.py` (copy or download), and the
|
|
112
|
-
figure itself as **PNG / SVG / PDF** (SVG and PDF are vector, for publication).
|
|
113
|
-
|
|
114
|
-
Everything is also available through a small JSON API (`/api/canvases`,
|
|
115
|
-
`/api/datasets`, `/api/palettes`, `/api/canvases/{slug}/render|code|figure|download`).
|
|
116
|
-
|
|
117
|
-
## Testing
|
|
118
|
-
|
|
119
|
-
The test suite lives in `tests/` (outside the package, so it is never shipped).
|
|
120
|
-
It proves the WYSIWYG invariant (for every plot type), the pickle-safety
|
|
121
|
-
whitelist, and dataset I/O (npz/pkl/csv), each against a throwaway data dir:
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
uv run --extra dev pytest -q
|
|
125
|
-
```
|
figfoundry-0.1.0/README.md
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# Fig Foundry
|
|
2
|
-
|
|
3
|
-
A WYSIWYG matplotlib figure composer. Load your data (`.npz` / `.pkl` / `.csv`),
|
|
4
|
-
drag signals onto a free canvas to build plots, arrange and style them, and export
|
|
5
|
-
clean, reproducible Python.
|
|
6
|
-
|
|
7
|
-
**The preview renders by executing the generated code**, so what you see is
|
|
8
|
-
byte-for-byte what the exported `.py` produces. There is no second renderer to
|
|
9
|
-
drift.
|
|
10
|
-
|
|
11
|
-
Fig Foundry is a standalone local app with no dependency on any particular
|
|
12
|
-
project's data. It ships with a small neutral `sample.npz` (1-D signals plus a
|
|
13
|
-
2-D field) so a fresh install has something to open; load your own dataset to work
|
|
14
|
-
with real data.
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
uv tool install ./path/to/fig_editor # installs a `figfoundry` command
|
|
20
|
-
# or, from a git repo:
|
|
21
|
-
uv tool install figfoundry --from "git+https://github.com/<you>/<repo>.git"
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
Then run:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
figfoundry # starts the web app on http://127.0.0.1:8037 and opens the browser
|
|
28
|
-
figfoundry serve --port 9000 --no-browser
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Command line
|
|
32
|
-
|
|
33
|
-
The same engines are available as subcommands for scripting:
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
figfoundry list # list saved figures
|
|
37
|
-
figfoundry new --name my-fig # create a blank figure, print its slug
|
|
38
|
-
figfoundry export SLUG [--out PATH] # write a figure's generated .py
|
|
39
|
-
figfoundry render SLUG --format svg|pdf|png # render a figure to a file
|
|
40
|
-
figfoundry datasets # list available datasets
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Data
|
|
44
|
-
|
|
45
|
-
A *dataset* is a named collection of signals (arrays). Supported formats:
|
|
46
|
-
|
|
47
|
-
- **`.npz`** — numpy zip of named arrays. Safe by construction.
|
|
48
|
-
- **`.pkl`** — Python pickle, loaded through a **whitelist unpickler** that only
|
|
49
|
-
permits numpy arrays and plain containers (blocks `os`, `subprocess`, `socket`,
|
|
50
|
-
`ctypes`, …). A malicious pickle cannot execute code.
|
|
51
|
-
- **`.csv`** — a table with a header row; each numeric column becomes a signal.
|
|
52
|
-
|
|
53
|
-
1-D and 2-D arrays are both supported (2-D for image/contour/vector plots). Load a
|
|
54
|
-
dataset with the **Load data…** button in the toolbar; each canvas is tied to one
|
|
55
|
-
dataset, and its signals populate the library.
|
|
56
|
-
|
|
57
|
-
Your figures, datasets, and exports live in `~/.figfoundry/`.
|
|
58
|
-
|
|
59
|
-
## What you can do
|
|
60
|
-
|
|
61
|
-
- Drag a signal onto the canvas to make a plot (or onto an existing plot to
|
|
62
|
-
overlay a line / σ-band / error bars / replace).
|
|
63
|
-
- **Plot types (23):** line, step, scatter, grouped scatter (clustering), bar,
|
|
64
|
-
bar (horizontal), histogram, box plot, violin plot, 2-D histogram, hexbin, image,
|
|
65
|
-
contour (filled / lines), pcolormesh, vector field, streamplot, decision boundary,
|
|
66
|
-
pie, polar, 3-D scatter, surface (3-D), wireframe (3-D).
|
|
67
|
-
- Move, resize (with snapping), and nudge (arrow keys) plots and elements.
|
|
68
|
-
- Multi-select (Shift+click) to nudge / move / delete several at once.
|
|
69
|
-
- Per-plot: data, color, line/marker, σ-band, error bars, ticks, labels, limits,
|
|
70
|
-
spines, title, colormap + colorbar, and artists (vline, hline, text, annotate,
|
|
71
|
-
arrow, box, circle, ellipse, polygon, span, legend).
|
|
72
|
-
- **Suggest accessible colors** — one click applies the colorblind-safe Okabe-Ito
|
|
73
|
-
palette to a plot's series.
|
|
74
|
-
- **LaTeX mathtext** in labels, titles, and text (e.g. `$x_0$`).
|
|
75
|
-
- Layout: grid, align, distribute, margins, sync axis ranges.
|
|
76
|
-
- Figure elements: axis labels, legend, suptitle, free text.
|
|
77
|
-
- Name / duplicate / delete figures. Export the `.py` (copy or download), and the
|
|
78
|
-
figure itself as **PNG / SVG / PDF** (SVG and PDF are vector, for publication).
|
|
79
|
-
|
|
80
|
-
Everything is also available through a small JSON API (`/api/canvases`,
|
|
81
|
-
`/api/datasets`, `/api/palettes`, `/api/canvases/{slug}/render|code|figure|download`).
|
|
82
|
-
|
|
83
|
-
## Testing
|
|
84
|
-
|
|
85
|
-
The test suite lives in `tests/` (outside the package, so it is never shipped).
|
|
86
|
-
It proves the WYSIWYG invariant (for every plot type), the pickle-safety
|
|
87
|
-
whitelist, and dataset I/O (npz/pkl/csv), each against a throwaway data dir:
|
|
88
|
-
|
|
89
|
-
```bash
|
|
90
|
-
uv run --extra dev pytest -q
|
|
91
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|