{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Tutorial 09 - plotext Backend\n", "\n", "The **plotext backend** renders a `maxplotlib` canvas directly in the terminal. That makes it a good fit for SSH sessions, CLI workflows, quick diagnostics, and situations where you want the same `Canvas` API without a GUI browser or desktop window.\n", "\n", "## What this tutorial covers\n", "\n", "| Area | Status |\n", "| --- | --- |\n", "| `plot()` / line graphs | ✅ |\n", "| `scatter()` | ✅ |\n", "| `bar()` | ✅ |\n", "| `fill_between()` to a baseline | ✅ |\n", "| `fill_between()` between two curves | ✅ |\n", "| `errorbar()` | ✅ |\n", "| `axhline()` / `axvline()` / `hlines()` / `vlines()` | ✅ |\n", "| `text()` / `annotate()` | ✅ |\n", "| Titles, labels, captions, limits, ticks, grid, log/symlog scales | ✅ |\n", "| `set_aspect()` | ✅ |\n", "| Layers | ✅ |\n", "| Multi-subplot canvases | ✅ |\n", "| `add_imshow()` matrix-style rendering | ✅ |\n", "| `add_colorbar()` note-style summary | ✅ |\n", "| Generic matplotlib patch geometry | ✅ (best effort) |\n", "| Terminal animation redraw loop | ✅ |\n", "\n", "The goal is not pixel-identical matplotlib output. The goal is a **faithful terminal representation** of the same plot intent." ] }, { "cell_type": "code", "execution_count": null, "id": "1", "metadata": {}, "outputs": [], "source": [ "from pathlib import Path\n", "\n", "import matplotlib.patches as mpatches\n", "import numpy as np\n", "\n", "from maxplotlib import Canvas" ] }, { "cell_type": "markdown", "id": "2", "metadata": {}, "source": [ "## 1 · Figure lifecycle: `plot()`, `build()`, `show()`, and `savefig()`\n", "\n", "Use `canvas.plot(backend=\"plotext\")` when you want a reusable terminal figure object. The returned object supports:\n", "\n", "- `.build()` to get the rendered terminal plot as a string\n", "- `.show()` to print it immediately\n", "- `.savefig(path)` to write the terminal output to a text file\n", "\n", "Use `canvas.show(backend=\"plotext\")` when you just want direct terminal output." ] }, { "cell_type": "code", "execution_count": null, "id": "3", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 2 * np.pi, 100)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", "ax.set_title(\"Demo\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"y\")\n", "\n", "terminal_fig = canvas.plot(backend=\"plotext\")\n", "preview = terminal_fig.build(keep_colors=False)\n", "print(preview)\n", "\n", "# terminal_fig.show()\n", "# canvas.show(backend=\"plotext\")" ] }, { "cell_type": "markdown", "id": "4", "metadata": {}, "source": [ "## 2 · Line plots\n", "\n", "Line plots are the natural fit for `plotext`. Labels, markers, grid lines, axis titles, and legends all carry over cleanly." ] }, { "cell_type": "code", "execution_count": null, "id": "5", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 2 * np.pi, 120)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", "ax.plot(x, np.cos(x), color=\"yellow\", label=\"cos(x)\", marker=\"dot\")\n", "ax.plot(x, np.sin(2 * x), color=\"green\", label=\"sin(2x)\")\n", "ax.set_title(\"Multiple terminal lines\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"value\")\n", "ax.set_grid(True)\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "6", "metadata": {}, "source": [ "## 3 · Scatter plots\n", "\n", "Scatter traces use `plotext.scatter(...)` under the hood. They combine well with a line on the same axes." ] }, { "cell_type": "code", "execution_count": null, "id": "7", "metadata": {}, "outputs": [], "source": [ "rng = np.random.default_rng(42)\n", "x = np.linspace(0, 8, 60)\n", "samples_x = np.linspace(0, 8, 15)\n", "samples_y = np.sin(samples_x) + rng.normal(0, 0.15, len(samples_x))\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, np.sin(x), color=\"white\", label=\"sin(x)\")\n", "ax.scatter(samples_x, samples_y, color=\"red\", marker=\"x\", label=\"samples\")\n", "ax.set_title(\"Scatter + line\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"y\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "8", "metadata": {}, "source": [ "## 4 · Bar charts\n", "\n", "Bars are supported too, so summary views and simple dashboards work well in the terminal." ] }, { "cell_type": "code", "execution_count": null, "id": "9", "metadata": {}, "outputs": [], "source": [ "bins = np.arange(5)\n", "values = np.array([4.0, 6.5, 3.2, 7.4, 5.8])\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.bar(bins, values, color=\"green\", label=\"count\")\n", "ax.scatter(bins, values, color=\"yellow\", label=\"sample mean\")\n", "ax.set_title(\"Bar + scatter overlay\")\n", "ax.set_xlabel(\"bin\")\n", "ax.set_ylabel(\"value\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "10", "metadata": {}, "source": [ "## 5 · Filled regions\n", "\n", "The backend supports both common `fill_between(...)` shapes:\n", "\n", "1. Filling down to a scalar baseline\n", "2. Filling the region between two full curves" ] }, { "cell_type": "code", "execution_count": null, "id": "11", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 5, 100)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.fill_between(\n", " x,\n", " np.exp(-0.5 * x) * np.sin(3 * x) + 1.0,\n", " 0.0,\n", " color=\"cyan\",\n", " label=\"signal envelope\",\n", ")\n", "ax.set_title(\"fill_between() to a scalar baseline\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"amplitude\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "code", "execution_count": null, "id": "12", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 4, 100)\n", "upper = np.sin(x) + 1.8\n", "lower = np.cos(x) + 0.8\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.fill_between(x, upper, lower, color=\"blue\", label=\"between curves\")\n", "ax.plot(x, upper, color=\"white\", label=\"upper\")\n", "ax.plot(x, lower, color=\"yellow\", label=\"lower\")\n", "ax.set_title(\"fill_between() between two curves\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "13", "metadata": {}, "source": [ "## 6 · Error bars and reference lines\n", "\n", "Reference-line helpers (`axhline`, `axvline`, `hlines`, `vlines`) are all available, and `errorbar()` works for scalar or array-shaped errors." ] }, { "cell_type": "code", "execution_count": null, "id": "14", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(1, 10, 9)\n", "y = np.sqrt(x)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.errorbar(x, y, yerr=0.15, color=\"cyan\", label=\"sqrt(x)\")\n", "ax.axhline(2.0, color=\"white\")\n", "ax.axvline(4.0, color=\"yellow\")\n", "ax.hlines([1.2, 2.7], xmin=[1, 5], xmax=[3, 9], color=\"green\")\n", "ax.vlines([2.0, 8.0], ymin=[1.0, 2.0], ymax=[1.8, 3.0], color=\"red\")\n", "ax.set_title(\"Error bars + reference lines\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"y\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "15", "metadata": {}, "source": [ "## 7 · Text and annotations\n", "\n", "`text()` is mapped directly. `annotate()` works too, and when you pass `arrowprops`, the backend draws a connector line toward the annotated point." ] }, { "cell_type": "code", "execution_count": null, "id": "16", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 6, 120)\n", "y = np.sin(x)\n", "peak_x = x[np.argmax(y)]\n", "peak_y = y.max()\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, y, color=\"cyan\")\n", "ax.text(0.8, -0.8, \"terminal note\", color=\"yellow\")\n", "ax.annotate(\n", " \"peak\",\n", " xy=(peak_x, peak_y),\n", " xytext=(4.4, 0.4),\n", " color=\"white\",\n", " arrowprops={\"color\": \"green\"},\n", ")\n", "ax.set_title(\"Text and annotations\")\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "17", "metadata": {}, "source": [ "## 8 · Limits, ticks, grid, and log scales\n", "\n", "Axis metadata is one of the nice parts of keeping the same `LinePlot` API across backends." ] }, { "cell_type": "code", "execution_count": null, "id": "18", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(1, 20, 120)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, x**0.5, color=\"cyan\", label=\"sqrt(x)\")\n", "ax.plot(x, np.log(x + 1), color=\"yellow\", label=\"log(x + 1)\")\n", "ax.set_title(\"Axis controls\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(\"value\")\n", "ax.set_xlim(1, 20)\n", "ax.set_ylim(0, 5)\n", "ax.set_xticks([1, 2, 5, 10, 20], [\"1\", \"2\", \"5\", \"10\", \"20\"])\n", "ax.set_yticks([0, 1, 2, 3, 4, 5])\n", "ax.set_xscale(\"log\")\n", "ax.set_grid(True)\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "19", "metadata": {}, "source": [ "## 9 · Layers\n", "\n", "Layer filtering works with the terminal backend too. This is useful when you want progressive reveals or to inspect subsets of a figure." ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 2 * np.pi, 100)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, np.sin(x), color=\"cyan\", label=\"layer 0\", layer=0)\n", "ax.plot(x, np.cos(x), color=\"yellow\", label=\"layer 1\", layer=1)\n", "ax.fill_between(x, np.sin(x) + 1.5, 0.0, color=\"green\", label=\"layer 2\", layer=2)\n", "ax.set_title(\"Layers 0 and 1 only\")\n", "ax.set_legend(True)" ] }, { "cell_type": "code", "execution_count": null, "id": "21", "metadata": {}, "outputs": [], "source": [ "print(canvas.plot(backend=\"plotext\", layers=[0]).build(keep_colors=False))" ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [], "source": [ "print(canvas.plot(backend=\"plotext\", layers=[1]).build(keep_colors=False))" ] }, { "cell_type": "code", "execution_count": null, "id": "23", "metadata": {}, "outputs": [], "source": [ "print(canvas.plot(backend=\"plotext\", layers=[0, 1]).build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "24", "metadata": {}, "source": [ "## 10 · Multi-subplot canvases\n", "\n", "Subplots are fully supported, including figure-level titles via `canvas.suptitle(...)`." ] }, { "cell_type": "code", "execution_count": null, "id": "25", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 2 * np.pi, 80)\n", "rng = np.random.default_rng(5)\n", "\n", "canvas, (ax1, ax2) = Canvas.subplots(ncols=2)\n", "\n", "ax1.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", "ax1.plot(x, np.cos(x), color=\"yellow\", label=\"cos(x)\")\n", "ax1.set_title(\"Signals\")\n", "ax1.set_xlabel(\"x\")\n", "ax1.set_ylabel(\"value\")\n", "ax1.set_legend(True)\n", "\n", "cats = np.arange(6)\n", "vals = rng.integers(2, 9, size=6)\n", "ax2.bar(cats, vals, color=\"green\", label=\"count\")\n", "ax2.scatter(cats, vals, color=\"red\", label=\"points\")\n", "ax2.set_title(\"Counts\")\n", "ax2.set_xlabel(\"bin\")\n", "ax2.set_ylabel(\"value\")\n", "ax2.set_legend(True)\n", "\n", "canvas.suptitle(\"Terminal dashboard\")\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "26", "metadata": {}, "source": [ "## 11 · Matrix-style `imshow()` output\n", "\n", "`add_imshow()` is rendered as a terminal matrix plot. This is the terminal approximation of image-like numeric data, not a full matplotlib colormap implementation." ] }, { "cell_type": "code", "execution_count": null, "id": "27", "metadata": {}, "outputs": [], "source": [ "data = np.arange(1, 26).reshape(5, 5)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.add_imshow(data)\n", "ax.set_title(\"Matrix-style imshow\")\n", "ax.set_xlabel(\"column\")\n", "ax.set_ylabel(\"row\")\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "28", "metadata": {}, "source": [ "## 12 · Patches and arbitrary patch geometry\n", "\n", "The backend supports common matplotlib patch types directly and also uses matplotlib patch geometry as a best-effort fallback for many other patch subclasses.\n", "\n", "- `matplotlib.patches.Rectangle`\n", "- `matplotlib.patches.Circle`\n", "- `matplotlib.patches.Polygon`\n", "- `matplotlib.patches.Ellipse`\n", "- many other patch types that expose a usable matplotlib path\n", "\n", "That is enough for many annotations, regions of interest, and geometric callouts." ] }, { "cell_type": "code", "execution_count": null, "id": "29", "metadata": {}, "outputs": [], "source": [ "canvas, ax = Canvas.subplots()\n", "ax.add_patch(\n", " mpatches.Rectangle(\n", " (0.2, 0.2), 1.3, 0.7, fill=False, edgecolor=\"yellow\", label=\"window\"\n", " )\n", ")\n", "ax.add_patch(\n", " mpatches.Circle((2.2, 1.6), 0.45, fill=False, edgecolor=\"cyan\", label=\"sensor\")\n", ")\n", "ax.add_patch(\n", " mpatches.Polygon(\n", " [[3.0, 0.5], [3.8, 1.2], [3.4, 2.0]],\n", " fill=True,\n", " facecolor=\"green\",\n", " label=\"region\",\n", " )\n", ")\n", "ax.add_patch(\n", " mpatches.Ellipse(\n", " (2.8, 1.0), 0.8, 0.5, fill=False, edgecolor=\"white\", label=\"ellipse\"\n", " )\n", ")\n", "ax.set_xlim(0, 4.5)\n", "ax.set_ylim(0, 2.5)\n", "ax.set_title(\"Supported patch types\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "30", "metadata": {}, "source": [ "### Plotly backend note (patches)\n", "\n", "Plotly can render common Matplotlib patches too.\n", "Note: Plotly “shapes” do not appear in legends, so maxplotlib adds a small dummy legend entry for labeled patches.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "31", "metadata": {}, "outputs": [], "source": [ "# Interactive rendering of the same canvas\n", "canvas.show(backend=\"plotly\")" ] }, { "cell_type": "markdown", "id": "32", "metadata": {}, "source": [ "## 13 · Captions, symlog scales, aspect, and colorbar notes\n", "\n", "The last backend-specific pieces of the `LinePlot` surface also work in the terminal backend:\n", "\n", "- `add_caption(...)`\n", "- `set_xscale('symlog')` and `set_yscale('symlog')`\n", "- `set_aspect(...)` with a best-effort terminal plotsize approximation\n", "- `add_colorbar(...)` as a note-style value summary for matrix/image output" ] }, { "cell_type": "code", "execution_count": null, "id": "33", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(-20, 20, 161)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, x**3, color=\"cyan\", label=\"x^3\")\n", "ax.set_title(\"Symlog example\")\n", "ax.add_caption(\"caption text\")\n", "ax.set_xscale(\"symlog\")\n", "ax.set_yscale(\"symlog\")\n", "ax.set_aspect(\"equal\")\n", "ax.set_legend(True)\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "code", "execution_count": null, "id": "34", "metadata": {}, "outputs": [], "source": [ "heat = np.arange(1, 26).reshape(5, 5)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.add_imshow(heat)\n", "ax.add_colorbar(label=\"intensity\")\n", "ax.set_title(\"Matrix + colorbar note\")\n", "\n", "print(canvas.plot(backend=\"plotext\").build(keep_colors=False))" ] }, { "cell_type": "markdown", "id": "35", "metadata": {}, "source": [ "## 14 · Saving terminal output to a file\n", "\n", "Saving with the plotext backend writes the rendered terminal figure to a text file. By default the saved text is plain and easy to inspect in editors, CI logs, or generated artifacts." ] }, { "cell_type": "markdown", "id": "36", "metadata": {}, "source": [ "### Plotly backend note (symlog)\n", "\n", "Plotly has no native symlog axis type. For `symlog`, maxplotlib applies a symmetric log transform to the data and uses a linear Plotly axis.\n", "This keeps the plot working across backends, but tick formatting may differ from Matplotlib/plotext.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "id": "37", "metadata": {}, "outputs": [], "source": [ "canvas.show(backend=\"plotly\")" ] }, { "cell_type": "code", "execution_count": null, "id": "38", "metadata": {}, "outputs": [], "source": [ "x = np.linspace(0, 2 * np.pi, 60)\n", "\n", "canvas, ax = Canvas.subplots()\n", "ax.plot(x, np.sin(x), color=\"cyan\", label=\"sin(x)\")\n", "ax.set_title(\"Saved terminal figure\")\n", "terminal_fig = canvas.plot(backend=\"plotext\")\n", "\n", "output_path = Path(\"plotext_output.txt\")\n", "terminal_fig.savefig(output_path)\n", "print(output_path.read_text().splitlines()[0])" ] }, { "cell_type": "markdown", "id": "39", "metadata": {}, "source": [ "## 15 · A simple terminal animation\n", "\n", "`plotext` does not provide browser-widget animation like Plotly or Matplotlib's GUI animation stack, but terminal animation is still practical: render a frame, clear the output, sleep briefly, and repeat.\n", "\n", "The example below works well in a notebook cell or a terminal Python session. In notebooks, `clear_output(wait=True)` keeps the cell output updating in place." ] }, { "cell_type": "code", "execution_count": null, "id": "40", "metadata": {}, "outputs": [], "source": [ "import time\n", "\n", "from IPython.display import clear_output\n", "\n", "x = np.linspace(0, 2 * np.pi, 120)\n", "\n", "for phase in np.linspace(0, 2 * np.pi, 24):\n", " canvas, ax = Canvas.subplots()\n", " ax.plot(x, np.sin(x + phase), color=\"cyan\", label=\"sin(x + phase)\")\n", " ax.plot(x, np.cos(x + phase), color=\"yellow\", label=\"cos(x + phase)\")\n", " ax.set_ylim(-1.2, 1.2)\n", " ax.set_title(f\"Animated phase = {phase:.2f}\")\n", " ax.set_legend(True)\n", "\n", " clear_output(wait=True)\n", " print(canvas.plot(backend=\"plotext\").build(keep_colors=False))\n", " time.sleep(0.08)" ] }, { "cell_type": "markdown", "id": "41", "metadata": {}, "source": [ "## 16 · Current limitations\n", "\n", "| Feature | Status | Notes |\n", "| --- | --- | --- |\n", "| `add_colorbar()` | ⚠️ | rendered as a compact note-style min/max summary rather than a true continuous side bar |\n", "| Arbitrary matplotlib patch subclasses | ⚠️ | best-effort path extraction works for many patch types, but not every custom patch will map perfectly |\n", "| Exact matplotlib styling parity | ❌ | Terminal rendering is approximate by design |\n", "| Notebook-style high-frame-rate animation | ⚠️ | possible via redraw loops, but not a substitute for GUI animation widgets |\n", "\n", "For terminal work, this backend now covers a large and practical portion of the `LinePlot` API. When you need richer styling, publication export, or interactive browser behavior, use the matplotlib, tikzfigure, or plotly backends instead." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.3" } }, "nbformat": 4, "nbformat_minor": 5 }