ggstyle
A date axis for matplotlib that is easy to use and easy to manipulate.
v0.1 is the date axis plus themes. No palettes module and no line() yet — those
remain future additions once the axis ergonomics have real usage behind them.
This is the first public release. The date-axis behavior is tested, but the project is still young and follows semantic versioning. See the known limits before using collapsed mode in production.
Why
Most of the pain in Python time-series plotting is not the grammar, it's the axis: ticks in
the wrong places, labels rotated to hide the fact that there are too many of them, weekend
gaps shredding an intraday chart, and annotation code that quietly puts your vertical line
three days off. ggstyle fixes the axis first.
Install
pip install ggstyle
For development from a clone:
pip install -e ".[dev]"
Use
It adopts any Axes, including plots it never made:
import matplotlib.pyplot as plt
import ggstyle as gs
gs.use_theme() # "minimal" is the default
fig, ax = plt.subplots()
ax.plot(df["date"], df["close"]) # plain matplotlib, seaborn, or df.plot()
gs.dates(ax).ticks("quarterly").fmt("month-year").zoom("2020", "2022")
Configuration and drawing methods return the handle, so calls chain.
Axis semantics are also available as structured data rather than only rendered output:
summary = gs.dates(ax).summary()
caption = gs.dates(ax).caption(add=True)
Ticks — where they go
.ticks("monthly") # daily | weekly | monthly | quarterly | yearly
.ticks("month-end") # anchored: month-start, quarter-end, year-start, ...
.ticks(every="3M") # any offset alias; legacy M/Q/Y/H accepted
.ticks(n=6) # about six ticks, snapped to a natural cadence
.ticks(at=["2020-01-01", "2021-07-01"])
.ticks(major="yearly", minor="monthly")
Anchoring is not cosmetic: month-start vs. month-end is the difference between labels that line up with your observations and labels that float between them.
Labels — what they say
.fmt("concise") # default: year shown once, not on every label
.fmt("month-year") # Jun 2020
.fmt("quarter") # Q2 2020
.fmt("year") / .fmt("month") / .fmt("day") / .fmt("iso") / .fmt("time")
.fmt("%b '%y") # any strftime string
.fmt(lambda d: f"week {d.isocalendar().week}")
Changing the format never moves a tick, and changing the cadence never changes the format. That orthogonality is a test, not an aspiration.
Range
Partial strings expand to whole periods, pandas-style:
.zoom("2020", "2022") # three complete years
.zoom("2020-03", None) # open-ended
.zoom(last="6M") # trailing window from the last observation, not from today
.zoom(ytd=True)
.pad(left="1M", right="1M")
Gaps
.collapse() # unobserved dates get no space
.expand() # true datetime axis, gaps restored
Collapsed mode is defined by the dates present in your data, not by a holiday calendar. Anything not observed is not allocated space. That is correct for any market or region and needs no extra dependency. With several series, the axis uses the union of observed dates.
Annotation in date space
Every one of these is correct in both modes — that is the whole point of the handle:
.loc("2020-03-23") # -> axis position; the escape-hatch primitive
.vline("2020-03-23", label="trough")
.span("2020-02-19", "2020-03-23", label="drawdown")
.spans(events_df, start="begin", end="end", label="name")
.grid("yearly") # gridline cadence, independent of ticks
In collapsed mode a date that falls inside a gap (a Sunday, a holiday) is placed by linear
interpolation between its neighbours. loc(date, snap=True) rounds to the nearest
observation instead; loc(date, strict=True) raises if the date was never observed.
Escape hatch
.loc() is the primitive that keeps raw matplotlib correct:
handle = gs.dates(ax).collapse()
ax.axvline(handle.loc("2020-03-23")) # lands in the right place
ax.set_xlim(handle.loc("2020-01"), handle.loc("2021-01"))
Themes
Two ship. minimal is the default; grey is the ggplot2 theme_grey analogue.
gs.use_theme() # minimal, process-wide
gs.use_theme("grey") # "gray" also accepted
with gs.theme("grey"): # scoped; restores every rcParam on exit
...
plt.style.use(gs.stylesheet()) # the .mplstyle on its own, no ggstyle import needed
Both spell out the same type scale, colour cycle, and layout, so switching changes the panel surface and nothing else — the same separation ggplot2 makes. The colour cycle is Okabe–Ito-derived and capped at eight; past eight, direct labelling or faceting is the right answer, not a ninth colour.
Importing ggstyle never mutates rcParams. Theming is always something you ask for.
Almost all of it is plain rcParams in a .mplstyle file, including spine removal
(axes.spines.left: False), which an earlier draft of the design wrongly assumed needed
Python.
Data frames
pandas and polars both work, as do pyarrow arrays, numpy datetime64, and plain lists:
gs.dates(ax, data=frame["date"]) # pandas Series, polars Series, or Index
Polars is detected by module name rather than imported, so installing ggstyle never
pulls it in and pandas-only users pay nothing for the support. Timezone-aware input from
either library is converted to UTC instants for positioning; display timezones stay a
separate concern handled by .tz().
Two things are deliberately not guessed: a whole DataFrame passed where a column was meant, and a string column that might be dates. Both raise.
Missing values in explicit date data also raise unless exclusion is requested with
missing="drop". The number excluded remains available through .summary() and in
generated captions.
Multiple panels
Synchronize comparable axes with a common observation registry and limits:
handles = gs.sync_dates(axes, mode="collapse", limits="union")
This prevents the same date from receiving different ordinal positions in independently collapsed panels.
Design rules
- The date axis is a standalone object, not a side effect of plotting.
- Importing the package is inert; theming is opt-in.
- Placement, labels, gridline cadence, and range are four independent knobs.
- Fail loudly: a non-date axis raises, and mixed tz-aware/naive input raises rather than guessing UTC.
- Never resample or interpolate the data silently.
- Never rotate tick labels by default. Rotation is a symptom of bad tick selection.
Known limits in v0.1
- Collapsed mode remaps
Line2Dartists only. Collections (fill_between,scatter) are not yet remapped; annotate through the handle instead. - Native
ax.axvline(timestamp)is still wrong in collapsed mode — go through.loc(). A registered matplotlib scale would remove that caveat and is the v0.2 candidate. .tz()assumes naive data is UTC when converting for display.- No palettes module yet: the colour cycle lives in the stylesheets.
Tests
python -m pytest -q
ruff check .
mypy src
See CONTRIBUTING.md for the complete development workflow and SECURITY.md for vulnerability reporting.
The structured documentation follows the same user-guide, API-reference, pitfalls, and release-note separation used by statsmodels. Build it locally with:
pip install -e ".[docs]"
python -m sphinx -W --keep-going -b html docs/source docs/build/html
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ggstyle-0.1.1.tar.gz.
File metadata
- Download URL: ggstyle-0.1.1.tar.gz
- Upload date:
- Size: 521.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8031d4fd71fcdcd33d2ce29ee75070dda003d832bdd738866a3808974798e71e
|
|
| MD5 |
ca792b2939bf307c438d940cfe764c62
|
|
| BLAKE2b-256 |
8bf93c8b0dbd7ece3dac269cc621f3c5a7d8fa4d885ca9f0562394c2ea583505
|
Provenance
The following attestation bundles were made for ggstyle-0.1.1.tar.gz:
Publisher:
publish.yml on joshuamyers22/ggstyle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ggstyle-0.1.1.tar.gz -
Subject digest:
8031d4fd71fcdcd33d2ce29ee75070dda003d832bdd738866a3808974798e71e - Sigstore transparency entry: 2773711921
- Sigstore integration time:
-
Permalink:
joshuamyers22/ggstyle@6d3c5c2ebd4a219c57fbd293cbd33802c9c96de9 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/joshuamyers22
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6d3c5c2ebd4a219c57fbd293cbd33802c9c96de9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file ggstyle-0.1.1-py3-none-any.whl.
File metadata
- Download URL: ggstyle-0.1.1-py3-none-any.whl
- Upload date:
- Size: 40.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d24cd2c88b6e4a2eaaf3638fd81a4fe38c1f804da323b03a171538633143de28
|
|
| MD5 |
6a8c41be5ed51c4c7c71b9d8be1c98c8
|
|
| BLAKE2b-256 |
9ccea0290a7e0002b620245cd4c8b6d67d7b406134d8f9536d6dd53d72b995c6
|
Provenance
The following attestation bundles were made for ggstyle-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on joshuamyers22/ggstyle
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ggstyle-0.1.1-py3-none-any.whl -
Subject digest:
d24cd2c88b6e4a2eaaf3638fd81a4fe38c1f804da323b03a171538633143de28 - Sigstore transparency entry: 2773711957
- Sigstore integration time:
-
Permalink:
joshuamyers22/ggstyle@6d3c5c2ebd4a219c57fbd293cbd33802c9c96de9 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/joshuamyers22
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@6d3c5c2ebd4a219c57fbd293cbd33802c9c96de9 -
Trigger Event:
push
-
Statement type: