ggstyle 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.
- ggstyle-0.1.0/.gitignore +16 -0
- ggstyle-0.1.0/CHANGELOG.md +23 -0
- ggstyle-0.1.0/CONTRIBUTING.md +44 -0
- ggstyle-0.1.0/LICENSE +21 -0
- ggstyle-0.1.0/PKG-INFO +268 -0
- ggstyle-0.1.0/README.md +225 -0
- ggstyle-0.1.0/SECURITY.md +9 -0
- ggstyle-0.1.0/docs/source/api.rst +33 -0
- ggstyle-0.1.0/docs/source/conf.py +39 -0
- ggstyle-0.1.0/docs/source/gettingstarted.rst +49 -0
- ggstyle-0.1.0/docs/source/index.rst +29 -0
- ggstyle-0.1.0/docs/source/pitfalls.rst +54 -0
- ggstyle-0.1.0/docs/source/principles.rst +49 -0
- ggstyle-0.1.0/docs/source/release.rst +11 -0
- ggstyle-0.1.0/docs/source/user-guide.rst +100 -0
- ggstyle-0.1.0/examples/smoke.png +0 -0
- ggstyle-0.1.0/examples/smoke.py +61 -0
- ggstyle-0.1.0/examples/theme-grey.png +0 -0
- ggstyle-0.1.0/examples/theme-minimal.png +0 -0
- ggstyle-0.1.0/examples/themes.py +48 -0
- ggstyle-0.1.0/pyproject.toml +88 -0
- ggstyle-0.1.0/src/ggstyle/__init__.py +35 -0
- ggstyle-0.1.0/src/ggstyle/_cadence.py +298 -0
- ggstyle-0.1.0/src/ggstyle/_formats.py +118 -0
- ggstyle-0.1.0/src/ggstyle/_frames.py +171 -0
- ggstyle-0.1.0/src/ggstyle/_parse.py +138 -0
- ggstyle-0.1.0/src/ggstyle/dates.py +1355 -0
- ggstyle-0.1.0/src/ggstyle/py.typed +1 -0
- ggstyle-0.1.0/src/ggstyle/theme.py +191 -0
- ggstyle-0.1.0/src/ggstyle/themes/ggstyle-grey.mplstyle +69 -0
- ggstyle-0.1.0/src/ggstyle/themes/ggstyle-minimal.mplstyle +71 -0
- ggstyle-0.1.0/tests/test_dates.py +542 -0
- ggstyle-0.1.0/tests/test_frames_themes.py +200 -0
- ggstyle-0.1.0/tests/test_parse_cadence.py +127 -0
- ggstyle-0.1.0/tools/validate_docstrings.py +60 -0
ggstyle-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This project follows [Semantic Versioning](https://semver.org/).
|
|
4
|
+
|
|
5
|
+
## 0.1.0 - 2026-08-19
|
|
6
|
+
|
|
7
|
+
Initial public release.
|
|
8
|
+
|
|
9
|
+
- Add a date-axis handle that adopts existing matplotlib axes.
|
|
10
|
+
- Add independent tick cadence, label formatting, range, and grid controls.
|
|
11
|
+
- Add observation-based gap collapsing and date-space annotations.
|
|
12
|
+
- Add pandas, polars, pyarrow, NumPy, and plain-sequence date extraction.
|
|
13
|
+
- Add opt-in minimal and grey themes without import-time global state changes.
|
|
14
|
+
- Add NumPy-style API documentation and a warning-free Sphinx user guide modeled on the
|
|
15
|
+
documentation structure used by statsmodels.
|
|
16
|
+
- Replace loosely typed annotation dictionaries with explicit internal state objects and
|
|
17
|
+
broaden automated clean-code checks.
|
|
18
|
+
- Add structured ``AxisSummary`` metadata and captions generated from the same source.
|
|
19
|
+
- Add explicit ``missing="raise"`` and ``missing="drop"`` date policies.
|
|
20
|
+
- Add ``sync_dates`` for comparable date coordinates and limits across multiple panels.
|
|
21
|
+
|
|
22
|
+
Collapsed mode currently remaps line artists only. Collection remapping remains planned
|
|
23
|
+
for a later release.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Bug reports and focused pull requests are welcome. For behavior changes, open an issue
|
|
4
|
+
first so the public API and date-axis semantics can be agreed before implementation.
|
|
5
|
+
|
|
6
|
+
## Development
|
|
7
|
+
|
|
8
|
+
Use Python 3.10 or newer:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python -m venv .venv
|
|
12
|
+
.venv/bin/python -m pip install -e ".[dev]"
|
|
13
|
+
.venv/bin/python -m pytest -q --cov=ggstyle --cov-report=term-missing
|
|
14
|
+
.venv/bin/ruff check .
|
|
15
|
+
.venv/bin/mypy src
|
|
16
|
+
.venv/bin/python tools/validate_docstrings.py
|
|
17
|
+
.venv/bin/python -m sphinx -W --keep-going -b html docs/source docs/build/html
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Public functions, classes, methods, and attributes use the NumPy docstring standard, as
|
|
21
|
+
in statsmodels. New functionality also belongs in the appropriate page under
|
|
22
|
+
`docs/source` and in the changelog. Documentation must build without warnings.
|
|
23
|
+
|
|
24
|
+
New behavior needs tests. Changes to collapsed coordinates should test both collapsed and
|
|
25
|
+
expanded modes, including switching between them. Do not commit generated distributions,
|
|
26
|
+
virtual environments, caches, or platform metadata.
|
|
27
|
+
|
|
28
|
+
By contributing, you agree that your contributions are licensed under the MIT License.
|
|
29
|
+
|
|
30
|
+
## Releasing
|
|
31
|
+
|
|
32
|
+
Releases use PyPI trusted publishing; maintainers must not store a long-lived PyPI token
|
|
33
|
+
in GitHub. Before the first release, register a pending publisher for project ``ggstyle``
|
|
34
|
+
on PyPI with these values:
|
|
35
|
+
|
|
36
|
+
- Owner: ``joshuamyers22``
|
|
37
|
+
- Repository: ``ggstyle``
|
|
38
|
+
- Workflow: ``publish.yml``
|
|
39
|
+
- Environment: ``pypi``
|
|
40
|
+
|
|
41
|
+
After the release commit passes CI, create and push a tag matching the package version,
|
|
42
|
+
for example ``v0.1.0``. The publish workflow independently repeats the test, type,
|
|
43
|
+
documentation, and package checks; publishes the distributions to PyPI; and creates the
|
|
44
|
+
GitHub release only after publication succeeds.
|
ggstyle-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joshua Myers
|
|
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.
|
ggstyle-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ggstyle
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A date axis for matplotlib that is easy to use and manipulate
|
|
5
|
+
Project-URL: Homepage, https://github.com/joshuamyers22/ggstyle
|
|
6
|
+
Project-URL: Repository, https://github.com/joshuamyers22/ggstyle
|
|
7
|
+
Project-URL: Issues, https://github.com/joshuamyers22/ggstyle/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/joshuamyers22/ggstyle/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Joshua Myers <joshua.myers22@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: dates,ggplot2,matplotlib,visualization
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: matplotlib>=3.7
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Requires-Dist: pandas>=2.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: build; extra == 'dev'
|
|
29
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
30
|
+
Requires-Dist: pandas-stubs; extra == 'dev'
|
|
31
|
+
Requires-Dist: polars>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
35
|
+
Requires-Dist: twine; extra == 'dev'
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: numpydoc>=1.6; extra == 'docs'
|
|
38
|
+
Requires-Dist: pydata-sphinx-theme>=0.15; extra == 'docs'
|
|
39
|
+
Requires-Dist: sphinx>=7; extra == 'docs'
|
|
40
|
+
Provides-Extra: polars
|
|
41
|
+
Requires-Dist: polars>=1.0; extra == 'polars'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# ggstyle
|
|
45
|
+
|
|
46
|
+
A date axis for matplotlib that is easy to use and easy to manipulate.
|
|
47
|
+
|
|
48
|
+
**v0.1 is the date axis plus themes.** No palettes module and no `line()` yet — those
|
|
49
|
+
remain future additions once the axis ergonomics have real usage behind them.
|
|
50
|
+
|
|
51
|
+
This is the first public release. The date-axis behavior is tested, but the project is
|
|
52
|
+
still young and follows semantic versioning. See the [known limits](#known-limits-in-v01)
|
|
53
|
+
before using collapsed mode in production.
|
|
54
|
+
|
|
55
|
+
## Why
|
|
56
|
+
|
|
57
|
+
Most of the pain in Python time-series plotting is not the grammar, it's the axis: ticks in
|
|
58
|
+
the wrong places, labels rotated to hide the fact that there are too many of them, weekend
|
|
59
|
+
gaps shredding an intraday chart, and annotation code that quietly puts your vertical line
|
|
60
|
+
three days off. `ggstyle` fixes the axis first.
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install ggstyle
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For development from a clone:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install -e ".[dev]"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Use
|
|
75
|
+
|
|
76
|
+
It adopts any Axes, including plots it never made:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import matplotlib.pyplot as plt
|
|
80
|
+
import ggstyle as gs
|
|
81
|
+
|
|
82
|
+
gs.use_theme() # "minimal" is the default
|
|
83
|
+
|
|
84
|
+
fig, ax = plt.subplots()
|
|
85
|
+
ax.plot(df["date"], df["close"]) # plain matplotlib, seaborn, or df.plot()
|
|
86
|
+
|
|
87
|
+
gs.dates(ax).ticks("quarterly").fmt("month-year").zoom("2020", "2022")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Configuration and drawing methods return the handle, so calls chain.
|
|
91
|
+
|
|
92
|
+
Axis semantics are also available as structured data rather than only rendered output:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
summary = gs.dates(ax).summary()
|
|
96
|
+
caption = gs.dates(ax).caption(add=True)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Ticks — where they go
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
.ticks("monthly") # daily | weekly | monthly | quarterly | yearly
|
|
103
|
+
.ticks("month-end") # anchored: month-start, quarter-end, year-start, ...
|
|
104
|
+
.ticks(every="3M") # any offset alias; legacy M/Q/Y/H accepted
|
|
105
|
+
.ticks(n=6) # about six ticks, snapped to a natural cadence
|
|
106
|
+
.ticks(at=["2020-01-01", "2021-07-01"])
|
|
107
|
+
.ticks(major="yearly", minor="monthly")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Anchoring is not cosmetic: month-start vs. month-end is the difference between labels that
|
|
111
|
+
line up with your observations and labels that float between them.
|
|
112
|
+
|
|
113
|
+
### Labels — what they say
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
.fmt("concise") # default: year shown once, not on every label
|
|
117
|
+
.fmt("month-year") # Jun 2020
|
|
118
|
+
.fmt("quarter") # Q2 2020
|
|
119
|
+
.fmt("year") / .fmt("month") / .fmt("day") / .fmt("iso") / .fmt("time")
|
|
120
|
+
.fmt("%b '%y") # any strftime string
|
|
121
|
+
.fmt(lambda d: f"week {d.isocalendar().week}")
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Changing the format never moves a tick, and changing the cadence never changes the format.
|
|
125
|
+
That orthogonality is a test, not an aspiration.
|
|
126
|
+
|
|
127
|
+
### Range
|
|
128
|
+
|
|
129
|
+
Partial strings expand to whole periods, pandas-style:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
.zoom("2020", "2022") # three complete years
|
|
133
|
+
.zoom("2020-03", None) # open-ended
|
|
134
|
+
.zoom(last="6M") # trailing window from the last observation, not from today
|
|
135
|
+
.zoom(ytd=True)
|
|
136
|
+
.pad(left="1M", right="1M")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Gaps
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
.collapse() # unobserved dates get no space
|
|
143
|
+
.expand() # true datetime axis, gaps restored
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Collapsed mode is defined by **the dates present in your data**, not by a holiday calendar.
|
|
147
|
+
Anything not observed is not allocated space. That is correct for any market or region and
|
|
148
|
+
needs no extra dependency. With several series, the axis uses the union of observed dates.
|
|
149
|
+
|
|
150
|
+
### Annotation in date space
|
|
151
|
+
|
|
152
|
+
Every one of these is correct in both modes — that is the whole point of the handle:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
.loc("2020-03-23") # -> axis position; the escape-hatch primitive
|
|
156
|
+
.vline("2020-03-23", label="trough")
|
|
157
|
+
.span("2020-02-19", "2020-03-23", label="drawdown")
|
|
158
|
+
.spans(events_df, start="begin", end="end", label="name")
|
|
159
|
+
.grid("yearly") # gridline cadence, independent of ticks
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
In collapsed mode a date that falls inside a gap (a Sunday, a holiday) is placed by linear
|
|
163
|
+
interpolation between its neighbours. `loc(date, snap=True)` rounds to the nearest
|
|
164
|
+
observation instead; `loc(date, strict=True)` raises if the date was never observed.
|
|
165
|
+
|
|
166
|
+
### Escape hatch
|
|
167
|
+
|
|
168
|
+
`.loc()` is the primitive that keeps raw matplotlib correct:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
handle = gs.dates(ax).collapse()
|
|
172
|
+
ax.axvline(handle.loc("2020-03-23")) # lands in the right place
|
|
173
|
+
ax.set_xlim(handle.loc("2020-01"), handle.loc("2021-01"))
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Themes
|
|
177
|
+
|
|
178
|
+
Two ship. `minimal` is the default; `grey` is the ggplot2 `theme_grey` analogue.
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
gs.use_theme() # minimal, process-wide
|
|
182
|
+
gs.use_theme("grey") # "gray" also accepted
|
|
183
|
+
|
|
184
|
+
with gs.theme("grey"): # scoped; restores every rcParam on exit
|
|
185
|
+
...
|
|
186
|
+
|
|
187
|
+
plt.style.use(gs.stylesheet()) # the .mplstyle on its own, no ggstyle import needed
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Both spell out the same type scale, colour cycle, and layout, so switching changes the
|
|
191
|
+
panel surface and nothing else — the same separation ggplot2 makes. The colour cycle is
|
|
192
|
+
Okabe–Ito-derived and capped at eight; past eight, direct labelling or faceting is the
|
|
193
|
+
right answer, not a ninth colour.
|
|
194
|
+
|
|
195
|
+
Importing `ggstyle` never mutates `rcParams`. Theming is always something you ask for.
|
|
196
|
+
|
|
197
|
+
Almost all of it is plain rcParams in a `.mplstyle` file, including spine removal
|
|
198
|
+
(`axes.spines.left: False`), which an earlier draft of the design wrongly assumed needed
|
|
199
|
+
Python.
|
|
200
|
+
|
|
201
|
+
## Data frames
|
|
202
|
+
|
|
203
|
+
pandas and polars both work, as do pyarrow arrays, numpy `datetime64`, and plain lists:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
gs.dates(ax, data=frame["date"]) # pandas Series, polars Series, or Index
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Polars is detected by module name rather than imported, so installing `ggstyle` never
|
|
210
|
+
pulls it in and pandas-only users pay nothing for the support. Timezone-aware input from
|
|
211
|
+
either library is converted to UTC instants for positioning; display timezones stay a
|
|
212
|
+
separate concern handled by `.tz()`.
|
|
213
|
+
|
|
214
|
+
Two things are deliberately *not* guessed: a whole DataFrame passed where a column was
|
|
215
|
+
meant, and a string column that might be dates. Both raise.
|
|
216
|
+
|
|
217
|
+
Missing values in explicit date data also raise unless exclusion is requested with
|
|
218
|
+
``missing="drop"``. The number excluded remains available through ``.summary()`` and in
|
|
219
|
+
generated captions.
|
|
220
|
+
|
|
221
|
+
## Multiple panels
|
|
222
|
+
|
|
223
|
+
Synchronize comparable axes with a common observation registry and limits:
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
handles = gs.sync_dates(axes, mode="collapse", limits="union")
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
This prevents the same date from receiving different ordinal positions in independently
|
|
230
|
+
collapsed panels.
|
|
231
|
+
|
|
232
|
+
## Design rules
|
|
233
|
+
|
|
234
|
+
- The date axis is a standalone object, not a side effect of plotting.
|
|
235
|
+
- Importing the package is inert; theming is opt-in.
|
|
236
|
+
- Placement, labels, gridline cadence, and range are four independent knobs.
|
|
237
|
+
- Fail loudly: a non-date axis raises, and mixed tz-aware/naive input raises rather than
|
|
238
|
+
guessing UTC.
|
|
239
|
+
- Never resample or interpolate the data silently.
|
|
240
|
+
- Never rotate tick labels by default. Rotation is a symptom of bad tick selection.
|
|
241
|
+
|
|
242
|
+
## Known limits in v0.1
|
|
243
|
+
|
|
244
|
+
- Collapsed mode remaps `Line2D` artists only. Collections (`fill_between`, `scatter`) are
|
|
245
|
+
not yet remapped; annotate through the handle instead.
|
|
246
|
+
- Native `ax.axvline(timestamp)` is still wrong in collapsed mode — go through `.loc()`.
|
|
247
|
+
A registered matplotlib scale would remove that caveat and is the v0.2 candidate.
|
|
248
|
+
- `.tz()` assumes naive data is UTC when converting for display.
|
|
249
|
+
- No palettes module yet: the colour cycle lives in the stylesheets.
|
|
250
|
+
|
|
251
|
+
## Tests
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
python -m pytest -q
|
|
255
|
+
ruff check .
|
|
256
|
+
mypy src
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete development workflow and
|
|
260
|
+
[SECURITY.md](SECURITY.md) for vulnerability reporting.
|
|
261
|
+
|
|
262
|
+
The structured documentation follows the same user-guide, API-reference, pitfalls, and
|
|
263
|
+
release-note separation used by statsmodels. Build it locally with:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
pip install -e ".[docs]"
|
|
267
|
+
python -m sphinx -W --keep-going -b html docs/source docs/build/html
|
|
268
|
+
```
|
ggstyle-0.1.0/README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# ggstyle
|
|
2
|
+
|
|
3
|
+
A date axis for matplotlib that is easy to use and easy to manipulate.
|
|
4
|
+
|
|
5
|
+
**v0.1 is the date axis plus themes.** No palettes module and no `line()` yet — those
|
|
6
|
+
remain future additions once the axis ergonomics have real usage behind them.
|
|
7
|
+
|
|
8
|
+
This is the first public release. The date-axis behavior is tested, but the project is
|
|
9
|
+
still young and follows semantic versioning. See the [known limits](#known-limits-in-v01)
|
|
10
|
+
before using collapsed mode in production.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Most of the pain in Python time-series plotting is not the grammar, it's the axis: ticks in
|
|
15
|
+
the wrong places, labels rotated to hide the fact that there are too many of them, weekend
|
|
16
|
+
gaps shredding an intraday chart, and annotation code that quietly puts your vertical line
|
|
17
|
+
three days off. `ggstyle` fixes the axis first.
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install ggstyle
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For development from a clone:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
It adopts any Axes, including plots it never made:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import matplotlib.pyplot as plt
|
|
37
|
+
import ggstyle as gs
|
|
38
|
+
|
|
39
|
+
gs.use_theme() # "minimal" is the default
|
|
40
|
+
|
|
41
|
+
fig, ax = plt.subplots()
|
|
42
|
+
ax.plot(df["date"], df["close"]) # plain matplotlib, seaborn, or df.plot()
|
|
43
|
+
|
|
44
|
+
gs.dates(ax).ticks("quarterly").fmt("month-year").zoom("2020", "2022")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Configuration and drawing methods return the handle, so calls chain.
|
|
48
|
+
|
|
49
|
+
Axis semantics are also available as structured data rather than only rendered output:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
summary = gs.dates(ax).summary()
|
|
53
|
+
caption = gs.dates(ax).caption(add=True)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Ticks — where they go
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
.ticks("monthly") # daily | weekly | monthly | quarterly | yearly
|
|
60
|
+
.ticks("month-end") # anchored: month-start, quarter-end, year-start, ...
|
|
61
|
+
.ticks(every="3M") # any offset alias; legacy M/Q/Y/H accepted
|
|
62
|
+
.ticks(n=6) # about six ticks, snapped to a natural cadence
|
|
63
|
+
.ticks(at=["2020-01-01", "2021-07-01"])
|
|
64
|
+
.ticks(major="yearly", minor="monthly")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Anchoring is not cosmetic: month-start vs. month-end is the difference between labels that
|
|
68
|
+
line up with your observations and labels that float between them.
|
|
69
|
+
|
|
70
|
+
### Labels — what they say
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
.fmt("concise") # default: year shown once, not on every label
|
|
74
|
+
.fmt("month-year") # Jun 2020
|
|
75
|
+
.fmt("quarter") # Q2 2020
|
|
76
|
+
.fmt("year") / .fmt("month") / .fmt("day") / .fmt("iso") / .fmt("time")
|
|
77
|
+
.fmt("%b '%y") # any strftime string
|
|
78
|
+
.fmt(lambda d: f"week {d.isocalendar().week}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Changing the format never moves a tick, and changing the cadence never changes the format.
|
|
82
|
+
That orthogonality is a test, not an aspiration.
|
|
83
|
+
|
|
84
|
+
### Range
|
|
85
|
+
|
|
86
|
+
Partial strings expand to whole periods, pandas-style:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
.zoom("2020", "2022") # three complete years
|
|
90
|
+
.zoom("2020-03", None) # open-ended
|
|
91
|
+
.zoom(last="6M") # trailing window from the last observation, not from today
|
|
92
|
+
.zoom(ytd=True)
|
|
93
|
+
.pad(left="1M", right="1M")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Gaps
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
.collapse() # unobserved dates get no space
|
|
100
|
+
.expand() # true datetime axis, gaps restored
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Collapsed mode is defined by **the dates present in your data**, not by a holiday calendar.
|
|
104
|
+
Anything not observed is not allocated space. That is correct for any market or region and
|
|
105
|
+
needs no extra dependency. With several series, the axis uses the union of observed dates.
|
|
106
|
+
|
|
107
|
+
### Annotation in date space
|
|
108
|
+
|
|
109
|
+
Every one of these is correct in both modes — that is the whole point of the handle:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
.loc("2020-03-23") # -> axis position; the escape-hatch primitive
|
|
113
|
+
.vline("2020-03-23", label="trough")
|
|
114
|
+
.span("2020-02-19", "2020-03-23", label="drawdown")
|
|
115
|
+
.spans(events_df, start="begin", end="end", label="name")
|
|
116
|
+
.grid("yearly") # gridline cadence, independent of ticks
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
In collapsed mode a date that falls inside a gap (a Sunday, a holiday) is placed by linear
|
|
120
|
+
interpolation between its neighbours. `loc(date, snap=True)` rounds to the nearest
|
|
121
|
+
observation instead; `loc(date, strict=True)` raises if the date was never observed.
|
|
122
|
+
|
|
123
|
+
### Escape hatch
|
|
124
|
+
|
|
125
|
+
`.loc()` is the primitive that keeps raw matplotlib correct:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
handle = gs.dates(ax).collapse()
|
|
129
|
+
ax.axvline(handle.loc("2020-03-23")) # lands in the right place
|
|
130
|
+
ax.set_xlim(handle.loc("2020-01"), handle.loc("2021-01"))
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Themes
|
|
134
|
+
|
|
135
|
+
Two ship. `minimal` is the default; `grey` is the ggplot2 `theme_grey` analogue.
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
gs.use_theme() # minimal, process-wide
|
|
139
|
+
gs.use_theme("grey") # "gray" also accepted
|
|
140
|
+
|
|
141
|
+
with gs.theme("grey"): # scoped; restores every rcParam on exit
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
plt.style.use(gs.stylesheet()) # the .mplstyle on its own, no ggstyle import needed
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Both spell out the same type scale, colour cycle, and layout, so switching changes the
|
|
148
|
+
panel surface and nothing else — the same separation ggplot2 makes. The colour cycle is
|
|
149
|
+
Okabe–Ito-derived and capped at eight; past eight, direct labelling or faceting is the
|
|
150
|
+
right answer, not a ninth colour.
|
|
151
|
+
|
|
152
|
+
Importing `ggstyle` never mutates `rcParams`. Theming is always something you ask for.
|
|
153
|
+
|
|
154
|
+
Almost all of it is plain rcParams in a `.mplstyle` file, including spine removal
|
|
155
|
+
(`axes.spines.left: False`), which an earlier draft of the design wrongly assumed needed
|
|
156
|
+
Python.
|
|
157
|
+
|
|
158
|
+
## Data frames
|
|
159
|
+
|
|
160
|
+
pandas and polars both work, as do pyarrow arrays, numpy `datetime64`, and plain lists:
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
gs.dates(ax, data=frame["date"]) # pandas Series, polars Series, or Index
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Polars is detected by module name rather than imported, so installing `ggstyle` never
|
|
167
|
+
pulls it in and pandas-only users pay nothing for the support. Timezone-aware input from
|
|
168
|
+
either library is converted to UTC instants for positioning; display timezones stay a
|
|
169
|
+
separate concern handled by `.tz()`.
|
|
170
|
+
|
|
171
|
+
Two things are deliberately *not* guessed: a whole DataFrame passed where a column was
|
|
172
|
+
meant, and a string column that might be dates. Both raise.
|
|
173
|
+
|
|
174
|
+
Missing values in explicit date data also raise unless exclusion is requested with
|
|
175
|
+
``missing="drop"``. The number excluded remains available through ``.summary()`` and in
|
|
176
|
+
generated captions.
|
|
177
|
+
|
|
178
|
+
## Multiple panels
|
|
179
|
+
|
|
180
|
+
Synchronize comparable axes with a common observation registry and limits:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
handles = gs.sync_dates(axes, mode="collapse", limits="union")
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
This prevents the same date from receiving different ordinal positions in independently
|
|
187
|
+
collapsed panels.
|
|
188
|
+
|
|
189
|
+
## Design rules
|
|
190
|
+
|
|
191
|
+
- The date axis is a standalone object, not a side effect of plotting.
|
|
192
|
+
- Importing the package is inert; theming is opt-in.
|
|
193
|
+
- Placement, labels, gridline cadence, and range are four independent knobs.
|
|
194
|
+
- Fail loudly: a non-date axis raises, and mixed tz-aware/naive input raises rather than
|
|
195
|
+
guessing UTC.
|
|
196
|
+
- Never resample or interpolate the data silently.
|
|
197
|
+
- Never rotate tick labels by default. Rotation is a symptom of bad tick selection.
|
|
198
|
+
|
|
199
|
+
## Known limits in v0.1
|
|
200
|
+
|
|
201
|
+
- Collapsed mode remaps `Line2D` artists only. Collections (`fill_between`, `scatter`) are
|
|
202
|
+
not yet remapped; annotate through the handle instead.
|
|
203
|
+
- Native `ax.axvline(timestamp)` is still wrong in collapsed mode — go through `.loc()`.
|
|
204
|
+
A registered matplotlib scale would remove that caveat and is the v0.2 candidate.
|
|
205
|
+
- `.tz()` assumes naive data is UTC when converting for display.
|
|
206
|
+
- No palettes module yet: the colour cycle lives in the stylesheets.
|
|
207
|
+
|
|
208
|
+
## Tests
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
python -m pytest -q
|
|
212
|
+
ruff check .
|
|
213
|
+
mypy src
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the complete development workflow and
|
|
217
|
+
[SECURITY.md](SECURITY.md) for vulnerability reporting.
|
|
218
|
+
|
|
219
|
+
The structured documentation follows the same user-guide, API-reference, pitfalls, and
|
|
220
|
+
release-note separation used by statsmodels. Build it locally with:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pip install -e ".[docs]"
|
|
224
|
+
python -m sphinx -W --keep-going -b html docs/source docs/build/html
|
|
225
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
`ggstyle` is a local plotting library: it does not make network requests, execute input,
|
|
4
|
+
or deserialize untrusted objects. Treat custom matplotlib formatters and user-provided
|
|
5
|
+
callables as trusted Python code.
|
|
6
|
+
|
|
7
|
+
Please report a suspected vulnerability privately to `joshua.myers22@gmail.com` rather
|
|
8
|
+
than opening a public issue. Include the affected version, a minimal reproducer, and the
|
|
9
|
+
impact. Security fixes are made on the latest public release only.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
API reference
|
|
2
|
+
=============
|
|
3
|
+
|
|
4
|
+
The top-level API contains the date-axis handle, cadence specification, and opt-in theme
|
|
5
|
+
helpers. Internal modules are not compatibility guarantees.
|
|
6
|
+
|
|
7
|
+
Date axes
|
|
8
|
+
---------
|
|
9
|
+
|
|
10
|
+
.. autofunction:: ggstyle.dates
|
|
11
|
+
|
|
12
|
+
.. autoclass:: ggstyle.DateAxis
|
|
13
|
+
:members:
|
|
14
|
+
|
|
15
|
+
.. autoclass:: ggstyle.AxisSummary
|
|
16
|
+
:members:
|
|
17
|
+
|
|
18
|
+
.. autoclass:: ggstyle.Cadence
|
|
19
|
+
:members:
|
|
20
|
+
|
|
21
|
+
.. autofunction:: ggstyle.sync_dates
|
|
22
|
+
|
|
23
|
+
Themes
|
|
24
|
+
------
|
|
25
|
+
|
|
26
|
+
.. autofunction:: ggstyle.use_theme
|
|
27
|
+
|
|
28
|
+
.. autoclass:: ggstyle.theme
|
|
29
|
+
:members:
|
|
30
|
+
|
|
31
|
+
.. autofunction:: ggstyle.stylesheet
|
|
32
|
+
|
|
33
|
+
.. autofunction:: ggstyle.available_themes
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Sphinx configuration for the ggstyle documentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version as metadata_version
|
|
6
|
+
|
|
7
|
+
project = "ggstyle"
|
|
8
|
+
author = "Joshua Myers"
|
|
9
|
+
copyright = "2026, Joshua Myers"
|
|
10
|
+
release = metadata_version("ggstyle")
|
|
11
|
+
|
|
12
|
+
extensions = [
|
|
13
|
+
"sphinx.ext.autodoc",
|
|
14
|
+
"sphinx.ext.doctest",
|
|
15
|
+
"sphinx.ext.viewcode",
|
|
16
|
+
"numpydoc",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
autodoc_typehints = "none"
|
|
20
|
+
numpydoc_class_members_toctree = False
|
|
21
|
+
numpydoc_show_class_members = False
|
|
22
|
+
numpydoc_xref_param_type = False
|
|
23
|
+
|
|
24
|
+
doctest_global_setup = """
|
|
25
|
+
from ggstyle import Cadence, available_themes, dates, use_theme
|
|
26
|
+
"""
|
|
27
|
+
doctest_global_cleanup = """
|
|
28
|
+
import matplotlib.pyplot as plt
|
|
29
|
+
plt.close("all")
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
html_theme = "pydata_sphinx_theme"
|
|
33
|
+
html_title = f"ggstyle {release}"
|
|
34
|
+
html_theme_options = {
|
|
35
|
+
"github_url": "https://github.com/joshuamyers22/ggstyle",
|
|
36
|
+
"show_toc_level": 2,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
exclude_patterns = ["_build", "generated"]
|