Skip to content

Commit b7513a0

Browse files
authored
Added plotext backend (#42)
* Added plotext backend * Formatting
1 parent 5db1b57 commit b7513a0

14 files changed

Lines changed: 1649 additions & 20 deletions

File tree

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,11 @@ env*
189189

190190
# VS code
191191
.vscode/*
192+
193+
# Outputs
194+
docs/.astro/
195+
docs/node_modules/
196+
docs/superpowers/
197+
docs/tutorials/
198+
tutorials/*.txt
199+
tutorials/*.png

README.md

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
# Maxlotlib
2-
3-
41
# Maxplotlib
52

6-
A clean, expressive wrapper around **Matplotlib** **tikzfigure** for
7-
producing publication-quality figures with minimal boilerplate. Swap
8-
backends without rewriting your data — render the same canvas as a crisp
9-
PNG, an interactive Plotly chart, or camera-ready **TikZ** code for
10-
LaTeX.
3+
A clean, expressive wrapper around **Matplotlib**, **Plotly**,
4+
**plotext**, and **tikzfigure** for producing publication-quality
5+
figures with minimal boilerplate. Swap backends without rewriting your
6+
data — render the same canvas as a crisp PNG, an interactive Plotly
7+
chart, a terminal-native plotext figure, or camera-ready **TikZ** code
8+
for LaTeX.
119

1210
## Install
1311

@@ -38,14 +36,45 @@ canvas.show()
3836

3937
![](README_files/figure-markdown_strict/cell-3-output-1.png)
4038

41-
Alternatively, plot with the TikZ backend (not done yet):
39+
Render the same line graph directly in the terminal with the `plotext`
40+
backend:
41+
42+
``` python
43+
terminal_fig = canvas.plot(backend="plotext")
44+
print(terminal_fig.build(keep_colors=False))
45+
```
46+
47+
Or plot with the TikZ backend:
4248

4349
``` python
4450
canvas.show(backend="tikzfigure")
4551
```
4652

4753
![](README_files/figure-markdown_strict/cell-4-output-1.png)
4854

55+
### Terminal backend
56+
57+
The `plotext` backend is designed for terminal-first workflows. It
58+
currently supports line plots, scatter plots, bars, filled regions,
59+
error bars, reference lines, text/annotations, labels/titles, log
60+
axes, layers, matrix-style `imshow()` rendering, common patches, and
61+
multi-subplot canvases.
62+
63+
``` python
64+
x = np.linspace(1, 10, 40)
65+
66+
canvas, ax = Canvas.subplots()
67+
ax.plot(x, np.sqrt(x), color="cyan", label="sqrt(x)")
68+
ax.errorbar(x[::8], np.sqrt(x[::8]), yerr=0.15, color="yellow", label="samples")
69+
ax.set_title("Terminal plot")
70+
ax.set_xlabel("x")
71+
ax.set_ylabel("y")
72+
ax.set_xscale("log")
73+
ax.set_legend(True)
74+
75+
canvas.show(backend="plotext")
76+
```
77+
4978
### Layers
5079

5180
``` python

README.qmd

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ fig-dpi: 150
66

77
# Maxplotlib
88

9-
A clean, expressive wrapper around **Matplotlib** **tikzfigure** for producing publication-quality figures
10-
with minimal boilerplate. Swap backends without rewriting your data — render the same canvas
11-
as a crisp PNG, an interactive Plotly chart, or camera-ready **TikZ** code for LaTeX.
9+
A clean, expressive wrapper around **Matplotlib**, **Plotly**, **plotext**, and **tikzfigure**
10+
for producing publication-quality figures with minimal boilerplate. Swap backends without
11+
rewriting your data — render the same canvas as a crisp PNG, an interactive Plotly chart, a
12+
terminal-native plotext figure, or camera-ready **TikZ** code for LaTeX.
1213

1314
## Install
1415

@@ -41,7 +42,14 @@ Plot the figure with the default (matplotlib) backend:
4142
canvas.show()
4243
```
4344

44-
Alternatively, plot with the TikZ backend:
45+
Render the same line graph directly in the terminal with the `plotext` backend:
46+
47+
```{python}
48+
terminal_fig = canvas.plot(backend="plotext")
49+
print(terminal_fig.build(keep_colors=False))
50+
```
51+
52+
Or plot with the TikZ backend:
4553

4654
```{python}
4755
canvas.show(backend="tikzfigure")
@@ -71,6 +79,27 @@ canvas.show(backend="tikzfigure") # Generates LaTeX subfigures
7179

7280
**Note:** Only horizontal layouts (1×n) are currently supported with the tikzfigure backend. Vertical/grid layouts will raise `NotImplementedError`. See the tutorials for more examples.
7381

82+
### Terminal Backend with plotext
83+
84+
The `plotext` backend is designed for terminal-first workflows. It currently supports line plots,
85+
scatter plots, bars, filled regions, error bars, reference lines, text/annotations, labels/titles,
86+
log axes, layers, matrix-style `imshow()` rendering, common patches, and multi-subplot canvases.
87+
88+
```{python}
89+
x = np.linspace(1, 10, 40)
90+
91+
canvas, ax = Canvas.subplots()
92+
ax.plot(x, np.sqrt(x), color="cyan", label="sqrt(x)")
93+
ax.errorbar(x[::8], np.sqrt(x[::8]), yerr=0.15, color="yellow", label="samples")
94+
ax.set_title("Terminal plot")
95+
ax.set_xlabel("x")
96+
ax.set_ylabel("y")
97+
ax.set_xscale("log")
98+
ax.set_legend(True)
99+
100+
canvas.show(backend="plotext")
101+
```
102+
74103
### Layers
75104

76105
```{python}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ dependencies = [
1818
"matplotlib",
1919
"pint",
2020
"plotly",
21+
"plotext",
2122
"tikzfigure[vis]>=0.2.1",
2223
]
2324
[project.optional-dependencies]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from maxplotlib.backends.plotext.figure import PlotextFigure, create_plotext_figure
2+
3+
__all__ = ["PlotextFigure", "create_plotext_figure"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from pathlib import Path
5+
6+
from plotext._figure import _figure_class
7+
8+
_ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
9+
10+
11+
def strip_ansi(text: str) -> str:
12+
return _ANSI_ESCAPE_RE.sub("", text)
13+
14+
15+
def create_plotext_figure(nrows: int = 1, ncols: int = 1) -> _figure_class:
16+
figure = _figure_class()
17+
if nrows > 1 or ncols > 1:
18+
figure.subplots(nrows, ncols)
19+
return figure
20+
21+
22+
class PlotextFigure:
23+
def __init__(self, figure: _figure_class, suptitle: str | None = None):
24+
self.figure = figure
25+
self.suptitle = suptitle
26+
27+
def build(self, keep_colors: bool = True) -> str:
28+
output = self.figure.build()
29+
if self.suptitle:
30+
output = f"{self.suptitle}\n{output}"
31+
return output if keep_colors else strip_ansi(output)
32+
33+
def show(self) -> str:
34+
output = self.build()
35+
print(output)
36+
return output
37+
38+
def savefig(self, path, append: bool = False, keep_colors: bool = False) -> None:
39+
destination = Path(path)
40+
mode = "a" if append else "w"
41+
with destination.open(mode, encoding="utf-8") as handle:
42+
handle.write(self.build(keep_colors=keep_colors))
43+
handle.write("\n")
44+
45+
save_fig = savefig
46+
47+
def __getattr__(self, name):
48+
return getattr(self.figure, name)
49+
50+
def __str__(self) -> str:
51+
return self.build(keep_colors=False)

src/maxplotlib/canvas/canvas.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
setup_plotstyle,
1313
setup_tex_fonts,
1414
)
15+
from maxplotlib.backends.plotext import PlotextFigure, create_plotext_figure
1516
from maxplotlib.colors.colors import Color
1617
from maxplotlib.linestyle.linestyle import Linestyle
1718
from maxplotlib.subfigure.line_plot import LinePlot
@@ -199,6 +200,7 @@ def __init__(
199200
self._plotted = False
200201
self._matplotlib_fig = None
201202
self._matplotlib_axes = None
203+
self._plotext_figure = None
202204
self._suptitle: str | None = None
203205
self._suptitle_kwargs: dict = {}
204206

@@ -681,7 +683,6 @@ def savefig(
681683
if self._plotted:
682684
self._matplotlib_fig.savefig(full_filepath)
683685
else:
684-
685686
fig, axs = self.plot(
686687
backend="matplotlib",
687688
savefig=True,
@@ -690,6 +691,33 @@ def savefig(
690691
fig.savefig(full_filepath)
691692
if verbose:
692693
print(f"Saved {full_filepath}")
694+
elif backend == "plotext":
695+
if layer_by_layer:
696+
layers = []
697+
for layer in self.layers:
698+
layers.append(layer)
699+
figure = self.plot(
700+
backend="plotext",
701+
savefig=False,
702+
layers=layers,
703+
)
704+
_fn = f"{filename_no_extension}_{layers}.{extension}"
705+
figure.savefig(_fn)
706+
print(f"Saved {_fn}")
707+
else:
708+
if layers is None:
709+
layers = self.layers
710+
full_filepath = filename
711+
else:
712+
full_filepath = f"{filename_no_extension}_{layers}.{extension}"
713+
figure = self.plot(
714+
backend="plotext",
715+
savefig=False,
716+
layers=layers,
717+
)
718+
figure.savefig(full_filepath)
719+
if verbose:
720+
print(f"Saved {full_filepath}")
693721

694722
def plot(
695723
self,
@@ -709,6 +737,12 @@ def plot(
709737
)
710738
elif backend == "plotly":
711739
return self.plot_plotly(savefig=savefig)
740+
elif backend == "plotext":
741+
return self.plot_plotext(
742+
savefig=savefig,
743+
layers=layers,
744+
verbose=verbose,
745+
)
712746
elif backend == "tikzfigure":
713747
return self.plot_tikzfigure(savefig=savefig)
714748
else:
@@ -733,6 +767,14 @@ def show(
733767
# self._matplotlib_fig.show()
734768
elif backend == "plotly":
735769
self.plot_plotly(savefig=False)
770+
elif backend == "plotext":
771+
figure = self.plot_plotext(
772+
savefig=False,
773+
layers=layers,
774+
verbose=verbose,
775+
)
776+
figure.show()
777+
return figure
736778
elif backend == "tikzfigure":
737779
fig = self.plot_tikzfigure(savefig=False, verbose=verbose)
738780
# TikzFigure handles all rendering (single or multi-subplot)
@@ -862,7 +904,7 @@ def plot_tikzfigure(
862904
else None
863905
),
864906
grid=line_plot._grid,
865-
caption=line_plot._title or f"Subplot {col+1}",
907+
caption=line_plot._title or f"Subplot {col + 1}",
866908
width=0.45,
867909
)
868910

@@ -890,6 +932,36 @@ def plot_tikzfigure(
890932

891933
return fig
892934

935+
def plot_plotext(
936+
self,
937+
savefig: bool = False,
938+
layers: list | None = None,
939+
verbose: bool = False,
940+
) -> PlotextFigure:
941+
if verbose:
942+
print("Generating plotext figure...")
943+
944+
figure = create_plotext_figure(self.nrows, self.ncols)
945+
946+
for row, col, subplot in self.iter_subplots():
947+
ax = (
948+
figure
949+
if (self.nrows, self.ncols) == (1, 1)
950+
else figure.subplot(row + 1, col + 1)
951+
)
952+
if isinstance(subplot, TikzFigure):
953+
raise NotImplementedError(
954+
"tikzfigure subplots cannot be rendered with the plotext backend."
955+
)
956+
subplot.plot_plotext(ax, layers=layers)
957+
958+
wrapped = PlotextFigure(figure=figure, suptitle=self._suptitle)
959+
if savefig and isinstance(savefig, str):
960+
wrapped.savefig(savefig)
961+
962+
self._plotext_figure = wrapped
963+
return wrapped
964+
893965
def plot_plotly(self, show=True, savefig=None, usetex=False):
894966
"""
895967
Generate and optionally display the subplots using Plotly.

src/maxplotlib/colors/colors.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66

77
class Color:
8-
98
def _parse_color(self, color_spec):
109
"""
1110
Internal method to parse the color specification and convert it to an RGB tuple.

src/maxplotlib/linestyle/linestyle.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33

44
class Linestyle:
5-
65
def _parse_style(self, style_spec):
76
"""
87
Internal method to parse the style specification and convert it to a Matplotlib linestyle.

0 commit comments

Comments
 (0)