figkit 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.
- {figkit-0.1.0 → figkit-0.2.0}/AI_MANUAL.md +120 -4
- figkit-0.2.0/CHANGELOG.md +181 -0
- {figkit-0.1.0 → figkit-0.2.0}/CONTRIBUTING.md +27 -1
- {figkit-0.1.0 → figkit-0.2.0}/PKG-INFO +2 -2
- {figkit-0.1.0 → figkit-0.2.0}/README.md +1 -1
- {figkit-0.1.0 → figkit-0.2.0}/examples/05_rich_text_and_components.py +2 -2
- {figkit-0.1.0 → figkit-0.2.0}/figkit/__init__.py +5 -5
- {figkit-0.1.0 → figkit-0.2.0}/figkit/audit.py +27 -14
- {figkit-0.1.0 → figkit-0.2.0}/figkit/components.py +19 -6
- {figkit-0.1.0 → figkit-0.2.0}/figkit/connectors.py +175 -35
- {figkit-0.1.0 → figkit-0.2.0}/figkit/core.py +20 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/export.py +9 -3
- {figkit-0.1.0 → figkit-0.2.0}/figkit/figure.py +15 -4
- {figkit-0.1.0 → figkit-0.2.0}/figkit/fonts.py +79 -10
- {figkit-0.1.0 → figkit-0.2.0}/figkit/frame.py +43 -8
- {figkit-0.1.0 → figkit-0.2.0}/figkit/image.py +5 -1
- {figkit-0.1.0 → figkit-0.2.0}/figkit/layout.py +28 -14
- {figkit-0.1.0 → figkit-0.2.0}/figkit/mathtext.py +5 -5
- {figkit-0.1.0 → figkit-0.2.0}/figkit/shapes.py +32 -5
- {figkit-0.1.0 → figkit-0.2.0}/figkit/style.py +73 -5
- {figkit-0.1.0 → figkit-0.2.0}/figkit/text.py +90 -15
- {figkit-0.1.0 → figkit-0.2.0}/figkit.egg-info/PKG-INFO +2 -2
- {figkit-0.1.0 → figkit-0.2.0}/figkit.egg-info/SOURCES.txt +2 -0
- {figkit-0.1.0 → figkit-0.2.0}/pyproject.toml +6 -1
- figkit-0.2.0/tests/test_audit_fixes.py +220 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_connectors.py +144 -1
- figkit-0.2.0/tests/test_release_prep.py +104 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_style.py +55 -1
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_text_and_shapes.py +76 -2
- figkit-0.1.0/CHANGELOG.md +0 -74
- {figkit-0.1.0 → figkit-0.2.0}/LICENSE +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/MANIFEST.in +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/examples/00_quickstart.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/examples/01_pipeline.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/examples/02_attribution.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/examples/03_data_and_plots.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/examples/04_themes.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/colors.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/component.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/geom.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/paint.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/py.typed +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/svgdoc.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/svgpath.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit/themes.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit.egg-info/dependency_links.txt +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit.egg-info/requires.txt +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/figkit.egg-info/top_level.txt +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/setup.cfg +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_audit.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_colors.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_composition.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_core.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_examples.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_export.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_figure_and_frame.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_geom.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_layout.py +0 -0
- {figkit-0.1.0 → figkit-0.2.0}/tests/test_svgpath_and_math.py +0 -0
|
@@ -34,9 +34,12 @@ problem with coordinates, and `no issues` when the figure is clean.
|
|
|
34
34
|
|
|
35
35
|
* **Units are px. `y` grows downward** (SVG convention): `n` is the top edge,
|
|
36
36
|
`s` the bottom.
|
|
37
|
-
* **
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
* **Place things relative to each other, not at coordinates.** figkit measures
|
|
38
|
+
text from the real font file, so `right_of`, `hstack`, `grid` and `fit` land
|
|
39
|
+
exactly. Typing absolute numbers works but it is the slow path: a redesign
|
|
40
|
+
then means editing every number by hand, which is the single biggest source
|
|
41
|
+
of pain reported by people using this library. Reach for §3 and §9 first and
|
|
42
|
+
drop to coordinates only for the few things that genuinely need them.
|
|
40
43
|
* **Anchors are live references.** `box.e` is not a coordinate — it resolves
|
|
41
44
|
when read. Move the box afterwards and every arrow pointing at it follows.
|
|
42
45
|
* **Every placement call returns `self`**, so it chains:
|
|
@@ -55,6 +58,25 @@ problem with coordinates, and `no issues` when the figure is clean.
|
|
|
55
58
|
|
|
56
59
|
---
|
|
57
60
|
|
|
61
|
+
### Use the layout engine
|
|
62
|
+
|
|
63
|
+
Most figures are rows, columns, grids and enclosures. Say that, and a later
|
|
64
|
+
change to one label re-flows the rest:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
row = hstack([enc, mid, dec], gap=60, align="center") # a Group
|
|
68
|
+
col = vstack([row, caption], gap=18, align="center")
|
|
69
|
+
grid(cells, cols=4, gap=(12, 12))
|
|
70
|
+
fit(enc, mid, pad=20, label="Encoder stack") # boxed cluster
|
|
71
|
+
align([a, b, c], "top"); distribute_h([a, b, c], gap=24)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The full list is §9. `Matrix`, `LabelledMatrix`, `Table`, `Brace`, `Bracket`,
|
|
75
|
+
`Legend`, `ColorBar` and `Panel` (§5, §8) already exist — check before building
|
|
76
|
+
your own.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
58
80
|
## 2. Cheat sheet
|
|
59
81
|
|
|
60
82
|
| Need | Call |
|
|
@@ -74,6 +96,7 @@ problem with coordinates, and `no issues` when the figure is clean.
|
|
|
74
96
|
| arrow | `arrow(a.e, b.w, label="x")` |
|
|
75
97
|
| orthogonal | `elbow(a.e, b.w, stub=14, corner=6)` |
|
|
76
98
|
| curved | `curve(a.s, b.s, bend=0.4)` |
|
|
99
|
+
| shaped curve | `curve(a.e, b.w, start_handle=0.6, end_handle=(0.2, -30))` |
|
|
77
100
|
| align a group | `align([a, b, c], "top")` |
|
|
78
101
|
| even spacing | `distribute_h(items, gap=20)` / `spread_h(items, x0, x1)` |
|
|
79
102
|
| row / column | `hstack(items, gap=16, align="center")` / `vstack(...)` |
|
|
@@ -174,6 +197,55 @@ Common kwargs: `w, h, min_w, min_h, max_w, padding, wrap, align, valign,
|
|
|
174
197
|
radius, fill, stroke, stroke_width, stroke_dash, opacity, shadow, rotate, z,
|
|
175
198
|
name, style, theme`.
|
|
176
199
|
|
|
200
|
+
**Every style property, and the aliases that reach it.** A name outside this
|
|
201
|
+
table raises `UnknownProperty` with a suggestion rather than being accepted
|
|
202
|
+
and ignored, so a typo costs you a traceback instead of a wrong figure. The
|
|
203
|
+
booleans `bold=`, `italic=`, `underline=` and `monospace=` expand into these.
|
|
204
|
+
|
|
205
|
+
| Property | Also accepted as |
|
|
206
|
+
|---|---|
|
|
207
|
+
| **Paint** | |
|
|
208
|
+
| `fill` | `background`, `background_color`, `bg`, `facecolor`, `fill_color` |
|
|
209
|
+
| `fill_opacity` | — |
|
|
210
|
+
| `stroke` | `border`, `border_color`, `edgecolor`, `line_color`, `stroke_color` |
|
|
211
|
+
| `stroke_width` | `border_width`, `linewidth`, `lw`, `stroke_w` |
|
|
212
|
+
| `stroke_opacity` | — |
|
|
213
|
+
| `stroke_dash` | `dash`, `dasharray`, `dashes`, `linestyle`, `ls`, `stroke_dasharray` |
|
|
214
|
+
| `stroke_dashoffset` | — |
|
|
215
|
+
| `stroke_linecap` | `cap`, `linecap` |
|
|
216
|
+
| `stroke_linejoin` | `join`, `linejoin` |
|
|
217
|
+
| `opacity` | `alpha` |
|
|
218
|
+
| `shadow` | — |
|
|
219
|
+
| `background` | — |
|
|
220
|
+
| `image_opacity` | — |
|
|
221
|
+
| **Geometry** | |
|
|
222
|
+
| `radius` | `border_radius`, `corner_radius`, `rounding` |
|
|
223
|
+
| `padding` | — |
|
|
224
|
+
| **Type** | |
|
|
225
|
+
| `font_family` | `family`, `font`, `fontfamily`, `typeface` |
|
|
226
|
+
| `font_size` | `fontsize`, `size` |
|
|
227
|
+
| `font_weight` | `fontweight`, `weight` |
|
|
228
|
+
| `font_style` | `fontstyle` |
|
|
229
|
+
| `color` | `fg`, `foreground`, `text_color` |
|
|
230
|
+
| `line_height` | `leading`, `linespacing` |
|
|
231
|
+
| `letter_spacing` | `tracking` |
|
|
232
|
+
| `word_spacing` | Extra space between words, in figure units |
|
|
233
|
+
| `text_align` | `align`, `ha`, `halign` |
|
|
234
|
+
| `valign` | `va`, `vertical_align` |
|
|
235
|
+
| `text_transform` | `none`, `uppercase`, `lowercase`, or `capitalize` |
|
|
236
|
+
| `text_decoration` | — |
|
|
237
|
+
| **Math** | |
|
|
238
|
+
| `math_backend` | — |
|
|
239
|
+
| `math_color` | — |
|
|
240
|
+
| `math_scale` | — |
|
|
241
|
+
| **Connectors** | |
|
|
242
|
+
| `head` | `arrowhead` |
|
|
243
|
+
| `head_size` | `headsize` |
|
|
244
|
+
| `tail` | `arrowtail` |
|
|
245
|
+
| `tail_size` | `tailsize` |
|
|
246
|
+
|
|
247
|
+
`register_props("my_prop")` declares extras for a component of your own.
|
|
248
|
+
|
|
177
249
|
**Geometry** — `Line(a, b)`, `Polyline(points)`, `Polygon(points)`,
|
|
178
250
|
`Path("M0 0 L10 10 …")`, `Dot(center, r)`, `Marker(center, size, "diamond")`.
|
|
179
251
|
`Line`/`Polyline`/`Polygon` accept **live anchors** as points, so they track
|
|
@@ -260,7 +332,28 @@ On connectors and other line-like elements (`Line`, `Polyline`, `Path`,
|
|
|
260
332
|
`bend` deepens the bow **along the anchors' facing direction** — `curve(a.s,
|
|
261
333
|
b.s, bend=0.5)` dips below both boxes. For plain points there is no normal to
|
|
262
334
|
follow so it bows sideways. `bow=` always pushes sideways (positive = left of
|
|
263
|
-
travel).
|
|
335
|
+
travel). Both are symmetric, and `bend` acts as a *floor* on the curve's
|
|
336
|
+
natural reach, so small values often change nothing.
|
|
337
|
+
|
|
338
|
+
For real control, place either end's Bezier handle yourself — the "whisker" a
|
|
339
|
+
vector editor lets you drag:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
curve(a.e, b.w, start_handle=0.6) # leaves east on a long handle
|
|
343
|
+
curve(a.e, b.w, start_handle=0.8, end_handle=0.15) # asymmetric
|
|
344
|
+
curve(a.e, b.w, start_handle=(0.5, -40)) # turned 40 deg off the face
|
|
345
|
+
curve(a.e, b.w, start_handle=Handle(px=60)) # absolute reach
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The handle's **direction** is the asymptote — which way the curve insists on
|
|
349
|
+
leaving — and its **length** is how long it clings to it. Length is a fraction
|
|
350
|
+
of the straight-line distance between the endpoints, so the curve keeps its
|
|
351
|
+
shape when the things it connects move apart; `Handle(px=...)` opts out of
|
|
352
|
+
that. Angles are degrees off the face you attached to, positive clockwise on
|
|
353
|
+
screen. An end you give a handle ignores `bend` and `bow`.
|
|
354
|
+
|
|
355
|
+
Handles work with `waypoints=` too, and `tension=` (default 0.5) loosens the
|
|
356
|
+
spline that threads them.
|
|
264
357
|
|
|
265
358
|
`self_loop(element, side="top", size=36, label="retry")` draws an arrow that
|
|
266
359
|
leaves an element and returns to it — the staple of state machines.
|
|
@@ -446,6 +539,11 @@ PNG/PDF need a converter: `pip install "figkit[export]"` (cairosvg), or
|
|
|
446
539
|
`rsvg-convert` / `resvg` / `inkscape` / headless chromium on `PATH`.
|
|
447
540
|
PNG and PDF outline text automatically, so they always match the SVG.
|
|
448
541
|
|
|
542
|
+
On macOS, cairosvg often fails to import with `dlopen ... libcairo.2.dylib`
|
|
543
|
+
even with Homebrew's cairo installed, because the loader does not look in
|
|
544
|
+
`/opt/homebrew`. Either export `DYLD_FALLBACK_LIBRARY_PATH=$(brew --prefix
|
|
545
|
+
cairo)/lib` or use `rsvg-convert` instead. SVG export needs nothing.
|
|
546
|
+
|
|
449
547
|
Install: `pip install figkit` · `figkit[latex]` (math) · `figkit[export]`
|
|
450
548
|
(PNG/PDF) · `figkit[all]`.
|
|
451
549
|
|
|
@@ -522,6 +620,24 @@ fig.audit(min_contrast=4.5) # WCAG AA for body text
|
|
|
522
620
|
SVG and HTML but not in a cairosvg-rendered PNG/PDF; figkit warns when
|
|
523
621
|
that happens. Use `rsvg-convert`/`resvg`/chromium, or skip shadows for
|
|
524
622
|
figures headed to PNG.
|
|
623
|
+
13. **A misspelled property raises.** `Text("hi", sizee=8)` is an error, not a
|
|
624
|
+
silent default. `size` and `font_size` are the same thing, and match
|
|
625
|
+
`measure_text(..., size=)`.
|
|
626
|
+
14. **`rotate=` turns the block about `(x, y)`,** so a rotated label lands in
|
|
627
|
+
the same place whatever its length. `rotate_about=` picks another pivot;
|
|
628
|
+
`el.rotate(deg)` after construction still turns about the centre.
|
|
629
|
+
15. **A missing glyph warns.** If the font has no glyph for a character, it
|
|
630
|
+
renders as an empty box and its measured width is `.notdef`'s, so figkit
|
|
631
|
+
says so and names the codepoint. `register_font` makes text export as
|
|
632
|
+
outlines, which also removes the viewer's fallback chain — a character
|
|
633
|
+
the registered font lacks becomes a guaranteed empty box.
|
|
634
|
+
16. **A tiny `Box` looks like a circle,** because the theme's `radius=6` is
|
|
635
|
+
clamped to half the shorter side. Pass `radius=0` for small swatches and
|
|
636
|
+
data cells.
|
|
637
|
+
17. **Arrow heads shrink to fit short connectors.** Below roughly 15pt the
|
|
638
|
+
head would swallow the shaft, so figkit scales it down and warns when the
|
|
639
|
+
result is much smaller than you asked for. Give short links more room, or
|
|
640
|
+
set `head_size=`.
|
|
525
641
|
|
|
526
642
|
---
|
|
527
643
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to figkit are recorded here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and figkit uses
|
|
5
|
+
[semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] — 2026-08-24
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **A style property figkit does not read now raises `UnknownProperty`**
|
|
12
|
+
instead of being stored and ignored. `Text("hi", sizee=8)` used to render at
|
|
13
|
+
the theme default with no complaint; it now fails with a suggestion. This is
|
|
14
|
+
a breaking change for code passing properties that never did anything.
|
|
15
|
+
Components with properties of their own declare them with
|
|
16
|
+
`register_props(...)`, and `PROPS` is the full list.
|
|
17
|
+
- **`rotate=` on `Text` turns the block about `(x, y)`**, not about its own
|
|
18
|
+
centre. Pivoting on the centre displaced the text by half its own length, so
|
|
19
|
+
where a rotated label landed depended on how many characters it had — a
|
|
20
|
+
50pt label drifted 25pt. `rotate_about=` takes an explicit pivot, and
|
|
21
|
+
`el.rotate(deg)` after construction is unchanged.
|
|
22
|
+
- Arrow heads shrink to fit connectors too short to hold them. A head larger
|
|
23
|
+
than the connector used to consume the whole shaft and overshoot its start,
|
|
24
|
+
drawing a stray triangle with no line.
|
|
25
|
+
- String options such as connector routes, marker shapes, grid order/alignment,
|
|
26
|
+
vector orientation, frame scales/sides and image fit now reject invalid
|
|
27
|
+
values with a suggestion instead of silently choosing another rendering.
|
|
28
|
+
- Raster export now rejects `.jpg`, `.jpeg` and `.webp`; these extensions were
|
|
29
|
+
accepted even though the bytes written were always PNG.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- `size=` is an alias for `font_size=`, matching `measure_text(..., size=)`.
|
|
34
|
+
Measuring at one size and rendering at another was silent before.
|
|
35
|
+
- `fill=` set directly on a `Text` means its colour. Text has no fill of its
|
|
36
|
+
own, so the property was being dropped.
|
|
37
|
+
- A warning when the font has no glyph for a character: it renders as an empty
|
|
38
|
+
box and its measured width is `.notdef`'s, not the character's, so layout
|
|
39
|
+
computed from it is wrong. Names the codepoint, warns once per font and
|
|
40
|
+
character.
|
|
41
|
+
- `Dot` accepts `Dot(x, y, r=...)` as well as `Dot(center, r)`, matching
|
|
42
|
+
`Circle` and every other element's `x, y` front door.
|
|
43
|
+
- `word_spacing=` participates in both measurement and SVG/path rendering, and
|
|
44
|
+
`text_transform=` supports `none`, `uppercase`, `lowercase` and `capitalize`.
|
|
45
|
+
The advertised but unimplemented `font_variant` property is now rejected
|
|
46
|
+
instead of being accepted as a silent no-op.
|
|
47
|
+
- `rotate=` and `rotate_about=` are common element constructor options, as the
|
|
48
|
+
manual already claimed, rather than Text-only constructor options. The
|
|
49
|
+
rotation is applied on first measurement rather than in `Element.__init__`,
|
|
50
|
+
because a subclass has not sized itself yet at that point: pivoting there
|
|
51
|
+
used the placeholder box, so a label-sized element landed somewhere that
|
|
52
|
+
depended on how long its text was.
|
|
53
|
+
- `Figure.to_html(embed=False)` isolates the SVG in an image data URI;
|
|
54
|
+
`embed=True` keeps the SVG inline and styleable.
|
|
55
|
+
|
|
56
|
+
### Documentation
|
|
57
|
+
|
|
58
|
+
- The manual led with "nothing is auto-laid-out", which read as an instruction
|
|
59
|
+
to place everything by hand. It now points at `hstack`/`vstack`/`grid`/`fit`
|
|
60
|
+
and the existing `Matrix`, `Table`, `Brace` and `Legend` components first.
|
|
61
|
+
- Every style property and its aliases are tabulated, so valid names no longer
|
|
62
|
+
have to be discovered by reading theme reprs.
|
|
63
|
+
- Gotchas for the rotation pivot, missing glyphs, small-`Box` corner radius,
|
|
64
|
+
short-connector heads, and the macOS `DYLD_FALLBACK_LIBRARY_PATH` fix for
|
|
65
|
+
cairosvg.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- `figkit.__version__` reported `0.1.0` from the 0.1.1 release. The number
|
|
70
|
+
lived in both `pyproject.toml` and `figkit/__init__.py`, and a release only
|
|
71
|
+
bumped the first. `pyproject.toml` now reads the version from the package,
|
|
72
|
+
so there is one number to change and it cannot drift again.
|
|
73
|
+
- macOS `.ttc`/`.otc` system fonts failed to load because the collection
|
|
74
|
+
loader received a TTFont-only argument. Collections now select the closest
|
|
75
|
+
family/weight/style face, restoring accurate metrics and text outlines.
|
|
76
|
+
- `hstack(..., at=...)` and `vstack(..., at=...)` now place the north-west
|
|
77
|
+
corner of the completed result at `at`, including an optional panel.
|
|
78
|
+
- `max_w=` now constrains and reflows a shape's label, including long tokens,
|
|
79
|
+
instead of shrinking only the container and leaving its text outside.
|
|
80
|
+
- `self_loop()` keeps live anchors for its feet and apex, so it follows an
|
|
81
|
+
element after movement or resizing.
|
|
82
|
+
- `Frame(clip_data=True)` clips only data marks; axes, ticks and titles remain
|
|
83
|
+
visible outside the plot area.
|
|
84
|
+
- `Matrix.highlight()` keeps the highlighted cell's value label in front.
|
|
85
|
+
- Contrast audit uses the same resolved text colour as rendering, including
|
|
86
|
+
`Text(fill=...)` and per-Span colours.
|
|
87
|
+
- `ColorBar` segments stay within the declared strip bounds.
|
|
88
|
+
- Dict-valued theme fills such as gradients are no longer mistaken for roles.
|
|
89
|
+
- Math glyph left overhang is included in measurement and normalized in the
|
|
90
|
+
generated path.
|
|
91
|
+
- Rich `Text` content has a safe repr, and outlined missing glyphs now draw the
|
|
92
|
+
`.notdef` box described by their warning instead of leaving a blank gap.
|
|
93
|
+
|
|
94
|
+
## [0.1.1] — 2026-08-21
|
|
95
|
+
|
|
96
|
+
### Connectors
|
|
97
|
+
|
|
98
|
+
- `start_handle=` and `end_handle=` place either end's Bezier handle — the
|
|
99
|
+
"whisker" a vector editor lets you drag. The direction is the asymptote the
|
|
100
|
+
curve leaves along, the length is how long it clings to it, given as a
|
|
101
|
+
fraction of the endpoint separation so the shape survives the layout
|
|
102
|
+
moving. Takes a bare fraction, a tuple, or a `Handle` (which also offers
|
|
103
|
+
`px=` for an absolute reach). An end given a handle ignores `bend`/`bow`.
|
|
104
|
+
- Handles also give curves between bare points a direction to leave in, which
|
|
105
|
+
previously only anchors could supply.
|
|
106
|
+
- `tension=` (default 0.5) loosens the spline threaded through `waypoints=`.
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- A curve with waypoints threw its endpoint normals away, so `curve(a.e, b.w,
|
|
111
|
+
waypoints=[p])` left the box diagonally instead of along the face it was
|
|
112
|
+
attached to. The outer tangents are now pinned to the attachment normals.
|
|
113
|
+
|
|
114
|
+
## [0.1.0] — 2026-08-21
|
|
115
|
+
|
|
116
|
+
First public release.
|
|
117
|
+
|
|
118
|
+
### Layout and geometry
|
|
119
|
+
|
|
120
|
+
- Elements with live anchors (`box.e` re-resolves on read, so arrows follow
|
|
121
|
+
the things they connect), exact bounding boxes, and chainable relative
|
|
122
|
+
placement: `at`, `right_of`, `left_of`, `above_of`, `below_of`, `inside`,
|
|
123
|
+
`align_to`, `span_x`, `resize`, `rotate`, `scale_by`.
|
|
124
|
+
- Batch arrangement: `align`, `distribute_h/v`, `spread_h/v`, `hstack`,
|
|
125
|
+
`vstack`, `grid`, `fit`, `circular`, `same_size`, `brace_around`.
|
|
126
|
+
- `hstack(align="baseline")` sets mixed text and matrices like an equation.
|
|
127
|
+
- Groups that own their children and report live bounds, with `z`-ordering.
|
|
128
|
+
|
|
129
|
+
### Text
|
|
130
|
+
|
|
131
|
+
- Text measured from real glyph advances via fontTools, with font resolution
|
|
132
|
+
through registered fonts, the usual system directories and `fc-match`, and a
|
|
133
|
+
fallback to the PostScript core-font metrics.
|
|
134
|
+
- Multi-line text, word wrapping, optical (cap-height) centring inside shapes.
|
|
135
|
+
- Inline `$math$` anywhere, typeset to vector outlines through matplotlib's
|
|
136
|
+
mathtext, or a real `latex` + `dvisvgm` toolchain when one is installed.
|
|
137
|
+
- `Span` for per-word colour, weight, style, size, family, strike-through and
|
|
138
|
+
underline. Decorations are drawn as geometry, so they survive rasterising
|
|
139
|
+
and outlining.
|
|
140
|
+
|
|
141
|
+
### Drawing
|
|
142
|
+
|
|
143
|
+
- Shapes: `Box`, `Pill`, `Ellipse`, `Circle`, `Diamond`, `Triangle`,
|
|
144
|
+
`Hexagon`, `Parallelogram`, `Chevron`, `Star`, `Cylinder`, `Note`,
|
|
145
|
+
`Callout`; geometry primitives `Line`, `Polyline`, `Polygon`, `Path`,
|
|
146
|
+
`Dot`, `Marker`.
|
|
147
|
+
- Connectors: straight, orthogonal (`elbow`), curved, arcs, explicit
|
|
148
|
+
waypoints, nine arrow-head shapes, path labels, and `self_loop`.
|
|
149
|
+
- Composites: `Matrix`, `LabelledMatrix`, `Vector`, `ColorBar`, `Table`,
|
|
150
|
+
`Legend`, `Brace`, `Bracket`, `Panel`.
|
|
151
|
+
- `Component` for reusable units that publish named anchors.
|
|
152
|
+
- `Image` embeds rasters as data URIs and inlines SVGs as vectors, rewriting
|
|
153
|
+
internal ids so repeated copies never collide.
|
|
154
|
+
|
|
155
|
+
### Style
|
|
156
|
+
|
|
157
|
+
- A cascading theme: base tokens, per-role overrides, CSS-style classes and a
|
|
158
|
+
colour palette, resolved kwargs → style → classes → inherited → theme →
|
|
159
|
+
default.
|
|
160
|
+
- Seven built-in themes: default, `PAPER`, `SLIDE`, `DARK`, `BLUEPRINT`,
|
|
161
|
+
`MINIMAL`, `SOFT`.
|
|
162
|
+
- Colour helpers: `mix`, `lighten`, `darken`, `alpha`, `saturate`,
|
|
163
|
+
`contrast_color`, `colormap`, `palette`.
|
|
164
|
+
|
|
165
|
+
### Data
|
|
166
|
+
|
|
167
|
+
- `Frame` maps a data domain onto figure coordinates; `line`, `scatter`,
|
|
168
|
+
`bars`, `area_fill`, `region`, `axes`, `gridlines`, log scales and
|
|
169
|
+
`nice_ticks`. Every mark is an ordinary element you can anchor to.
|
|
170
|
+
|
|
171
|
+
### Checking and output
|
|
172
|
+
|
|
173
|
+
- `fig.audit()` reports overlapping elements, labels escaping their shapes,
|
|
174
|
+
unreadable colour combinations, connectors crossing unrelated elements,
|
|
175
|
+
degenerate geometry and content outside a pinned canvas — and is built to
|
|
176
|
+
stay quiet about deliberate overlap, so a clean report means something.
|
|
177
|
+
- Export to SVG and HTML with no dependencies; PNG and PDF through cairosvg,
|
|
178
|
+
`rsvg-convert`, `resvg`, `inkscape` or headless Chromium. Raster and PDF
|
|
179
|
+
output outlines text so it cannot depend on the renderer's fonts.
|
|
180
|
+
- `AI_MANUAL.md`, a system-prompt-sized guide for driving figkit from an
|
|
181
|
+
agent.
|
|
@@ -37,7 +37,7 @@ skip rather than fail.
|
|
|
37
37
|
does, it usually wants renaming instead.
|
|
38
38
|
- Public functions get a docstring with a one-line summary and, where it
|
|
39
39
|
helps, a short example.
|
|
40
|
-
- Run `python -m pyflakes figkit` before opening a pull request.
|
|
40
|
+
- Run `python -m pyflakes figkit tools` before opening a pull request.
|
|
41
41
|
|
|
42
42
|
## Things worth knowing before changing the internals
|
|
43
43
|
|
|
@@ -47,3 +47,29 @@ skip rather than fail.
|
|
|
47
47
|
own labels use `Element.place_local` for parent-space arithmetic instead.
|
|
48
48
|
- `Group` takes ownership of its children. `bbox_of([...])` is the read-only
|
|
49
49
|
way to ask about a set of elements.
|
|
50
|
+
|
|
51
|
+
## Releasing
|
|
52
|
+
|
|
53
|
+
Releases are cut by the `publish` workflow, which uploads to PyPI through
|
|
54
|
+
Trusted Publishing — there is no API token to hold or rotate.
|
|
55
|
+
|
|
56
|
+
1. Write the entry for the new version at the top of `CHANGELOG.md`, under a
|
|
57
|
+
`## [Unreleased]` heading (or the version's own heading). The workflow
|
|
58
|
+
refuses to release if the top section is some older version, because that
|
|
59
|
+
means nobody wrote down what changed.
|
|
60
|
+
2. Run the workflow on `main` from the Actions tab (**publish → Run
|
|
61
|
+
workflow**), set **version** to the number being released, and untick
|
|
62
|
+
**dry_run**.
|
|
63
|
+
|
|
64
|
+
That is the whole procedure. The workflow sets the version in
|
|
65
|
+
`pyproject.toml`, dates the changelog section, runs the tests and the linter,
|
|
66
|
+
builds, and only then commits, tags `vX.Y.Z`, creates the GitHub Release with
|
|
67
|
+
the changelog section as its notes, and uploads to PyPI. Anything that fails
|
|
68
|
+
before the commit step leaves the repository untouched.
|
|
69
|
+
|
|
70
|
+
Leaving **dry_run** ticked builds and checks the current commit and changes
|
|
71
|
+
nothing — useful for confirming the packaging still passes.
|
|
72
|
+
|
|
73
|
+
Publishing a GitHub Release by hand also works, as long as the tag matches the
|
|
74
|
+
version already in `pyproject.toml`; the workflow verifies that and refuses
|
|
75
|
+
otherwise.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: figkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Design figures with Python code — boxes, arrows, LaTeX and data, exported to SVG/PNG/PDF/HTML
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Homepage, https://github.com/philippspohn/figkit
|
|
@@ -263,7 +263,7 @@ Anchors on every element: `n s e w ne nw se sw center`, plus `at_angle(deg)`,
|
|
|
263
263
|
|
|
264
264
|
```bash
|
|
265
265
|
pip install -e ".[dev]"
|
|
266
|
-
pytest #
|
|
266
|
+
pytest # full suite; every example must audit clean
|
|
267
267
|
python -m pyflakes figkit
|
|
268
268
|
python examples/01_pipeline.py
|
|
269
269
|
```
|
|
@@ -218,7 +218,7 @@ Anchors on every element: `n s e w ne nw se sw center`, plus `at_angle(deg)`,
|
|
|
218
218
|
|
|
219
219
|
```bash
|
|
220
220
|
pip install -e ".[dev]"
|
|
221
|
-
pytest #
|
|
221
|
+
pytest # full suite; every example must audit clean
|
|
222
222
|
python -m pyflakes figkit
|
|
223
223
|
python examples/01_pipeline.py
|
|
224
224
|
```
|
|
@@ -42,13 +42,13 @@ with Figure(pad=26, background="#ffffff") as fig:
|
|
|
42
42
|
Text([Span("Rich text", bold=True), " — one line, several styles"],
|
|
43
43
|
font_size=16, align="left").at(0, 0)
|
|
44
44
|
Text(["accuracy ", Span("76.1", strike=True, color="@muted"), " → ",
|
|
45
|
-
Span("94.6", color="
|
|
45
|
+
Span("94.6", color="#4b965c", bold=True), " after ",
|
|
46
46
|
Span("pretraining", italic=True), ", measured on ",
|
|
47
47
|
Span("held-out data", underline=True)],
|
|
48
48
|
font_size=14, align="left").at(0, 30)
|
|
49
49
|
|
|
50
50
|
# ---- 2. a matrix expression, set like an equation -------------------
|
|
51
|
-
lhs = Text("
|
|
51
|
+
lhs = Text(r"$\Pi_{\mathcal{NM}} \;=\;$", font_size=19)
|
|
52
52
|
left = LabelledMatrix([[rng.random() for _ in range(4)] for _ in range(5)],
|
|
53
53
|
cell=17, cmap="grays", stroke="#ffffff",
|
|
54
54
|
row_label="seq len", col_label="$d$",
|
|
@@ -16,7 +16,7 @@ Everything is explicit: you place things, figkit measures them accurately
|
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
|
-
__version__ = "0.
|
|
19
|
+
__version__ = "0.2.0"
|
|
20
20
|
|
|
21
21
|
# -- geometry ---------------------------------------------------------------
|
|
22
22
|
from .geom import Affine, BBox, Point, to_point
|
|
@@ -46,8 +46,8 @@ from .components import (Bracket, Brace, Callout, ColorBar, Heatmap,
|
|
|
46
46
|
Table, Vector)
|
|
47
47
|
|
|
48
48
|
# -- connectors -------------------------------------------------------------
|
|
49
|
-
from .connectors import (Connector, arrow, connect, curve,
|
|
50
|
-
elbow, line, self_loop)
|
|
49
|
+
from .connectors import (Connector, Handle, arrow, connect, curve,
|
|
50
|
+
double_arrow, elbow, line, self_loop)
|
|
51
51
|
|
|
52
52
|
# -- layout -----------------------------------------------------------------
|
|
53
53
|
from .layout import (align, align_h, align_v, baseline_of, between, bbox_of,
|
|
@@ -94,8 +94,8 @@ __all__ = [
|
|
|
94
94
|
"Bracket", "Legend", "Table", "Callout", "Spacer", "Component",
|
|
95
95
|
"LabelledMatrix",
|
|
96
96
|
# connectors
|
|
97
|
-
"Connector", "arrow", "line", "elbow", "curve", "connect",
|
|
98
|
-
"self_loop",
|
|
97
|
+
"Connector", "Handle", "arrow", "line", "elbow", "curve", "connect",
|
|
98
|
+
"double_arrow", "self_loop",
|
|
99
99
|
# layout
|
|
100
100
|
"align", "align_h", "align_v", "distribute_h", "distribute_v", "spread_h",
|
|
101
101
|
"spread_v", "hstack", "vstack", "grid", "fit", "frame_around", "between",
|
|
@@ -531,8 +531,19 @@ def _check_contrast(figure, min_contrast: float) -> list:
|
|
|
531
531
|
continue
|
|
532
532
|
if not getattr(el, "audit_enabled", True):
|
|
533
533
|
continue
|
|
534
|
-
|
|
535
|
-
|
|
534
|
+
colours = [label.text_color()]
|
|
535
|
+
try:
|
|
536
|
+
colours.extend(label._resolve_value(run.color)
|
|
537
|
+
for line in label.layout.lines for run in line.runs
|
|
538
|
+
if run.color is not None)
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
unique = []
|
|
542
|
+
for candidate in colours:
|
|
543
|
+
if candidate is not None and candidate not in unique:
|
|
544
|
+
unique.append(candidate)
|
|
545
|
+
colours = unique
|
|
546
|
+
if not colours:
|
|
536
547
|
continue
|
|
537
548
|
lb = label.bbox
|
|
538
549
|
pos = index.get(id(el), 0)
|
|
@@ -544,18 +555,20 @@ def _check_contrast(figure, min_contrast: float) -> list:
|
|
|
544
555
|
# box's own fill, which is the commonest backdrop of all.
|
|
545
556
|
if _contains(other.bbox, lb, tol=-0.5):
|
|
546
557
|
backdrop = _solid_fill(other)
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
558
|
+
for colour in colours:
|
|
559
|
+
try:
|
|
560
|
+
ratio = _contrast_ratio(colour, backdrop)
|
|
561
|
+
except (TypeError, ValueError):
|
|
562
|
+
continue
|
|
563
|
+
if ratio < min_contrast - 1e-9:
|
|
564
|
+
out.append(Finding(
|
|
565
|
+
"contrast",
|
|
566
|
+
f"{describe(el)}: text {colour} on {backdrop} has contrast "
|
|
567
|
+
f"{_floor2(ratio)}:1 (want {min_contrast:.2f}:1)",
|
|
568
|
+
severity="error" if ratio < 1.6 else "warning",
|
|
569
|
+
where=lb.center, elements=(el,),
|
|
570
|
+
detail={"ratio": ratio, "color": colour,
|
|
571
|
+
"background": backdrop}))
|
|
559
572
|
return out
|
|
560
573
|
|
|
561
574
|
|
|
@@ -7,7 +7,7 @@ import math
|
|
|
7
7
|
from .colors import colormap, contrast_color, to_hex
|
|
8
8
|
from .component import Component
|
|
9
9
|
from .core import Element, Group
|
|
10
|
-
from .style import Style
|
|
10
|
+
from .style import Style, enum_value
|
|
11
11
|
from .geom import BBox, Point, _expand_spec, to_point
|
|
12
12
|
from .paint import paint_attrs
|
|
13
13
|
from .shapes import Box, Marker
|
|
@@ -171,6 +171,7 @@ class Matrix(Group):
|
|
|
171
171
|
self.cmap = cmap
|
|
172
172
|
self._cells: list = []
|
|
173
173
|
self._value_labels: list = []
|
|
174
|
+
self._value_label_map: dict = {}
|
|
174
175
|
# Paint properties passed to Matrix(...) style the *cells*, which is
|
|
175
176
|
# what people mean by `Matrix(vals, stroke="#333")`.
|
|
176
177
|
group_kw = {k: v for k, v in kw.items() if k in _GROUP_KEYS}
|
|
@@ -211,6 +212,7 @@ class Matrix(Group):
|
|
|
211
212
|
self.add(lbl)
|
|
212
213
|
lbl.center_at(cell_el.bbox.cx, cell_el.bbox.cy)
|
|
213
214
|
self._value_labels.append(lbl)
|
|
215
|
+
self._value_label_map[(i, j)] = lbl
|
|
214
216
|
self._cells.append(row_cells)
|
|
215
217
|
|
|
216
218
|
if border:
|
|
@@ -263,6 +265,9 @@ class Matrix(Group):
|
|
|
263
265
|
c = self.cell(i, j)
|
|
264
266
|
c.restyle(**style)
|
|
265
267
|
c.to_front()
|
|
268
|
+
label = self._value_label_map.get((i % self.n_rows, j % self.n_cols))
|
|
269
|
+
if label is not None:
|
|
270
|
+
label.to_front()
|
|
266
271
|
return c
|
|
267
272
|
|
|
268
273
|
|
|
@@ -271,13 +276,16 @@ def Vector(values=None, *, orient: str = "v", **kw) -> Matrix:
|
|
|
271
276
|
if values is None:
|
|
272
277
|
values = []
|
|
273
278
|
flat = list(values)
|
|
274
|
-
|
|
279
|
+
orient = enum_value(orient, "orient", {
|
|
280
|
+
"v": "v", "vertical": "v", "h": "h", "horizontal": "h",
|
|
281
|
+
})
|
|
282
|
+
if orient == "v":
|
|
275
283
|
grid = [[v] for v in flat]
|
|
276
284
|
else:
|
|
277
285
|
grid = [flat]
|
|
278
286
|
colors = kw.pop("colors", None)
|
|
279
287
|
if colors is not None:
|
|
280
|
-
colors = [[c] for c in colors] if
|
|
288
|
+
colors = [[c] for c in colors] if orient == "v" \
|
|
281
289
|
else [list(colors)]
|
|
282
290
|
return Matrix(None, colors=colors, **kw)
|
|
283
291
|
return Matrix(grid, **kw)
|
|
@@ -296,16 +304,21 @@ class ColorBar(Group):
|
|
|
296
304
|
labels=True, label_fmt="{:.2g}", x: float = 0.0,
|
|
297
305
|
y: float = 0.0, **kw):
|
|
298
306
|
super().__init__(**kw)
|
|
299
|
-
|
|
307
|
+
orient = enum_value(orient, "orient", {
|
|
308
|
+
"v": "v", "vertical": "v", "h": "h", "horizontal": "h",
|
|
309
|
+
})
|
|
310
|
+
vertical = orient == "v"
|
|
300
311
|
n = max(2, int(steps))
|
|
301
312
|
for i in range(n):
|
|
302
313
|
t = i / (n - 1)
|
|
314
|
+
pos = i / n
|
|
315
|
+
span = (1.0 - pos) if i == n - 1 else (1.0 / n + 0.001)
|
|
303
316
|
col = colormap(cmap, 1.0 - t if vertical else t)
|
|
304
317
|
if vertical:
|
|
305
|
-
seg = Box(None, x, y +
|
|
318
|
+
seg = Box(None, x, y + pos * h, w, span * h, fill=col,
|
|
306
319
|
stroke="none", padding=0, radius=0, add=False)
|
|
307
320
|
else:
|
|
308
|
-
seg = Box(None, x +
|
|
321
|
+
seg = Box(None, x + pos * w, y, span * w, h, fill=col,
|
|
309
322
|
stroke="none", padding=0, radius=0, add=False)
|
|
310
323
|
self.add(seg)
|
|
311
324
|
frame = Box(None, x, y, w, h, fill="none", stroke="#6b7280",
|