Source code for glyphx.accessor

"""
GlyphX pandas DataFrame accessor.

After importing ``glyphx``, every DataFrame gains a ``.glyphx`` accessor
that creates fully configured, chainable Figure objects directly from
column names::

    import pandas as pd
    import glyphx  # registers the accessor

    df = pd.read_csv("sales.csv")

    # One-liner bar chart
    df.glyphx.bar(x="month", y="revenue", title="Monthly Revenue").share("report.html")

    # Full chain
    (
        df.glyphx
          .line(x="date", y="price", theme="dark", label="Price")
          .set_ylabel("USD")
          .annotate("Peak", x="2024-10", y=5400)
          .share("price_chart.html")
    )
"""
from __future__ import annotations

from typing import Any

import pandas as pd


[docs] @pd.api.extensions.register_dataframe_accessor("glyphx") class GlyphXAccessor: """ Pandas DataFrame accessor that exposes the full GlyphX plotting API. Registered automatically when ``glyphx`` is imported. Access via ``df.glyphx.<method>(...)``. All methods return a :class:`~glyphx.Figure` so results can be further customised via method chaining. """ def __init__(self, df: pd.DataFrame) -> None: self._df = df # ── Internal helpers ───────────────────────────────────────────────── def _col(self, name: str | None) -> list | None: """Return column as list, or None if name is None / not in df.""" if name is None or name not in self._df.columns: return None return self._df[name].tolist() def _fig( self, title: str | None, theme: str | dict | None, legend: str | bool | None, width: int, height: int, xlabel: str | None, ylabel: str | None, auto_display: bool, ): """Build a base Figure with common options pre-applied.""" from .figure import Figure fig = Figure( title=title, theme=theme, legend=legend, width=width, height=height, auto_display=auto_display, ) if xlabel: fig.axes.xlabel = xlabel if ylabel: fig.axes.ylabel = ylabel return fig # ── Chart methods ─────────────────────────────────────────────────────
[docs] def line( self, x: str | None = None, y: str | None = None, color: str | None = None, label: str | None = None, linestyle: str = "solid", yerr: str | None = None, title: str | None = None, theme: str | dict | None = None, legend: str | bool | None = "top-right", width: int = 640, height: int = 480, xlabel: str | None = None, ylabel: str | None = None, auto_display: bool = True, **kwargs: Any, ): """ Create a line chart from DataFrame columns. Args: x: Column name for X axis. y: Column name for Y axis. yerr: Column name for Y error bars (optional). label: Legend label; defaults to the ``y`` column name. Returns: :class:`~glyphx.Figure` — fully chainable. """ from .series import LineSeries fig = self._fig(title, theme, legend, width, height, xlabel or x, ylabel or y, auto_display) hue = kwargs.pop("hue", None) if hue and hue in self._df.columns: theme_colors = fig.theme.get("colors", ["#1f77b4", "#ff7f0e", "#2ca02c"]) for i, (grp_val, grp_df) in enumerate(self._df.groupby(hue)): fig.add(LineSeries( grp_df[x].tolist() if x else list(range(len(grp_df))), grp_df[y].tolist() if y else grp_df.select_dtypes("number").iloc[:, 0].tolist(), color=theme_colors[i % len(theme_colors)], label=str(grp_val), linestyle=linestyle, )) else: x_data = self._col(x) or list(range(len(self._df))) y_data = self._col(y) or self._df.select_dtypes("number").iloc[:, 0].tolist() err = self._col(yerr) fig.add(LineSeries( x_data, y_data, color=color, label=label or y, linestyle=linestyle, yerr=err, **kwargs, )) return fig
[docs] def bar( self, x: str | None = None, y: str | None = None, color: str | None = None, label: str | None = None, yerr: str | None = None, groupby: str | None = None, hue: str | None = None, agg: str = "sum", title: str | None = None, theme: str | dict | None = None, legend: str | bool | None = "top-right", width: int = 640, height: int = 480, xlabel: str | None = None, ylabel: str | None = None, auto_display: bool = True, **kwargs: Any, ): """ Create a bar chart from DataFrame columns. Pass ``groupby`` or ``hue`` to create one series per unique group, each colored automatically from the theme palette. ``hue`` splits by a column while keeping x/y semantics; ``groupby`` aggregates. Returns: :class:`~glyphx.Figure` """ from .series import BarSeries # Resolve hue alias: hue splits without aggregation effective_groupby = hue or groupby or None fig = self._fig(title, theme, legend, width, height, xlabel or x, ylabel or y, auto_display) if effective_groupby and effective_groupby in self._df.columns: from .grouped_bar import GroupedBarSeries theme_colors = fig.theme.get("colors", ["#1f77b4", "#ff7f0e", "#2ca02c"]) num_col = str(y or self._df.select_dtypes("number").columns[0]) if hue and not groupby and x and x in self._df.columns: # Hue mode with X column → one BarSeries per unique hue value, # each containing the rows that belong to that group. hue_vals = list(self._df[hue].unique()) for i, hv in enumerate(hue_vals): grp_df = self._df[self._df[hue] == hv] x_data = grp_df[x].tolist() y_data = grp_df[num_col].tolist() fig.add(BarSeries( x_data, y_data, color=theme_colors[i % len(theme_colors)], label=str(hv), )) elif hue and not groupby: # Hue without X → one aggregated bar per hue group agg_df = ( self._df.groupby(hue)[num_col] .agg(agg).reset_index().sort_values(hue) ) for i, row in enumerate(agg_df.itertuples(index=False)): fig.add(BarSeries( [str(getattr(row, hue))], [float(getattr(row, num_col))], color=theme_colors[i % len(theme_colors)], label=str(getattr(row, hue)), )) else: # groupby aggregation mode agg_df = ( self._df.groupby(effective_groupby)[num_col] .agg(agg).reset_index().sort_values(effective_groupby) ) for i, row in enumerate(agg_df.itertuples(index=False)): grp = getattr(row, effective_groupby) val = getattr(row, num_col) fig.add(BarSeries( [str(grp)], [float(val)], color=theme_colors[i % len(theme_colors)], label=str(grp), )) else: x_data = self._col(x) or list(range(len(self._df))) y_data = self._col(y) or self._df.select_dtypes("number").iloc[:, 0].tolist() err = self._col(yerr) fig.add(BarSeries( x_data, y_data, color=color, label=label or y, yerr=err, **kwargs, )) return fig
[docs] def scatter( self, x: str | None = None, y: str | None = None, color: str | None = None, label: str | None = None, size: int = 5, marker: str = "circle", title: str | None = None, theme: str | dict | None = None, legend: str | bool | None = "top-right", width: int = 640, height: int = 480, xlabel: str | None = None, ylabel: str | None = None, auto_display: bool = True, **kwargs: Any, ): """Create a scatter plot from DataFrame columns. Returns :class:`~glyphx.Figure`.""" from .series import ScatterSeries fig = self._fig(title, theme, legend, width, height, xlabel or x, ylabel or y, auto_display) hue = kwargs.pop("hue", None) if hue and hue in self._df.columns: theme_colors = fig.theme.get("colors", ["#1f77b4", "#ff7f0e", "#2ca02c"]) for i, (grp_val, grp_df) in enumerate(self._df.groupby(hue)): fig.add(ScatterSeries( grp_df[x].tolist() if x else list(range(len(grp_df))), grp_df[y].tolist() if y else grp_df.select_dtypes("number").iloc[:, 0].tolist(), color=theme_colors[i % len(theme_colors)], label=str(grp_val), size=size, marker=marker, )) else: x_data = self._col(x) or list(range(len(self._df))) y_data = self._col(y) or self._df.select_dtypes("number").iloc[:, 0].tolist() fig.add(ScatterSeries( x_data, y_data, color=color, label=label or y, size=size, marker=marker, **kwargs, )) return fig
[docs] def hist( self, col: str | None = None, bins: int = 10, color: str | None = None, label: str | None = None, title: str | None = None, theme: str | dict | None = None, width: int = 640, height: int = 480, xlabel: str | None = None, ylabel: str | None = None, auto_display: bool = True, **kwargs: Any, ): """Create a histogram of a numeric column. Returns :class:`~glyphx.Figure`.""" from .series import HistogramSeries target = col or self._df.select_dtypes("number").columns[0] data = self._df[target].dropna().tolist() fig = self._fig(title, theme, "top-right", width, height, xlabel or target, ylabel or "Count", auto_display) fig.add(HistogramSeries(data, bins=bins, color=color, label=label or target)) return fig
[docs] def box( self, col: str | None = None, groupby: str | None = None, color: str | None = None, title: str | None = None, theme: str | dict | None = None, width: int = 640, height: int = 480, auto_display: bool = True, **kwargs: Any, ): """Create a box plot. Pass ``groupby`` for multi-box comparison. Returns :class:`~glyphx.Figure`.""" from .series import BoxPlotSeries target = col or self._df.select_dtypes("number").columns[0] fig = self._fig(title, theme, False, width, height, None, target, auto_display) if groupby and groupby in self._df.columns: groups = self._df[groupby].unique().tolist() arrays = [ self._df[self._df[groupby] == g][target].dropna().tolist() for g in groups ] fig.add(BoxPlotSeries(arrays, categories=[str(g) for g in groups], color=color or "#1f77b4")) else: data = self._df[target].dropna().tolist() fig.add(BoxPlotSeries(data, color=color or "#1f77b4")) return fig
[docs] def pie( self, labels: str | None = None, values: str | None = None, title: str | None = None, theme: str | dict | None = None, width: int = 480, height: int = 480, auto_display: bool = True, **kwargs: Any, ): """Create a pie chart. Returns :class:`~glyphx.Figure`.""" from .series import PieSeries lbl_data = self._col(labels) val_data = self._col(values) or self._df.select_dtypes("number").iloc[:, 0].tolist() fig = self._fig(title, theme, False, width, height, None, None, auto_display) fig.add(PieSeries(val_data, labels=lbl_data, **kwargs)) return fig
[docs] def donut( self, labels: str | None = None, values: str | None = None, title: str | None = None, theme: str | dict | None = None, width: int = 480, height: int = 480, auto_display: bool = True, **kwargs: Any, ): """Create a donut chart. Returns :class:`~glyphx.Figure`.""" from .series import DonutSeries lbl_data = [str(v) for v in (self._col(labels) or range(len(self._df)))] val_data = self._col(values) or self._df.select_dtypes("number").iloc[:, 0].tolist() fig = self._fig(title, theme, False, width, height, None, None, auto_display) fig.add(DonutSeries(val_data, labels=lbl_data, **kwargs)) return fig
[docs] def heatmap( self, title: str | None = None, theme: str | dict | None = None, width: int = 640, height: int = 480, auto_display: bool = True, **kwargs: Any, ): """ Create a heatmap from the DataFrame's numeric values. The entire numeric portion of the DataFrame is treated as a 2-D matrix. Column names become column labels; index values become row labels. Returns: :class:`~glyphx.Figure` """ from .series import HeatmapSeries num_df = self._df.select_dtypes("number") matrix = num_df.values.tolist() fig = self._fig(title, theme, False, width, height, None, None, auto_display) fig.add(HeatmapSeries( matrix, col_labels=num_df.columns.tolist(), row_labels=[str(i) for i in self._df.index.tolist()], **kwargs, )) return fig
[docs] def plot( self, kind: str = "line", x: str | None = None, y: str | None = None, **kwargs: Any, ): """ Unified entry point — mirrors ``glyphx.plot()`` but operates on the DataFrame's columns. Args: kind: Chart type (same values as :func:`glyphx.plot`). x: Column name for X axis (used for line/bar/scatter). y: Column name for Y axis (used for line/bar/scatter). Returns: :class:`~glyphx.Figure` """ method = getattr(self, kind, None) if method is None: raise ValueError( f"Unknown chart kind '{kind}'. " "Use: line, bar, scatter, hist, box, pie, donut, heatmap." ) # hist() and box() use col= not x=/y=; pie/donut use labels=/values= # Route kwargs appropriately per chart type if kind in {"hist", "box"}: col = y or x return method(col=col, **kwargs) if kind in {"pie", "donut"}: return method(labels=x, values=y, **kwargs) return method(x=x, y=y, **kwargs)