rgrid-python 4.5.3__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.
- rgrid_python-4.5.3/.gitattributes +15 -0
- rgrid_python-4.5.3/.gitignore +31 -0
- rgrid_python-4.5.3/LICENSE +3 -0
- rgrid_python-4.5.3/PKG-INFO +489 -0
- rgrid_python-4.5.3/README.md +448 -0
- rgrid_python-4.5.3/grid_py/__init__.py +340 -0
- rgrid_python-4.5.3/grid_py/_arrow.py +331 -0
- rgrid_python-4.5.3/grid_py/_clippath.py +170 -0
- rgrid_python-4.5.3/grid_py/_colour.py +815 -0
- rgrid_python-4.5.3/grid_py/_coords.py +1534 -0
- rgrid_python-4.5.3/grid_py/_curve.py +1668 -0
- rgrid_python-4.5.3/grid_py/_display_list.py +507 -0
- rgrid_python-4.5.3/grid_py/_draw.py +1397 -0
- rgrid_python-4.5.3/grid_py/_edit.py +756 -0
- rgrid_python-4.5.3/grid_py/_font_metrics.py +319 -0
- rgrid_python-4.5.3/grid_py/_gpar.py +572 -0
- rgrid_python-4.5.3/grid_py/_grab.py +501 -0
- rgrid_python-4.5.3/grid_py/_grob.py +1377 -0
- rgrid_python-4.5.3/grid_py/_group.py +798 -0
- rgrid_python-4.5.3/grid_py/_highlevel.py +2176 -0
- rgrid_python-4.5.3/grid_py/_just.py +361 -0
- rgrid_python-4.5.3/grid_py/_layout.py +593 -0
- rgrid_python-4.5.3/grid_py/_ls.py +895 -0
- rgrid_python-4.5.3/grid_py/_mask.py +196 -0
- rgrid_python-4.5.3/grid_py/_path.py +414 -0
- rgrid_python-4.5.3/grid_py/_patterns.py +1049 -0
- rgrid_python-4.5.3/grid_py/_primitives.py +2198 -0
- rgrid_python-4.5.3/grid_py/_renderer_base.py +1184 -0
- rgrid_python-4.5.3/grid_py/_scene_graph.py +248 -0
- rgrid_python-4.5.3/grid_py/_size.py +1352 -0
- rgrid_python-4.5.3/grid_py/_state.py +683 -0
- rgrid_python-4.5.3/grid_py/_transforms.py +448 -0
- rgrid_python-4.5.3/grid_py/_typeset.py +384 -0
- rgrid_python-4.5.3/grid_py/_units.py +1924 -0
- rgrid_python-4.5.3/grid_py/_utils.py +310 -0
- rgrid_python-4.5.3/grid_py/_viewport.py +1649 -0
- rgrid_python-4.5.3/grid_py/_vp_calc.py +970 -0
- rgrid_python-4.5.3/grid_py/py.typed +0 -0
- rgrid_python-4.5.3/grid_py/renderer.py +1762 -0
- rgrid_python-4.5.3/grid_py/renderer_web.py +764 -0
- rgrid_python-4.5.3/grid_py/resources/d3.v7.min.js +2 -0
- rgrid_python-4.5.3/grid_py/resources/gridpy.css +80 -0
- rgrid_python-4.5.3/grid_py/resources/gridpy.js +813 -0
- rgrid_python-4.5.3/pyproject.toml +86 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# GitHub language statistics: only count Python
|
|
2
|
+
*.py linguist-detectable
|
|
3
|
+
*.ipynb linguist-documentation
|
|
4
|
+
*.html linguist-documentation
|
|
5
|
+
*.css linguist-documentation
|
|
6
|
+
*.js linguist-documentation
|
|
7
|
+
*.json linguist-documentation
|
|
8
|
+
*.yml linguist-documentation
|
|
9
|
+
*.yaml linguist-documentation
|
|
10
|
+
*.md linguist-documentation
|
|
11
|
+
*.R linguist-documentation
|
|
12
|
+
*.csv linguist-documentation
|
|
13
|
+
site/** linguist-documentation
|
|
14
|
+
docs/** linguist-documentation
|
|
15
|
+
tutorials/** linguist-documentation
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.eggs/
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv/
|
|
11
|
+
.tox/
|
|
12
|
+
|
|
13
|
+
# Compiled extensions
|
|
14
|
+
*.so
|
|
15
|
+
*.pyd
|
|
16
|
+
*.dylib
|
|
17
|
+
|
|
18
|
+
# IDE
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
*.swp
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
.coverage
|
|
27
|
+
|
|
28
|
+
# Docs build
|
|
29
|
+
site/
|
|
30
|
+
.cache/
|
|
31
|
+
.ipynb_checkpoints/
|
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rgrid-python
|
|
3
|
+
Version: 4.5.3
|
|
4
|
+
Summary: Python port of the R grid package (tracks R grid 4.5.3)
|
|
5
|
+
Project-URL: Homepage, https://github.com/Bio-Babel/grid_py
|
|
6
|
+
Project-URL: Repository, https://github.com/Bio-Babel/grid_py
|
|
7
|
+
Project-URL: Issues, https://github.com/Bio-Babel/grid_py/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/Bio-Babel/grid_py#readme
|
|
9
|
+
Author-email: Jeffery Liu <jeffliu.lucky@gmail.com>
|
|
10
|
+
License-Expression: Artistic-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: R-port,ggplot2,graphics,grid,scientific-visualization,visualization
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: Artistic License
|
|
17
|
+
Classifier: Operating System :: MacOS
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Requires-Dist: pycairo>=1.20
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: build; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine; extra == 'dev'
|
|
35
|
+
Provides-Extra: docs
|
|
36
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
37
|
+
Requires-Dist: mkdocs-jupyter; extra == 'docs'
|
|
38
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
39
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# grid_py
|
|
43
|
+
|
|
44
|
+
Python port of the R **grid** package.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install rgrid-python
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The importable package name is `grid_py`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import grid_py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### System requirements
|
|
59
|
+
|
|
60
|
+
`grid_py` depends on [`pycairo`](https://pypi.org/project/pycairo/), which
|
|
61
|
+
builds against the system **cairo** library. Install the system package
|
|
62
|
+
*before* `pip install rgrid-python`:
|
|
63
|
+
|
|
64
|
+
| Platform | Command |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Ubuntu / Debian | `sudo apt install libcairo2-dev pkg-config python3-dev` |
|
|
67
|
+
| Fedora / RHEL | `sudo dnf install cairo-devel pkgconf-pkg-config python3-devel` |
|
|
68
|
+
| macOS (Homebrew) | `brew install cairo pkg-config` |
|
|
69
|
+
| Windows | `conda install -c conda-forge pycairo` *(recommended; the MSVC build is fiddly)* |
|
|
70
|
+
|
|
71
|
+
`conda install -c conda-forge pycairo` is also the easiest route on Linux /
|
|
72
|
+
macOS if you prefer not to touch system packages.
|
|
73
|
+
|
|
74
|
+
### Development install
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/Bio-Babel/grid_py
|
|
78
|
+
cd grid_py
|
|
79
|
+
pip install -e ".[dev]"
|
|
80
|
+
pytest # 2600+ tests
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Quick Start
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from grid_py import (
|
|
87
|
+
CairoRenderer, Gpar, Unit, Viewport, GridLayout,
|
|
88
|
+
get_state, grid_draw, grid_newpage,
|
|
89
|
+
push_viewport, pop_viewport,
|
|
90
|
+
rect_grob, text_grob, points_grob, circle_grob,
|
|
91
|
+
unit_c, string_width,
|
|
92
|
+
)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## The Unit System — Why Layout "Just Works"
|
|
96
|
+
|
|
97
|
+
The central idea of grid is that **sizes are expressions, not numbers**.
|
|
98
|
+
A `Unit` carries both a value and a *strategy* for resolving that value.
|
|
99
|
+
Resolution is deferred until a viewport is pushed — at that point the parent
|
|
100
|
+
dimensions, font metrics, and device DPI are all known, so every unit can
|
|
101
|
+
evaluate itself in context.
|
|
102
|
+
|
|
103
|
+
This means the same layout specification produces correct results on a 72 dpi
|
|
104
|
+
screen, a 300 dpi PDF, a 7-inch plot or a 14-inch poster — with no manual
|
|
105
|
+
tweaking.
|
|
106
|
+
|
|
107
|
+
### Unit types at a glance
|
|
108
|
+
|
|
109
|
+
| Category | Units | Resolved from |
|
|
110
|
+
|----------|-------|---------------|
|
|
111
|
+
| **Absolute** | `"cm"`, `"inches"`, `"mm"`, `"points"` | Fixed physical conversion |
|
|
112
|
+
| **Relative** | `"npc"` (0-1 fraction of parent) | Parent viewport dimensions |
|
|
113
|
+
| **Font-relative** | `"lines"` (line height), `"char"` (char width) | Current `fontsize` × `lineheight` |
|
|
114
|
+
| **Content-measuring** | `"strwidth"`, `"strheight"` | Cairo text measurement |
|
|
115
|
+
| **Grob-measuring** | `"grobwidth"`, `"grobheight"` | Grob bounding box query |
|
|
116
|
+
| **Flex** | `"null"` | Remaining space (layout only) |
|
|
117
|
+
| **Data** | `"native"` | Viewport `xscale` / `yscale` mapping |
|
|
118
|
+
|
|
119
|
+
### Mixing units with arithmetic
|
|
120
|
+
|
|
121
|
+
Units of different types can be freely combined. The result is a **compound
|
|
122
|
+
unit** that is evaluated recursively at resolve time:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# "fill the parent, but leave 2 line-heights of margin on each side"
|
|
126
|
+
width = Unit(1, "npc") - Unit(4, "lines")
|
|
127
|
+
|
|
128
|
+
# "start 1 cm from the right edge"
|
|
129
|
+
x = Unit(1, "npc") - Unit(1, "cm")
|
|
130
|
+
|
|
131
|
+
# also supports min / max across types
|
|
132
|
+
from grid_py import unit_pmin
|
|
133
|
+
safe_width = unit_pmin(Unit(10, "cm"), Unit(1, "npc")) # whichever is smaller
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Internally `Unit(1,"npc") - Unit(4,"lines")` is stored as a tree:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
sum ──┬── 1.0 npc
|
|
140
|
+
└── -4.0 lines
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
When the viewport is pushed, each leaf is resolved to inches and summed.
|
|
144
|
+
Changing the font or the device size automatically changes the result.
|
|
145
|
+
|
|
146
|
+
## Layout Patterns
|
|
147
|
+
|
|
148
|
+
### Pattern 1 — Adaptive margins with `"lines"`
|
|
149
|
+
|
|
150
|
+
The most common pattern: margins that scale with the font.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
r = CairoRenderer(width=7, height=5, dpi=150)
|
|
154
|
+
get_state().init_device(r)
|
|
155
|
+
grid_newpage()
|
|
156
|
+
|
|
157
|
+
# Title: 2 line-heights tall, pinned to the top
|
|
158
|
+
title_vp = Viewport(
|
|
159
|
+
name="title",
|
|
160
|
+
x=Unit(0.5, "npc"),
|
|
161
|
+
y=Unit(1, "npc") - Unit(1, "lines"),
|
|
162
|
+
width=Unit(1, "npc"),
|
|
163
|
+
height=Unit(2, "lines"),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Plot area: fills the rest, with room for axis labels
|
|
167
|
+
plot_vp = Viewport(
|
|
168
|
+
name="plot",
|
|
169
|
+
x=Unit(0.5, "npc") + Unit(1, "lines"),
|
|
170
|
+
y=Unit(0.5, "npc") - Unit(0.5, "lines"),
|
|
171
|
+
width=Unit(1, "npc") - Unit(4, "lines"),
|
|
172
|
+
height=Unit(1, "npc") - Unit(5, "lines"),
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
If you later change `Gpar(fontsize=14)` to `fontsize=20`, the margins grow
|
|
177
|
+
proportionally — no constants to update.
|
|
178
|
+
|
|
179
|
+
### Pattern 2 — Content-driven margins with `string_width`
|
|
180
|
+
|
|
181
|
+
Let the label measure itself:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
label = "Sepal Length (cm)"
|
|
185
|
+
margin = string_width(label) # Unit whose value = rendered width of that string
|
|
186
|
+
|
|
187
|
+
plot_vp = Viewport(
|
|
188
|
+
x=margin + Unit(0.5, "cm"), # left edge = label width + gap
|
|
189
|
+
width=Unit(1, "npc") - margin - Unit(1, "cm"),
|
|
190
|
+
y=Unit(0.5, "npc"),
|
|
191
|
+
height=Unit(1, "npc") - Unit(3, "lines"),
|
|
192
|
+
)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Change the label text → the margin updates automatically.
|
|
196
|
+
|
|
197
|
+
### Pattern 3 — Flexible grid with `"null"` units
|
|
198
|
+
|
|
199
|
+
`"null"` units divide **remaining space** proportionally, after all absolute
|
|
200
|
+
and relative units have been allocated. This is how `GridLayout` works:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# Column 1: fixed 3 cm (e.g. y-axis labels)
|
|
204
|
+
# Columns 2-3: split remaining space 2:1
|
|
205
|
+
layout = GridLayout(
|
|
206
|
+
nrow=1, ncol=3,
|
|
207
|
+
widths=unit_c(Unit(3, "cm"), Unit(2, "null"), Unit(1, "null")),
|
|
208
|
+
heights=Unit([1], "null"),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
main_vp = Viewport(name="main", layout=layout,
|
|
212
|
+
x=Unit(0.5, "npc"), y=Unit(0.5, "npc"),
|
|
213
|
+
width=Unit(1, "npc"), height=Unit(1, "npc"))
|
|
214
|
+
push_viewport(main_vp)
|
|
215
|
+
|
|
216
|
+
# Place children into cells
|
|
217
|
+
for col in [1, 2, 3]:
|
|
218
|
+
cell_vp = Viewport(name=f"cell_{col}",
|
|
219
|
+
layout_pos_row=1, layout_pos_col=col)
|
|
220
|
+
push_viewport(cell_vp)
|
|
221
|
+
grid_draw(rect_grob(x=0.5, y=0.5, width=1, height=1,
|
|
222
|
+
gp=Gpar(fill="grey90", col="grey50")))
|
|
223
|
+
pop_viewport(1)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Resize the device → column 1 stays 3 cm, the rest reflows.
|
|
227
|
+
|
|
228
|
+
### Pattern 4 — Nested viewports for complex figures
|
|
229
|
+
|
|
230
|
+
Viewports nest. Each child resolves its units against its parent, so you
|
|
231
|
+
can build deeply structured layouts compositionally:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Outer: 2-row layout (title + body)
|
|
235
|
+
outer = GridLayout(nrow=2, ncol=1,
|
|
236
|
+
heights=unit_c(Unit(2, "lines"), Unit(1, "null")))
|
|
237
|
+
|
|
238
|
+
# Body: 1×3 panel grid
|
|
239
|
+
inner = GridLayout(nrow=1, ncol=3,
|
|
240
|
+
widths=Unit([1, 1, 1], "null"))
|
|
241
|
+
|
|
242
|
+
push_viewport(Viewport(name="page", layout=outer, ...))
|
|
243
|
+
|
|
244
|
+
# Row 1 — title
|
|
245
|
+
push_viewport(Viewport(layout_pos_row=1, layout_pos_col=1))
|
|
246
|
+
grid_draw(text_grob("My Title", x=0.5, y=0.5, gp=Gpar(fontsize=16, fontface="bold")))
|
|
247
|
+
pop_viewport(1)
|
|
248
|
+
|
|
249
|
+
# Row 2 — panels
|
|
250
|
+
push_viewport(Viewport(layout_pos_row=2, layout_pos_col=1, layout=inner))
|
|
251
|
+
for col in [1, 2, 3]:
|
|
252
|
+
push_viewport(Viewport(layout_pos_row=1, layout_pos_col=col))
|
|
253
|
+
# ... draw panel content using npc coordinates (0-1 within this cell)
|
|
254
|
+
pop_viewport(1)
|
|
255
|
+
pop_viewport(1)
|
|
256
|
+
|
|
257
|
+
pop_viewport(1)
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### The three-layer coordinate pipeline
|
|
261
|
+
|
|
262
|
+
Every coordinate in grid passes through three distinct transformations before
|
|
263
|
+
reaching the device. Understanding these layers is the key to reasoning about
|
|
264
|
+
why layouts are portable and how viewports compose.
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
268
|
+
│ Layer 1: Unit → Viewport-local inches │
|
|
269
|
+
│ │
|
|
270
|
+
│ _transform_to_inches() (_vp_calc.py) │
|
|
271
|
+
│ │
|
|
272
|
+
│ Each unit type has its own rule: │
|
|
273
|
+
│ "npc" → value × parent_width_inches │
|
|
274
|
+
│ "cm" → value / 2.54 │
|
|
275
|
+
│ "lines" → value × fontsize × cex × lineheight / 72 │
|
|
276
|
+
│ "native" → map [scalemin, scalemax] → [0, parent] │
|
|
277
|
+
│ "strwidth" → Cairo text_extents(string).width │
|
|
278
|
+
│ "grobwidth" → grob.width_details() recursive query │
|
|
279
|
+
│ "sum" → Σ recursive resolve of child units │
|
|
280
|
+
│ "null" → 0 (only meaningful inside GridLayout) │
|
|
281
|
+
│ │
|
|
282
|
+
│ Result: a position or dimension in inches, local to the │
|
|
283
|
+
│ current viewport's own coordinate system. │
|
|
284
|
+
├─────────────────────────────────────────────────────────────┤
|
|
285
|
+
│ Layer 2: Viewport-local inches → Absolute inches │
|
|
286
|
+
│ │
|
|
287
|
+
│ transform_loc_to_device() (_renderer_base.py) │
|
|
288
|
+
│ │
|
|
289
|
+
│ Applies the viewport's accumulated 3×3 affine transform: │
|
|
290
|
+
│ T = Justification × Rotation × Translation × Parent_T │
|
|
291
|
+
│ │
|
|
292
|
+
│ Built at push_viewport() time by calc_viewport_transform() │
|
|
293
|
+
│ Each push multiplies into the parent's matrix, so nested │
|
|
294
|
+
│ viewports compose naturally: │
|
|
295
|
+
│ │
|
|
296
|
+
│ abs_loc = [x_inches, y_inches, 1] @ vp_transform_3x3 │
|
|
297
|
+
│ │
|
|
298
|
+
│ Result: inches from device origin (bottom-left). │
|
|
299
|
+
├─────────────────────────────────────────────────────────────┤
|
|
300
|
+
│ Layer 3: Absolute inches → Device pixels │
|
|
301
|
+
│ │
|
|
302
|
+
│ inches_to_dev_x/y() (_renderer_base.py) │
|
|
303
|
+
│ │
|
|
304
|
+
│ dev_x = abs_inches_x × DPI │
|
|
305
|
+
│ dev_y = device_height - abs_inches_y × DPI (Y-flip) │
|
|
306
|
+
│ │
|
|
307
|
+
│ Grid uses bottom-left origin; devices use top-left. │
|
|
308
|
+
│ This layer bridges the two. │
|
|
309
|
+
└─────────────────────────────────────────────────────────────┘
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
A concrete example — drawing a point at `x = Unit(1,"npc") - Unit(2,"lines")`
|
|
313
|
+
inside a child viewport:
|
|
314
|
+
|
|
315
|
+
```
|
|
316
|
+
User writes: Unit(1, "npc") - Unit(2, "lines") + Unit(0.5, "cm")
|
|
317
|
+
│
|
|
318
|
+
▼ arithmetic builds a compound tree
|
|
319
|
+
Stored as: sum ──┬── 1.0 npc
|
|
320
|
+
├── -2.0 lines
|
|
321
|
+
└── 0.5 cm
|
|
322
|
+
|
|
323
|
+
── Layer 1 ─────────────────────────────────────────
|
|
324
|
+
│
|
|
325
|
+
▼ _transform_to_inches(parent_context)
|
|
326
|
+
Resolve: npc → 1.0 × parent_width_inches = 5.000"
|
|
327
|
+
lines → -2 × fontsize × lineheight / 72 = -0.333"
|
|
328
|
+
cm → 0.5 / 2.54 = 0.197"
|
|
329
|
+
sum → 5.000 - 0.333 + 0.197 = 4.864" (viewport-local)
|
|
330
|
+
|
|
331
|
+
── Layer 2 ─────────────────────────────────────────
|
|
332
|
+
│
|
|
333
|
+
▼ [4.864, y, 1] @ viewport_transform_3x3
|
|
334
|
+
Absolute: account for viewport position, rotation = 5.214" (from device origin)
|
|
335
|
+
|
|
336
|
+
── Layer 3 ─────────────────────────────────────────
|
|
337
|
+
│
|
|
338
|
+
▼ × DPI, Y-flip
|
|
339
|
+
Device: 5.214 × 150 = 782 px
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Why this matters for users:**
|
|
343
|
+
|
|
344
|
+
- **Layer 1** is where your Unit expressions are evaluated. Because each unit
|
|
345
|
+
type knows how to measure itself (text metrics, parent size, font size),
|
|
346
|
+
your layout adapts to context automatically.
|
|
347
|
+
- **Layer 2** is where viewport nesting works. You never compute global
|
|
348
|
+
positions — you work in local coordinates and the transform stack composes
|
|
349
|
+
them.
|
|
350
|
+
- **Layer 3** is invisible to you. It just makes sure the same inches
|
|
351
|
+
produce correct pixels on any device.
|
|
352
|
+
|
|
353
|
+
The net effect: you describe layout in meaningful terms (`"lines"`, `"cm"`,
|
|
354
|
+
`"npc"`, `"strwidth"`), nest viewports freely, and the three-layer pipeline
|
|
355
|
+
ensures the result is correct on every device and at every DPI.
|
|
356
|
+
|
|
357
|
+
## Backend Architecture
|
|
358
|
+
|
|
359
|
+
grid\_py ships with a pluggable rendering backend system built on a single
|
|
360
|
+
abstract base class (`GridRenderer`). User code is identical across backends —
|
|
361
|
+
swap the renderer instance to switch output formats.
|
|
362
|
+
|
|
363
|
+
```
|
|
364
|
+
GridRenderer (ABC)
|
|
365
|
+
┌──────────────────────────┐
|
|
366
|
+
│ viewport transform stack │
|
|
367
|
+
│ unit resolution │
|
|
368
|
+
│ coordinate system mgmt │
|
|
369
|
+
│ 29 abstract methods │
|
|
370
|
+
└────────────┬─────────────┘
|
|
371
|
+
┌─────┴──────┐
|
|
372
|
+
│ │
|
|
373
|
+
CairoRenderer WebRenderer
|
|
374
|
+
(immediate) (scene graph)
|
|
375
|
+
┌──────────┐ ┌──────────────┐
|
|
376
|
+
│ PNG/PDF/ │ │ JSON Scene → │
|
|
377
|
+
│ SVG/PS │ │ HTML + D3.js │
|
|
378
|
+
└──────────┘ └──────────────┘
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Renderers
|
|
382
|
+
|
|
383
|
+
| Backend | Module | Output Formats | Rendering Mode |
|
|
384
|
+
|---------|--------|----------------|----------------|
|
|
385
|
+
| **CairoRenderer** | `renderer.py` | PNG, PDF, SVG, PS | Immediate — draws directly to a pycairo surface |
|
|
386
|
+
| **WebRenderer** | `renderer_web.py` | Standalone HTML (SVG + Canvas + D3.js) | Deferred — builds a JSON scene graph, rendered browser-side |
|
|
387
|
+
|
|
388
|
+
### Supporting Components
|
|
389
|
+
|
|
390
|
+
| Component | Module | Role |
|
|
391
|
+
|-----------|--------|------|
|
|
392
|
+
| GridRenderer (ABC) | `_renderer_base.py` | Abstract base class defining the renderer interface (29 abstract methods) |
|
|
393
|
+
| Scene Graph | `_scene_graph.py` | `SceneNode` / `ViewportNode` / `GrobNode` tree used by WebRenderer |
|
|
394
|
+
| Font Metrics | `_font_metrics.py` | Pluggable text measurement (Cairo, fonttools, or heuristic backends) |
|
|
395
|
+
| GridState | `_state.py` | Global singleton that binds the active renderer to the drawing API |
|
|
396
|
+
|
|
397
|
+
### Backend Selection
|
|
398
|
+
|
|
399
|
+
There is no registry or factory — instantiate the renderer you need and bind it:
|
|
400
|
+
|
|
401
|
+
```python
|
|
402
|
+
from grid_py import WebRenderer, get_state, grid_draw
|
|
403
|
+
|
|
404
|
+
r = WebRenderer(width=7, height=5, dpi=100)
|
|
405
|
+
state = get_state()
|
|
406
|
+
state.init_device(r)
|
|
407
|
+
|
|
408
|
+
grid_draw(my_grob) # draws into the scene graph
|
|
409
|
+
html = r.to_html() # export interactive HTML
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`grid_newpage()` creates a `CairoRenderer` by default when no renderer is bound.
|
|
413
|
+
|
|
414
|
+
## Interactive Web Visualization
|
|
415
|
+
|
|
416
|
+
`WebRenderer` turns any grid plot into an interactive HTML document. The same
|
|
417
|
+
layout code that produces a static PNG via `CairoRenderer` can produce a
|
|
418
|
+
zoomable, pannable, tooltip-enabled web page — with zero API changes.
|
|
419
|
+
|
|
420
|
+
### How it works
|
|
421
|
+
|
|
422
|
+
```
|
|
423
|
+
Python (grid_py) Browser (gridpy.js)
|
|
424
|
+
┌──────────────┐ ┌──────────────────────┐
|
|
425
|
+
│ grid_draw() │──→ Scene Graph JSON ──→ │ SVG layer (text, │
|
|
426
|
+
│ viewports, │ {root, defs, dpi} │ shapes, clip/mask) │
|
|
427
|
+
│ grobs, gpar │ │ Canvas layer (>2000 │
|
|
428
|
+
│ │ │ points batch) │
|
|
429
|
+
│ .metadata │──→ node.data[] ───────→ │ Quadtree spatial │
|
|
430
|
+
│ (per-point) │ │ index → tooltips │
|
|
431
|
+
└──────────────┘ └──────────────────────┘
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
1. Python builds a JSON scene graph during `grid_draw()` calls
|
|
435
|
+
2. `gridpy.js` renders SVG for shapes/text, Canvas for large point clouds
|
|
436
|
+
3. All data-carrying points register in a spatial index (quadtree)
|
|
437
|
+
4. Hover triggers a proximity query — no DOM hit-testing needed
|
|
438
|
+
|
|
439
|
+
### Tooltip data
|
|
440
|
+
|
|
441
|
+
Attach a `metadata` dict to any grob before drawing. Keys become tooltip
|
|
442
|
+
labels; list values are indexed per point:
|
|
443
|
+
|
|
444
|
+
```python
|
|
445
|
+
grob = points_grob(x=x_data, y=y_data, pch=19,
|
|
446
|
+
gp=Gpar(col=colors, fill=colors))
|
|
447
|
+
grob.metadata = {
|
|
448
|
+
"species": species_list, # per-point label
|
|
449
|
+
"value": [f"{v:.1f}" for v in values],
|
|
450
|
+
}
|
|
451
|
+
grid_draw(grob) # metadata flows into the scene graph
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Output modes
|
|
455
|
+
|
|
456
|
+
```python
|
|
457
|
+
r = WebRenderer(width=7, height=5, dpi=100)
|
|
458
|
+
get_state().init_device(r)
|
|
459
|
+
# ... draw with grid_draw() ...
|
|
460
|
+
|
|
461
|
+
# Jupyter notebook — inline display with D3 inlined (no CDN dependency)
|
|
462
|
+
display(r) # uses _repr_html_() → <iframe srcdoc>
|
|
463
|
+
|
|
464
|
+
# Standalone HTML — lightweight, loads D3 from CDN
|
|
465
|
+
r.save("plot.html") # open in any browser
|
|
466
|
+
|
|
467
|
+
# Raw scene graph — for custom frontends (Vue, React, etc.)
|
|
468
|
+
json_str = r.to_scene_json() # framework calls gridpy.render(el, json)
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Rendering layers
|
|
472
|
+
|
|
473
|
+
The browser runtime uses a layered architecture for performance:
|
|
474
|
+
|
|
475
|
+
| Layer | z-index | Content | When used |
|
|
476
|
+
|-------|---------|---------|-----------|
|
|
477
|
+
| Canvas | 1 | Batch-drawn points | Point count > 2000 |
|
|
478
|
+
| SVG | 2 | Text, shapes, small point sets | Default for most grobs |
|
|
479
|
+
| Overlay | 3 | D3 zoom/brush handlers | When `interactive: true` |
|
|
480
|
+
|
|
481
|
+
Routing is automatic (`render_hint="auto"`), or you can force a layer per grob
|
|
482
|
+
with `render_hint="svg"` or `render_hint="canvas"`.
|
|
483
|
+
|
|
484
|
+
## Documentation
|
|
485
|
+
|
|
486
|
+
```bash
|
|
487
|
+
pip install -e ".[docs]"
|
|
488
|
+
mkdocs serve
|
|
489
|
+
```
|