Skip to content

Charts

Charts: the .plot accessor on results, compare, and the light and dark styles. See the Gallery.

plot

Charting.

Reached through a .plot accessor on the result objects (IndexResult, BacktestResult, AttributionResult, OptimisationResult, RiskModel) rather than through free functions taking a result. That follows the same stance as the asset views: plotting is a result-layer concern, so a result knows how to draw itself and nothing else grows a chart method.

result.plot.level()
backtest.plot.performance()

Needs matplotlib, which ships in the plot extra:

pip install "py-beacon-kit[plot]"

Importing beacon costs nothing extra. The accessor is a descriptor that resolves on first access, so matplotlib is imported when a chart is drawn and not before. import beacon works without matplotlib installed and does not import it, and a test asserts that.

To compare several results on one chart, use beacon.plot.compare(a, b). To restyle any matplotlib chart, use beacon.plot.use().

Every method takes ax= and returns the Axes it drew on, so charts compose into a figure the caller laid out. The signatures carry nothing matplotlib-specific, which is what keeps an interactive backend possible later without changing any call site.

ChartMethods

ChartMethods(result: Any)

Base for the accessor classes, providing the listing repr.

A caller who types result.plot and presses enter should be told what they can do with it. Without this they get the default object repr, which answers a question nobody asked.

methods

methods() -> list[str]

Chart methods this accessor offers, alphabetically.

PlotAccessor

PlotAccessor(accessor_name: str)

Descriptor that resolves to a result's plotting methods on first use.

Attributes:

Name Type Description
accessor_name

Class in beacon.plot.accessors to instantiate, named rather than imported so this module stays free of matplotlib.

accessors

The chart methods reached through result.plot.

Every method takes an optional ax= and returns the Axes it drew on, so a chart composes into a figure the caller laid out rather than insisting on its own. That is also what keeps the signatures backend-agnostic: nothing here returns a matplotlib-specific wrapper, so an interactive backend can offer the same names later without the call sites changing.

Sizes come from style.FIGSIZE per kind (a level chart is wide because time is the long axis, a weights chart tall because names stack) and only apply when this module creates the figure. A caller passing ax= has already decided.

Where the numbers come from

Nowhere here. Every method reads a result object and draws it; the arithmetic belongs to the analysis layer and is tested there. The one exception is the reconciliation total annotated on the contributions chart, which the renderer recomputes from the bars it actually drew, because an annotation claiming a total that does not match the bars beside it is worse than no annotation, and the only way to be sure is to add up what is on the page.

IndexPlots

IndexPlots(result: Any)

Bases: ChartMethods

Charts for an IndexResult.

level
level(
    benchmark: Series | None = None,
    ax: Axes | None = None,
    label: str = "Index",
) -> Axes

The index level over time, rebased to 100.

Parameters:

Name Type Description Default
benchmark Series | None

Optional comparison series, drawn subordinate.

None
ax Axes | None

Axes to draw on. A new figure is created when absent.

None
label str

Legend label for the index.

'Index'

Returns:

Name Type Description
Axes Axes

What was drawn on.

weights
weights(
    date: Timestamp | None = None, ax: Axes | None = None
) -> Axes

Constituent weights at a rebalance, with cap markers.

Parameters:

Name Type Description Default
date Timestamp | None

Which rebalance. Defaults to the latest.

None
ax Axes | None

Axes to draw on.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

BacktestPlots

BacktestPlots(result: Any)

Bases: ChartMethods

Charts for a BacktestResult.

performance
performance(ax: Axes | None = None) -> Axes

Growth of 100 with a drawdown panel beneath it.

The two share an x axis and sit in one gridspec, because a drawdown is only meaningful against the path that produced it, and reading them side by side would mean matching dates by eye.

Parameters:

Name Type Description Default
ax Axes | None

Ignored for this chart, which owns a two-panel figure. Accepted so every method has the same signature.

None

Returns:

Name Type Description
Axes Axes

The upper (level) panel.

annual_returns
annual_returns(ax: Axes | None = None) -> Axes

Calendar-year returns as signed bars.

Parameters:

Name Type Description Default
ax Axes | None

Axes to draw on.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

AttributionPlots

AttributionPlots(result: Any)

Bases: ChartMethods

Charts for an AttributionResult.

contributions
contributions(ax: Axes | None = None) -> Axes

Per-constituent contributions as diverging bars.

The drags are stated in the chart's note rather than drawn as bars, because they are comparisons against a counterfactual rather than terms in the decomposition: adding them to the same total would mix two different questions.

The annotated total is recomputed from the bars actually drawn. An annotation claiming a total that does not match what is beside it is worse than none, and adding up the page is the only way to be sure.

Parameters:

Name Type Description Default
ax Axes | None

Axes to draw on.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

OptimisationPlots

OptimisationPlots(result: Any)

Bases: ChartMethods

Charts for an OptimisationResult.

exposures
exposures(ax: Axes | None = None) -> Axes

Active weights as sign-coloured tilts against the index.

Parameters:

Name Type Description Default
ax Axes | None

Axes to draw on.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

frontier
frontier(
    frontier: Any,
    risk_free_rate: float = 0.0,
    ax: Axes | None = None,
) -> Axes

The efficient frontier, with the named points and the capital line.

Parameters:

Name Type Description Default
frontier Any

An EfficientFrontier over the same universe.

required
risk_free_rate float

Where the capital market line starts.

0.0
ax Axes | None

Axes to draw on.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

RiskPlots

RiskPlots(result: Any)

Bases: ChartMethods

Charts for a RiskModel.

correlation
correlation(ax: Axes | None = None) -> Axes

The correlation matrix as a heatmap.

Parameters:

Name Type Description Default
ax Axes | None

Axes to draw on.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

base

The lazy .plot accessor.

Every result object carries one, and none of them cost anything until it is touched. That is the whole point of this module: it imports nothing beyond the standard library, so import beacon does not import matplotlib, and a user who never draws a chart never pays for the ability to.

The mechanism is a descriptor. IndexResult.plot is an attribute lookup that resolves, on first access, to a class in beacon.plot.accessors, which is where matplotlib is required. Accessed on the class rather than an instance, it returns the descriptor itself, so help() and hasattr work without matplotlib.

PlotAccessor

PlotAccessor(accessor_name: str)

Descriptor that resolves to a result's plotting methods on first use.

Attributes:

Name Type Description
accessor_name

Class in beacon.plot.accessors to instantiate, named rather than imported so this module stays free of matplotlib.

ChartMethods

ChartMethods(result: Any)

Base for the accessor classes, providing the listing repr.

A caller who types result.plot and presses enter should be told what they can do with it. Without this they get the default object repr, which answers a question nobody asked.

methods
methods() -> list[str]

Chart methods this accessor offers, alphabetically.

comparison

Comparing several results on one axis.

A free function rather than an accessor because it is not about any one result: compare(a, b, c) is a statement about the set, and hanging it off the first argument would make that arbitrary. Import it as from beacon.plot import compare.

Aligned, not concatenated

Every series is clipped to the window they all share and rebased to 100 on the first shared date. Two indices with different histories compared over different periods differ for no reason but their spans, and the one with the shorter history looks better or worse than it is. Rebasing on the shared start means the lines begin together and the comparison is of shape.

compare

compare(
    *results: Any,
    labels: list[str] | None = None,
    ax: Axes | None = None,
) -> Axes

Plot several results on one rebased axis, with a metrics table.

Parameters:

Name Type Description Default
*results Any

Two or more IndexResult or BacktestResult objects.

()
labels list[str] | None

Display names. Defaults to each result's own identifier (its index id or portfolio id), or "Series N" when it has none.

None
ax Axes | None

Axes to draw on. A new figure is created when absent.

None

Returns:

Name Type Description
Axes Axes

What was drawn on.

Raises:

Type Description
ValueError

If fewer than two results are given, or they share no dates.

style

The "beacon" matplotlib styles, generated from the design tokens.

Not hand-written: every colour comes from beacon.tokens, which is a vendored copy of the file the desktop client generates its CSS from. That is the point: a chart embedded in the application should be the same colours as the panel around it, and the only way to guarantee that is for both to read one source. A palette retyped here would be right on the day it was written.

Two styles, beacon and beacon-dark, because the client has two modes and a chart has to follow. Both are registered with matplotlib by name, so once :func:register has run (every chart method, :func:use and beacon.plot.compare run it) plt.style.use("beacon") restyles any plot, including one this library never drew.

What the grammar is
  • an accent line at 1.5pt for the primary series, text-secondary for a benchmark: the comparison should read as subordinate without being hidden
  • divider gridlines on the y axis only, and behind the data
  • muted axis labels and tick text, with the top and right spines removed: a chart is mostly data, and the frame is not the data
The correlation colormap

beacon_corr is registered here too, from the raw.heatmap-* tokens. Mode-independent by design: a correlation of 0.8 must look the same whichever theme the surrounding application is wearing, or two screenshots of the same matrix disagree.

palette

palette(mode: str = LIGHT) -> dict[str, str]

The colours a chart draws with, in one mode.

Parameters:

Name Type Description Default
mode str

LIGHT or DARK.

LIGHT

Returns:

Name Type Description
dict dict[str, str]

The token names this module uses, so a caller composing a custom

dict[str, str]

chart can reach the same values rather than sampling them off a figure.

style_dict

style_dict(mode: str = LIGHT) -> dict[str, Any]

The rcParams for one mode.

Built as a mapping rather than written to an .mplstyle file so the values stay derived from the tokens. A generated file would be a second copy to keep in step, which is the thing this module exists to avoid.

Parameters:

Name Type Description Default
mode str

LIGHT or DARK.

LIGHT

Returns:

Name Type Description
dict dict[str, Any]

matplotlib rcParams names to values.

correlation_colormap

correlation_colormap() -> Any

The beacon_corr colormap, green through amber to red.

Built from the mode-independent raw.heatmap-* tokens: a correlation of 0.8 must look the same whichever theme the application is wearing, or two screenshots of one matrix disagree.

Returns:

Name Type Description
Colormap Any

Registered under CORRELATION_COLORMAP.

register

register() -> None

Register both styles and the colormap with matplotlib.

Idempotent, and called on first use of any accessor, so plt.style.use("beacon") works as soon as anything in this package has been touched. Registering at import of beacon.plot instead would mean importing matplotlib to do it, which is exactly what the lazy accessor exists to avoid.

use

use(mode: str = LIGHT) -> None

Apply a beacon style globally.

Parameters:

Name Type Description Default
mode str

LIGHT or DARK.

LIGHT