plotlet 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.
plotlet-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gitbamboo42
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.
plotlet-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: plotlet
3
+ Version: 0.1.0
4
+ Summary: Small, hackable Python library that emits matplotlib-style SVG plots.
5
+ Author: gitbamboo42
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/gitbamboo42/plotlet
8
+ Project-URL: Repository, https://github.com/gitbamboo42/plotlet
9
+ Project-URL: Issues, https://github.com/gitbamboo42/plotlet/issues
10
+ Keywords: plot,svg,matplotlib,scientific,visualization,jupyter,reproducible
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Science/Research
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Visualization
20
+ Classifier: Topic :: Multimedia :: Graphics
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: fonttools>=4.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: jupyter; extra == "dev"
27
+ Requires-Dist: nbconvert; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # plotlet
31
+
32
+ A small, hackable Python library that emits matplotlib-style SVG plots.
33
+
34
+ ## Why
35
+
36
+ matplotlib is the right tool when you want the kitchen sink. plotlet's niche is **custom plot types** — genome tracks, Manhattan plots, phylogenetic trees, anything matplotlib's extension API makes painful. The whole library is ~700 lines of Python with a deliberately tiny, exposed core: adding a new plot type is a 3-step recipe, not an architecture project.
37
+
38
+ It's a **scaffold, not a feature catalog**: the core ships ~5 standard plots and the infrastructure for extending. Custom plot types live in your own project (or [`cookbook/`](cookbook/)), not upstream. See [docs/PHILOSOPHY.md](docs/PHILOSOPHY.md) for the full framing.
39
+
40
+ ```python
41
+ import plotlet as pt
42
+
43
+ data = {
44
+ "x": [1, 2, 3, 4, 5, 1, 2, 3, 4, 5],
45
+ "y": [1, 4, 9, 16, 25, 1, 8, 27, 64, 125],
46
+ "series": ["squares"] * 5 + ["cubes"] * 5,
47
+ }
48
+
49
+ c = pt.chart(data, title="Hello", xlabel="x", ylabel="y", legend=True, grid=True)
50
+ c.line(x="x", y="y", hue="series")
51
+ c # auto-renders in Jupyter
52
+ ```
53
+
54
+ ## Install
55
+
56
+ Not on PyPI yet — clone and install editable:
57
+
58
+ ```bash
59
+ git clone <repo>
60
+ cd plotlet
61
+ pip install -e .
62
+ ```
63
+
64
+ ## Properties
65
+
66
+ - **Lightweight.** `fonttools` for font handling. numpy / pandas / polars inputs work transparently if you have them.
67
+ - **Static SVG output.** No interactivity, no animation. Same script → byte-identical SVG.
68
+ - **Cross-machine reproducible.** Bundled DejaVu Sans + text-as-paths means rendering is identical on Linux, macOS, Windows, headless CI.
69
+ - **Jupyter-native.** `Figure._repr_html_` auto-renders the last expression in a cell.
70
+ - **Tiny output.** Each plot is ~50 KB SVG, self-contained.
71
+
72
+ ## API
73
+
74
+ `pt.chart(data, **opts)` returns a `Chart` bound to a table — any object that supports `data[col_name]` returning an iterable (pandas / polars DataFrames, dict-of-lists, dict-of-arrays). All methods return `self`; `_repr_html_` makes the chart auto-render as the last expression in a Jupyter cell.
75
+
76
+ ### Frame options
77
+
78
+ Pass at construction (`pt.chart(data, title=..., grid=True, ...)`) or as chained setters (`c.title(...)`, etc.):
79
+
80
+ `title`, `xlabel`, `ylabel`, `xlim=(a, b)`, `ylim=(a, b)`, `xscale="log"|"linear"`, `yscale=...`, `grid=True/False`, `legend=True/False`, `width`, `height`
81
+
82
+ ### Mark methods
83
+
84
+ | call | options |
85
+ | --- | --- |
86
+ | `.line(x=, y=, hue=, **opts)` | `color`, `label`, `linewidth`, `linestyle` (`"-"`, `"--"`, `":"`, `"-."`), `marker` (`"o"`, `"s"`, `"^"`, `"v"`, `"x"`, `"+"`), `markersize` |
87
+ | `.scatter(x=, y=, hue=, **opts)` | `color`, `label`, `s` (size), `alpha`, `marker` |
88
+ | `.bar(x=, y=, **opts)` | `color`, `label`, `alpha` |
89
+ | `.hist(x=, **opts)` | `bins`, `color`, `alpha`, `label` |
90
+ | `.fill_between(x=, y1=, y2=, **opts)` | `color`, `alpha`, `label` |
91
+
92
+ `hue=<col>` (on `.line` / `.scatter`) splits into one call per unique value with auto-labels and tab10 colors.
93
+
94
+ ### Render / save
95
+
96
+ ```python
97
+ c.show() # explicit display() inside a cell
98
+ c.to_svg() # raw SVG string
99
+ c.save_svg("plot.svg") # SVG file
100
+ c.write_html("plot.html") # standalone HTML
101
+ ```
102
+
103
+ ### Color shortcuts
104
+
105
+ - `"C0"`–`"C9"` → tab10 (matches matplotlib)
106
+ - Named: `"blue"`, `"orange"`, `"green"`, `"red"`, `"purple"`, `"brown"`, `"pink"`, `"gray"`, `"olive"`, `"cyan"`
107
+ - Single-letter: `"k"`, `"w"`, `"b"`, `"g"`, `"r"`
108
+ - Any hex / CSS color string passes through
109
+
110
+ ## Adding a new plot type
111
+
112
+ plotlet's central hackability claim: a custom plot type is a 3-step recipe (~50–100 lines) that gets axes, scales, legend, grid, and composability for free. The recommended home is your own project, or [`cookbook/`](cookbook/) as reference. Full guide: [docs/EXTENDING.md](docs/EXTENDING.md).
113
+
114
+ ## Testing
115
+
116
+ ```bash
117
+ python tests/test_chart.py # check vs. committed baselines
118
+ python tests/test_chart.py --update # regenerate (review the diff!)
119
+ python tests/test_chart.py --gallery # build tests/baseline_images/chart/index.html
120
+ ```
121
+
122
+ ## Non-goals
123
+
124
+ - No interactivity (hover, zoom, click). Static rendering is the point.
125
+ - Not competing with matplotlib on standard plots; matplotlib is bigger and battle-tested.
126
+ - Not a 3D plotter, not a dashboard tool.
127
+ - Not a feature catalog — new plot types belong in user projects or `cookbook/`, not in the core.
128
+
129
+ ## License
130
+
131
+ MIT
@@ -0,0 +1,102 @@
1
+ # plotlet
2
+
3
+ A small, hackable Python library that emits matplotlib-style SVG plots.
4
+
5
+ ## Why
6
+
7
+ matplotlib is the right tool when you want the kitchen sink. plotlet's niche is **custom plot types** — genome tracks, Manhattan plots, phylogenetic trees, anything matplotlib's extension API makes painful. The whole library is ~700 lines of Python with a deliberately tiny, exposed core: adding a new plot type is a 3-step recipe, not an architecture project.
8
+
9
+ It's a **scaffold, not a feature catalog**: the core ships ~5 standard plots and the infrastructure for extending. Custom plot types live in your own project (or [`cookbook/`](cookbook/)), not upstream. See [docs/PHILOSOPHY.md](docs/PHILOSOPHY.md) for the full framing.
10
+
11
+ ```python
12
+ import plotlet as pt
13
+
14
+ data = {
15
+ "x": [1, 2, 3, 4, 5, 1, 2, 3, 4, 5],
16
+ "y": [1, 4, 9, 16, 25, 1, 8, 27, 64, 125],
17
+ "series": ["squares"] * 5 + ["cubes"] * 5,
18
+ }
19
+
20
+ c = pt.chart(data, title="Hello", xlabel="x", ylabel="y", legend=True, grid=True)
21
+ c.line(x="x", y="y", hue="series")
22
+ c # auto-renders in Jupyter
23
+ ```
24
+
25
+ ## Install
26
+
27
+ Not on PyPI yet — clone and install editable:
28
+
29
+ ```bash
30
+ git clone <repo>
31
+ cd plotlet
32
+ pip install -e .
33
+ ```
34
+
35
+ ## Properties
36
+
37
+ - **Lightweight.** `fonttools` for font handling. numpy / pandas / polars inputs work transparently if you have them.
38
+ - **Static SVG output.** No interactivity, no animation. Same script → byte-identical SVG.
39
+ - **Cross-machine reproducible.** Bundled DejaVu Sans + text-as-paths means rendering is identical on Linux, macOS, Windows, headless CI.
40
+ - **Jupyter-native.** `Figure._repr_html_` auto-renders the last expression in a cell.
41
+ - **Tiny output.** Each plot is ~50 KB SVG, self-contained.
42
+
43
+ ## API
44
+
45
+ `pt.chart(data, **opts)` returns a `Chart` bound to a table — any object that supports `data[col_name]` returning an iterable (pandas / polars DataFrames, dict-of-lists, dict-of-arrays). All methods return `self`; `_repr_html_` makes the chart auto-render as the last expression in a Jupyter cell.
46
+
47
+ ### Frame options
48
+
49
+ Pass at construction (`pt.chart(data, title=..., grid=True, ...)`) or as chained setters (`c.title(...)`, etc.):
50
+
51
+ `title`, `xlabel`, `ylabel`, `xlim=(a, b)`, `ylim=(a, b)`, `xscale="log"|"linear"`, `yscale=...`, `grid=True/False`, `legend=True/False`, `width`, `height`
52
+
53
+ ### Mark methods
54
+
55
+ | call | options |
56
+ | --- | --- |
57
+ | `.line(x=, y=, hue=, **opts)` | `color`, `label`, `linewidth`, `linestyle` (`"-"`, `"--"`, `":"`, `"-."`), `marker` (`"o"`, `"s"`, `"^"`, `"v"`, `"x"`, `"+"`), `markersize` |
58
+ | `.scatter(x=, y=, hue=, **opts)` | `color`, `label`, `s` (size), `alpha`, `marker` |
59
+ | `.bar(x=, y=, **opts)` | `color`, `label`, `alpha` |
60
+ | `.hist(x=, **opts)` | `bins`, `color`, `alpha`, `label` |
61
+ | `.fill_between(x=, y1=, y2=, **opts)` | `color`, `alpha`, `label` |
62
+
63
+ `hue=<col>` (on `.line` / `.scatter`) splits into one call per unique value with auto-labels and tab10 colors.
64
+
65
+ ### Render / save
66
+
67
+ ```python
68
+ c.show() # explicit display() inside a cell
69
+ c.to_svg() # raw SVG string
70
+ c.save_svg("plot.svg") # SVG file
71
+ c.write_html("plot.html") # standalone HTML
72
+ ```
73
+
74
+ ### Color shortcuts
75
+
76
+ - `"C0"`–`"C9"` → tab10 (matches matplotlib)
77
+ - Named: `"blue"`, `"orange"`, `"green"`, `"red"`, `"purple"`, `"brown"`, `"pink"`, `"gray"`, `"olive"`, `"cyan"`
78
+ - Single-letter: `"k"`, `"w"`, `"b"`, `"g"`, `"r"`
79
+ - Any hex / CSS color string passes through
80
+
81
+ ## Adding a new plot type
82
+
83
+ plotlet's central hackability claim: a custom plot type is a 3-step recipe (~50–100 lines) that gets axes, scales, legend, grid, and composability for free. The recommended home is your own project, or [`cookbook/`](cookbook/) as reference. Full guide: [docs/EXTENDING.md](docs/EXTENDING.md).
84
+
85
+ ## Testing
86
+
87
+ ```bash
88
+ python tests/test_chart.py # check vs. committed baselines
89
+ python tests/test_chart.py --update # regenerate (review the diff!)
90
+ python tests/test_chart.py --gallery # build tests/baseline_images/chart/index.html
91
+ ```
92
+
93
+ ## Non-goals
94
+
95
+ - No interactivity (hover, zoom, click). Static rendering is the point.
96
+ - Not competing with matplotlib on standard plots; matplotlib is bigger and battle-tested.
97
+ - Not a 3D plotter, not a dashboard tool.
98
+ - Not a feature catalog — new plot types belong in user projects or `cookbook/`, not in the core.
99
+
100
+ ## License
101
+
102
+ MIT
@@ -0,0 +1,56 @@
1
+ [build-system]
2
+ requires = ["setuptools>=64", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "plotlet"
7
+ version = "0.1.0"
8
+ description = "Small, hackable Python library that emits matplotlib-style SVG plots."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [
14
+ { name = "gitbamboo42" },
15
+ ]
16
+ keywords = [
17
+ "plot",
18
+ "svg",
19
+ "matplotlib",
20
+ "scientific",
21
+ "visualization",
22
+ "jupyter",
23
+ "reproducible",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Intended Audience :: Science/Research",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.10",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Topic :: Scientific/Engineering :: Visualization",
35
+ "Topic :: Multimedia :: Graphics",
36
+ ]
37
+ dependencies = [
38
+ "fonttools>=4.0",
39
+ ]
40
+
41
+ [project.optional-dependencies]
42
+ dev = [
43
+ "jupyter",
44
+ "nbconvert",
45
+ ]
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/gitbamboo42/plotlet"
49
+ Repository = "https://github.com/gitbamboo42/plotlet"
50
+ Issues = "https://github.com/gitbamboo42/plotlet/issues"
51
+
52
+ [tool.setuptools.packages.find]
53
+ where = ["src"]
54
+
55
+ [tool.setuptools.package-data]
56
+ plotlet = ["spec.json", "fonts/*.ttf"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,23 @@
1
+ """plotlet — pure-Python SVG renderer, matplotlib-flavored.
2
+
3
+ Tabular API (recommended):
4
+
5
+ import plotlet as pt
6
+ c = pt.chart(df, title="...", xlabel="x", ylabel="y", legend=True, grid=True)
7
+ c.line(x="time", y="value", hue="series")
8
+ c # auto-renders in Jupyter
9
+
10
+ Chained API (legacy, still supported):
11
+
12
+ fig = pt.figure()
13
+ fig.plot([1, 2, 3], [1, 4, 9], label="squares")
14
+ fig.title("Hello").legend().grid(True)
15
+ fig
16
+ """
17
+ from ._spec import SPEC
18
+ from .colors import TAB10, colors
19
+ from .core import Figure, figure
20
+ from .chart import Chart, chart
21
+
22
+ __all__ = ["chart", "Chart", "figure", "Figure", "SPEC", "TAB10", "colors"]
23
+ __version__ = "0.1.0"
@@ -0,0 +1,21 @@
1
+ """Visual spec — loaded from bundled spec.json. Internal.
2
+
3
+ The spec is the locked visual contract: colors, fonts, sizes, default alphas,
4
+ legend dimensions. Submodules read from here to avoid hardcoding literals.
5
+ """
6
+ import json
7
+ from pathlib import Path
8
+
9
+ _HERE = Path(__file__).parent
10
+ SPEC = json.loads((_HERE / "spec.json").read_text())
11
+
12
+ # Convenience handles to subsections — used by other modules.
13
+ _TAB10 = SPEC["colors"]["tab10"]
14
+ _COLOR_NAMES = SPEC["colors"]["named"]
15
+ _DASH = SPEC["linestyles"]
16
+ _D = SPEC["defaults"]
17
+ _FRAME = SPEC["frame"]
18
+ _GRIDSPEC = SPEC["grid"]
19
+ _FONTSPEC = SPEC["font"]
20
+ _LEGSPEC = SPEC["legend"]
21
+ _SIZESPEC = SPEC["size"]
@@ -0,0 +1,154 @@
1
+ """Per-artist SVG-emit helpers, marker primitive, histogram binning.
2
+
3
+ Each `_artist_<type>` takes the recorded artist dict, the x and y scales,
4
+ and the resolved color, and returns an SVG fragment. They're called from
5
+ `core._render`.
6
+ """
7
+ import math
8
+
9
+ from ._spec import _D, _DASH
10
+
11
+
12
+ def _to_pylist(obj):
13
+ """Convert numpy / pandas / arbitrary iterables to plain Python lists."""
14
+ if hasattr(obj, "tolist"):
15
+ return obj.tolist()
16
+ if isinstance(obj, (list, tuple)):
17
+ return list(obj)
18
+ return list(obj)
19
+
20
+
21
+ def _histogram(data, bins):
22
+ """Equal-width binning. Returns list of {'x0', 'x1', 'count'} dicts."""
23
+ data = [v for v in _to_pylist(data)
24
+ if v is not None and not (isinstance(v, float) and math.isnan(v))]
25
+ if not data:
26
+ return []
27
+ lo, hi = min(data), max(data)
28
+ if lo == hi:
29
+ hi = lo + 1
30
+ n = bins if isinstance(bins, int) else 10
31
+ width = (hi - lo) / n
32
+ counts = [0] * n
33
+ for v in data:
34
+ if v == hi:
35
+ counts[-1] += 1
36
+ else:
37
+ i = int((v - lo) / width)
38
+ if 0 <= i < n:
39
+ counts[i] += 1
40
+ return [{"x0": lo + i * width, "x1": lo + (i + 1) * width, "count": counts[i]}
41
+ for i in range(n)]
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Artist helpers
46
+ # ---------------------------------------------------------------------------
47
+
48
+ def _artist_plot(a, xs_, ys_, col):
49
+ out = []
50
+ opts = a["opts"]
51
+ pts = [(xs_(x), ys_(y)) for x, y in zip(a["xs"], a["ys"])]
52
+ pts = [(px, py) if (math.isfinite(px) and math.isfinite(py)) else None
53
+ for px, py in pts]
54
+ d_segs, started = [], False
55
+ for p in pts:
56
+ if p is None:
57
+ started = False
58
+ continue
59
+ d_segs.append(f'{"M" if not started else "L"}{p[0]:.2f},{p[1]:.2f}')
60
+ started = True
61
+ ls = opts.get("linestyle")
62
+ if ls not in ("", "none"):
63
+ da = f' stroke-dasharray="{_DASH[ls]}"' if ls and _DASH.get(ls) else ""
64
+ out.append(f'<path d="{"".join(d_segs)}" fill="none" stroke="{col}" '
65
+ f'stroke-width="{opts.get("linewidth", _D["linewidth"])}"{da}/>')
66
+ if opts.get("marker"):
67
+ sz = opts.get("markersize", _D["markersize"])
68
+ for p in pts:
69
+ if p is None:
70
+ continue
71
+ out.append(_marker_at(opts["marker"], p[0], p[1], sz, col, 1))
72
+ return "".join(out)
73
+
74
+
75
+ def _artist_scatter(a, xs_, ys_, col):
76
+ opts = a["opts"]
77
+ sz = math.sqrt(opts.get("s", _D["scatter_s"])) / 2
78
+ alpha = opts.get("alpha", _D["scatter_alpha"])
79
+ marker = opts.get("marker", "o")
80
+ out = []
81
+ for x, y in zip(a["xs"], a["ys"]):
82
+ px, py = xs_(x), ys_(y)
83
+ if not (math.isfinite(px) and math.isfinite(py)):
84
+ continue
85
+ out.append(_marker_at(marker, px, py, sz, col, alpha))
86
+ return "".join(out)
87
+
88
+
89
+ def _artist_bar(a, xs_, ys_, col):
90
+ out = []
91
+ opts = a["opts"]
92
+ bw = xs_.bandwidth
93
+ y0 = ys_(0)
94
+ alpha = opts.get("alpha", _D["bar_alpha"])
95
+ for c, v in zip(a["cats"], a["vals"]):
96
+ x = xs_(c)
97
+ y = ys_(v)
98
+ out.append(f'<rect x="{x:.2f}" y="{min(y0, y):.2f}" width="{bw:.2f}" '
99
+ f'height="{abs(y - y0):.2f}" fill="{col}" opacity="{alpha}"/>')
100
+ return "".join(out)
101
+
102
+
103
+ def _artist_hist(a, xs_, ys_, ih, col):
104
+ out = []
105
+ alpha = a["opts"].get("alpha", _D["hist_alpha"])
106
+ half_gap = _D["hist_gap"] / 2
107
+ for b in a["_bins"]:
108
+ x0 = xs_(b["x0"]) + half_gap
109
+ x1 = xs_(b["x1"]) - half_gap
110
+ w = max(0, x1 - x0)
111
+ y = ys_(b["count"])
112
+ h = ih - y
113
+ out.append(f'<rect x="{x0:.2f}" y="{y:.2f}" width="{w:.2f}" height="{h:.2f}" '
114
+ f'fill="{col}" opacity="{alpha}"/>')
115
+ return "".join(out)
116
+
117
+
118
+ def _artist_fill_between(a, xs_, ys_, col):
119
+ upper = [(xs_(x), ys_(y)) for x, y in zip(a["xs"], a["y1"])]
120
+ lower = [(xs_(x), ys_(y)) for x, y in zip(a["xs"], a["y2"])]
121
+ pts = upper + list(reversed(lower))
122
+ if not pts:
123
+ return ""
124
+ d = "M" + "L".join(f"{p[0]:.2f},{p[1]:.2f}" for p in pts) + "Z"
125
+ alpha = a["opts"].get("alpha", _D["fill_alpha"])
126
+ return f'<path d="{d}" fill="{col}" opacity="{alpha}"/>'
127
+
128
+
129
+ # ---------------------------------------------------------------------------
130
+ # Marker primitive — used by plot/scatter and the legend
131
+ # ---------------------------------------------------------------------------
132
+
133
+ def _marker_at(marker, x, y, size, col, alpha):
134
+ msw = _D["marker_stroke_width"]
135
+ if marker == "o":
136
+ return f'<circle cx="{x:.2f}" cy="{y:.2f}" r="{size}" fill="{col}" opacity="{alpha}"/>'
137
+ if marker == "s":
138
+ return (f'<rect x="{x - size:.2f}" y="{y - size:.2f}" width="{2 * size}" '
139
+ f'height="{2 * size}" fill="{col}" opacity="{alpha}"/>')
140
+ if marker == "^":
141
+ return (f'<path d="M{x:.2f},{y - size:.2f}L{x + size:.2f},{y + size:.2f}'
142
+ f'L{x - size:.2f},{y + size:.2f}Z" fill="{col}" opacity="{alpha}"/>')
143
+ if marker == "v":
144
+ return (f'<path d="M{x:.2f},{y + size:.2f}L{x + size:.2f},{y - size:.2f}'
145
+ f'L{x - size:.2f},{y - size:.2f}Z" fill="{col}" opacity="{alpha}"/>')
146
+ if marker == "x":
147
+ return (f'<path d="M{x - size:.2f},{y - size:.2f}L{x + size:.2f},{y + size:.2f}'
148
+ f'M{x - size:.2f},{y + size:.2f}L{x + size:.2f},{y - size:.2f}" '
149
+ f'stroke="{col}" stroke-width="{msw}" opacity="{alpha}"/>')
150
+ if marker == "+":
151
+ return (f'<path d="M{x - size:.2f},{y:.2f}L{x + size:.2f},{y:.2f}'
152
+ f'M{x:.2f},{y - size:.2f}L{x:.2f},{y + size:.2f}" '
153
+ f'stroke="{col}" stroke-width="{msw}" opacity="{alpha}"/>')
154
+ return ""
@@ -0,0 +1,150 @@
1
+ """Tidy-table-friendly facade over Figure.
2
+
3
+ c = chart(df, title="...", xlabel="...", legend=True, grid=True)
4
+ c.line(x="time", y="value", hue="series")
5
+ c # auto-renders in Jupyter
6
+
7
+ Each mark method (`line`, `scatter`, `bar`, `hist`, `fill_between`) accepts
8
+ either column-name kwargs against the bound table, or the existing array form
9
+ as positional args. `hue=<col>` splits into one call per unique value with
10
+ auto-labels and tab10 colors.
11
+
12
+ The bound table can be anything that supports `df[col_name]` returning an
13
+ iterable: pandas / polars DataFrames, dict-of-lists, dict-of-arrays. No
14
+ pandas dependency.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from .core import Figure
19
+ from .artists import _to_pylist
20
+
21
+
22
+ class Chart:
23
+ def __init__(self, data=None, *, width: int | None = None, height: int | None = None,
24
+ margin: dict | None = None,
25
+ title: str | None = None,
26
+ xlabel: str | None = None, ylabel: str | None = None,
27
+ xlim: tuple | None = None, ylim: tuple | None = None,
28
+ xscale: str | None = None, yscale: str | None = None,
29
+ legend: bool | None = None, grid: bool | None = None):
30
+ self._fig = Figure(width=width, height=height, margin=margin)
31
+ self._data = data
32
+ if title is not None: self._fig.title(title)
33
+ if xlabel is not None: self._fig.xlabel(xlabel)
34
+ if ylabel is not None: self._fig.ylabel(ylabel)
35
+ if xlim is not None: self._fig.xlim(*xlim)
36
+ if ylim is not None: self._fig.ylim(*ylim)
37
+ if xscale is not None: self._fig.xscale(xscale)
38
+ if yscale is not None: self._fig.yscale(yscale)
39
+ if legend is not None: self._fig.legend(legend)
40
+ if grid is not None: self._fig.grid(grid)
41
+
42
+ # ---------- mark methods ----------
43
+
44
+ def line(self, *args, x=None, y=None, hue=None, data=None, **opts):
45
+ if x is not None or y is not None:
46
+ self._tabular("line", "plot", data, x, y, hue, opts)
47
+ else:
48
+ self._fig.plot(*args, **opts)
49
+ return self
50
+
51
+ def scatter(self, *args, x=None, y=None, hue=None, data=None, **opts):
52
+ if x is not None or y is not None:
53
+ self._tabular("scatter", "scatter", data, x, y, hue, opts)
54
+ else:
55
+ self._fig.scatter(*args, **opts)
56
+ return self
57
+
58
+ def bar(self, *args, x=None, y=None, data=None, **opts):
59
+ if x is not None or y is not None:
60
+ df = self._resolve_data(data, "bar")
61
+ self._fig.bar(_to_pylist(df[x]), _to_pylist(df[y]), **opts)
62
+ else:
63
+ self._fig.bar(*args, **opts)
64
+ return self
65
+
66
+ def hist(self, *args, x=None, data=None, **opts):
67
+ if x is not None:
68
+ df = self._resolve_data(data, "hist")
69
+ self._fig.hist(_to_pylist(df[x]), **opts)
70
+ else:
71
+ self._fig.hist(*args, **opts)
72
+ return self
73
+
74
+ def fill_between(self, *args, x=None, y1=None, y2=None, data=None, **opts):
75
+ if x is not None or y1 is not None or y2 is not None:
76
+ df = self._resolve_data(data, "fill_between")
77
+ self._fig.fill_between(
78
+ _to_pylist(df[x]), _to_pylist(df[y1]), _to_pylist(df[y2]), **opts)
79
+ else:
80
+ self._fig.fill_between(*args, **opts)
81
+ return self
82
+
83
+ # ---------- helpers ----------
84
+
85
+ def _resolve_data(self, data, public_name):
86
+ df = data if data is not None else self._data
87
+ if df is None:
88
+ raise ValueError(
89
+ f"Chart.{public_name}() with column-name kwargs requires a bound table; "
90
+ f"pass data=<table> or use chart(<table>)."
91
+ )
92
+ return df
93
+
94
+ def _tabular(self, public_name, kind, data, x_col, y_col, hue, opts):
95
+ df = self._resolve_data(data, public_name)
96
+ method = getattr(self._fig, kind)
97
+ if hue is None:
98
+ method(_to_pylist(df[x_col]), _to_pylist(df[y_col]), **opts)
99
+ return
100
+ hue_vals = _to_pylist(df[hue])
101
+ xs_all = _to_pylist(df[x_col])
102
+ ys_all = _to_pylist(df[y_col])
103
+ seen: list = []
104
+ for v in hue_vals:
105
+ if v not in seen:
106
+ seen.append(v)
107
+ opts.pop("label", None) # hue overrides any user-provided label
108
+ for v in seen:
109
+ xs_g = [xs_all[i] for i, h in enumerate(hue_vals) if h == v]
110
+ ys_g = [ys_all[i] for i, h in enumerate(hue_vals) if h == v]
111
+ method(xs_g, ys_g, label=str(v), **opts)
112
+
113
+ # ---------- frame metadata (passthrough) ----------
114
+
115
+ def title(self, s): self._fig.title(s); return self
116
+ def xlabel(self, s): self._fig.xlabel(s); return self
117
+ def ylabel(self, s): self._fig.ylabel(s); return self
118
+ def xlim(self, a, b): self._fig.xlim(a, b); return self
119
+ def ylim(self, a, b): self._fig.ylim(a, b); return self
120
+ def xscale(self, s): self._fig.xscale(s); return self
121
+ def yscale(self, s): self._fig.yscale(s); return self
122
+ def grid(self, on=True): self._fig.grid(on); return self
123
+ def legend(self, on=True): self._fig.legend(on); return self
124
+
125
+ # ---------- render ----------
126
+
127
+ def to_svg(self) -> str:
128
+ return self._fig.to_svg()
129
+
130
+ def to_html(self, full_page: bool = False) -> str:
131
+ return self._fig.to_html(full_page=full_page)
132
+
133
+ def _repr_html_(self) -> str:
134
+ return self._fig.to_svg()
135
+
136
+ def show(self):
137
+ self._fig.show()
138
+
139
+ def save_svg(self, path):
140
+ self._fig.save_svg(path)
141
+ return self
142
+
143
+ def write_html(self, path):
144
+ self._fig.write_html(path)
145
+ return self
146
+
147
+
148
+ def chart(data=None, **opts) -> Chart:
149
+ """Construct a table-bound Chart. See `Chart` for keyword arguments."""
150
+ return Chart(data, **opts)