"""Chart — the central fluent builder object."""
from dataclasses import replace
import matplotlib.pyplot as plt
from ..data.adapter import DataAdapter
from ..layout.facet import render_facet
from ..marks import render_layer
from ..stats.kde import KDE
from ..style.color import resolve_palette
from ..style.theme import _resolve_theme
from .canvas import Canvas
from .layer import Layer
[docs]
class Chart:
"""
The main interface for building a gufo chart.
Methods register layers and options; nothing is drawn until .show() or
.save() is called. This deferred rendering allows the theme and figure size
to be finalized before any matplotlib calls are made.
Users create Chart instances via gufo.chart(), never directly.
"""
_SIMPLE_SETTERS = [
("_title", "set_title"),
("_xlabel", "set_xlabel"),
("_ylabel", "set_ylabel"),
("_xscale", "set_xscale"),
("_yscale", "set_yscale"),
]
[docs]
def __init__(self, data=None):
self._data = data
self._layers = []
self._title = None
self._subtitle = None
self._xlabel = None
self._ylabel = None
self._caption = None
self._annotations = []
self._xlim = None
self._ylim = None
self._xscale = None
self._yscale = None
self._xticks = {}
self._yticks = {}
self._legend_opts = None
self._theme_override = None
self._facet_column = None
self._facet_row = None
self._facet_cols = None
self._facet_sharex = True
self._facet_sharey = True
self._palette = None
self._references = []
self._label_config = None
self._apply_funcs = []
self._canvas = Canvas()
# ------------------------------------------------------------------
# Mark methods
# ------------------------------------------------------------------
[docs]
def scatter(self, x, y, *, color=None, size=None, alpha=None, label=None,
cmap=None, vmin=None, vmax=None, colorbar=True,
fit=None, y_error=None, x_error=None,
color_order=None, **kwargs):
"""Add a scatter plot layer.
y may be a list of column names for wide-form DataFrames — each column
becomes its own series without requiring pd.melt().
When color is a numeric column, cmap/vmin/vmax control the colormap
and range. A colorbar is shown by default (set colorbar=False to hide).
fit accepts a gufo.regression() config object to overlay a fit line.
y_error / x_error accept a column name or array for error bars.
"""
self._layers.append(Layer(
mark_type="scatter", x=x, y=y,
encodings={"color": color, "size": size, "alpha": alpha,
"label": label, "cmap": cmap, "vmin": vmin,
"vmax": vmax, "colorbar": colorbar,
"fit": fit,
"y_error": y_error, "x_error": x_error,
"color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def line(self, x, y, *, color=None, stroke_dash=None, label=None,
cmap=None, vmin=None, vmax=None, colorbar=True,
y_error=None, x_error=None, color_order=None, **kwargs):
"""Add a line plot layer.
y may be a list of column names for wide-form DataFrames — each column
becomes its own series without requiring pd.melt().
When color is a numeric column, the line is drawn as a gradient
segment-by-segment using cmap/vmin/vmax. A colorbar is shown by
default (set colorbar=False to hide).
y_error / x_error accept a column name or array for error bars.
"""
self._layers.append(Layer(
mark_type="line", x=x, y=y,
encodings={"color": color, "stroke_dash": stroke_dash,
"label": label, "cmap": cmap, "vmin": vmin,
"vmax": vmax, "colorbar": colorbar,
"y_error": y_error, "x_error": x_error,
"color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def bar(self, x, y, *, color=None, horizontal=False, stacked=False,
label=None, y_error=None, x_error=None,
order=None, color_order=None, **kwargs):
"""Add a bar chart layer.
y may be a list of column names for wide-form DataFrames — each column
becomes a grouped bar without requiring pd.melt().
When color is a categorical column, bars are grouped (dodged) by
default. Set stacked=True to stack bars instead.
y_error / x_error accept a column name or array for error bars.
"""
self._layers.append(Layer(
mark_type="bar", x=x, y=y,
encodings={"color": color, "horizontal": horizontal,
"stacked": stacked, "label": label,
"y_error": y_error, "x_error": x_error,
"order": order, "color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def histogram(self, x, *, bins="auto", color=None, horizontal=False,
density=False, multiple="layer", fill=True,
label=None, kde=None, color_order=None, **kwargs):
"""Add a histogram layer.
Set horizontal=True to orient the histogram sideways.
Set density=True to normalize the histogram so the area under it sums to 1.
multiple controls grouped histogram layout: "layer" (overlay with
transparency), "stack", or "dodge".
Set fill=False for step (outline-only) histograms.
kde accepts a gufo.kde() config object to overlay a density curve.
"""
self._layers.append(Layer(
mark_type="histogram", x=x, y=None,
encodings={"bins": bins, "color": color, "horizontal": horizontal,
"density": density, "multiple": multiple, "fill": fill,
"label": label, "kde": kde,
"color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def kdeplot(self, x, *, color=None, bw_method=None, linestyle="-",
linewidth=2.0, alpha=1.0, fill=False, n_points=200, label=None,
color_order=None, **kwargs):
"""Add a KDE (kernel density estimation) density layer.
Set fill=True to shade under the curve. bw_method controls scipy's
gaussian_kde bandwidth. n_points sets the resolution of the curve.
Extra keyword arguments are passed through to matplotlib.
"""
kde_config = KDE(
bw_method=bw_method, linestyle=linestyle, linewidth=linewidth,
alpha=alpha, fill=fill, n_points=n_points,
)
self._layers.append(Layer(
mark_type="kde", x=x, y=None,
encodings={"color": color, "label": label, "kde_config": kde_config,
"color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def strip(self, x, y, *, color=None, size=None, alpha=None, jitter=0.2,
horizontal=False, label=None, order=None, color_order=None,
**kwargs):
"""Add a strip plot layer (jittered points along a categorical axis).
x is the grouping column (categorical). y is the values column (numeric).
y may be a list of column names for wide-form DataFrames — each column
becomes its own strip without requiring pd.melt().
"""
self._layers.append(Layer(
mark_type="strip", x=x, y=y,
encodings={
"color": color, "size": size, "alpha": alpha,
"jitter": jitter, "horizontal": horizontal, "label": label,
"order": order, "color_order": color_order,
},
kwargs=kwargs,
))
return self
[docs]
def swarm(self, x, y, *, color=None, size=None, alpha=None,
horizontal=False, label=None, order=None, color_order=None,
**kwargs):
"""Add a swarm plot layer (non-overlapping points along a categorical axis).
x is the grouping column (categorical). y is the values column (numeric).
y may be a list of column names for wide-form DataFrames — each column
becomes its own swarm without requiring pd.melt().
"""
self._layers.append(Layer(
mark_type="swarm", x=x, y=y,
encodings={
"color": color, "size": size, "alpha": alpha,
"horizontal": horizontal, "label": label,
"order": order, "color_order": color_order,
},
kwargs=kwargs,
))
return self
[docs]
def boxplot(self, x, y, *, color=None, horizontal=False, label=None,
order=None, color_order=None, **kwargs):
"""Add a box plot layer.
x is the grouping column (categorical). y is the values column (numeric).
Each unique x value produces one box.
y may be a list of column names for wide-form DataFrames — each column
becomes its own box without requiring pd.melt().
"""
self._layers.append(Layer(
mark_type="boxplot", x=x, y=y,
encodings={"color": color, "horizontal": horizontal, "label": label,
"order": order, "color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def violin(self, x, y, *, color=None, horizontal=False, label=None,
order=None, color_order=None, **kwargs):
"""Add a violin plot layer.
x is the grouping column (categorical). y is the values column (numeric).
Each unique x value produces one violin.
y may be a list of column names for wide-form DataFrames — each column
becomes its own violin without requiring pd.melt().
"""
self._layers.append(Layer(
mark_type="violin", x=x, y=y,
encodings={"color": color, "horizontal": horizontal, "label": label,
"order": order, "color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def heatmap(self, x=None, y=None, *, color=None, cmap=None, annotate=False, **kwargs):
"""Add a heatmap layer.
Two usage modes:
- Matrix form: gufo.chart(pivot_df).heatmap() — DataFrame is the matrix.
- Long-form: gufo.chart(df).heatmap("x", "y", color="value") — pivoted
internally.
"""
self._layers.append(Layer(
mark_type="heatmap", x=x, y=y,
encodings={"color": color, "cmap": cmap, "annotate": annotate},
kwargs=kwargs,
))
return self
[docs]
def area(self, x, y, *, color=None, alpha=None, label=None,
y_error=None, color_order=None, **kwargs):
"""Add an area chart layer.
y may be a list of column names for wide-form DataFrames — each column
becomes a stacked area without requiring pd.melt().
y_error accepts a column name or array and draws a lighter error
band around the top edge of the area (long-form only).
"""
self._layers.append(Layer(
mark_type="area", x=x, y=y,
encodings={"color": color, "alpha": alpha, "label": label,
"y_error": y_error, "color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def countplot(self, x, *, color=None, horizontal=False, label=None,
order=None, color_order=None, **kwargs):
"""Add a count plot layer — bars showing frequency of each x category."""
self._layers.append(Layer(
mark_type="countplot", x=x, y=None,
encodings={"color": color, "horizontal": horizontal, "label": label,
"order": order, "color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def ecdf(self, x, *, color=None, label=None, color_order=None, **kwargs):
"""Add an ECDF (empirical cumulative distribution function) layer."""
self._layers.append(Layer(
mark_type="ecdf", x=x, y=None,
encodings={"color": color, "label": label,
"color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def rug(self, x, *, color=None, height=0.05, alpha=0.5, label=None,
color_order=None, **kwargs):
"""Add a rug plot layer — tick marks along the x axis."""
self._layers.append(Layer(
mark_type="rug", x=x, y=None,
encodings={"color": color, "height": height, "alpha": alpha,
"label": label, "color_order": color_order},
kwargs=kwargs,
))
return self
[docs]
def pointplot(self, x, y, *, color=None, horizontal=False, label=None,
order=None, color_order=None, **kwargs):
"""Add a point plot layer — connected category means with 95% CI.
Shows the mean of y for each unique x value, connected by lines,
with error bars representing the 95% confidence interval (via
standard error). When color is set, groups are dodged.
x is the grouping column (categorical). y is the values column
(numeric).
"""
self._layers.append(Layer(
mark_type="pointplot", x=x, y=y,
encodings={"color": color, "horizontal": horizontal,
"label": label, "order": order,
"color_order": color_order},
kwargs=kwargs,
))
return self
# ------------------------------------------------------------------
# Labels and annotations
# ------------------------------------------------------------------
[docs]
def title(self, text):
"""Set the chart title."""
self._title = text
return self
[docs]
def subtitle(self, text):
"""Set a subtitle displayed below the title in smaller gray text."""
self._subtitle = text
return self
[docs]
def xlabel(self, text):
"""Set the x-axis label."""
self._xlabel = text
return self
[docs]
def ylabel(self, text):
"""Set the y-axis label."""
self._ylabel = text
return self
[docs]
def caption(self, text):
"""Set a caption displayed below the chart in small gray text."""
self._caption = text
return self
[docs]
def annotate(self, text, xy):
"""Add a text annotation with an arrow pointing to *xy*.
Parameters
----------
text : str
The annotation text.
xy : tuple of float
The (x, y) data coordinates the arrow points to.
"""
self._annotations.append({"text": text, "xy": xy})
return self
[docs]
def label(self, column=None, *, fmt=None, fontsize=None, offset=None):
"""Add data labels to bars or scatter points.
For bar charts, labels are placed at the end of each bar using
``axes.bar_label()``. For scatter plots, pass a column name to
label each point with that column's value.
Parameters
----------
column : str or None
Column name whose values label each point (scatter only).
For bar charts, omit or pass None to label with bar heights.
fmt : str or None
Format string (e.g. ``".1f"``, ``".0%"``). For bars, passed
directly to ``bar_label(fmt=...)``.
fontsize : int or str or None
Font size for the labels.
offset : float or None
Padding in points between the bar end and the label.
"""
self._label_config = {
"column": column, "fmt": fmt,
"fontsize": fontsize, "offset": offset,
}
return self
# ------------------------------------------------------------------
# Axis control
# ------------------------------------------------------------------
[docs]
def xlim(self, low, high):
"""Set the x-axis limits."""
self._xlim = (low, high)
return self
[docs]
def ylim(self, low, high):
"""Set the y-axis limits."""
self._ylim = (low, high)
return self
[docs]
def xscale(self, scale):
"""Set the x-axis scale (e.g. ``"log"``, ``"symlog"``)."""
self._xscale = scale
return self
[docs]
def yscale(self, scale):
"""Set the y-axis scale (e.g. ``"log"``, ``"symlog"``)."""
self._yscale = scale
return self
[docs]
def xticks(self, ticks=None, labels=None, rotation=None):
"""Configure x-axis tick positions, labels, and rotation.
Parameters
----------
ticks : list or None
Explicit tick positions.
labels : list or None
Labels corresponding to *ticks*.
rotation : float or None
Rotation angle in degrees for tick labels.
"""
self._xticks = {"ticks": ticks, "labels": labels, "rotation": rotation}
return self
[docs]
def yticks(self, ticks=None, labels=None, rotation=None):
"""Configure y-axis tick positions, labels, and rotation.
Parameters
----------
ticks : list or None
Explicit tick positions.
labels : list or None
Labels corresponding to *ticks*.
rotation : float or None
Rotation angle in degrees for tick labels.
"""
self._yticks = {"ticks": ticks, "labels": labels, "rotation": rotation}
return self
# ------------------------------------------------------------------
# Other chart options
# ------------------------------------------------------------------
[docs]
def legend(self, *, position="best", title=None, hide=False):
"""Configure the legend.
Parameters
----------
position : str
Legend location. Any matplotlib loc string (e.g. ``"best"``,
``"upper right"``) or one of ``"outside right"``,
``"outside left"``, ``"outside top"``, ``"outside bottom"``
to place the legend outside the axes.
title : str or None
Optional legend title.
hide : bool
If True, suppress the legend entirely.
"""
self._legend_opts = {"position": position, "title": title, "hide": hide}
return self
[docs]
def theme(self, name_or_theme):
"""Set the theme for this chart.
Accepts a theme name (``"gufo_modern"``, ``"gufo_dark"``, etc.)
or a ``Theme`` instance.
"""
self._theme_override = name_or_theme
return self
[docs]
def facet(self, column=None, *, row=None, cols=3,
sharex=True, sharey=True):
"""Split the chart into subplots by one or two categorical columns.
With column only, panels wrap after cols columns. With row, panels
form a grid where row categories go down and column categories go
across. With row only, each row category gets one panel in a
single column.
Parameters
----------
column : str or None
Column for horizontal faceting.
row : str or None
Column for vertical faceting.
cols : int
Max columns before wrapping (single-variable faceting only).
sharex : bool
Share x-axis range across panels. Default True.
sharey : bool
Share y-axis range across panels. Default True.
"""
if self._data is None:
raise ValueError(
"facet() requires data bound to the chart. "
"Pass data to gufo.chart(data)."
)
if column is None and row is None:
raise ValueError(
"facet() requires at least one of column or row."
)
self._facet_column = column
self._facet_row = row
self._facet_cols = cols
self._facet_sharex = sharex
self._facet_sharey = sharey
return self
[docs]
def apply(self, func):
"""
Call func(figure, axes) after all layers are rendered.
func receives the underlying matplotlib Figure and Axes and may call
any matplotlib method on them. Its return value is ignored. The Chart
is returned so the chain continues.
Use this for operations gufo does not yet support natively.
"""
self._apply_funcs.append(func)
return self
[docs]
def palette(self, colors):
"""Set the color palette for this chart.
Accepts a list of color strings or a named palette
('gufo', 'pastel', 'bold', 'colorblind').
"""
self._palette = colors
return self
# ------------------------------------------------------------------
# Reference lines and bands
# ------------------------------------------------------------------
[docs]
def hline(self, y, *, color="black", linestyle="--", linewidth=1,
alpha=0.8, label=None, **kwargs):
"""Add a horizontal reference line."""
self._references.append(("hline", {"y": y, "color": color,
"linestyle": linestyle, "linewidth": linewidth, "alpha": alpha,
"label": label, **kwargs}))
return self
[docs]
def vline(self, x, *, color="black", linestyle="--", linewidth=1,
alpha=0.8, label=None, **kwargs):
"""Add a vertical reference line."""
self._references.append(("vline", {"x": x, "color": color,
"linestyle": linestyle, "linewidth": linewidth, "alpha": alpha,
"label": label, **kwargs}))
return self
[docs]
def hband(self, y1, y2, *, color="gray", alpha=0.2, label=None, **kwargs):
"""Add a horizontal reference band."""
self._references.append(("hband", {"y1": y1, "y2": y2, "color": color,
"alpha": alpha, "label": label, **kwargs}))
return self
[docs]
def vband(self, x1, x2, *, color="gray", alpha=0.2, label=None, **kwargs):
"""Add a vertical reference band."""
self._references.append(("vband", {"x1": x1, "x2": x2, "color": color,
"alpha": alpha, "label": label, **kwargs}))
return self
[docs]
def size(self, width, height):
"""Set the figure size in inches as (width, height)."""
self._canvas = Canvas(figsize=(width, height))
return self
[docs]
def clear(self):
"""Remove all registered layers and decorators, keeping data and theme.
Gufo builds charts lazily: every mark or decorator call registers a
layer on the Chart, and nothing is drawn until .show() or .save().
Calling .show() does not clear those layers, so reusing a Chart
variable accumulates them. Use .clear() to reset the chart between
plots while keeping the bound data, theme, palette, figure size,
and facet configuration.
Returns self so it stays in the chain.
"""
self._layers = []
self._title = None
self._subtitle = None
self._xlabel = None
self._ylabel = None
self._caption = None
self._annotations = []
self._xlim = None
self._ylim = None
self._xscale = None
self._yscale = None
self._xticks = {}
self._yticks = {}
self._legend_opts = None
self._references = []
self._label_config = None
self._apply_funcs = []
return self
# ------------------------------------------------------------------
# Output
# ------------------------------------------------------------------
[docs]
def show(self):
"""Render and display the chart.
The figure is closed afterwards so repeated calls do not accumulate
open figures. In interactive mode the figure is left open, since the
window or widget is still live and the user is expected to keep
interacting with it.
"""
fig, axes = self._render()
plt.show()
if not plt.isinteractive():
plt.close(fig)
return self
[docs]
def save(self, path, *, dpi=150):
"""Render and save the chart to a file."""
fig, axes = self._render()
fig.savefig(path, dpi=dpi, bbox_inches="tight")
plt.close(fig)
return self
# ------------------------------------------------------------------
# Internal rendering pipeline
# ------------------------------------------------------------------
def _render(self):
"""
Build the figure and draw all registered layers.
Rendering order:
1. Resolve theme (override or global)
2. Reset the Canvas, then create figure / axes
3. Draw each Layer via the appropriate Mark
4. Apply decorators (title, labels, axis options, legend)
5. Call each .apply() function
The Canvas is reset first so every render starts from a clean figure.
Without it, re-rendering a Chart would draw on top of the previous
render's artists.
"""
if self._facet_column is not None or self._facet_row is not None:
return render_facet(self, self._facet_column, self._facet_cols,
facet_row=self._facet_row,
sharex=self._facet_sharex,
sharey=self._facet_sharey)
theme = _resolve_theme(self._theme_override)
with theme.as_context():
self._canvas.reset()
fig, axes = self._canvas.build()
self._render_onto(fig, axes)
return fig, axes
def _render_onto(self, figure, axes, adapter=None, suppress_legend=False):
"""Render this chart's layers onto an externally provided axes.
Used by Grid and facet rendering. When adapter is provided, it is
used instead of creating one from self._data. When suppress_legend
is True, the per-axes legend is skipped — facet rendering uses this
to draw a single figure-level legend after all panels are rendered.
"""
theme = _resolve_theme(self._theme_override)
if adapter is None:
adapter = DataAdapter.from_any(self._data)
resolved_palette = resolve_palette(self._palette)
with theme.as_context():
for layer in self._layers:
render_layer(replace(layer, palette=resolved_palette),
adapter, axes)
self._apply_decorators(figure, axes, adapter,
suppress_legend=suppress_legend)
for func in self._apply_funcs:
func(figure, axes)
def _apply_decorators(self, fig, axes, adapter=None, suppress_legend=False):
for attr, method in self._SIMPLE_SETTERS:
value = getattr(self, attr)
if value is not None:
getattr(axes, method)(value)
if self._xlim:
axes.set_xlim(*self._xlim)
if self._ylim:
axes.set_ylim(*self._ylim)
self._apply_labels(axes, adapter)
self._apply_references(axes)
self._apply_subtitle(fig)
self._apply_caption(fig)
self._apply_ticks(axes, "x", self._xticks)
self._apply_ticks(axes, "y", self._yticks)
self._apply_annotations(axes)
if not suppress_legend:
self._apply_legend(axes)
def _apply_labels(self, axes, adapter=None):
if not self._label_config:
return
mark_types = {layer.mark_type for layer in self._layers}
line_only = bool(mark_types & {"line", "pointplot"}) and not (
mark_types & {"bar", "countplot", "scatter"}
)
if line_only:
self._label_line_points(axes, adapter)
return
if axes.containers:
self._label_bar_containers(axes)
return
if self._label_config.get("column") is not None:
self._label_scatter_offsets(axes, adapter)
def _label_text_kwargs(self):
"""Return (annotate_kwargs, text_offset) from the label config."""
cfg = self._label_config
kw = {}
if cfg.get("fontsize") is not None:
kw["fontsize"] = cfg["fontsize"]
offset = cfg.get("offset")
return kw, offset if offset is not None else 5
def _label_bar_containers(self, axes):
cfg = self._label_config
kw, _ = self._label_text_kwargs()
fmt = cfg.get("fmt")
if fmt is not None:
kw["fmt"] = f"%{fmt}"
if cfg.get("offset") is not None:
kw["padding"] = cfg["offset"]
for container in axes.containers:
axes.bar_label(container, **kw)
def _label_scatter_offsets(self, axes, adapter):
column = self._label_config.get("column")
if adapter is None:
adapter = DataAdapter.from_any(self._data)
labels = adapter.resolve(column)
kw, text_offset = self._label_text_kwargs()
for collection in axes.collections:
offsets = collection.get_offsets()
if len(offsets) == len(labels):
for (px, py), lbl in zip(offsets, labels):
axes.annotate(str(lbl), (px, py),
textcoords="offset points",
xytext=(0, text_offset),
ha="center", **kw)
break
def _label_line_points(self, axes, adapter):
"""Label each point on a line or pointplot mark.
If a column is configured, its values are used as labels; otherwise
the y-values are formatted via *fmt* (default two decimals).
"""
data_lines = [ln for ln in axes.lines if len(ln.get_xdata()) > 0]
if not data_lines:
return
cfg = self._label_config
column = cfg.get("column")
fmt = cfg.get("fmt")
line = data_lines[0]
xs = line.get_xdata()
ys = line.get_ydata()
if column is not None:
if adapter is None:
adapter = DataAdapter.from_any(self._data)
labels = adapter.resolve(column)
if len(labels) != len(xs):
return
text_values = [str(lbl) for lbl in labels]
else:
fmt_str = "{:" + fmt + "}" if fmt else "{:.2f}"
text_values = [fmt_str.format(y) for y in ys]
kw, text_offset = self._label_text_kwargs()
for px, py, lbl in zip(xs, ys, text_values):
axes.annotate(lbl, (px, py),
textcoords="offset points",
xytext=(0, text_offset),
ha="center", **kw)
def _apply_subtitle(self, fig):
if self._subtitle:
fig.text(0.5, 0.92, self._subtitle, ha="center", fontsize="medium",
color="gray", transform=fig.transFigure)
def _apply_caption(self, fig):
if self._caption:
fig.text(0.5, 0.01, self._caption, ha="center", fontsize="small",
color="gray", transform=fig.transFigure)
def _apply_ticks(self, axes, axis, opts):
if not opts:
return
if opts.get("ticks") is not None:
getattr(axes, f"set_{axis}ticks")(opts["ticks"])
if opts.get("labels") is not None:
getattr(axes, f"set_{axis}ticklabels")(opts["labels"])
if opts.get("rotation") is not None:
axes.tick_params(axis=axis, labelrotation=opts["rotation"])
def _apply_annotations(self, axes):
for ann in self._annotations:
axes.annotate(ann["text"], xy=ann["xy"],
xytext=(15, 15), textcoords="offset points",
arrowprops={"arrowstyle": "->", "color": "black"})
def _apply_references(self, axes):
for kind, opts in self._references:
kw = {k: v for k, v in opts.items() if v is not None}
if kind == "hline":
axes.axhline(kw.pop("y"), **kw)
elif kind == "vline":
axes.axvline(kw.pop("x"), **kw)
elif kind == "hband":
axes.axhspan(kw.pop("y1"), kw.pop("y2"), **kw)
elif kind == "vband":
axes.axvspan(kw.pop("x1"), kw.pop("x2"), **kw)
_OUTSIDE_LEGEND = {
"outside right": {"bbox_to_anchor": (1.02, 0.5), "loc": "center left"},
"outside left": {"bbox_to_anchor": (-0.02, 0.5), "loc": "center right"},
"outside top": {"bbox_to_anchor": (0.5, 1.12), "loc": "lower center"},
"outside bottom": {"bbox_to_anchor": (0.5, -0.12), "loc": "upper center"},
}
def _apply_legend(self, axes):
if not self._legend_opts:
return
if self._legend_opts.get("hide"):
return
position = self._legend_opts.get("position", "best")
title = self._legend_opts.get("title")
outside = self._OUTSIDE_LEGEND.get(position)
if outside is not None:
axes.legend(title=title, **outside)
else:
axes.legend(loc=position, title=title)
[docs]
def chart(data=None):
"""Create a new Chart. The entry point for all gufo charts."""
return Chart(data)