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 +21 -0
- plotlet-0.1.0/PKG-INFO +131 -0
- plotlet-0.1.0/README.md +102 -0
- plotlet-0.1.0/pyproject.toml +56 -0
- plotlet-0.1.0/setup.cfg +4 -0
- plotlet-0.1.0/src/plotlet/__init__.py +23 -0
- plotlet-0.1.0/src/plotlet/_spec.py +21 -0
- plotlet-0.1.0/src/plotlet/artists.py +154 -0
- plotlet-0.1.0/src/plotlet/chart.py +150 -0
- plotlet-0.1.0/src/plotlet/colors.py +20 -0
- plotlet-0.1.0/src/plotlet/core.py +336 -0
- plotlet-0.1.0/src/plotlet/font.py +63 -0
- plotlet-0.1.0/src/plotlet/fonts/DejaVuSans.ttf +0 -0
- plotlet-0.1.0/src/plotlet/scales.py +116 -0
- plotlet-0.1.0/src/plotlet/spec.json +75 -0
- plotlet-0.1.0/src/plotlet.egg-info/PKG-INFO +131 -0
- plotlet-0.1.0/src/plotlet.egg-info/SOURCES.txt +20 -0
- plotlet-0.1.0/src/plotlet.egg-info/dependency_links.txt +1 -0
- plotlet-0.1.0/src/plotlet.egg-info/requires.txt +5 -0
- plotlet-0.1.0/src/plotlet.egg-info/top_level.txt +1 -0
- plotlet-0.1.0/tests/test_chart.py +109 -0
- plotlet-0.1.0/tests/test_old.py +112 -0
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
|
plotlet-0.1.0/README.md
ADDED
|
@@ -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"]
|
plotlet-0.1.0/setup.cfg
ADDED
|
@@ -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)
|