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.
@@ -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 { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; text-align: right; }
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
- <span class="v3-val" id="v3eVal">30&deg;</span>
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
- <span class="v3-val" id="v3aVal">-60&deg;</span>
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
- <span class="v3-val" id="v3dVal">10</span>
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; const vv = document.createElement("span"); vv.className = "val"; vv.textContent = fmt(val, step);
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; inp.value = val;
1069
- inp.addEventListener("input", () => { if (state.sliderSnapshot === null) state.sliderSnapshot = clone(state.spec); setPath(state.spec, path, parseFloat(inp.value)); vv.textContent = fmt(parseFloat(inp.value), step); scheduleDraft(); });
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
- const e = $("#v3eVal"), a = $("#v3aVal"), z = $("#v3dVal");
1182
- if (e) e.textContent = Math.round(v3d.elev) + "°";
1183
- if (a) a.textContent = Math.round(v3d.azim) + "°";
1184
- if (z) z.textContent = (Math.round(v3d.dist * 10) / 10);
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 { font-size: 12.5px; color: var(--ink); font-variant-numeric: tabular-nums; font-weight: 600; }
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.1.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; keep it out of the published source distribution
52
- exclude = ["tests"]
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
- ```
@@ -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