plotlet 0.1.3__tar.gz → 0.3.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 (39) hide show
  1. {plotlet-0.1.3/src/plotlet.egg-info → plotlet-0.3.0}/PKG-INFO +9 -7
  2. {plotlet-0.1.3 → plotlet-0.3.0}/README.md +8 -6
  3. {plotlet-0.1.3 → plotlet-0.3.0}/pyproject.toml +1 -1
  4. plotlet-0.3.0/src/plotlet/__init__.py +33 -0
  5. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/builtin_artists.py +102 -0
  6. plotlet-0.3.0/src/plotlet/chart.py +563 -0
  7. plotlet-0.3.0/src/plotlet/core.py +971 -0
  8. plotlet-0.3.0/src/plotlet/layout.py +835 -0
  9. plotlet-0.3.0/src/plotlet/layout_diagram.py +235 -0
  10. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/legend.py +35 -25
  11. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/registry.py +8 -1
  12. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/spec.json +2 -2
  13. {plotlet-0.1.3 → plotlet-0.3.0/src/plotlet.egg-info}/PKG-INFO +9 -7
  14. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet.egg-info/SOURCES.txt +4 -2
  15. {plotlet-0.1.3 → plotlet-0.3.0}/tests/test_chart.py +56 -3
  16. plotlet-0.3.0/tests/test_layout_diagram.py +93 -0
  17. {plotlet-0.1.3 → plotlet-0.3.0}/tests/test_legend.py +32 -20
  18. plotlet-0.3.0/tests/test_subplots.py +365 -0
  19. plotlet-0.3.0/tests/test_units.py +208 -0
  20. plotlet-0.1.3/src/plotlet/__init__.py +0 -29
  21. plotlet-0.1.3/src/plotlet/chart.py +0 -337
  22. plotlet-0.1.3/src/plotlet/core.py +0 -581
  23. plotlet-0.1.3/src/plotlet/layout.py +0 -530
  24. plotlet-0.1.3/tests/test_old.py +0 -112
  25. plotlet-0.1.3/tests/test_subplots.py +0 -306
  26. {plotlet-0.1.3 → plotlet-0.3.0}/LICENSE +0 -0
  27. {plotlet-0.1.3 → plotlet-0.3.0}/setup.cfg +0 -0
  28. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/_cm_data.py +0 -0
  29. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/_png.py +0 -0
  30. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/_spec.py +0 -0
  31. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/artists.py +0 -0
  32. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/colormaps.py +0 -0
  33. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/colors.py +0 -0
  34. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/font.py +0 -0
  35. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/fonts/DejaVuSans.ttf +0 -0
  36. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet/scales.py +0 -0
  37. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet.egg-info/dependency_links.txt +0 -0
  38. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet.egg-info/requires.txt +0 -0
  39. {plotlet-0.1.3 → plotlet-0.3.0}/src/plotlet.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plotlet
3
- Version: 0.1.3
3
+ Version: 0.3.0
4
4
  Summary: Small, hackable Python library that emits matplotlib-style SVG plots.
5
5
  Author: gitbamboo42
6
6
  License-Expression: MIT
@@ -62,9 +62,10 @@ pip install plotlet
62
62
  - **Lightweight.** `fonttools` for font handling. numpy / pandas / polars inputs work transparently if you have them.
63
63
  - **Static SVG output.** No interactivity, no animation. Same script → byte-identical SVG.
64
64
  - **Cross-machine reproducible.** Bundled DejaVu Sans + text-as-paths means rendering is identical on Linux, macOS, Windows, headless CI.
65
- - **Jupyter-native.** `Figure._repr_html_` auto-renders the last expression in a cell.
65
+ - **Jupyter-native.** `Chart._repr_html_` auto-renders the last expression in a cell.
66
66
  - **Tiny output.** Each plot is ~50 KB SVG, self-contained.
67
- - **Compositional.** Multi-panel layouts via `|`, `/`, `pt.grid`; share scales with `share_x=` / `share_y=`; layout-level legend with `pt.legend()` covering both discrete swatches and continuous gradients (the colorbar).
67
+ - **Compositional.** Multi-panel layouts via `|`, `/`, `pt.grid`; share scales with `(a | b).share_x()` or `pt.grid(..., share_x="col")`; layout-level legend with `pt.legend()` covering both discrete swatches and continuous gradients (the colorbar).
68
+ - **AI-readable.** Every figure ships `data-plotlet-*` attributes describing plot type, axes, scales, ranges, and series labels — readable in one XML parse, no glyph-path OCR. Schema: [docs/AI_ATTRS.md](docs/AI_ATTRS.md).
68
69
 
69
70
  ## API
70
71
 
@@ -74,7 +75,7 @@ pip install plotlet
74
75
 
75
76
  Pass at construction (`pt.chart(data, title=..., grid=True, ...)`) or as chained setters (`c.title(...)`, etc.):
76
77
 
77
- `title`, `xlabel`, `ylabel`, `xlim=(a, b)`, `ylim=(a, b)`, `xscale="linear"|"log"|"category"` (chained: `c.xscale("category", order=[...], padding=0)`), `yscale=...`, `grid=True/False`, `legend=True/False`, `width`, `height`
78
+ `title`, `xlabel`, `ylabel`, `xlim=(a, b)`, `ylim=(a, b)`, `xscale="linear"|"log"|"category"` (chained: `c.xscale("category", order=[...], padding=0)`), `yscale=...`, `grid=True/False`, `legend=True/False`, `data_width`, `data_height` (the data region — preferred), or `canvas_width`, `canvas_height` (the full SVG canvas — mutually exclusive with the data form). Sizes accept bare pixels (`400`) or unit-suffixed strings (`"4in"`, `"10cm"`, `"100mm"`, `"72pt"`).
78
79
 
79
80
  String-valued data on either axis (`scatter(["a","b","c"], ...)`, `bar`, …) auto-switches to a categorical scale, alphabetical by default. `padding=0` makes category bands contiguous (heatmap-track look).
80
81
 
@@ -110,10 +111,11 @@ pt.grid([[a, b], # 2-D grid; cells may be `None`
110
111
  [c, d]])
111
112
 
112
113
  # Share x or y across panels — collapses the gap between them
113
- # and forces both onto the source's scale.
114
+ # and unions data ranges; the first leaf in reading order anchors
115
+ # the scale, others are aspect-scaled to match.
114
116
  top = pt.chart()
115
- main = pt.chart(share_x=top)
116
- top / main # vertically stacked, x-axis joined
117
+ main = pt.chart()
118
+ (top / main).share_x() # vertically stacked, x-axis joined
117
119
 
118
120
  # Layout-level legend (covers colorbar and discrete swatches in
119
121
  # one constructor — geometry follows from the source's color mapping).
@@ -33,9 +33,10 @@ pip install plotlet
33
33
  - **Lightweight.** `fonttools` for font handling. numpy / pandas / polars inputs work transparently if you have them.
34
34
  - **Static SVG output.** No interactivity, no animation. Same script → byte-identical SVG.
35
35
  - **Cross-machine reproducible.** Bundled DejaVu Sans + text-as-paths means rendering is identical on Linux, macOS, Windows, headless CI.
36
- - **Jupyter-native.** `Figure._repr_html_` auto-renders the last expression in a cell.
36
+ - **Jupyter-native.** `Chart._repr_html_` auto-renders the last expression in a cell.
37
37
  - **Tiny output.** Each plot is ~50 KB SVG, self-contained.
38
- - **Compositional.** Multi-panel layouts via `|`, `/`, `pt.grid`; share scales with `share_x=` / `share_y=`; layout-level legend with `pt.legend()` covering both discrete swatches and continuous gradients (the colorbar).
38
+ - **Compositional.** Multi-panel layouts via `|`, `/`, `pt.grid`; share scales with `(a | b).share_x()` or `pt.grid(..., share_x="col")`; layout-level legend with `pt.legend()` covering both discrete swatches and continuous gradients (the colorbar).
39
+ - **AI-readable.** Every figure ships `data-plotlet-*` attributes describing plot type, axes, scales, ranges, and series labels — readable in one XML parse, no glyph-path OCR. Schema: [docs/AI_ATTRS.md](docs/AI_ATTRS.md).
39
40
 
40
41
  ## API
41
42
 
@@ -45,7 +46,7 @@ pip install plotlet
45
46
 
46
47
  Pass at construction (`pt.chart(data, title=..., grid=True, ...)`) or as chained setters (`c.title(...)`, etc.):
47
48
 
48
- `title`, `xlabel`, `ylabel`, `xlim=(a, b)`, `ylim=(a, b)`, `xscale="linear"|"log"|"category"` (chained: `c.xscale("category", order=[...], padding=0)`), `yscale=...`, `grid=True/False`, `legend=True/False`, `width`, `height`
49
+ `title`, `xlabel`, `ylabel`, `xlim=(a, b)`, `ylim=(a, b)`, `xscale="linear"|"log"|"category"` (chained: `c.xscale("category", order=[...], padding=0)`), `yscale=...`, `grid=True/False`, `legend=True/False`, `data_width`, `data_height` (the data region — preferred), or `canvas_width`, `canvas_height` (the full SVG canvas — mutually exclusive with the data form). Sizes accept bare pixels (`400`) or unit-suffixed strings (`"4in"`, `"10cm"`, `"100mm"`, `"72pt"`).
49
50
 
50
51
  String-valued data on either axis (`scatter(["a","b","c"], ...)`, `bar`, …) auto-switches to a categorical scale, alphabetical by default. `padding=0` makes category bands contiguous (heatmap-track look).
51
52
 
@@ -81,10 +82,11 @@ pt.grid([[a, b], # 2-D grid; cells may be `None`
81
82
  [c, d]])
82
83
 
83
84
  # Share x or y across panels — collapses the gap between them
84
- # and forces both onto the source's scale.
85
+ # and unions data ranges; the first leaf in reading order anchors
86
+ # the scale, others are aspect-scaled to match.
85
87
  top = pt.chart()
86
- main = pt.chart(share_x=top)
87
- top / main # vertically stacked, x-axis joined
88
+ main = pt.chart()
89
+ (top / main).share_x() # vertically stacked, x-axis joined
88
90
 
89
91
  # Layout-level legend (covers colorbar and discrete swatches in
90
92
  # one constructor — geometry follows from the source's color mapping).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "plotlet"
7
- version = "0.1.3"
7
+ version = "0.3.0"
8
8
  description = "Small, hackable Python library that emits matplotlib-style SVG plots."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,33 @@
1
+ """plotlet — pure-Python SVG renderer, matplotlib-flavored.
2
+
3
+ import plotlet as pt
4
+ c = pt.chart(df, title="...", xlabel="x", ylabel="y", legend=True, grid=True)
5
+ c.line(x="time", y="value", hue="series")
6
+ c # auto-renders in Jupyter
7
+
8
+ For non-tabular use, the chained form works directly on the same Chart:
9
+
10
+ c = pt.chart()
11
+ c.plot([1, 2, 3], [1, 4, 9], label="squares")
12
+ c.title("Hello").legend().grid(True)
13
+ c
14
+ """
15
+ from ._spec import SPEC
16
+ from .colors import TAB10, colors
17
+ from .colormaps import colormap, list_colormaps
18
+ from .chart import Chart, chart
19
+ from .layout import grid
20
+ from .legend import legend
21
+ from .registry import ArtistSpec, add_artist
22
+ from .layout_diagram import layout_diagram
23
+
24
+ __all__ = ["chart", "Chart", "SPEC", "TAB10", "colors",
25
+ "colormap", "list_colormaps", "grid", "legend",
26
+ "ArtistSpec", "add_artist", "layout_diagram"]
27
+
28
+ # Single source of truth: pyproject.toml. importlib.metadata reads it at
29
+ # runtime from the installed package metadata (works for `pip install` and
30
+ # `pip install -e .` alike). Migrating to setuptools_scm later doesn't
31
+ # touch this code — it just changes how pyproject.toml's version is set.
32
+ from importlib.metadata import version as _pkg_version
33
+ __version__ = _pkg_version("plotlet")
@@ -79,6 +79,98 @@ def _refspan_legend_swatch(a, ctx, x0, y_mid):
79
79
  return _rect_swatch(a, x0, y_mid, ctx.defaults["refspan_alpha"])
80
80
 
81
81
 
82
+ # --- AI-readable structural attrs + payloads (0.3.0) ------------------------
83
+ # Each helper computes type-specific attrs from the recorded artist dict.
84
+ # The wrapper in core._wrap_artist supplies the common attrs (type, index,
85
+ # label, color); these add the per-artist fields. Returned dict keys map
86
+ # 1:1 to `data-plotlet-<key>` attribute names.
87
+
88
+ def _xy_minmax(xs, ys):
89
+ """Min/max for an x/y series, ignoring NaN. Returns dict ready to merge
90
+ into a data_attrs result."""
91
+ fxs = [x for x in xs if isinstance(x, (int, float)) and x == x]
92
+ fys = [y for y in ys if isinstance(y, (int, float)) and y == y]
93
+ out = {}
94
+ if fxs:
95
+ out["x-min"] = min(fxs)
96
+ out["x-max"] = max(fxs)
97
+ if fys:
98
+ out["y-min"] = min(fys)
99
+ out["y-max"] = max(fys)
100
+ return out
101
+
102
+
103
+ def _plot_data_attrs(a):
104
+ out = {"n": len(a["xs"])}
105
+ out.update(_xy_minmax(a["xs"], a["ys"]))
106
+ if a["opts"].get("linestyle"):
107
+ out["linestyle"] = a["opts"]["linestyle"]
108
+ if a["opts"].get("marker"):
109
+ out["marker"] = a["opts"]["marker"]
110
+ return out
111
+
112
+
113
+ def _scatter_data_attrs(a):
114
+ out = {"n": len(a["xs"])}
115
+ out.update(_xy_minmax(a["xs"], a["ys"]))
116
+ out["marker"] = a["opts"].get("marker", "o")
117
+ return out
118
+
119
+
120
+ def _bar_data_attrs(a):
121
+ fvals = [v for v in a["vals"] if isinstance(v, (int, float)) and v == v]
122
+ out = {"n": len(a["cats"])}
123
+ if fvals:
124
+ out["y-min"] = min(fvals)
125
+ out["y-max"] = max(fvals)
126
+ return out
127
+
128
+
129
+ def _hist_data_attrs(a):
130
+ raw = a["data"]
131
+ out = {"n": len(raw), "bins": len(a.get("_bins", [])) or a["opts"].get("bins", 10)}
132
+ bins = a.get("_bins") or []
133
+ if bins:
134
+ out["x-min"] = bins[0]["x0"]
135
+ out["x-max"] = bins[-1]["x1"]
136
+ out["count-max"] = max(b["count"] for b in bins)
137
+ return out
138
+
139
+
140
+ def _fill_between_data_attrs(a):
141
+ ys_all = list(a["y1"]) + list(a["y2"])
142
+ out = {"n": len(a["xs"])}
143
+ out.update(_xy_minmax(a["xs"], ys_all))
144
+ return out
145
+
146
+
147
+ def _axhline_data_attrs(a): return {"y": a["y"]}
148
+ def _axvline_data_attrs(a): return {"x": a["x"]}
149
+ def _axhspan_data_attrs(a): return {"ymin": a["ymin"], "ymax": a["ymax"]}
150
+ def _axvspan_data_attrs(a): return {"xmin": a["xmin"], "xmax": a["xmax"]}
151
+
152
+
153
+ def _imshow_data_attrs(a):
154
+ out = {
155
+ "rows": a["_nrows"],
156
+ "cols": a["_ncols"],
157
+ "vmin": a["_vmin"],
158
+ "vmax": a["_vmax"],
159
+ "cmap": a["opts"].get("cmap", _D["default_cmap"]),
160
+ # imshow is always raster (PNG-embedded above the rect threshold,
161
+ # individual <rect>s below). The flag is here so AI tools know
162
+ # which decoding strategy they're looking at.
163
+ "data-encoding": "png-embedded" if (a["_nrows"] * a["_ncols"]
164
+ > _D["imshow_max_rects"]) else "rects",
165
+ }
166
+ extent = a["opts"].get("extent")
167
+ if extent is not None:
168
+ out["extent"] = ",".join(repr(float(v)) for v in extent)
169
+ return out
170
+
171
+
172
+
173
+
82
174
  # --- plot (line) ------------------------------------------------------------
83
175
 
84
176
  add_artist(ArtistSpec(
@@ -89,6 +181,7 @@ add_artist(ArtistSpec(
89
181
  ydomain=_ys_of,
90
182
  draw=lambda a, ctx: _artist_plot(a, ctx.x_scale, ctx.y_scale, ctx.color),
91
183
  legend_swatch=_plot_legend_swatch,
184
+ data_attrs=_plot_data_attrs,
92
185
  ))
93
186
 
94
187
 
@@ -102,6 +195,7 @@ add_artist(ArtistSpec(
102
195
  ydomain=_ys_of,
103
196
  draw=lambda a, ctx: _artist_scatter(a, ctx.x_scale, ctx.y_scale, ctx.color),
104
197
  legend_swatch=_scatter_legend_swatch,
198
+ data_attrs=_scatter_data_attrs,
105
199
  ))
106
200
 
107
201
 
@@ -117,6 +211,7 @@ add_artist(ArtistSpec(
117
211
  ydomain=_vals_of,
118
212
  draw=lambda a, ctx: _artist_bar(a, ctx.x_scale, ctx.y_scale, ctx.color),
119
213
  legend_swatch=_bar_legend_swatch,
214
+ data_attrs=_bar_data_attrs,
120
215
  ))
121
216
 
122
217
 
@@ -129,6 +224,7 @@ add_artist(ArtistSpec(
129
224
  ydomain=_bin_ys,
130
225
  draw=lambda a, ctx: _artist_hist(a, ctx.x_scale, ctx.y_scale, ctx.ih, ctx.color),
131
226
  legend_swatch=_bar_legend_swatch,
227
+ data_attrs=_hist_data_attrs,
132
228
  ))
133
229
 
134
230
 
@@ -145,6 +241,7 @@ add_artist(ArtistSpec(
145
241
  ydomain=_y1y2_of,
146
242
  draw=lambda a, ctx: _artist_fill_between(a, ctx.x_scale, ctx.y_scale, ctx.color),
147
243
  legend_swatch=_plot_legend_swatch,
244
+ data_attrs=_fill_between_data_attrs,
148
245
  ))
149
246
 
150
247
 
@@ -160,6 +257,7 @@ add_artist(ArtistSpec(
160
257
  uses_color_cycle=False,
161
258
  default_color=_D["refline_color"],
162
259
  legend_swatch=_refline_legend_swatch,
260
+ data_attrs=_axhline_data_attrs,
163
261
  ))
164
262
 
165
263
  add_artist(ArtistSpec(
@@ -171,6 +269,7 @@ add_artist(ArtistSpec(
171
269
  uses_color_cycle=False,
172
270
  default_color=_D["refline_color"],
173
271
  legend_swatch=_refline_legend_swatch,
272
+ data_attrs=_axvline_data_attrs,
174
273
  ))
175
274
 
176
275
  add_artist(ArtistSpec(
@@ -182,6 +281,7 @@ add_artist(ArtistSpec(
182
281
  uses_color_cycle=False,
183
282
  default_color=_D["refspan_color"],
184
283
  legend_swatch=_refspan_legend_swatch,
284
+ data_attrs=_axhspan_data_attrs,
185
285
  ))
186
286
 
187
287
  add_artist(ArtistSpec(
@@ -193,6 +293,7 @@ add_artist(ArtistSpec(
193
293
  uses_color_cycle=False,
194
294
  default_color=_D["refspan_color"],
195
295
  legend_swatch=_refspan_legend_swatch,
296
+ data_attrs=_axvspan_data_attrs,
196
297
  ))
197
298
 
198
299
 
@@ -251,4 +352,5 @@ add_artist(ArtistSpec(
251
352
  draw=lambda a, ctx: _artist_imshow(a, ctx.x_scale, ctx.y_scale, None),
252
353
  legend_gradient=_imshow_legend_gradient,
253
354
  uses_color_cycle=False,
355
+ data_attrs=_imshow_data_attrs,
254
356
  ))