mosaickit 0.1.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.
- mosaickit-0.1.0/.gitignore +13 -0
- mosaickit-0.1.0/CHANGELOG.md +12 -0
- mosaickit-0.1.0/LICENSE +21 -0
- mosaickit-0.1.0/PKG-INFO +204 -0
- mosaickit-0.1.0/README.md +191 -0
- mosaickit-0.1.0/assets/banner.svg +20 -0
- mosaickit-0.1.0/pyproject.toml +54 -0
- mosaickit-0.1.0/src/mosaickit/__init__.py +103 -0
- mosaickit-0.1.0/src/mosaickit/canvas/__init__.py +7 -0
- mosaickit-0.1.0/src/mosaickit/canvas/animation.py +51 -0
- mosaickit-0.1.0/src/mosaickit/canvas/canvas.py +110 -0
- mosaickit-0.1.0/src/mosaickit/canvas/grid.py +186 -0
- mosaickit-0.1.0/src/mosaickit/canvas/layout.py +11 -0
- mosaickit-0.1.0/src/mosaickit/canvas/span.py +19 -0
- mosaickit-0.1.0/src/mosaickit/canvas_spec.py +56 -0
- mosaickit-0.1.0/src/mosaickit/colors.py +51 -0
- mosaickit-0.1.0/src/mosaickit/config.py +101 -0
- mosaickit-0.1.0/src/mosaickit/errors.py +17 -0
- mosaickit-0.1.0/src/mosaickit/interval.py +20 -0
- mosaickit-0.1.0/src/mosaickit/parameter/__init__.py +5 -0
- mosaickit-0.1.0/src/mosaickit/parameter/binding.py +55 -0
- mosaickit-0.1.0/src/mosaickit/parameter/expression.py +125 -0
- mosaickit-0.1.0/src/mosaickit/parameter/parameter.py +49 -0
- mosaickit-0.1.0/src/mosaickit/py.typed +0 -0
- mosaickit-0.1.0/src/mosaickit/rendering/__init__.py +12 -0
- mosaickit-0.1.0/src/mosaickit/rendering/cache.py +70 -0
- mosaickit-0.1.0/src/mosaickit/rendering/context.py +17 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/__init__.py +3 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/animation.py +51 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/arrows.py +55 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/__init__.py +14 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/fill_artist.py +21 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/marker_artist.py +18 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/path_artist.py +42 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/text_artist.py +25 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/fonts.py +5 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/legend.py +21 -0
- mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/renderer.py +145 -0
- mosaickit-0.1.0/src/mosaickit/rendering/plan.py +74 -0
- mosaickit-0.1.0/src/mosaickit/rendering/protocol.py +22 -0
- mosaickit-0.1.0/src/mosaickit/rendering/registry.py +23 -0
- mosaickit-0.1.0/src/mosaickit/scene/__init__.py +33 -0
- mosaickit-0.1.0/src/mosaickit/scene/arrow.py +22 -0
- mosaickit-0.1.0/src/mosaickit/scene/axis_spec.py +70 -0
- mosaickit-0.1.0/src/mosaickit/scene/fill.py +18 -0
- mosaickit-0.1.0/src/mosaickit/scene/group.py +15 -0
- mosaickit-0.1.0/src/mosaickit/scene/layer.py +61 -0
- mosaickit-0.1.0/src/mosaickit/scene/legend.py +17 -0
- mosaickit-0.1.0/src/mosaickit/scene/marker.py +17 -0
- mosaickit-0.1.0/src/mosaickit/scene/path.py +17 -0
- mosaickit-0.1.0/src/mosaickit/scene/scene.py +67 -0
- mosaickit-0.1.0/src/mosaickit/scene/text.py +28 -0
- mosaickit-0.1.0/src/mosaickit/styles/__init__.py +19 -0
- mosaickit-0.1.0/src/mosaickit/styles/arrow.py +14 -0
- mosaickit-0.1.0/src/mosaickit/styles/base.py +38 -0
- mosaickit-0.1.0/src/mosaickit/styles/fill.py +17 -0
- mosaickit-0.1.0/src/mosaickit/styles/legend.py +16 -0
- mosaickit-0.1.0/src/mosaickit/styles/marker.py +23 -0
- mosaickit-0.1.0/src/mosaickit/styles/stroke.py +33 -0
- mosaickit-0.1.0/src/mosaickit/styles/text.py +25 -0
- mosaickit-0.1.0/src/mosaickit/themes/__init__.py +15 -0
- mosaickit-0.1.0/src/mosaickit/themes/_walk.py +5 -0
- mosaickit-0.1.0/src/mosaickit/themes/builtins.py +26 -0
- mosaickit-0.1.0/src/mosaickit/themes/registry.py +29 -0
- mosaickit-0.1.0/src/mosaickit/themes/resolution.py +10 -0
- mosaickit-0.1.0/src/mosaickit/themes/role_pack.py +21 -0
- mosaickit-0.1.0/src/mosaickit/themes/theme.py +68 -0
- mosaickit-0.1.0/tests/conftest.py +5 -0
- mosaickit-0.1.0/tests/test_animation.py +57 -0
- mosaickit-0.1.0/tests/test_canvas_grid.py +90 -0
- mosaickit-0.1.0/tests/test_config.py +60 -0
- mosaickit-0.1.0/tests/test_matplotlib.py +117 -0
- mosaickit-0.1.0/tests/test_parameter.py +54 -0
- mosaickit-0.1.0/tests/test_public_api.py +45 -0
- mosaickit-0.1.0/tests/test_render_plan_cache.py +116 -0
- mosaickit-0.1.0/tests/test_scene.py +84 -0
- mosaickit-0.1.0/tests/test_themes.py +71 -0
- mosaickit-0.1.0/tests/test_values.py +122 -0
- mosaickit-0.1.0/uv.lock +2072 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-01
|
|
4
|
+
|
|
5
|
+
- Add immutable viewport, styles, namespaced themes, scenes, and generic layers.
|
|
6
|
+
- Add Canvas, spanning CanvasGrid, parameter expressions, and Animation.
|
|
7
|
+
- Add Matplotlib rendering with job-scoped caches.
|
|
8
|
+
- Add strict TOML config and context-local defaults.
|
|
9
|
+
- Add uv dependency management, pytest, Ruff, mypy, import-linter, and pre-commit.
|
|
10
|
+
- Keep curve mathematics and TikZ export outside MosaicKit so it remains an
|
|
11
|
+
independent layer-composition package.
|
|
12
|
+
- Add the MosaicKit brand asset and package overview README.
|
mosaickit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EconViz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mosaickit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mosaickit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Domain-neutral scenes, styles, and rendering for EconViz.
|
|
5
|
+
Author: EconViz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: matplotlib<4,>=3.6
|
|
10
|
+
Requires-Dist: numpy>=1.24
|
|
11
|
+
Requires-Dist: tomli>=2; python_version < '3.11'
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# mosaickit
|
|
15
|
+
|
|
16
|
+
<p align="center">
|
|
17
|
+
<img src="assets/banner.svg" alt="mosaickit" width="480">
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<a href="https://pypi.org/project/mosaickit/"><img alt="PyPI" src="https://img.shields.io/pypi/v/mosaickit?style=flat-square&color=181818&labelColor=f3f3f3&cacheSeconds=300"></a>
|
|
22
|
+
<a href="https://pypi.org/project/mosaickit/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/mosaickit?style=flat-square&color=181818&labelColor=f3f3f3"></a>
|
|
23
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License" src="https://img.shields.io/badge/License-MIT-181818?style=flat-square&color=181818&labelColor=f3f3f3"></a>
|
|
24
|
+
<a href="https://github.com/EconViz/mosaickit/actions/workflows/test.yml"><img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/EconViz/mosaickit/test.yml?branch=main&style=flat-square&label=tests&color=181818&labelColor=f3f3f3"></a>
|
|
25
|
+
</p>
|
|
26
|
+
|
|
27
|
+
A domain-neutral toolkit for assembling two-dimensional diagrams from reusable
|
|
28
|
+
scenes, layers, styles, parameters, and renderers.
|
|
29
|
+
|
|
30
|
+
`mosaickit` is the shared graphics foundation for the EconViz package family.
|
|
31
|
+
Domain libraries define economic models and semantic roles; MosaicKit composes
|
|
32
|
+
their visual layers and renders the result. It does not depend on an economic
|
|
33
|
+
model package or a curve-fitting library.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
Add MosaicKit to a project with [uv](https://docs.astral.sh/uv/):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv add mosaickit
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A plain `pip install mosaickit` also works. MosaicKit supports Python 3.10–3.13.
|
|
44
|
+
|
|
45
|
+
## Python API
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from mosaickit import (
|
|
49
|
+
Canvas,
|
|
50
|
+
Fill,
|
|
51
|
+
FillLayer,
|
|
52
|
+
MarkerLayer,
|
|
53
|
+
PathLayer,
|
|
54
|
+
Stroke,
|
|
55
|
+
TextLayer,
|
|
56
|
+
quadrant_axes,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
canvas = Canvas().extend(quadrant_axes(10, 10))
|
|
60
|
+
canvas.add(
|
|
61
|
+
FillLayer(
|
|
62
|
+
[(1, 1), (1, 7), (8, 1)],
|
|
63
|
+
fill=Fill(color="#377EB8", opacity=0.12),
|
|
64
|
+
z_index=-1,
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
canvas.add(
|
|
68
|
+
PathLayer(
|
|
69
|
+
[(1, 8), (2, 5), (4, 3), (7, 1.5), (9, 1)],
|
|
70
|
+
stroke=Stroke(color="#984EA3", width=2),
|
|
71
|
+
id="curve",
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
canvas.add(MarkerLayer([(4, 3)], id="point"))
|
|
75
|
+
canvas.add(TextLayer((4, 3), "A", offset=(8, 8)))
|
|
76
|
+
canvas.save("diagram.svg")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`Canvas.add()`, `extend()`, `remove()`, and `clear()` are fluent builder
|
|
80
|
+
operations. `snapshot()` returns an immutable `Scene`; `copy()` and `bind()`
|
|
81
|
+
produce a new canvas without changing the original. Public layers never contain
|
|
82
|
+
Matplotlib artists, and importing `mosaickit` does not import Matplotlib.
|
|
83
|
+
|
|
84
|
+
## Scenes and layers
|
|
85
|
+
|
|
86
|
+
The scene graph is deliberately small:
|
|
87
|
+
|
|
88
|
+
- `PathLayer` joins ordered finite `(x, y)` coordinates with straight segments.
|
|
89
|
+
- `FillLayer` describes filled regions.
|
|
90
|
+
- `MarkerLayer`, `TextLayer`, and `ArrowLayer` add annotations.
|
|
91
|
+
- `LegendLayer` builds legends from stable layer IDs.
|
|
92
|
+
- `GroupLayer` groups layers without adding renderer-specific state.
|
|
93
|
+
|
|
94
|
+
`AxisSpec` and `build_axes()` construct axes from the same ordinary path and text
|
|
95
|
+
layers. `quadrant_axes()`, `crosshair_axes()`, and `box_frame()` cover common
|
|
96
|
+
layouts. `CanvasSpec` owns ranges, physical size, DPI, labels, and title.
|
|
97
|
+
|
|
98
|
+
Curve construction and TikZ export intentionally live outside MosaicKit. Domain
|
|
99
|
+
packages can call a geometry package such as BezierKit directly, then pass the
|
|
100
|
+
resulting coordinates or renderer-specific output to their own export pipeline.
|
|
101
|
+
|
|
102
|
+
## Styles and themes
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from mosaickit import Config, Stroke, StyleBundle, Theme, use_config
|
|
106
|
+
|
|
107
|
+
paper = Theme(
|
|
108
|
+
"paper",
|
|
109
|
+
{"utility.budget": StyleBundle(stroke=Stroke(color="#984EA3", width=2))},
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
with use_config(Config(theme=paper)):
|
|
113
|
+
canvas = Canvas()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Style resolution proceeds from layer style to canvas role override, configuration
|
|
117
|
+
role override, theme, and finally primitive defaults. A field set to `None`
|
|
118
|
+
inherits; falsey values such as `opacity=0`, `width=0`, and
|
|
119
|
+
`LegendStyle(visible=False)` remain explicit overrides.
|
|
120
|
+
|
|
121
|
+
Domain packages register dotted role names through `ThemeRegistry`. Typed
|
|
122
|
+
`RolePack` dataclasses and `expand_roles()` let those packages author themes
|
|
123
|
+
without making MosaicKit import their domain types.
|
|
124
|
+
|
|
125
|
+
Configuration can also be loaded from TOML:
|
|
126
|
+
|
|
127
|
+
```toml
|
|
128
|
+
[canvas]
|
|
129
|
+
x_range = [0, 20]
|
|
130
|
+
dpi = 150
|
|
131
|
+
|
|
132
|
+
[styles."utility.budget".stroke]
|
|
133
|
+
color = "#984EA3"
|
|
134
|
+
width = 2
|
|
135
|
+
dash = "dashed"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use `Config.load(path)` to validate it. Runtime defaults are isolated with
|
|
139
|
+
`contextvars`, while explicit constructor arguments always take precedence.
|
|
140
|
+
|
|
141
|
+
## Grids, parameters, and animation
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from mosaickit import Animation, Canvas, CanvasGrid, Parameter, TextLayer
|
|
145
|
+
|
|
146
|
+
position = Parameter("position", value_type=float)
|
|
147
|
+
template = Canvas().add(TextLayer((position, 5), "moving"))
|
|
148
|
+
values = position.values([1.0, 3.0, 5.0])
|
|
149
|
+
|
|
150
|
+
CanvasGrid.sweep(template, values, cols=3).save("sweep.svg")
|
|
151
|
+
Animation.sweep(template, values, fps=2).save("sweep.gif")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`CanvasGrid` accepts flat canvases, nested rows, `None` cells, and
|
|
155
|
+
`Span(canvas, rows=..., cols=...)`. A flat list infers a roughly square grid;
|
|
156
|
+
explicit `rows` and `cols` may leave trailing cells empty.
|
|
157
|
+
|
|
158
|
+
Parameters and expressions support arithmetic, ordered comparisons, partial
|
|
159
|
+
binding, and stable structural equality. `Animation.frames()` yields
|
|
160
|
+
backend-neutral scenes. GIF output uses Pillow; MP4 output requires ffmpeg.
|
|
161
|
+
|
|
162
|
+
## Rendering
|
|
163
|
+
|
|
164
|
+
PNG, SVG, and PDF use the built-in Matplotlib renderer:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
result = canvas.render()
|
|
168
|
+
try:
|
|
169
|
+
result.show()
|
|
170
|
+
finally:
|
|
171
|
+
result.close()
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`canvas.save()` closes its temporary render result automatically. The public
|
|
175
|
+
`Renderer` protocol and `RendererRegistry` allow other backends without placing
|
|
176
|
+
backend state in the scene graph. Render plans and contexts remain private while
|
|
177
|
+
the built-in backends evolve.
|
|
178
|
+
|
|
179
|
+
Each render, grid, or animation job owns its cache. Pass an explicit
|
|
180
|
+
`RenderCache` to reuse work across jobs. Non-hashable models bypass caching and
|
|
181
|
+
emit one warning per model type and cache.
|
|
182
|
+
|
|
183
|
+
## Scope
|
|
184
|
+
|
|
185
|
+
MosaicKit owns domain-neutral scene composition, styles, themes, parameter
|
|
186
|
+
binding, grid layout, animation frames, renderer contracts, and static/animated
|
|
187
|
+
output. Economic semantics belong to packages such as UtilityViz and
|
|
188
|
+
PrincipleViz. Mathematical curve construction and native TikZ path generation
|
|
189
|
+
belong to geometry packages such as BezierKit.
|
|
190
|
+
|
|
191
|
+
## Development
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
uv sync --locked
|
|
195
|
+
uv run pre-commit install
|
|
196
|
+
uv run pytest
|
|
197
|
+
uv run ruff check .
|
|
198
|
+
uv run ruff format --check .
|
|
199
|
+
uv run mypy
|
|
200
|
+
uv run lint-imports
|
|
201
|
+
uv build
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The lockfile pins all development dependencies.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# mosaickit
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/banner.svg" alt="mosaickit" width="480">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://pypi.org/project/mosaickit/"><img alt="PyPI" src="https://img.shields.io/pypi/v/mosaickit?style=flat-square&color=181818&labelColor=f3f3f3&cacheSeconds=300"></a>
|
|
9
|
+
<a href="https://pypi.org/project/mosaickit/"><img alt="Python" src="https://img.shields.io/pypi/pyversions/mosaickit?style=flat-square&color=181818&labelColor=f3f3f3"></a>
|
|
10
|
+
<a href="https://opensource.org/licenses/MIT"><img alt="License" src="https://img.shields.io/badge/License-MIT-181818?style=flat-square&color=181818&labelColor=f3f3f3"></a>
|
|
11
|
+
<a href="https://github.com/EconViz/mosaickit/actions/workflows/test.yml"><img alt="Tests" src="https://img.shields.io/github/actions/workflow/status/EconViz/mosaickit/test.yml?branch=main&style=flat-square&label=tests&color=181818&labelColor=f3f3f3"></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
A domain-neutral toolkit for assembling two-dimensional diagrams from reusable
|
|
15
|
+
scenes, layers, styles, parameters, and renderers.
|
|
16
|
+
|
|
17
|
+
`mosaickit` is the shared graphics foundation for the EconViz package family.
|
|
18
|
+
Domain libraries define economic models and semantic roles; MosaicKit composes
|
|
19
|
+
their visual layers and renders the result. It does not depend on an economic
|
|
20
|
+
model package or a curve-fitting library.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
Add MosaicKit to a project with [uv](https://docs.astral.sh/uv/):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
uv add mosaickit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A plain `pip install mosaickit` also works. MosaicKit supports Python 3.10–3.13.
|
|
31
|
+
|
|
32
|
+
## Python API
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from mosaickit import (
|
|
36
|
+
Canvas,
|
|
37
|
+
Fill,
|
|
38
|
+
FillLayer,
|
|
39
|
+
MarkerLayer,
|
|
40
|
+
PathLayer,
|
|
41
|
+
Stroke,
|
|
42
|
+
TextLayer,
|
|
43
|
+
quadrant_axes,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
canvas = Canvas().extend(quadrant_axes(10, 10))
|
|
47
|
+
canvas.add(
|
|
48
|
+
FillLayer(
|
|
49
|
+
[(1, 1), (1, 7), (8, 1)],
|
|
50
|
+
fill=Fill(color="#377EB8", opacity=0.12),
|
|
51
|
+
z_index=-1,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
canvas.add(
|
|
55
|
+
PathLayer(
|
|
56
|
+
[(1, 8), (2, 5), (4, 3), (7, 1.5), (9, 1)],
|
|
57
|
+
stroke=Stroke(color="#984EA3", width=2),
|
|
58
|
+
id="curve",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
canvas.add(MarkerLayer([(4, 3)], id="point"))
|
|
62
|
+
canvas.add(TextLayer((4, 3), "A", offset=(8, 8)))
|
|
63
|
+
canvas.save("diagram.svg")
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`Canvas.add()`, `extend()`, `remove()`, and `clear()` are fluent builder
|
|
67
|
+
operations. `snapshot()` returns an immutable `Scene`; `copy()` and `bind()`
|
|
68
|
+
produce a new canvas without changing the original. Public layers never contain
|
|
69
|
+
Matplotlib artists, and importing `mosaickit` does not import Matplotlib.
|
|
70
|
+
|
|
71
|
+
## Scenes and layers
|
|
72
|
+
|
|
73
|
+
The scene graph is deliberately small:
|
|
74
|
+
|
|
75
|
+
- `PathLayer` joins ordered finite `(x, y)` coordinates with straight segments.
|
|
76
|
+
- `FillLayer` describes filled regions.
|
|
77
|
+
- `MarkerLayer`, `TextLayer`, and `ArrowLayer` add annotations.
|
|
78
|
+
- `LegendLayer` builds legends from stable layer IDs.
|
|
79
|
+
- `GroupLayer` groups layers without adding renderer-specific state.
|
|
80
|
+
|
|
81
|
+
`AxisSpec` and `build_axes()` construct axes from the same ordinary path and text
|
|
82
|
+
layers. `quadrant_axes()`, `crosshair_axes()`, and `box_frame()` cover common
|
|
83
|
+
layouts. `CanvasSpec` owns ranges, physical size, DPI, labels, and title.
|
|
84
|
+
|
|
85
|
+
Curve construction and TikZ export intentionally live outside MosaicKit. Domain
|
|
86
|
+
packages can call a geometry package such as BezierKit directly, then pass the
|
|
87
|
+
resulting coordinates or renderer-specific output to their own export pipeline.
|
|
88
|
+
|
|
89
|
+
## Styles and themes
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from mosaickit import Config, Stroke, StyleBundle, Theme, use_config
|
|
93
|
+
|
|
94
|
+
paper = Theme(
|
|
95
|
+
"paper",
|
|
96
|
+
{"utility.budget": StyleBundle(stroke=Stroke(color="#984EA3", width=2))},
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
with use_config(Config(theme=paper)):
|
|
100
|
+
canvas = Canvas()
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Style resolution proceeds from layer style to canvas role override, configuration
|
|
104
|
+
role override, theme, and finally primitive defaults. A field set to `None`
|
|
105
|
+
inherits; falsey values such as `opacity=0`, `width=0`, and
|
|
106
|
+
`LegendStyle(visible=False)` remain explicit overrides.
|
|
107
|
+
|
|
108
|
+
Domain packages register dotted role names through `ThemeRegistry`. Typed
|
|
109
|
+
`RolePack` dataclasses and `expand_roles()` let those packages author themes
|
|
110
|
+
without making MosaicKit import their domain types.
|
|
111
|
+
|
|
112
|
+
Configuration can also be loaded from TOML:
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
[canvas]
|
|
116
|
+
x_range = [0, 20]
|
|
117
|
+
dpi = 150
|
|
118
|
+
|
|
119
|
+
[styles."utility.budget".stroke]
|
|
120
|
+
color = "#984EA3"
|
|
121
|
+
width = 2
|
|
122
|
+
dash = "dashed"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Use `Config.load(path)` to validate it. Runtime defaults are isolated with
|
|
126
|
+
`contextvars`, while explicit constructor arguments always take precedence.
|
|
127
|
+
|
|
128
|
+
## Grids, parameters, and animation
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from mosaickit import Animation, Canvas, CanvasGrid, Parameter, TextLayer
|
|
132
|
+
|
|
133
|
+
position = Parameter("position", value_type=float)
|
|
134
|
+
template = Canvas().add(TextLayer((position, 5), "moving"))
|
|
135
|
+
values = position.values([1.0, 3.0, 5.0])
|
|
136
|
+
|
|
137
|
+
CanvasGrid.sweep(template, values, cols=3).save("sweep.svg")
|
|
138
|
+
Animation.sweep(template, values, fps=2).save("sweep.gif")
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`CanvasGrid` accepts flat canvases, nested rows, `None` cells, and
|
|
142
|
+
`Span(canvas, rows=..., cols=...)`. A flat list infers a roughly square grid;
|
|
143
|
+
explicit `rows` and `cols` may leave trailing cells empty.
|
|
144
|
+
|
|
145
|
+
Parameters and expressions support arithmetic, ordered comparisons, partial
|
|
146
|
+
binding, and stable structural equality. `Animation.frames()` yields
|
|
147
|
+
backend-neutral scenes. GIF output uses Pillow; MP4 output requires ffmpeg.
|
|
148
|
+
|
|
149
|
+
## Rendering
|
|
150
|
+
|
|
151
|
+
PNG, SVG, and PDF use the built-in Matplotlib renderer:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
result = canvas.render()
|
|
155
|
+
try:
|
|
156
|
+
result.show()
|
|
157
|
+
finally:
|
|
158
|
+
result.close()
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`canvas.save()` closes its temporary render result automatically. The public
|
|
162
|
+
`Renderer` protocol and `RendererRegistry` allow other backends without placing
|
|
163
|
+
backend state in the scene graph. Render plans and contexts remain private while
|
|
164
|
+
the built-in backends evolve.
|
|
165
|
+
|
|
166
|
+
Each render, grid, or animation job owns its cache. Pass an explicit
|
|
167
|
+
`RenderCache` to reuse work across jobs. Non-hashable models bypass caching and
|
|
168
|
+
emit one warning per model type and cache.
|
|
169
|
+
|
|
170
|
+
## Scope
|
|
171
|
+
|
|
172
|
+
MosaicKit owns domain-neutral scene composition, styles, themes, parameter
|
|
173
|
+
binding, grid layout, animation frames, renderer contracts, and static/animated
|
|
174
|
+
output. Economic semantics belong to packages such as UtilityViz and
|
|
175
|
+
PrincipleViz. Mathematical curve construction and native TikZ path generation
|
|
176
|
+
belong to geometry packages such as BezierKit.
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
uv sync --locked
|
|
182
|
+
uv run pre-commit install
|
|
183
|
+
uv run pytest
|
|
184
|
+
uv run ruff check .
|
|
185
|
+
uv run ruff format --check .
|
|
186
|
+
uv run mypy
|
|
187
|
+
uv run lint-imports
|
|
188
|
+
uv build
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The lockfile pins all development dependencies.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 160" width="640" height="160" role="img" aria-label="mosaickit">
|
|
2
|
+
<title>mosaickit</title>
|
|
3
|
+
|
|
4
|
+
<!-- A small scene assembled from independent tiles/layers. -->
|
|
5
|
+
<g transform="translate(24, 24)" stroke-linecap="round" stroke-linejoin="round">
|
|
6
|
+
<rect x="4" y="4" width="48" height="48" rx="8" fill="#181818"/>
|
|
7
|
+
<rect x="60" y="4" width="48" height="48" rx="8" fill="#8a8a8a"/>
|
|
8
|
+
<rect x="4" y="60" width="48" height="48" rx="8" fill="#c9c9c9"/>
|
|
9
|
+
<rect x="60" y="60" width="48" height="48" rx="8" fill="#f3f3f3" stroke="#181818" stroke-width="3"/>
|
|
10
|
+
|
|
11
|
+
<!-- Primitive layers crossing tile boundaries form one coherent diagram. -->
|
|
12
|
+
<path d="M 16 91 L 34 72 L 49 80 L 70 35 L 94 18" fill="none" stroke="#ffffff" stroke-width="5"/>
|
|
13
|
+
<circle cx="34" cy="72" r="5" fill="#181818" stroke="#ffffff" stroke-width="2.5"/>
|
|
14
|
+
<path d="M 69 92 L 92 69" fill="none" stroke="#181818" stroke-width="5"/>
|
|
15
|
+
<path d="M 84 69 H 92 V 77" fill="none" stroke="#181818" stroke-width="4"/>
|
|
16
|
+
</g>
|
|
17
|
+
|
|
18
|
+
<!-- Wordmark follows the BezierKit family treatment. -->
|
|
19
|
+
<text x="160" y="98" font-family="'JetBrains Mono','SFMono-Regular',Consolas,monospace" font-size="52" font-weight="700" letter-spacing="-1" fill="#181818">mosaic<tspan fill="#8a8a8a">kit</tspan></text>
|
|
20
|
+
</svg>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mosaickit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Domain-neutral scenes, styles, and rendering for EconViz."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [{ name = "EconViz" }]
|
|
9
|
+
dependencies = ["numpy>=1.24", "matplotlib>=3.6,<4", "tomli>=2; python_version < '3.11'"]
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["hatchling"]
|
|
13
|
+
build-backend = "hatchling.build"
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/mosaickit"]
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.sdist]
|
|
19
|
+
include = ["src", "tests", "assets", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml", "uv.lock"]
|
|
20
|
+
|
|
21
|
+
[dependency-groups]
|
|
22
|
+
dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.8", "pre-commit>=4", "mypy>=1.13", "import-linter>=2", "build>=1", "pillow>=10"]
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
line-length = 100
|
|
26
|
+
target-version = "py310"
|
|
27
|
+
|
|
28
|
+
[tool.ruff.lint]
|
|
29
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
addopts = "-ra --import-mode=importlib"
|
|
34
|
+
|
|
35
|
+
[tool.mypy]
|
|
36
|
+
files = ["src/mosaickit"]
|
|
37
|
+
ignore_missing_imports = true
|
|
38
|
+
check_untyped_defs = true
|
|
39
|
+
|
|
40
|
+
[tool.importlinter]
|
|
41
|
+
root_package = "mosaickit"
|
|
42
|
+
include_external_packages = true
|
|
43
|
+
|
|
44
|
+
[[tool.importlinter.contracts]]
|
|
45
|
+
name = "Models do not depend on rendering or canvas"
|
|
46
|
+
type = "forbidden"
|
|
47
|
+
source_modules = ["mosaickit.scene", "mosaickit.styles", "mosaickit.themes", "mosaickit.parameter"]
|
|
48
|
+
forbidden_modules = ["mosaickit.rendering", "mosaickit.canvas"]
|
|
49
|
+
|
|
50
|
+
[[tool.importlinter.contracts]]
|
|
51
|
+
name = "Core never imports domain packages"
|
|
52
|
+
type = "forbidden"
|
|
53
|
+
source_modules = ["mosaickit"]
|
|
54
|
+
forbidden_modules = ["econ_viz", "utility_viz", "micro_viz", "macro_viz", "trade_viz", "dsge_viz", "principle_viz"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Domain-neutral diagram construction and rendering."""
|
|
2
|
+
|
|
3
|
+
from mosaickit.canvas import Animation, Canvas, CanvasGrid, Layout, Span
|
|
4
|
+
from mosaickit.canvas_spec import CanvasSpec
|
|
5
|
+
from mosaickit.colors import TRANSPARENT, Color
|
|
6
|
+
from mosaickit.config import Config, use_config
|
|
7
|
+
from mosaickit.errors import BindingError, ConfigurationError, MosaicKitError, RenderError
|
|
8
|
+
from mosaickit.interval import Interval
|
|
9
|
+
from mosaickit.parameter import Constant, Expression, Parameter, ParameterValues
|
|
10
|
+
from mosaickit.rendering import (
|
|
11
|
+
CacheBypassWarning,
|
|
12
|
+
CacheKey,
|
|
13
|
+
RenderCache,
|
|
14
|
+
Renderer,
|
|
15
|
+
RendererRegistry,
|
|
16
|
+
SaveOptions,
|
|
17
|
+
)
|
|
18
|
+
from mosaickit.scene import (
|
|
19
|
+
ArrowLayer,
|
|
20
|
+
AxisSpec,
|
|
21
|
+
FillLayer,
|
|
22
|
+
GroupLayer,
|
|
23
|
+
Layer,
|
|
24
|
+
LegendLayer,
|
|
25
|
+
MarkerLayer,
|
|
26
|
+
PathLayer,
|
|
27
|
+
Scene,
|
|
28
|
+
TextLayer,
|
|
29
|
+
box_frame,
|
|
30
|
+
build_axes,
|
|
31
|
+
crosshair_axes,
|
|
32
|
+
quadrant_axes,
|
|
33
|
+
)
|
|
34
|
+
from mosaickit.styles import (
|
|
35
|
+
ArrowPlacement,
|
|
36
|
+
ArrowStyle,
|
|
37
|
+
DashStyle,
|
|
38
|
+
Fill,
|
|
39
|
+
LegendStyle,
|
|
40
|
+
Marker,
|
|
41
|
+
SparseStyle,
|
|
42
|
+
Stroke,
|
|
43
|
+
TextStyle,
|
|
44
|
+
)
|
|
45
|
+
from mosaickit.themes import RolePack, StyleBundle, Theme, ThemeRegistry, expand_roles
|
|
46
|
+
|
|
47
|
+
__version__ = "0.1.0"
|
|
48
|
+
__all__ = [
|
|
49
|
+
"Animation",
|
|
50
|
+
"ArrowLayer",
|
|
51
|
+
"ArrowPlacement",
|
|
52
|
+
"ArrowStyle",
|
|
53
|
+
"AxisSpec",
|
|
54
|
+
"BindingError",
|
|
55
|
+
"CacheBypassWarning",
|
|
56
|
+
"CacheKey",
|
|
57
|
+
"Canvas",
|
|
58
|
+
"CanvasGrid",
|
|
59
|
+
"CanvasSpec",
|
|
60
|
+
"Color",
|
|
61
|
+
"Config",
|
|
62
|
+
"ConfigurationError",
|
|
63
|
+
"Constant",
|
|
64
|
+
"DashStyle",
|
|
65
|
+
"MosaicKitError",
|
|
66
|
+
"Expression",
|
|
67
|
+
"Fill",
|
|
68
|
+
"FillLayer",
|
|
69
|
+
"GroupLayer",
|
|
70
|
+
"Interval",
|
|
71
|
+
"Layer",
|
|
72
|
+
"Layout",
|
|
73
|
+
"LegendLayer",
|
|
74
|
+
"LegendStyle",
|
|
75
|
+
"Marker",
|
|
76
|
+
"MarkerLayer",
|
|
77
|
+
"Parameter",
|
|
78
|
+
"ParameterValues",
|
|
79
|
+
"PathLayer",
|
|
80
|
+
"RenderCache",
|
|
81
|
+
"RenderError",
|
|
82
|
+
"Renderer",
|
|
83
|
+
"RendererRegistry",
|
|
84
|
+
"RolePack",
|
|
85
|
+
"SaveOptions",
|
|
86
|
+
"Scene",
|
|
87
|
+
"Span",
|
|
88
|
+
"SparseStyle",
|
|
89
|
+
"Stroke",
|
|
90
|
+
"StyleBundle",
|
|
91
|
+
"TRANSPARENT",
|
|
92
|
+
"TextLayer",
|
|
93
|
+
"TextStyle",
|
|
94
|
+
"Theme",
|
|
95
|
+
"ThemeRegistry",
|
|
96
|
+
"__version__",
|
|
97
|
+
"box_frame",
|
|
98
|
+
"build_axes",
|
|
99
|
+
"crosshair_axes",
|
|
100
|
+
"expand_roles",
|
|
101
|
+
"quadrant_axes",
|
|
102
|
+
"use_config",
|
|
103
|
+
]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from mosaickit.canvas.animation import Animation
|
|
2
|
+
from mosaickit.canvas.canvas import Canvas
|
|
3
|
+
from mosaickit.canvas.grid import CanvasGrid
|
|
4
|
+
from mosaickit.canvas.layout import Layout
|
|
5
|
+
from mosaickit.canvas.span import Span
|
|
6
|
+
|
|
7
|
+
__all__ = ["Animation", "Canvas", "CanvasGrid", "Layout", "Span"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mosaickit.canvas.canvas import Canvas
|
|
9
|
+
from mosaickit.errors import ConfigurationError, RenderError
|
|
10
|
+
from mosaickit.parameter import Parameter, ParameterValues
|
|
11
|
+
from mosaickit.rendering import RenderCache, Renderer
|
|
12
|
+
from mosaickit.scene import Scene
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class Animation:
|
|
17
|
+
template: Canvas
|
|
18
|
+
parameter: Parameter
|
|
19
|
+
values: tuple[Any, ...]
|
|
20
|
+
fps: int = 30
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
if isinstance(self.fps, bool) or not isinstance(self.fps, int) or self.fps < 1:
|
|
24
|
+
raise ConfigurationError("Animation.fps must be a positive integer")
|
|
25
|
+
values = ParameterValues(self.parameter, self.values).values
|
|
26
|
+
if not values:
|
|
27
|
+
raise ConfigurationError("Animation requires at least one value")
|
|
28
|
+
object.__setattr__(self, "values", values)
|
|
29
|
+
object.__setattr__(self, "template", self.template.copy())
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def sweep(canvas: Canvas, values: ParameterValues, *, fps: int = 30) -> Animation:
|
|
33
|
+
return Animation(canvas, values.parameter, values.values, fps)
|
|
34
|
+
|
|
35
|
+
def frames(self) -> Iterator[Scene]:
|
|
36
|
+
for value in self.values:
|
|
37
|
+
yield self.template.bind(self.parameter, value).snapshot()
|
|
38
|
+
|
|
39
|
+
def save(
|
|
40
|
+
self,
|
|
41
|
+
path: str | Path,
|
|
42
|
+
*,
|
|
43
|
+
renderer: Renderer | str | None = None,
|
|
44
|
+
cache: RenderCache | None = None,
|
|
45
|
+
) -> list[Path]:
|
|
46
|
+
selected = self.template._renderer(renderer)
|
|
47
|
+
if not hasattr(selected, "save_animation"):
|
|
48
|
+
raise RenderError(f"Renderer {selected.name!r} does not support animation output")
|
|
49
|
+
return selected.save_animation(
|
|
50
|
+
self, Path(path), cache if cache is not None else RenderCache()
|
|
51
|
+
)
|