A Textual widget wrapper library for Plotext
"""A small self-contained example of a Plotext plot in a Textual app."""

from textual.app import App, ComposeResult

from textual_plotext import PlotextPlot


class ScatterApp(App[None]):
    """Example Textual application showing a Plotext plot."""

    def compose(self) -> ComposeResult:
        """Compose the plotting widget."""
        yield PlotextPlot()

    def on_mount(self) -> None:
        """Set up the plot."""
        plt = self.query_one(PlotextPlot).plt
        plt.title("Scatter Plot")
        plt.scatter(plt.sin())


if __name__ == "__main__":
    ScatterApp().run()

examples/scatter.py
Textualize/textual-plotext
"""A longer-form example of using textual-plotext."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta
from itertools import cycle
from json import loads
from typing import Any
from urllib.error import URLError
from urllib.request import Request, urlopen

from textual import on, work
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.message import Message
from textual.reactive import reactive, var
from textual.widgets import Footer, Header
from typing_extensions import Final

from textual_plotext import PlotextPlot

TEXTUAL_ICBM: Final[tuple[float, float]] = (55.9533, -3.1883)
"""The ICBM address of the approximate location of Textualize HQ."""


class Weather(PlotextPlot):
    """A widget for plotting weather data."""

    marker: reactive[str] = reactive("sd")
    """The type of marker to use for the plot."""

    def __init__(
        self,
        title: str,
        *,
        name: str | None = None,
        id: str | None = None,  # pylint:disable=redefined-builtin
        classes: str | None = None,
        disabled: bool = False,
    ) -> None:
        """Initialise the weather widget.

        Args:
            name: The name of the weather widget.
            id: The ID of the weather widget in the DOM.
            classes: The CSS classes of the weather widget.
            disabled: Whether the weather widget is disabled or not.
        """
        super().__init__(name=name, id=id, classes=classes, disabled=disabled)
        self._title = title
        self._unit = "Loading..."
        self._data: list[float] = []
        self._time: list[str] = []

    def on_mount(self) -> None:
        """Plot the data using Plotext."""
        self.plt.date_form("Y-m-d H:M")
        self.plt.title(self._title)
        self.plt.xlabel("Time")

    def replot(self) -> None:
        """Redraw the plot."""
        self.plt.clear_data()
        self.plt.ylabel(self._unit)
        self.plt.plot(self._time, self._data, marker=self.marker)
        self.refresh()

    def update(self, data: dict[str, Any], values: str) -> None:
        """Update the data for the weather plot.

        Args:
            data: The data from the weather API.
            values: The name of the values to plot.
        """
        self._data = data["hourly"][values]
        self._time = [moment.replace("T", " ") for moment in data["hourly"]["time"]]
        self._unit = data["hourly_units"][values]
        self.replot()

    def _watch_marker(self) -> None:
        """React to the marker being changed."""
        self.replot()


class TextualTowersWeatherApp(App[None]):
    """An application for showing recent Textualize weather."""

    CSS = """
    Grid {
        grid-size: 2;
    }

    Weather {
        padding: 1 2;
    }
    """

    TITLE = "Weather at Textual Towers Around a Year Ago"

    BINDINGS = [
        ("d", "app.toggle_dark", "Toggle light/dark mode"),
        ("m", "marker", "Cycle example markers"),
        ("q", "app.quit", "Quit the example"),
    ]

    MARKERS = {
        "dot": "Dot",
        "hd": "High Definition",
        "fhd": "Higher Definition",
        "braille": "Braille",
        "sd": "Standard Definition",
    }

    marker: var[str] = var("sd")
    """The marker used for each of the plots."""

    def __init__(self) -> None:
        """Initialise the application."""
        super().__init__()
        self._markers = cycle(self.MARKERS.keys())

    def compose(self) -> ComposeResult:
        """Compose the display of the example app."""
        yield Header()
        with Grid():
            yield Weather("Temperature", id="temperature")
            yield Weather("Wind Speed (10m)", id="windspeed")
            yield Weather("Precipitation", id="precipitation")
            yield Weather("Surface Pressure", id="pressure")
        yield Footer()

    def on_mount(self) -> None:
        """Start the process of gathering the weather data."""
        self.gather_weather()

    @dataclass
    class WeatherData(Message):
        """Message posted once the weather data has been gathered."""

        history: dict[str, Any]
        """The history data gathered from the weather API."""

    @work(thread=True, exclusive=True)
    def gather_weather(self) -> None:
        """Worker thread that gathers historical weather data."""
        end_date = (
            datetime.now() - timedelta(days=365) + timedelta(weeks=1)
        )  # Yes, yes, I know. It's just an example.
        start_date = end_date - timedelta(weeks=2)  # Two! Weeks!
        try:
            with urlopen(
                Request(
                    "https://archive-api.open-meteo.com/v1/archive?"
                    f"latitude={TEXTUAL_ICBM[0]}&longitude={TEXTUAL_ICBM[1]}"
                    f"&start_date={start_date.strftime('%Y-%m-%d')}"
                    f"&end_date={end_date.strftime('%Y-%m-%d')}"
                    "&hourly=temperature_2m,precipitation,surface_pressure,windspeed_10m"
                )
            ) as result:
                self.post_message(
                    self.WeatherData(loads(result.read().decode("utf-8")))
                )
        except URLError as error:
            self.notify(
                str(error),
                title="Error loading weather data",
                severity="error",
                timeout=6,
            )

    @on(WeatherData)
    def populate_plots(self, event: WeatherData) -> None:
        """Populate the weather plots with data received from the API.

        Args:
            event: The weather data reception event.
        """
        with self.batch_update():
            self.query_one("#temperature", Weather).update(
                event.history, "temperature_2m"
            )
            self.query_one("#windspeed", Weather).update(event.history, "windspeed_10m")
            self.query_one("#precipitation", Weather).update(
                event.history, "precipitation"
            )
            self.query_one("#pressure", Weather).update(
                event.history, "surface_pressure"
            )

    def watch_marker(self) -> None:
        """React to the marker type being changed."""
        self.sub_title = self.MARKERS[self.marker]
        for plot in self.query(Weather).results(Weather):
            plot.marker = self.marker

    def action_marker(self) -> None:
        """Cycle to the next marker type."""
        self.marker = next(self._markers)


if __name__ == "__main__":
    TextualTowersWeatherApp().run()

examples/textual_towers_weather.py
Textualize/textual-plotext
"""A Textual widget library for wrapping the Plotext terminal plotting library."""

from .plot import Plot, themes
from .plotext_plot import PlotextPlot

__all__ = ["Plot", "PlotextPlot", "themes"]

src/textual_plotext/__init__.py
Textualize/textual-plotext
"""Main entry point for the library.

When run as:

```sh
$ python -m textual_plotext
```

it will show a demonstration of the library in action.
"""

# The following code borrows heavily from the Plotext readme files, and as
# such has some variables names and the like that would generally annoy
# pylint; so here we ask pylint to ease up for this particular file.
#
# Also, because it's common with Textual to introduce a property in
# `on_mount`, we ask pylint to ease up on that too.
#
# pylint:disable=invalid-name, attribute-defined-outside-init

from __future__ import annotations

import random
from datetime import datetime
from itertools import chain, cycle

from textual.app import App, ComposeResult
from textual.containers import VerticalScroll
from textual.widgets import Header, Label, Rule, TabbedContent, TabPane

from textual_plotext import PlotextPlot


class ExamplesPane(VerticalScroll):
    """Base class for a pane of examples."""

    DEFAULT_CSS = """
    ExamplesPane {
        scrollbar-gutter: stable;
    }

    ExamplesPane Label {
        margin: 2;
        width: 1fr;
        text-align: center;
    }
    """

    def examples(self, source: str, examples: list[PlotextPlot]) -> ComposeResult:
        """Provide the composed examples.

        Args:
            source: The source for the examples within the Plotext readme files.
            examples: A list of the example widgets.
        """
        yield Label(
            "Examples taken from https://github.com/piccolomo/"
            f"plotext/blob/master/readme/{source}.md"
        )
        for example in examples:
            yield example
            yield Rule()


class BasicPlots(ExamplesPane):
    """Examples from the basic section of the Plotext documentation."""

    class ScatterPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/basic.md#scatter-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.scatter(self.plt.sin())
            self.plt.title("Scatter Plot")

    class LinePlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/basic.md#line-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.plot(self.plt.sin())
            self.plt.title("Line Plot")

    class LogPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/basic.md#log-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.plot(self.plt.sin(periods=2, length=10**4))
            self.plt.xscale("log")
            self.plt.yscale("linear")
            self.plt.grid(0, 1)
            self.plt.title("Logarithmic Plot")
            self.plt.xlabel("logarithmic scale")
            self.plt.ylabel("linear scale")
            # A slightly hacky workaround for what seems to be a bug in
            # Plotext itself when it comes to log scales. We force a build
            # of the data...
            _ = self.plt.build()
            # ...then put the scale back to linear so it doesn't try and
            # apply log again and again on each render.
            self.plt.xscale("linear")

    class StemPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/basic.md#stem-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.plot(self.plt.sin(), fillx=True)
            self.plt.title("Stem Plot")

    class MultipleDataSets(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/basic.md#multiple-data-sets"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.plot(self.plt.sin(), label="plot")
            self.plt.scatter(self.plt.sin(phase=-1), label="scatter")
            self.plt.title("Multiple Data Set")

    class MultipleAxesPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/basic.md#multiple-axes-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.plot(
                self.plt.sin(), xside="lower", yside="left", label="lower left"
            )
            self.plt.plot(
                self.plt.sin(2, phase=-1),
                xside="upper",
                yside="right",
                label="upper right",
            )
            self.plt.title("Multiple Axes Plot")

    def compose(self) -> ComposeResult:
        """Compose the child widgets."""
        return self.examples(
            "basic",
            [
                self.ScatterPlot(),
                self.LinePlot(),
                self.LogPlot(),
                self.StemPlot(),
                self.MultipleDataSets(),
                self.MultipleAxesPlot(),
            ],
        )


class BarPlots(ExamplesPane):
    """Examples from the bar plots section of the Plotext documentation."""

    class VerticalBarPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/bar.md#vertical-bar-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            pizzas = ["Sausage", "Pepperoni", "Mushrooms", "Cheese", "Chicken", "Beef"]
            percentages = [14, 36, 11, 8, 7, 4]
            self.plt.bar(pizzas, percentages)
            self.plt.title("Most Favored Pizzas in the World")

    class HorizontalBarPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/bar.md#horizontal-bar-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            pizzas = ["Sausage", "Pepperoni", "Mushrooms", "Cheese", "Chicken", "Beef"]
            percentages = [14, 36, 11, 8, 7, 4]
            self.plt.bar(
                pizzas, percentages, orientation="horizontal", width=3 / 5
            )  # or in short orientation = 'h'
            self.plt.title("Most Favoured Pizzas in the World")

    class MultipleBarPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/bar.md#multiple-bar-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            pizzas = ["Sausage", "Pepperoni", "Mushrooms", "Cheese", "Chicken", "Beef"]
            male_percentages = [14, 36, 11, 8, 7, 4]
            female_percentages = [12, 20, 35, 15, 2, 1]
            self.plt.multiple_bar(
                pizzas, [male_percentages, female_percentages]
            )  # , labels = ["men", "women"])
            self.plt.title("Most Favored Pizzas in the World by Gender")

    class StackedBarPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/bar.md#stacked-bar-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            pizzas = ["Sausage", "Pepperoni", "Mushrooms", "Cheese", "Chicken", "Beef"]
            male_percentages = [14, 36, 11, 8, 7, 4]
            female_percentages = [12, 20, 35, 15, 2, 1]
            self.plt.stacked_bar(
                pizzas, [male_percentages, female_percentages]
            )  # , labels = ["men", "women"])
            self.plt.title("Most Favored Pizzas in the World by Gender")

    class HistogramPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/bar.md#histogram-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            l = 7 * 10**4
            data1 = [random.gauss(0, 1) for _ in range(10 * l)]
            data2 = [random.gauss(3, 1) for _ in range(6 * l)]
            data3 = [random.gauss(6, 1) for _ in range(4 * l)]
            bins = 60
            self.plt.hist(data1, bins, label="mean 0")
            self.plt.hist(data2, bins, label="mean 3")
            self.plt.hist(data3, bins, label="mean 6")
            self.plt.title("Histogram Plot")

    def compose(self) -> ComposeResult:
        """Compose the child widgets."""
        return self.examples(
            "bar",
            [
                self.VerticalBarPlot(),
                self.HorizontalBarPlot(),
                self.MultipleBarPlot(),
                self.StackedBarPlot(),
                self.HistogramPlot(),
            ],
        )


class SpecialPlots(ExamplesPane):
    """Examples from the special plots section of the Plotext documentation."""

    class ErrorPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/special.md#error-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            l = 20
            ye = [random.random() for _ in range(l)]
            xe = [random.random() for _ in range(l)]
            data = self.plt.sin(length=l)
            self.plt.error(data, xerr=xe, yerr=ye)
            self.plt.title("Error Plot")

    class EventPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/special.md#event-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.date_form("H:M")
            times = self.plt.datetimes_to_string(
                [
                    datetime(
                        2022,
                        3,
                        27,
                        random.randint(0, 23),
                        random.randint(0, 59),
                        random.randint(0, 59),
                    )
                    for _ in range(100)
                ]
            )
            self.plt.event_plot(times)

    class StreamingDataPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/special.md#streaming-data"""

        def on_mount(self) -> None:
            """Set up the initial conditions for the 'streaming' data."""
            self.frame = 0
            self.plt.title("Streaming Data")
            self.set_interval(0.25, self.plot)

        def plot(self) -> None:
            """Plot the current frame of the stream."""
            self.plt.clear_data()
            self.plt.scatter(
                self.plt.sin(periods=2, length=1_000, phase=(2 * self.frame) / 50)
            )
            self.refresh()
            self.frame += 1

    class MatrixPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/special.md#matrix-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.title("Matrix Plot")

        def on_resize(self) -> None:
            """Remake the data when the size changes."""
            p = 1
            matrix = [
                [
                    (abs(r - self.size.height / 2) + abs(c - self.size.width / 2)) ** p
                    for c in range(self.size.width)
                ]
                for r in range(self.size.height)
            ]
            self.plt.clear_data()
            self.plt.matrix_plot(matrix)

    class ConfusionMatrix(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/special.md#confusion-matrix"""

        def on_mount(self) -> None:
            """Set up the plot."""
            l = 300
            actual = [random.randrange(0, 4) for _ in range(l)]
            predicted = [random.randrange(0, 4) for _ in range(l)]
            labels = ["Autumn", "Spring", "Summer", "Winter"]
            self.plt.cmatrix(actual, predicted, labels=labels)

    def compose(self) -> ComposeResult:
        """Compose the child widgets."""
        return self.examples(
            "special",
            [
                self.ErrorPlot(),
                self.EventPlot(),
                self.StreamingDataPlot(),
                self.MatrixPlot(),
                self.ConfusionMatrix(),
            ],
        )


class DecoratorPlots(ExamplesPane):
    """Examples from the decorator plots section of the Plotext documentation."""

    class LinePlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/decorator.md#line-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.scatter(self.plt.sin())
            self.plt.title("Extra Lines")
            self.plt.vline(100, "magenta")
            self.plt.hline(0.5, "blue+")
            self.plt.plotsize(100, 30)

    class TextPlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/decorator.md#text-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            pizzas = ["Sausage", "Pepperoni", "Mushrooms", "Cheese", "Chicken", "Beef"]
            percentages = [14, 36, 11, 8, 7, 4]
            self.plt.bar(pizzas, percentages)
            self.plt.title("Labelled Bar Plot using Text()")
            for i, pizza in enumerate(pizzas):
                self.plt.text(
                    pizza,
                    x=i + 1,
                    y=percentages[i] + 1.5,
                    alignment="center",
                    color="red",
                )
            self.plt.ylim(0, 38)

    class ShapePlot(PlotextPlot):
        """https://github.com/piccolomo/plotext/blob/master/readme/decorator.md#shape-plot"""

        def on_mount(self) -> None:
            """Set up the plot."""
            self.plt.title("Shapes")
            self.plt.polygon()
            self.plt.rectangle()
            self.plt.polygon(sides=100)  # to simulate a circle
            self.plt.show()

    class PulsePlot(PlotextPlot):
        """I made this one up myself."""

        def on_mount(self) -> None:
            """Set up the pulsing polygon example."""
            self.plt.title("Pulse")
            self.steps = cycle(chain(range(3, 21), range(20, 2, -1)))
            self.set_interval(0.5, self.plot)

        def plot(self) -> None:
            """Plot a polygon with a changing number of sides."""
            self.plt.clear_data()
            self.plt.polygon(sides=next(self.steps))
            self.refresh()

    def compose(self) -> ComposeResult:
        return self.examples(
            "decorator",
            [self.LinePlot(), self.TextPlot(), self.ShapePlot(), self.PulsePlot()],
        )


class DemoApp(App[None]):
    """Demonstration application for the library."""

    CSS = """
    PlotextPlot {
        height: 40%
    }

    ShapePlot, PulsePlot {
        height: 100%;
    }
    """

    TITLE = "textual-plotext Demonstration"

    def compose(self) -> ComposeResult:
        """Compose the child widgets."""
        yield Header()
        with TabbedContent():
            with TabPane("Basic Plots"):
                yield BasicPlots()
            with TabPane("Bar Plots"):
                yield BarPlots()
            with TabPane("Special Plots"):
                yield SpecialPlots()
            with TabPane("Decorator Plots"):
                yield DecoratorPlots()


if __name__ == "__main__":
    DemoApp().run()

src/textual_plotext/__main__.py
Textualize/textual-plotext
"""Provides a class that acts as a thin wrapper around Plottext.

Because (as of the time of writing) there doesn't seem to be a method of
getting a plotting object from Plottext, this class pulls a figure out from
inside Plotext and promotes it to the top level, so to speak, and then takes
care of adding in some of the utility methods that will bee needed.
"""

from __future__ import annotations

from typing import Any, Tuple, Union

from typing_extensions import Literal, TypeAlias, get_args

from . import plotext
from .plotext._figure import _figure_class as Figure

ThemeName = Literal[
    "clear",
    "dark",
    "default",
    "dreamland",
    "elegant",
    "girly",
    "grandpa",
    "matrix",
    "mature",
    "pro",
    "retro",
    "sahara",
    "salad",
    "scream",
    "serious",
    "windows",
]
"""Literal type that is the list of theme names defined in Plotext.

Note:
    This is a copy of the theme names and will need to be updated if Plotext
    ever adds more. The main reasons for taking this approach are:

    1. There is no public interface to access this information.
    2. Turning runtime information into type-checking data isn't simple.
"""

Color: TypeAlias = Union[str, int, Tuple[int, int, int]]
"""Type of a Plotext colour."""


class Plot(Figure):
    """A class that provides a Textual-friendly interface to Plotext.

    This class inherits from Plotext's `_figure_class` and then adds access
    to some global functions, making it appear like the top-level `plotext`
    module.

    The aim of the class is to make this:

    ```python
    import plotext as plt

    plt.title("Scatter Plot")
    plt.scatter(plt.sin())
    ```

    and this:

    ```python
    plt = Plot()
    plt.title("Scatter Plot")
    plt.scatter(plt.sin())
    ```

    functionally equivalent, but with the advantage that the latter has no
    global state and is free of external side-effects.
    """

    @staticmethod
    def sin(
        periods: int = 2,
        length: int = 200,
        amplitude: int = 1,
        phase: float = 0,
        decay: int = 0,
    ) -> list[float]:
        """A wrapper around `Plotext.sin`."""
        return plotext.sin(
            periods=periods,
            length=length,
            amplitude=amplitude,
            phase=phase,
            decay=decay,
        )

    @staticmethod
    def square(periods: int = 2, length: int = 200, amplitude: float = 1) -> list[int]:
        """A wrapper around `Plotext.square`."""
        return plotext.square(periods=periods, length=length, amplitude=amplitude)

    @staticmethod
    def colorize(
        string: str,
        fullground: Color | None = None,
        style: str | None = None,
        background: Color | None = None,
        show: bool = False,
    ) -> str:
        """A wrapper around `Plotet.colorize`."""
        return plotext.colorize(
            string,
            fullground=fullground,
            style=style,
            background=background,
            show=show,
        )

    @staticmethod
    def uncolorize(string: str) -> str:
        """A wrapper around `Plotext.uncolorize`."""
        return plotext.uncolorize(string)

    @staticmethod
    def transpose(data: list[list[Any]]) -> list[list[Any]]:
        """A wrapper around `Plotext.transpose`."""
        return plotext.transpose(data)

    platform = plotext.platform
    """The platform, as recognised by Plotext.

    One of either `unix` or `windows`.
    """

    def show(self) -> None:
        """Stub function. This should never be called within Textual."""

    def save_fig(
        self, path: str | None = None, append: bool = False, keep_colors: bool = False
    ) -> None:
        """Stub function. This should not be called within Textual."""
        # Actually... it might make sense to support this at some point and
        # might not be too tricky to handle. But for now this is about
        # allowing plotting within the application.
        del path, append, keep_colors


# Hoist the docstrings for the wrapper functions we've added above.
Plot.sin.__doc__ = plotext.sin.__doc__
Plot.square.__doc__ = plotext.square.__doc__
Plot.colorize.__doc__ = plotext.colorize.__doc__
Plot.uncolorize.__doc__ = plotext.uncolorize.__doc__
Plot.transpose.__doc__ = plotext.transpose.__doc__


def themes() -> tuple[str, ...]:
    """Get the list of available theme names.

    Returns:
        A tuple of the names of the themes defined in Plotext.
    """
    return get_args(ThemeName)

src/textual_plotext/plot.py
Textualize/textual-plotext
"""Wrapper module for the main Plotext module.

Designed to help type hint the public symbols in the module. See also the
accompanying pyi file for the types.
"""

from plotext import *

src/textual_plotext/plotext/__init__.py
Textualize/textual-plotext
"""Wrapper module for the Plotext _figure_class.

Designed to help type hint the methods in the class. See also the
accompanying pyi file for the types.
"""

from plotext._figure import _figure_class

__all__ = ["_figure_class"]

src/textual_plotext/plotext/_figure.py
Textualize/textual-plotext
"""Provides a widget for creating and displaying a Plotext plot."""

from __future__ import annotations

from rich.text import Text
from textual.app import RenderResult
from textual.reactive import var
from textual.widget import Widget

from .plot import Plot, ThemeName


class PlotextPlot(Widget):
    """A Plotext plot display widget."""

    DEFAULT_CSS = """
    PlotextPlot {
        width: 1fr;
        height: 1fr;
    }
    """

    auto_theme: var[bool] = var(True)
    """Should the plot automatically adjust the theme depending on light and dark mode?

    If set to `True` the theme of the plot will be adjusted to either
    [`light_mode_theme`][PlotextPlot.light_mode_theme] or
    [`dark_mode_theme`][PlotextPlot.dark_mode_theme] when
    [`App.dark`](https://textual.textualize.io/api/app/#textual.app.App.dark)
    is changed.
    """

    light_mode_theme: var[ThemeName] = var("default")
    """The Plotext theme to use for light mode.

    Note:
        This theme is only used when [`auto_theme`][PlotextPlot.auto_theme] is `True`.
    """

    dark_mode_theme: var[ThemeName] = var("dark")
    """The Plotext theme to use for dark mode.

    Note:
        This theme is only used when [`auto_theme`][PlotextPlot.auto_theme] is `True`.
    """

    def __init__(
        self,
        *,
        name: str | None = None,
        id: str | None = None,  # pylint:disable=redefined-builtin
        classes: str | None = None,
        disabled: bool = False,
    ) -> None:
        """Initialise the Plotext plot widget.

        Args:
            name: The name of the Plotext plot widget.
            id: The ID of the Plotext plot widget in the DOM.
            classes: The CSS classes of the Plotext plot widget.
            disabled: Whether the Plotext plot widget is disabled or not.
        """
        super().__init__(name=name, id=id, classes=classes, disabled=disabled)
        self._plot = Plot()
        self.watch(self.app, "dark", self._dark_mode, init=False)

    @property
    def plt(self) -> Plot:
        """The Plotext plotting object.

        Whereas normally Plotext-using code would so something like this:

        ```python
        import plotext as plt

        ...

        plt.some_call()
        plt.some_other_call()
        ```

        use this `plt` property instead of importing `plotext`.
        """
        return self._plot

    def render(self) -> RenderResult:
        """Render the plot.

        Returns:
            The renderable for displaying the plot.
        """
        self._plot.plotsize(self.size.width, self.size.height)
        if self.auto_theme:
            self._plot.theme(
                self.dark_mode_theme if self.app.dark else self.light_mode_theme
            )
        return Text.from_ansi(self._plot.build())

    def _watch_light_mode_theme(self) -> None:
        """React to changes to the light mode theme."""
        if self.auto_theme and not self.app.dark:
            self.refresh()

    def _watch_dark_mode_theme(self) -> None:
        """React to changes to the dark mode theme."""
        if self.auto_theme and self.app.dark:
            self.refresh()

    def _dark_mode(self) -> None:
        """React to dark mode being toggled."""
        if self.auto_theme:
            self.refresh()

src/textual_plotext/plotext_plot.py
Textualize/textual-plotext