Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,11 @@ env*

# VS code
.vscode/*

# Outputs
docs/.astro/
docs/node_modules/
docs/superpowers/
docs/tutorials/
tutorials/*.txt
tutorials/*.png
47 changes: 38 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
# Maxlotlib


# Maxplotlib

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

## Install

Expand Down Expand Up @@ -38,14 +36,45 @@ canvas.show()

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

Alternatively, plot with the TikZ backend (not done yet):
Render the same line graph directly in the terminal with the `plotext`
backend:

``` python
terminal_fig = canvas.plot(backend="plotext")
print(terminal_fig.build(keep_colors=False))
```

Or plot with the TikZ backend:

``` python
canvas.show(backend="tikzfigure")
```

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

### Terminal backend

The `plotext` backend is designed for terminal-first workflows. It
currently supports line plots, scatter plots, bars, filled regions,
error bars, reference lines, text/annotations, labels/titles, log
axes, layers, matrix-style `imshow()` rendering, common patches, and
multi-subplot canvases.

``` python
x = np.linspace(1, 10, 40)

canvas, ax = Canvas.subplots()
ax.plot(x, np.sqrt(x), color="cyan", label="sqrt(x)")
ax.errorbar(x[::8], np.sqrt(x[::8]), yerr=0.15, color="yellow", label="samples")
ax.set_title("Terminal plot")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_xscale("log")
ax.set_legend(True)

canvas.show(backend="plotext")
```

### Layers

``` python
Expand Down
37 changes: 33 additions & 4 deletions README.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ fig-dpi: 150

# Maxplotlib

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

## Install

Expand Down Expand Up @@ -41,7 +42,14 @@ Plot the figure with the default (matplotlib) backend:
canvas.show()
```

Alternatively, plot with the TikZ backend:
Render the same line graph directly in the terminal with the `plotext` backend:

```{python}
terminal_fig = canvas.plot(backend="plotext")
print(terminal_fig.build(keep_colors=False))
```

Or plot with the TikZ backend:

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

**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.

### Terminal Backend with plotext

The `plotext` backend is designed for terminal-first workflows. It currently supports line plots,
scatter plots, bars, filled regions, error bars, reference lines, text/annotations, labels/titles,
log axes, layers, matrix-style `imshow()` rendering, common patches, and multi-subplot canvases.

```{python}
x = np.linspace(1, 10, 40)

canvas, ax = Canvas.subplots()
ax.plot(x, np.sqrt(x), color="cyan", label="sqrt(x)")
ax.errorbar(x[::8], np.sqrt(x[::8]), yerr=0.15, color="yellow", label="samples")
ax.set_title("Terminal plot")
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_xscale("log")
ax.set_legend(True)

canvas.show(backend="plotext")
```

### Layers

```{python}
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies = [
"matplotlib",
"pint",
"plotly",
"plotext",
"tikzfigure[vis]>=0.2.1",
]
[project.optional-dependencies]
Expand Down
3 changes: 3 additions & 0 deletions src/maxplotlib/backends/plotext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from maxplotlib.backends.plotext.figure import PlotextFigure, create_plotext_figure

__all__ = ["PlotextFigure", "create_plotext_figure"]
51 changes: 51 additions & 0 deletions src/maxplotlib/backends/plotext/figure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

import re
from pathlib import Path

from plotext._figure import _figure_class

_ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")


def strip_ansi(text: str) -> str:
return _ANSI_ESCAPE_RE.sub("", text)


def create_plotext_figure(nrows: int = 1, ncols: int = 1) -> _figure_class:
figure = _figure_class()
if nrows > 1 or ncols > 1:
figure.subplots(nrows, ncols)
return figure


class PlotextFigure:
def __init__(self, figure: _figure_class, suptitle: str | None = None):
self.figure = figure
self.suptitle = suptitle

def build(self, keep_colors: bool = True) -> str:
output = self.figure.build()
if self.suptitle:
output = f"{self.suptitle}\n{output}"
return output if keep_colors else strip_ansi(output)

def show(self) -> str:
output = self.build()
print(output)
return output

def savefig(self, path, append: bool = False, keep_colors: bool = False) -> None:
destination = Path(path)
mode = "a" if append else "w"
with destination.open(mode, encoding="utf-8") as handle:
handle.write(self.build(keep_colors=keep_colors))
handle.write("\n")

save_fig = savefig

def __getattr__(self, name):
return getattr(self.figure, name)

def __str__(self) -> str:
return self.build(keep_colors=False)
76 changes: 74 additions & 2 deletions src/maxplotlib/canvas/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
setup_plotstyle,
setup_tex_fonts,
)
from maxplotlib.backends.plotext import PlotextFigure, create_plotext_figure
from maxplotlib.colors.colors import Color
from maxplotlib.linestyle.linestyle import Linestyle
from maxplotlib.subfigure.line_plot import LinePlot
Expand Down Expand Up @@ -199,6 +200,7 @@ def __init__(
self._plotted = False
self._matplotlib_fig = None
self._matplotlib_axes = None
self._plotext_figure = None
self._suptitle: str | None = None
self._suptitle_kwargs: dict = {}

Expand Down Expand Up @@ -681,7 +683,6 @@ def savefig(
if self._plotted:
self._matplotlib_fig.savefig(full_filepath)
else:

fig, axs = self.plot(
backend="matplotlib",
savefig=True,
Expand All @@ -690,6 +691,33 @@ def savefig(
fig.savefig(full_filepath)
if verbose:
print(f"Saved {full_filepath}")
elif backend == "plotext":
if layer_by_layer:
layers = []
for layer in self.layers:
layers.append(layer)
figure = self.plot(
backend="plotext",
savefig=False,
layers=layers,
)
_fn = f"{filename_no_extension}_{layers}.{extension}"
figure.savefig(_fn)
print(f"Saved {_fn}")
else:
if layers is None:
layers = self.layers
full_filepath = filename
else:
full_filepath = f"{filename_no_extension}_{layers}.{extension}"
figure = self.plot(
backend="plotext",
savefig=False,
layers=layers,
)
figure.savefig(full_filepath)
if verbose:
print(f"Saved {full_filepath}")

def plot(
self,
Expand All @@ -709,6 +737,12 @@ def plot(
)
elif backend == "plotly":
return self.plot_plotly(savefig=savefig)
elif backend == "plotext":
return self.plot_plotext(
savefig=savefig,
layers=layers,
verbose=verbose,
)
elif backend == "tikzfigure":
return self.plot_tikzfigure(savefig=savefig)
else:
Expand All @@ -733,6 +767,14 @@ def show(
# self._matplotlib_fig.show()
elif backend == "plotly":
self.plot_plotly(savefig=False)
elif backend == "plotext":
figure = self.plot_plotext(
savefig=False,
layers=layers,
verbose=verbose,
)
figure.show()
return figure
elif backend == "tikzfigure":
fig = self.plot_tikzfigure(savefig=False, verbose=verbose)
# TikzFigure handles all rendering (single or multi-subplot)
Expand Down Expand Up @@ -862,7 +904,7 @@ def plot_tikzfigure(
else None
),
grid=line_plot._grid,
caption=line_plot._title or f"Subplot {col+1}",
caption=line_plot._title or f"Subplot {col + 1}",
width=0.45,
)

Expand Down Expand Up @@ -890,6 +932,36 @@ def plot_tikzfigure(

return fig

def plot_plotext(
self,
savefig: bool = False,
layers: list | None = None,
verbose: bool = False,
) -> PlotextFigure:
if verbose:
print("Generating plotext figure...")

figure = create_plotext_figure(self.nrows, self.ncols)

for row, col, subplot in self.iter_subplots():
ax = (
figure
if (self.nrows, self.ncols) == (1, 1)
else figure.subplot(row + 1, col + 1)
)
if isinstance(subplot, TikzFigure):
raise NotImplementedError(
"tikzfigure subplots cannot be rendered with the plotext backend."
)
subplot.plot_plotext(ax, layers=layers)

wrapped = PlotextFigure(figure=figure, suptitle=self._suptitle)
if savefig and isinstance(savefig, str):
wrapped.savefig(savefig)

self._plotext_figure = wrapped
return wrapped

def plot_plotly(self, show=True, savefig=None, usetex=False):
"""
Generate and optionally display the subplots using Plotly.
Expand Down
1 change: 0 additions & 1 deletion src/maxplotlib/colors/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


class Color:

def _parse_color(self, color_spec):
"""
Internal method to parse the color specification and convert it to an RGB tuple.
Expand Down
1 change: 0 additions & 1 deletion src/maxplotlib/linestyle/linestyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


class Linestyle:

def _parse_style(self, style_spec):
"""
Internal method to parse the style specification and convert it to a Matplotlib linestyle.
Expand Down
Loading
Loading