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.
Files changed (79) hide show
  1. mosaickit-0.1.0/.gitignore +13 -0
  2. mosaickit-0.1.0/CHANGELOG.md +12 -0
  3. mosaickit-0.1.0/LICENSE +21 -0
  4. mosaickit-0.1.0/PKG-INFO +204 -0
  5. mosaickit-0.1.0/README.md +191 -0
  6. mosaickit-0.1.0/assets/banner.svg +20 -0
  7. mosaickit-0.1.0/pyproject.toml +54 -0
  8. mosaickit-0.1.0/src/mosaickit/__init__.py +103 -0
  9. mosaickit-0.1.0/src/mosaickit/canvas/__init__.py +7 -0
  10. mosaickit-0.1.0/src/mosaickit/canvas/animation.py +51 -0
  11. mosaickit-0.1.0/src/mosaickit/canvas/canvas.py +110 -0
  12. mosaickit-0.1.0/src/mosaickit/canvas/grid.py +186 -0
  13. mosaickit-0.1.0/src/mosaickit/canvas/layout.py +11 -0
  14. mosaickit-0.1.0/src/mosaickit/canvas/span.py +19 -0
  15. mosaickit-0.1.0/src/mosaickit/canvas_spec.py +56 -0
  16. mosaickit-0.1.0/src/mosaickit/colors.py +51 -0
  17. mosaickit-0.1.0/src/mosaickit/config.py +101 -0
  18. mosaickit-0.1.0/src/mosaickit/errors.py +17 -0
  19. mosaickit-0.1.0/src/mosaickit/interval.py +20 -0
  20. mosaickit-0.1.0/src/mosaickit/parameter/__init__.py +5 -0
  21. mosaickit-0.1.0/src/mosaickit/parameter/binding.py +55 -0
  22. mosaickit-0.1.0/src/mosaickit/parameter/expression.py +125 -0
  23. mosaickit-0.1.0/src/mosaickit/parameter/parameter.py +49 -0
  24. mosaickit-0.1.0/src/mosaickit/py.typed +0 -0
  25. mosaickit-0.1.0/src/mosaickit/rendering/__init__.py +12 -0
  26. mosaickit-0.1.0/src/mosaickit/rendering/cache.py +70 -0
  27. mosaickit-0.1.0/src/mosaickit/rendering/context.py +17 -0
  28. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/__init__.py +3 -0
  29. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/animation.py +51 -0
  30. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/arrows.py +55 -0
  31. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/__init__.py +14 -0
  32. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/fill_artist.py +21 -0
  33. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/marker_artist.py +18 -0
  34. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/path_artist.py +42 -0
  35. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/artists/text_artist.py +25 -0
  36. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/fonts.py +5 -0
  37. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/legend.py +21 -0
  38. mosaickit-0.1.0/src/mosaickit/rendering/matplotlib/renderer.py +145 -0
  39. mosaickit-0.1.0/src/mosaickit/rendering/plan.py +74 -0
  40. mosaickit-0.1.0/src/mosaickit/rendering/protocol.py +22 -0
  41. mosaickit-0.1.0/src/mosaickit/rendering/registry.py +23 -0
  42. mosaickit-0.1.0/src/mosaickit/scene/__init__.py +33 -0
  43. mosaickit-0.1.0/src/mosaickit/scene/arrow.py +22 -0
  44. mosaickit-0.1.0/src/mosaickit/scene/axis_spec.py +70 -0
  45. mosaickit-0.1.0/src/mosaickit/scene/fill.py +18 -0
  46. mosaickit-0.1.0/src/mosaickit/scene/group.py +15 -0
  47. mosaickit-0.1.0/src/mosaickit/scene/layer.py +61 -0
  48. mosaickit-0.1.0/src/mosaickit/scene/legend.py +17 -0
  49. mosaickit-0.1.0/src/mosaickit/scene/marker.py +17 -0
  50. mosaickit-0.1.0/src/mosaickit/scene/path.py +17 -0
  51. mosaickit-0.1.0/src/mosaickit/scene/scene.py +67 -0
  52. mosaickit-0.1.0/src/mosaickit/scene/text.py +28 -0
  53. mosaickit-0.1.0/src/mosaickit/styles/__init__.py +19 -0
  54. mosaickit-0.1.0/src/mosaickit/styles/arrow.py +14 -0
  55. mosaickit-0.1.0/src/mosaickit/styles/base.py +38 -0
  56. mosaickit-0.1.0/src/mosaickit/styles/fill.py +17 -0
  57. mosaickit-0.1.0/src/mosaickit/styles/legend.py +16 -0
  58. mosaickit-0.1.0/src/mosaickit/styles/marker.py +23 -0
  59. mosaickit-0.1.0/src/mosaickit/styles/stroke.py +33 -0
  60. mosaickit-0.1.0/src/mosaickit/styles/text.py +25 -0
  61. mosaickit-0.1.0/src/mosaickit/themes/__init__.py +15 -0
  62. mosaickit-0.1.0/src/mosaickit/themes/_walk.py +5 -0
  63. mosaickit-0.1.0/src/mosaickit/themes/builtins.py +26 -0
  64. mosaickit-0.1.0/src/mosaickit/themes/registry.py +29 -0
  65. mosaickit-0.1.0/src/mosaickit/themes/resolution.py +10 -0
  66. mosaickit-0.1.0/src/mosaickit/themes/role_pack.py +21 -0
  67. mosaickit-0.1.0/src/mosaickit/themes/theme.py +68 -0
  68. mosaickit-0.1.0/tests/conftest.py +5 -0
  69. mosaickit-0.1.0/tests/test_animation.py +57 -0
  70. mosaickit-0.1.0/tests/test_canvas_grid.py +90 -0
  71. mosaickit-0.1.0/tests/test_config.py +60 -0
  72. mosaickit-0.1.0/tests/test_matplotlib.py +117 -0
  73. mosaickit-0.1.0/tests/test_parameter.py +54 -0
  74. mosaickit-0.1.0/tests/test_public_api.py +45 -0
  75. mosaickit-0.1.0/tests/test_render_plan_cache.py +116 -0
  76. mosaickit-0.1.0/tests/test_scene.py +84 -0
  77. mosaickit-0.1.0/tests/test_themes.py +71 -0
  78. mosaickit-0.1.0/tests/test_values.py +122 -0
  79. mosaickit-0.1.0/uv.lock +2072 -0
@@ -0,0 +1,13 @@
1
+ .DS_Store
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ .mypy_cache/
8
+ .coverage
9
+ htmlcov/
10
+ dist/
11
+ .import_linter_cache/
12
+ .superpowers/
13
+ docs/
@@ -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.
@@ -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.
@@ -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
+ )