hastyplot 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.
- hastyplot-0.1.0/PKG-INFO +131 -0
- hastyplot-0.1.0/README.md +123 -0
- hastyplot-0.1.0/pyproject.toml +12 -0
- hastyplot-0.1.0/setup.cfg +4 -0
- hastyplot-0.1.0/src/hastyplot/qplot.py +251 -0
- hastyplot-0.1.0/src/hastyplot.egg-info/PKG-INFO +131 -0
- hastyplot-0.1.0/src/hastyplot.egg-info/SOURCES.txt +9 -0
- hastyplot-0.1.0/src/hastyplot.egg-info/dependency_links.txt +1 -0
- hastyplot-0.1.0/src/hastyplot.egg-info/requires.txt +1 -0
- hastyplot-0.1.0/src/hastyplot.egg-info/top_level.txt +1 -0
- hastyplot-0.1.0/tests/test_basic.py +20 -0
hastyplot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hastyplot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hasty plotting for Altair, inspired by ggplot2's qplot
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: altair>=5
|
|
8
|
+
|
|
9
|
+
# hastyplot
|
|
10
|
+
|
|
11
|
+
Hasty plotting for Altair, inspired by ggplot2's `qplot`.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from hastyplot import qplot
|
|
15
|
+
|
|
16
|
+
# Scatter plot
|
|
17
|
+
qplot(df, "x", "y")
|
|
18
|
+
|
|
19
|
+
# Histogram (x-only)
|
|
20
|
+
qplot(df, "x")
|
|
21
|
+
|
|
22
|
+
# With pipe
|
|
23
|
+
df.pipe(qplot, "x", "y", color="group")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add hastyplot
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Auto geom selection** -- x-only gives a histogram, x+y gives a scatter.
|
|
35
|
+
- **Geoms** -- `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
36
|
+
- **Aesthetics** -- `color`, `size`, `opacity`, `group`.
|
|
37
|
+
- **Smoothing** -- `smooth="loess"` overlays a trend line. Also supports `"linear"`, `"poly"`, `"log"`, `"exp"`, `"pow"`. Control loess wiggliness with `bandwidth`.
|
|
38
|
+
- **Faceting** -- `facet_col`, `facet_row` for grids. `facet_wrap` with `columns` for wrapped layouts.
|
|
39
|
+
- **Themes** -- `"default"`, `"clean"`, `"minimal"` (data journalism style).
|
|
40
|
+
- **Pipe-friendly** -- `data` is the first argument.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Scatter with color and smooth
|
|
46
|
+
qplot(cars, "Horsepower", "Miles_per_Gallon",
|
|
47
|
+
color="Origin", smooth="loess",
|
|
48
|
+
title="HP vs MPG", theme="minimal")
|
|
49
|
+
|
|
50
|
+
# Boxplot
|
|
51
|
+
qplot(cars, "Origin", "Miles_per_Gallon", geom="boxplot")
|
|
52
|
+
|
|
53
|
+
# Faceted scatter
|
|
54
|
+
qplot(cars, "Horsepower", "Miles_per_Gallon",
|
|
55
|
+
facet_wrap="Cylinders", columns=3, width=200, height=150)
|
|
56
|
+
|
|
57
|
+
# Lines grouped without color
|
|
58
|
+
qplot(stocks, "date", "price", geom="line", group="symbol")
|
|
59
|
+
|
|
60
|
+
# Histogram with custom bins
|
|
61
|
+
qplot(cars, "Horsepower", bins=20, theme="clean")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
<!-- API_DOCS_START -->
|
|
65
|
+
## `hastyplot.qplot.qplot`
|
|
66
|
+
|
|
67
|
+
> function
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
qplot(data, x: str, y: str | None = None, *, color: str | None = None, size: str | None = None, opacity: float | str = 0.7, group: str | None = None, geom: str = 'auto', smooth: str | None = None, bandwidth: float = 0.3, bins: int | None = None, facet_col: str | None = None, facet_row: str | None = None, facet_wrap: str | None = None, columns: int | None = None, width: int | None = None, height: int | None = None, title: str | None = None, subtitle: str | None = None, theme: str = 'default', actions: bool = False) -> altair.vegalite.v6.api.Chart
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Quick plot for Altair. Inspired by ggplot2's qplot.
|
|
74
|
+
|
|
75
|
+
`data` is the first argument so you can use `df.pipe(qplot, "x", "y")`.
|
|
76
|
+
|
|
77
|
+
**Data & axes**
|
|
78
|
+
- `data` — DataFrame to plot.
|
|
79
|
+
- `x` — column for the x-axis.
|
|
80
|
+
- `y` — column for the y-axis. Omit for a histogram.
|
|
81
|
+
|
|
82
|
+
**Aesthetics**
|
|
83
|
+
- `color` — column to map to color.
|
|
84
|
+
- `size` — column to map to point size.
|
|
85
|
+
- `opacity` — a fixed float (e.g. `0.5`) or a column name.
|
|
86
|
+
- `group` — column to group by *without* changing color.
|
|
87
|
+
Useful for separate lines per group in a uniform color.
|
|
88
|
+
|
|
89
|
+
**Geom & smoothing**
|
|
90
|
+
- `geom` — `"auto"` picks `"hist"` for x-only, `"scatter"` for x+y.
|
|
91
|
+
Options: `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
92
|
+
- `smooth` — overlay a trend line: `"loess"`, `"linear"`, `"poly"`,
|
|
93
|
+
`"log"`, `"exp"`, `"pow"`.
|
|
94
|
+
- `bandwidth` — loess bandwidth, 0 to 1 (default `0.3`). Lower = wigglier.
|
|
95
|
+
- `bins` — number of histogram bins. Omit for Altair's default.
|
|
96
|
+
|
|
97
|
+
**Faceting**
|
|
98
|
+
- `facet_col` / `facet_row` — column names for a facet grid.
|
|
99
|
+
- `facet_wrap` — single column, wraps into rows.
|
|
100
|
+
- `columns` — max columns before wrapping (default `3`).
|
|
101
|
+
|
|
102
|
+
**Layout & appearance**
|
|
103
|
+
- `width` / `height` — chart size in pixels (per panel when faceted).
|
|
104
|
+
- `title` / `subtitle` — chart title and subtitle.
|
|
105
|
+
- `theme` — `"default"`, `"clean"`, or `"minimal"`.
|
|
106
|
+
- `actions` — show the Vega-Lite export menu (default `False`).
|
|
107
|
+
|
|
108
|
+
| Name | Type | Default |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `data` | | |
|
|
111
|
+
| `x` | `str` | |
|
|
112
|
+
| `y` | `str | None` | `None` |
|
|
113
|
+
| `color` | `str | None` | `None` |
|
|
114
|
+
| `size` | `str | None` | `None` |
|
|
115
|
+
| `opacity` | `float | str` | `0.7` |
|
|
116
|
+
| `group` | `str | None` | `None` |
|
|
117
|
+
| `geom` | `str` | `'auto'` |
|
|
118
|
+
| `smooth` | `str | None` | `None` |
|
|
119
|
+
| `bandwidth` | `float` | `0.3` |
|
|
120
|
+
| `bins` | `int | None` | `None` |
|
|
121
|
+
| `facet_col` | `str | None` | `None` |
|
|
122
|
+
| `facet_row` | `str | None` | `None` |
|
|
123
|
+
| `facet_wrap` | `str | None` | `None` |
|
|
124
|
+
| `columns` | `int | None` | `None` |
|
|
125
|
+
| `width` | `int | None` | `None` |
|
|
126
|
+
| `height` | `int | None` | `None` |
|
|
127
|
+
| `title` | `str | None` | `None` |
|
|
128
|
+
| `subtitle` | `str | None` | `None` |
|
|
129
|
+
| `theme` | `str` | `'default'` |
|
|
130
|
+
| `actions` | `bool` | `False` |
|
|
131
|
+
<!-- API_DOCS_END -->
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# hastyplot
|
|
2
|
+
|
|
3
|
+
Hasty plotting for Altair, inspired by ggplot2's `qplot`.
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from hastyplot import qplot
|
|
7
|
+
|
|
8
|
+
# Scatter plot
|
|
9
|
+
qplot(df, "x", "y")
|
|
10
|
+
|
|
11
|
+
# Histogram (x-only)
|
|
12
|
+
qplot(df, "x")
|
|
13
|
+
|
|
14
|
+
# With pipe
|
|
15
|
+
df.pipe(qplot, "x", "y", color="group")
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv add hastyplot
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Auto geom selection** -- x-only gives a histogram, x+y gives a scatter.
|
|
27
|
+
- **Geoms** -- `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
28
|
+
- **Aesthetics** -- `color`, `size`, `opacity`, `group`.
|
|
29
|
+
- **Smoothing** -- `smooth="loess"` overlays a trend line. Also supports `"linear"`, `"poly"`, `"log"`, `"exp"`, `"pow"`. Control loess wiggliness with `bandwidth`.
|
|
30
|
+
- **Faceting** -- `facet_col`, `facet_row` for grids. `facet_wrap` with `columns` for wrapped layouts.
|
|
31
|
+
- **Themes** -- `"default"`, `"clean"`, `"minimal"` (data journalism style).
|
|
32
|
+
- **Pipe-friendly** -- `data` is the first argument.
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
# Scatter with color and smooth
|
|
38
|
+
qplot(cars, "Horsepower", "Miles_per_Gallon",
|
|
39
|
+
color="Origin", smooth="loess",
|
|
40
|
+
title="HP vs MPG", theme="minimal")
|
|
41
|
+
|
|
42
|
+
# Boxplot
|
|
43
|
+
qplot(cars, "Origin", "Miles_per_Gallon", geom="boxplot")
|
|
44
|
+
|
|
45
|
+
# Faceted scatter
|
|
46
|
+
qplot(cars, "Horsepower", "Miles_per_Gallon",
|
|
47
|
+
facet_wrap="Cylinders", columns=3, width=200, height=150)
|
|
48
|
+
|
|
49
|
+
# Lines grouped without color
|
|
50
|
+
qplot(stocks, "date", "price", geom="line", group="symbol")
|
|
51
|
+
|
|
52
|
+
# Histogram with custom bins
|
|
53
|
+
qplot(cars, "Horsepower", bins=20, theme="clean")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
<!-- API_DOCS_START -->
|
|
57
|
+
## `hastyplot.qplot.qplot`
|
|
58
|
+
|
|
59
|
+
> function
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
qplot(data, x: str, y: str | None = None, *, color: str | None = None, size: str | None = None, opacity: float | str = 0.7, group: str | None = None, geom: str = 'auto', smooth: str | None = None, bandwidth: float = 0.3, bins: int | None = None, facet_col: str | None = None, facet_row: str | None = None, facet_wrap: str | None = None, columns: int | None = None, width: int | None = None, height: int | None = None, title: str | None = None, subtitle: str | None = None, theme: str = 'default', actions: bool = False) -> altair.vegalite.v6.api.Chart
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Quick plot for Altair. Inspired by ggplot2's qplot.
|
|
66
|
+
|
|
67
|
+
`data` is the first argument so you can use `df.pipe(qplot, "x", "y")`.
|
|
68
|
+
|
|
69
|
+
**Data & axes**
|
|
70
|
+
- `data` — DataFrame to plot.
|
|
71
|
+
- `x` — column for the x-axis.
|
|
72
|
+
- `y` — column for the y-axis. Omit for a histogram.
|
|
73
|
+
|
|
74
|
+
**Aesthetics**
|
|
75
|
+
- `color` — column to map to color.
|
|
76
|
+
- `size` — column to map to point size.
|
|
77
|
+
- `opacity` — a fixed float (e.g. `0.5`) or a column name.
|
|
78
|
+
- `group` — column to group by *without* changing color.
|
|
79
|
+
Useful for separate lines per group in a uniform color.
|
|
80
|
+
|
|
81
|
+
**Geom & smoothing**
|
|
82
|
+
- `geom` — `"auto"` picks `"hist"` for x-only, `"scatter"` for x+y.
|
|
83
|
+
Options: `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
84
|
+
- `smooth` — overlay a trend line: `"loess"`, `"linear"`, `"poly"`,
|
|
85
|
+
`"log"`, `"exp"`, `"pow"`.
|
|
86
|
+
- `bandwidth` — loess bandwidth, 0 to 1 (default `0.3`). Lower = wigglier.
|
|
87
|
+
- `bins` — number of histogram bins. Omit for Altair's default.
|
|
88
|
+
|
|
89
|
+
**Faceting**
|
|
90
|
+
- `facet_col` / `facet_row` — column names for a facet grid.
|
|
91
|
+
- `facet_wrap` — single column, wraps into rows.
|
|
92
|
+
- `columns` — max columns before wrapping (default `3`).
|
|
93
|
+
|
|
94
|
+
**Layout & appearance**
|
|
95
|
+
- `width` / `height` — chart size in pixels (per panel when faceted).
|
|
96
|
+
- `title` / `subtitle` — chart title and subtitle.
|
|
97
|
+
- `theme` — `"default"`, `"clean"`, or `"minimal"`.
|
|
98
|
+
- `actions` — show the Vega-Lite export menu (default `False`).
|
|
99
|
+
|
|
100
|
+
| Name | Type | Default |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `data` | | |
|
|
103
|
+
| `x` | `str` | |
|
|
104
|
+
| `y` | `str | None` | `None` |
|
|
105
|
+
| `color` | `str | None` | `None` |
|
|
106
|
+
| `size` | `str | None` | `None` |
|
|
107
|
+
| `opacity` | `float | str` | `0.7` |
|
|
108
|
+
| `group` | `str | None` | `None` |
|
|
109
|
+
| `geom` | `str` | `'auto'` |
|
|
110
|
+
| `smooth` | `str | None` | `None` |
|
|
111
|
+
| `bandwidth` | `float` | `0.3` |
|
|
112
|
+
| `bins` | `int | None` | `None` |
|
|
113
|
+
| `facet_col` | `str | None` | `None` |
|
|
114
|
+
| `facet_row` | `str | None` | `None` |
|
|
115
|
+
| `facet_wrap` | `str | None` | `None` |
|
|
116
|
+
| `columns` | `int | None` | `None` |
|
|
117
|
+
| `width` | `int | None` | `None` |
|
|
118
|
+
| `height` | `int | None` | `None` |
|
|
119
|
+
| `title` | `str | None` | `None` |
|
|
120
|
+
| `subtitle` | `str | None` | `None` |
|
|
121
|
+
| `theme` | `str` | `'default'` |
|
|
122
|
+
| `actions` | `bool` | `False` |
|
|
123
|
+
<!-- API_DOCS_END -->
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hastyplot"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Hasty plotting for Altair, inspired by ggplot2's qplot"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = []
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = ["altair>=5"]
|
|
9
|
+
|
|
10
|
+
[tool.pytest.ini_options]
|
|
11
|
+
pythonpath = ["src"]
|
|
12
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
__all__ = ['qplot']
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
import altair as alt
|
|
5
|
+
|
|
6
|
+
def _clean_label(name):
|
|
7
|
+
"""Lowercase and replace -/_ with spaces."""
|
|
8
|
+
return name.replace("_", " ").replace("-", " ").lower()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def qplot(
|
|
12
|
+
data,
|
|
13
|
+
x: str,
|
|
14
|
+
y: str | None = None,
|
|
15
|
+
*,
|
|
16
|
+
# Aesthetics
|
|
17
|
+
color: str | None = None,
|
|
18
|
+
size: str | None = None,
|
|
19
|
+
opacity: float | str = 0.7,
|
|
20
|
+
group: str | None = None,
|
|
21
|
+
# Geom & smoothing
|
|
22
|
+
geom: str = "auto",
|
|
23
|
+
smooth: str | None = None,
|
|
24
|
+
bandwidth: float = 0.3,
|
|
25
|
+
bins: int | None = None,
|
|
26
|
+
# Faceting
|
|
27
|
+
facet_col: str | None = None,
|
|
28
|
+
facet_row: str | None = None,
|
|
29
|
+
facet_wrap: str | None = None,
|
|
30
|
+
columns: int | None = None,
|
|
31
|
+
# Layout & appearance
|
|
32
|
+
width: int | None = None,
|
|
33
|
+
height: int | None = None,
|
|
34
|
+
title: str | None = None,
|
|
35
|
+
subtitle: str | None = None,
|
|
36
|
+
theme: str = "default",
|
|
37
|
+
actions: bool = False,
|
|
38
|
+
) -> alt.Chart:
|
|
39
|
+
"""Quick plot for Altair. Inspired by ggplot2's qplot.
|
|
40
|
+
|
|
41
|
+
`data` is the first argument so you can use `df.pipe(qplot, "x", "y")`.
|
|
42
|
+
|
|
43
|
+
**Data & axes**
|
|
44
|
+
- `data` — DataFrame to plot.
|
|
45
|
+
- `x` — column for the x-axis.
|
|
46
|
+
- `y` — column for the y-axis. Omit for a histogram.
|
|
47
|
+
|
|
48
|
+
**Aesthetics**
|
|
49
|
+
- `color` — column to map to color.
|
|
50
|
+
- `size` — column to map to point size.
|
|
51
|
+
- `opacity` — a fixed float (e.g. `0.5`) or a column name.
|
|
52
|
+
- `group` — column to group by *without* changing color.
|
|
53
|
+
Useful for separate lines per group in a uniform color.
|
|
54
|
+
|
|
55
|
+
**Geom & smoothing**
|
|
56
|
+
- `geom` — `"auto"` picks `"hist"` for x-only, `"scatter"` for x+y.
|
|
57
|
+
Options: `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
58
|
+
- `smooth` — overlay a trend line: `"loess"`, `"linear"`, `"poly"`,
|
|
59
|
+
`"log"`, `"exp"`, `"pow"`.
|
|
60
|
+
- `bandwidth` — loess bandwidth, 0 to 1 (default `0.3`). Lower = wigglier.
|
|
61
|
+
- `bins` — number of histogram bins. Omit for Altair's default.
|
|
62
|
+
|
|
63
|
+
**Faceting**
|
|
64
|
+
- `facet_col` / `facet_row` — column names for a facet grid.
|
|
65
|
+
- `facet_wrap` — single column, wraps into rows.
|
|
66
|
+
- `columns` — max columns before wrapping (default `3`).
|
|
67
|
+
|
|
68
|
+
**Layout & appearance**
|
|
69
|
+
- `width` / `height` — chart size in pixels (per panel when faceted).
|
|
70
|
+
- `title` / `subtitle` — chart title and subtitle.
|
|
71
|
+
- `theme` — `"default"`, `"clean"`, or `"minimal"`.
|
|
72
|
+
- `actions` — show the Vega-Lite export menu (default `False`).
|
|
73
|
+
"""
|
|
74
|
+
chart = alt.Chart(data)
|
|
75
|
+
|
|
76
|
+
# Auto-select geom
|
|
77
|
+
if geom == "auto":
|
|
78
|
+
geom = "hist" if y is None else "scatter"
|
|
79
|
+
|
|
80
|
+
# Clean axis labels
|
|
81
|
+
x_enc = alt.X(x, bin=alt.Bin(maxbins=bins) if bins is not None else True, title=_clean_label(x)) if geom == "hist" else alt.X(x, title=_clean_label(x))
|
|
82
|
+
y_enc = alt.Y("count()", title="count") if geom == "hist" else (alt.Y(y, title=_clean_label(y)) if y else None)
|
|
83
|
+
|
|
84
|
+
# Build the mark + encoding
|
|
85
|
+
if geom == "scatter":
|
|
86
|
+
chart = chart.mark_point(filled=True, opacity=opacity if isinstance(opacity, (int, float)) else 0.7).encode(x=x_enc, y=y_enc)
|
|
87
|
+
elif geom == "circle":
|
|
88
|
+
chart = chart.mark_circle(opacity=opacity if isinstance(opacity, (int, float)) else 0.7).encode(x=x_enc, y=y_enc)
|
|
89
|
+
elif geom == "hist":
|
|
90
|
+
chart = chart.mark_bar().encode(x=x_enc, y=y_enc)
|
|
91
|
+
elif geom == "line":
|
|
92
|
+
chart = chart.mark_line(strokeWidth=2).encode(x=x_enc, y=y_enc)
|
|
93
|
+
elif geom == "bar":
|
|
94
|
+
chart = chart.mark_bar().encode(x=x_enc, y=y_enc)
|
|
95
|
+
elif geom == "boxplot":
|
|
96
|
+
chart = chart.mark_boxplot().encode(x=x_enc, y=y_enc)
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError(f"Unknown geom: {geom}")
|
|
99
|
+
|
|
100
|
+
# Group: splits data by a column (separate marks) but no color distinction
|
|
101
|
+
if group is not None:
|
|
102
|
+
chart = chart.encode(detail=alt.Detail(group))
|
|
103
|
+
|
|
104
|
+
# Optional encodings
|
|
105
|
+
if color is not None:
|
|
106
|
+
chart = chart.encode(color=alt.Color(color, title=_clean_label(color)))
|
|
107
|
+
if size is not None:
|
|
108
|
+
chart = chart.encode(size=alt.Size(size, title=_clean_label(size)))
|
|
109
|
+
if isinstance(opacity, str):
|
|
110
|
+
chart = chart.encode(opacity=alt.Opacity(opacity, title=_clean_label(opacity)))
|
|
111
|
+
|
|
112
|
+
# Smooth: overlay a trend line
|
|
113
|
+
if smooth is not None and y is not None:
|
|
114
|
+
smooth_base = alt.Chart(data)
|
|
115
|
+
groupby = [color] if color is not None else []
|
|
116
|
+
if smooth == "loess":
|
|
117
|
+
trend = (
|
|
118
|
+
smooth_base
|
|
119
|
+
.transform_loess(x, y, groupby=groupby, bandwidth=bandwidth)
|
|
120
|
+
.mark_line(strokeWidth=3, opacity=0.9)
|
|
121
|
+
.encode(x=alt.X(x, title=_clean_label(x)), y=alt.Y(y, title=_clean_label(y)))
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
trend = (
|
|
125
|
+
smooth_base
|
|
126
|
+
.transform_regression(x, y, method=smooth, groupby=groupby)
|
|
127
|
+
.mark_line(strokeWidth=3, opacity=0.9)
|
|
128
|
+
.encode(x=alt.X(x, title=_clean_label(x)), y=alt.Y(y, title=_clean_label(y)))
|
|
129
|
+
)
|
|
130
|
+
if color is not None:
|
|
131
|
+
trend = trend.encode(color=alt.Color(color, title=_clean_label(color)))
|
|
132
|
+
chart = chart + trend
|
|
133
|
+
|
|
134
|
+
# Width and height (applied per facet panel or to whole chart)
|
|
135
|
+
if width is not None or height is not None:
|
|
136
|
+
props = {}
|
|
137
|
+
if width is not None:
|
|
138
|
+
props["width"] = width
|
|
139
|
+
if height is not None:
|
|
140
|
+
props["height"] = height
|
|
141
|
+
chart = chart.properties(**props)
|
|
142
|
+
|
|
143
|
+
# Faceting
|
|
144
|
+
if facet_wrap is not None:
|
|
145
|
+
chart = chart.facet(
|
|
146
|
+
alt.Facet(facet_wrap, title=_clean_label(facet_wrap)),
|
|
147
|
+
columns=columns or 3,
|
|
148
|
+
)
|
|
149
|
+
elif facet_col is not None and facet_row is not None:
|
|
150
|
+
chart = chart.facet(column=alt.Column(facet_col, title=_clean_label(facet_col)),
|
|
151
|
+
row=alt.Row(facet_row, title=_clean_label(facet_row)))
|
|
152
|
+
elif facet_col is not None:
|
|
153
|
+
chart = chart.facet(column=alt.Column(facet_col, title=_clean_label(facet_col)))
|
|
154
|
+
elif facet_row is not None:
|
|
155
|
+
chart = chart.facet(row=alt.Row(facet_row, title=_clean_label(facet_row)))
|
|
156
|
+
|
|
157
|
+
# Title + subtitle
|
|
158
|
+
if title is not None:
|
|
159
|
+
title_obj = alt.TitleParams(text=title, subtitle=subtitle or "")
|
|
160
|
+
chart = chart.properties(title=title_obj)
|
|
161
|
+
elif subtitle is not None:
|
|
162
|
+
title_obj = alt.TitleParams(text="", subtitle=subtitle)
|
|
163
|
+
chart = chart.properties(title=title_obj)
|
|
164
|
+
|
|
165
|
+
# Embed options to control action menu
|
|
166
|
+
chart = chart.properties(
|
|
167
|
+
usermeta={"embedOptions": {"actions": actions}}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Apply theme
|
|
171
|
+
chart = _apply_theme(chart, theme)
|
|
172
|
+
|
|
173
|
+
return chart
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
_TITLE_COMMON = dict(anchor="start", offset=10, dx=40)
|
|
177
|
+
|
|
178
|
+
def _apply_theme(chart, theme):
|
|
179
|
+
if theme == "default":
|
|
180
|
+
return chart
|
|
181
|
+
elif theme == "clean":
|
|
182
|
+
return (
|
|
183
|
+
chart
|
|
184
|
+
.configure_axis(
|
|
185
|
+
grid=False,
|
|
186
|
+
domainColor="#333",
|
|
187
|
+
tickColor="#333",
|
|
188
|
+
labelFontSize=11,
|
|
189
|
+
titleFontSize=12,
|
|
190
|
+
titleFont="system-ui",
|
|
191
|
+
labelFont="system-ui",
|
|
192
|
+
labelColor="#555",
|
|
193
|
+
titleColor="#333",
|
|
194
|
+
)
|
|
195
|
+
.configure_view(strokeWidth=0)
|
|
196
|
+
.configure_title(
|
|
197
|
+
fontSize=18, fontWeight="bold",
|
|
198
|
+
font="system-ui", subtitleFont="system-ui",
|
|
199
|
+
subtitleFontSize=13, subtitleColor="#666",
|
|
200
|
+
color="#1a1a1a",
|
|
201
|
+
**_TITLE_COMMON,
|
|
202
|
+
)
|
|
203
|
+
.configure_legend(
|
|
204
|
+
labelFont="system-ui", titleFont="system-ui",
|
|
205
|
+
labelFontSize=11, titleFontSize=11,
|
|
206
|
+
symbolSize=80,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
elif theme == "minimal":
|
|
210
|
+
return (
|
|
211
|
+
chart
|
|
212
|
+
.configure_axis(
|
|
213
|
+
grid=True,
|
|
214
|
+
gridColor="#e5e5e5",
|
|
215
|
+
gridWidth=0.5,
|
|
216
|
+
domain=False,
|
|
217
|
+
tickSize=0,
|
|
218
|
+
labelFontSize=11,
|
|
219
|
+
titleFontSize=11,
|
|
220
|
+
titleFont="'Libre Franklin', 'Helvetica Neue', sans-serif",
|
|
221
|
+
labelFont="'Libre Franklin', 'Helvetica Neue', sans-serif",
|
|
222
|
+
labelColor="#666",
|
|
223
|
+
titleColor="#666",
|
|
224
|
+
titleFontWeight="normal",
|
|
225
|
+
labelPadding=8,
|
|
226
|
+
)
|
|
227
|
+
.configure_view(strokeWidth=0)
|
|
228
|
+
.configure_title(
|
|
229
|
+
fontSize=20, fontWeight=700,
|
|
230
|
+
font="'Libre Franklin', 'Helvetica Neue', sans-serif",
|
|
231
|
+
color="#1a1a1a",
|
|
232
|
+
subtitleFont="'Libre Franklin', 'Helvetica Neue', sans-serif",
|
|
233
|
+
subtitleFontSize=14, subtitleColor="#888",
|
|
234
|
+
subtitleFontWeight="normal",
|
|
235
|
+
subtitlePadding=4,
|
|
236
|
+
**_TITLE_COMMON,
|
|
237
|
+
)
|
|
238
|
+
.configure_legend(
|
|
239
|
+
labelFont="'Libre Franklin', 'Helvetica Neue', sans-serif",
|
|
240
|
+
titleFont="'Libre Franklin', 'Helvetica Neue', sans-serif",
|
|
241
|
+
labelFontSize=11, titleFontSize=11,
|
|
242
|
+
titleFontWeight="normal",
|
|
243
|
+
symbolSize=80, orient="bottom",
|
|
244
|
+
)
|
|
245
|
+
.configure_range(
|
|
246
|
+
category=["#e15759", "#4e79a7", "#f28e2b", "#76b7b2", "#59a14f",
|
|
247
|
+
"#edc948", "#b07aa1", "#ff9da7", "#9c755f", "#bab0ac"]
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
raise ValueError(f"Unknown theme: {theme}")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hastyplot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Hasty plotting for Altair, inspired by ggplot2's qplot
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: altair>=5
|
|
8
|
+
|
|
9
|
+
# hastyplot
|
|
10
|
+
|
|
11
|
+
Hasty plotting for Altair, inspired by ggplot2's `qplot`.
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from hastyplot import qplot
|
|
15
|
+
|
|
16
|
+
# Scatter plot
|
|
17
|
+
qplot(df, "x", "y")
|
|
18
|
+
|
|
19
|
+
# Histogram (x-only)
|
|
20
|
+
qplot(df, "x")
|
|
21
|
+
|
|
22
|
+
# With pipe
|
|
23
|
+
df.pipe(qplot, "x", "y", color="group")
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
uv add hastyplot
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
|
|
34
|
+
- **Auto geom selection** -- x-only gives a histogram, x+y gives a scatter.
|
|
35
|
+
- **Geoms** -- `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
36
|
+
- **Aesthetics** -- `color`, `size`, `opacity`, `group`.
|
|
37
|
+
- **Smoothing** -- `smooth="loess"` overlays a trend line. Also supports `"linear"`, `"poly"`, `"log"`, `"exp"`, `"pow"`. Control loess wiggliness with `bandwidth`.
|
|
38
|
+
- **Faceting** -- `facet_col`, `facet_row` for grids. `facet_wrap` with `columns` for wrapped layouts.
|
|
39
|
+
- **Themes** -- `"default"`, `"clean"`, `"minimal"` (data journalism style).
|
|
40
|
+
- **Pipe-friendly** -- `data` is the first argument.
|
|
41
|
+
|
|
42
|
+
## Examples
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# Scatter with color and smooth
|
|
46
|
+
qplot(cars, "Horsepower", "Miles_per_Gallon",
|
|
47
|
+
color="Origin", smooth="loess",
|
|
48
|
+
title="HP vs MPG", theme="minimal")
|
|
49
|
+
|
|
50
|
+
# Boxplot
|
|
51
|
+
qplot(cars, "Origin", "Miles_per_Gallon", geom="boxplot")
|
|
52
|
+
|
|
53
|
+
# Faceted scatter
|
|
54
|
+
qplot(cars, "Horsepower", "Miles_per_Gallon",
|
|
55
|
+
facet_wrap="Cylinders", columns=3, width=200, height=150)
|
|
56
|
+
|
|
57
|
+
# Lines grouped without color
|
|
58
|
+
qplot(stocks, "date", "price", geom="line", group="symbol")
|
|
59
|
+
|
|
60
|
+
# Histogram with custom bins
|
|
61
|
+
qplot(cars, "Horsepower", bins=20, theme="clean")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
<!-- API_DOCS_START -->
|
|
65
|
+
## `hastyplot.qplot.qplot`
|
|
66
|
+
|
|
67
|
+
> function
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
qplot(data, x: str, y: str | None = None, *, color: str | None = None, size: str | None = None, opacity: float | str = 0.7, group: str | None = None, geom: str = 'auto', smooth: str | None = None, bandwidth: float = 0.3, bins: int | None = None, facet_col: str | None = None, facet_row: str | None = None, facet_wrap: str | None = None, columns: int | None = None, width: int | None = None, height: int | None = None, title: str | None = None, subtitle: str | None = None, theme: str = 'default', actions: bool = False) -> altair.vegalite.v6.api.Chart
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Quick plot for Altair. Inspired by ggplot2's qplot.
|
|
74
|
+
|
|
75
|
+
`data` is the first argument so you can use `df.pipe(qplot, "x", "y")`.
|
|
76
|
+
|
|
77
|
+
**Data & axes**
|
|
78
|
+
- `data` — DataFrame to plot.
|
|
79
|
+
- `x` — column for the x-axis.
|
|
80
|
+
- `y` — column for the y-axis. Omit for a histogram.
|
|
81
|
+
|
|
82
|
+
**Aesthetics**
|
|
83
|
+
- `color` — column to map to color.
|
|
84
|
+
- `size` — column to map to point size.
|
|
85
|
+
- `opacity` — a fixed float (e.g. `0.5`) or a column name.
|
|
86
|
+
- `group` — column to group by *without* changing color.
|
|
87
|
+
Useful for separate lines per group in a uniform color.
|
|
88
|
+
|
|
89
|
+
**Geom & smoothing**
|
|
90
|
+
- `geom` — `"auto"` picks `"hist"` for x-only, `"scatter"` for x+y.
|
|
91
|
+
Options: `"scatter"`, `"circle"`, `"line"`, `"bar"`, `"boxplot"`, `"hist"`.
|
|
92
|
+
- `smooth` — overlay a trend line: `"loess"`, `"linear"`, `"poly"`,
|
|
93
|
+
`"log"`, `"exp"`, `"pow"`.
|
|
94
|
+
- `bandwidth` — loess bandwidth, 0 to 1 (default `0.3`). Lower = wigglier.
|
|
95
|
+
- `bins` — number of histogram bins. Omit for Altair's default.
|
|
96
|
+
|
|
97
|
+
**Faceting**
|
|
98
|
+
- `facet_col` / `facet_row` — column names for a facet grid.
|
|
99
|
+
- `facet_wrap` — single column, wraps into rows.
|
|
100
|
+
- `columns` — max columns before wrapping (default `3`).
|
|
101
|
+
|
|
102
|
+
**Layout & appearance**
|
|
103
|
+
- `width` / `height` — chart size in pixels (per panel when faceted).
|
|
104
|
+
- `title` / `subtitle` — chart title and subtitle.
|
|
105
|
+
- `theme` — `"default"`, `"clean"`, or `"minimal"`.
|
|
106
|
+
- `actions` — show the Vega-Lite export menu (default `False`).
|
|
107
|
+
|
|
108
|
+
| Name | Type | Default |
|
|
109
|
+
| --- | --- | --- |
|
|
110
|
+
| `data` | | |
|
|
111
|
+
| `x` | `str` | |
|
|
112
|
+
| `y` | `str | None` | `None` |
|
|
113
|
+
| `color` | `str | None` | `None` |
|
|
114
|
+
| `size` | `str | None` | `None` |
|
|
115
|
+
| `opacity` | `float | str` | `0.7` |
|
|
116
|
+
| `group` | `str | None` | `None` |
|
|
117
|
+
| `geom` | `str` | `'auto'` |
|
|
118
|
+
| `smooth` | `str | None` | `None` |
|
|
119
|
+
| `bandwidth` | `float` | `0.3` |
|
|
120
|
+
| `bins` | `int | None` | `None` |
|
|
121
|
+
| `facet_col` | `str | None` | `None` |
|
|
122
|
+
| `facet_row` | `str | None` | `None` |
|
|
123
|
+
| `facet_wrap` | `str | None` | `None` |
|
|
124
|
+
| `columns` | `int | None` | `None` |
|
|
125
|
+
| `width` | `int | None` | `None` |
|
|
126
|
+
| `height` | `int | None` | `None` |
|
|
127
|
+
| `title` | `str | None` | `None` |
|
|
128
|
+
| `subtitle` | `str | None` | `None` |
|
|
129
|
+
| `theme` | `str` | `'default'` |
|
|
130
|
+
| `actions` | `bool` | `False` |
|
|
131
|
+
<!-- API_DOCS_END -->
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/hastyplot/qplot.py
|
|
4
|
+
src/hastyplot.egg-info/PKG-INFO
|
|
5
|
+
src/hastyplot.egg-info/SOURCES.txt
|
|
6
|
+
src/hastyplot.egg-info/dependency_links.txt
|
|
7
|
+
src/hastyplot.egg-info/requires.txt
|
|
8
|
+
src/hastyplot.egg-info/top_level.txt
|
|
9
|
+
tests/test_basic.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
altair>=5
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hastyplot
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from hastyplot import qplot
|
|
2
|
+
import altair as alt
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_qplot_importable():
|
|
6
|
+
assert callable(qplot)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_scatter():
|
|
10
|
+
import pandas as pd
|
|
11
|
+
df = pd.DataFrame({"x": [1, 2, 3], "y": [4, 5, 6]})
|
|
12
|
+
chart = qplot(df, "x", "y")
|
|
13
|
+
assert isinstance(chart, alt.Chart)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_histogram():
|
|
17
|
+
import pandas as pd
|
|
18
|
+
df = pd.DataFrame({"x": [1, 2, 3, 4, 5]})
|
|
19
|
+
chart = qplot(df, "x")
|
|
20
|
+
assert isinstance(chart, alt.Chart)
|