Custom plots and the reporting framework#

GAT has two tiers of plotting API. This page explains both, when to use which, and how to register your own plot so it shows up in the reporting framework’s automatic deck generation.

The two-tier plot API#

Tier

Module

Input

Output

When you’d use it

Low-level

gat.quickplots.*

pd.DataFrame (already-aggregated dispatch, capacity, etc.)

matplotlib.axes.Axes

One-off analysis. The gallery is built on this tier.

High-level

gat.quickplots.{generation,system,transmission}_plots

BaseScenario (any handler)

(name: str, ax: Axes, df: pd.DataFrame | None)

Batch reporting, scenario comparisons, plugin extensions. The reporting framework consumes this tier.

The high-level functions are typically thin wrappers around the low-level ones. For example:

# Low-level (what the gallery shows)
import gat.quickplots as qp
dispatch = scenario.get_area_dispatch(include_charging=False)
ax = qp.plot_annual_area_dispatch_stack(dispatch)

# High-level (what the reporter calls)
from gat.quickplots.generation_plots import plot_total_area_dispatch
name, ax, df = plot_total_area_dispatch(scenario)

The high-level wrapper does three additional things:

  1. Pulls its own data from the scenario, so the caller doesn’t have to know which get_* method to use.

  2. Returns the dataframe alongside the axes, so the reporter can save name.csv next to name.png.

  3. Owns its output filename via the name (typically a path-like string with subfolders, e.g. "generation/area/total_dispatch") — the reporter writes the PNG to <output_path>/<name>.png.

The @plot_function decorator#

@plot_function (from gat.registry) marks a function as a registered high-level plot. The decorator captures the function’s signature so the reporting framework can show its options in the report config YAML, and filters discovery by scenario type and plot category.

from gat.registry import plot_function

@plot_function("BaseScenario", plot_type="generation")
def plot_my_thing(scenario, threshold_mw=100, **kwargs):
    """Docstring becomes the description in the report config."""
    df = compute_something(scenario, threshold_mw)
    fig, ax = plt.subplots()
    df.plot(ax=ax)
    return ("generation/my_thing", ax, df)

Decorator arguments#

Argument

Default

Description

scenario_type

"BaseScenario"

Filters discovery. "BaseScenario" plots run for any handler. "SiennaScenario" (etc.) plots only run for that handler.

plot_type

"system"

Free-form category. The reporter’s report_type field filters by this — common values are "generation", "transmission", "cost", "emissions", "system".

Return contract#

The function must return a tuple of either two or three elements:

(name: str, ax: matplotlib.axes.Axes)
(name: str, ax: matplotlib.axes.Axes, df: pd.DataFrame | None)

Element

Used for

name

Filename for the saved PNG (and CSV, if df provided). Slashes are interpreted as subdirectories under the report’s output_path.

ax

The matplotlib axes — saved as PNG via plt.savefig(ax.figure).

df

Optional underlying data — saved as <name>.csv when the report config has save_data=True.

Generator-style plots#

If a function yields tuples instead of returning one, the reporter iterates over them — useful for “one plot per area” or “one plot per month” styles. The yielded tuple shape is the same.

The reporting framework#

gat.reports.scenario_single.run(config) is the entry point. It:

  1. Discovers all @plot_function-decorated callables (internal and external).

  2. Loads a scenario via gat.load(...) from the SystemReportConfig’s scenario field.

  3. Iterates config.output_plots, calling each plot via gat.registry.utils.execute_plot.

  4. Writes <output_path>/<name>.png and (if save_data=True) <output_path>/<name>.csv for each.

  5. Optionally bundles everything into a PowerPoint when output_fmt='pptx'.

from gat.models.scenario import ScenarioConfig
from gat.reports.scenario_single import SystemReportConfig, run

config = SystemReportConfig(
    model_type="Sienna",
    output_path="./report_out",
    output_fmt="pptx",  # also creates report_out/system_comparison.pptx
    scenario=ScenarioConfig(
        model_type="Sienna",
        simulation_paths="example_data/sienna/v4/simulation_store.h5",
        system_path="example_data/sienna/v4/sys.json",
    ),
)
run(config)

config.output_plots is a list of plot-config dicts (one per plot) with options pulled from each function’s signature. When empty, the reporter auto-discovers and runs everything that matches model_type / report_type. To run a curated subset, populate it explicitly:

config.output_plots = [
    {"name": "plot_total_system_dispatch", "options": {"units": "GWh"}},
    {"name": "plot_capacity_by_fuel", "options": {}},
]
run(config)

Worked example: capacity by fuel#

A custom plot that summarizes installed thermal and renewable capacity by fuel type, sourced from the scenario’s system file.

"""Custom plot: total installed capacity by fuel."""
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)

import matplotlib.pyplot as plt
import pandas as pd

from gat.scenariohandlers import SiennaScenario
from gat.registry import plot_function, discover_all_plots


@plot_function("BaseScenario", plot_type="generation")
def plot_capacity_by_fuel(scenario, **kwargs):
    """Total installed capacity by fuel, drawn from the system definition.

    Walks ThermalStandard + RenewableDispatch + RenewableNonDispatch
    components, scales each unit's per-unit rating by its base_power,
    sums by fuel.
    """
    rows = []
    for ctype in ["ThermalStandard", "RenewableDispatch", "RenewableNonDispatch"]:
        try:
            df = scenario.system.get_component_data(ctype)
        except Exception:
            continue
        for _, comp in df.iterrows():
            if not comp.get("available", True):
                continue
            rating_pu = comp.get("rating") or 0
            base = comp.get("base_power") or 100
            mw = float(rating_pu) * float(base)
            rows.append({"fuel": comp.get("fuel") or ctype, "capacity_mw": mw})

    cap = (
        pd.DataFrame(rows)
        .groupby("fuel")["capacity_mw"]
        .sum()
        .sort_values(ascending=True)
    )

    fig, ax = plt.subplots(figsize=(7, 4))
    cap.plot.barh(ax=ax)
    ax.set_xlabel("Installed Capacity (MW)")
    ax.set_title("Installed Capacity by Fuel")
    fig.tight_layout()

    return ("generation/capacity_by_fuel", ax, cap.to_frame())


# Confirm the plot is discoverable
discover_all_plots()
from gat.registry import get_plot_names
assert "plot_capacity_by_fuel" in get_plot_names()

# Run it directly
scenario = SiennaScenario(
    simulation_files="example_data/sienna/v4/simulation_store.h5",
    system_file="example_data/sienna/v4/sys.json",
)
name, ax, df = plot_capacity_by_fuel(scenario)
print(name)
print(df)
plt.show()

Output:

generation/capacity_by_fuel
                       capacity_mw
fuel
RenewableNonDispatch    348.42
NUCLEAR                 447.21
COAL                   2554.52
NATURAL_GAS            5448.21
RenewableDispatch      5616.90

To wire this into the reporting framework, drop the function definition into a module that’s imported (or registered as a plugin — see below) before calling gat.reports.scenario_single.run(...).

External plugins#

When your plot lives outside the GAT repo (in your own analysis package), register it via the gat_ext entry-point group in your package’s pyproject.toml:

[project.entry-points."gat_ext"]
my_plots = "my_package.plots"

my_package.plots is a module that imports your @plot_function- decorated callables. When GAT calls discover_all_plots(), it walks all gat_ext entry points and imports them — your plot functions register themselves on import.

The discovery mechanism is in gat.registry.discover_external_plots. No further setup is required.

Notes#

  • Argument types in **kwargs are introspected by the reporter and written into the report config YAML. Tuples are not supported because they don’t round-trip through YAML cleanly — use lists or scalars.

  • Optional arguments without defaults show up as disabled in the report config (the reporter won’t run a plot whose required arg beyond scenario is unset).

  • Logging: use loguru (from loguru import logger) inside plot functions for status messages — the reporter captures it into a per-run log file.

  • Avoid heavy state in plot functions — they’re called once per scenario in a fresh process when the reporter parallelizes (planned; serial today).

  • The older ScenarioPlotter class in gat.quickplots.scenario_plotter is an experimental sketch; @plot_function is the supported path.