Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Weave = "44d3d7a6-8a23-5bf8-98c5-b353f8df5ec9"
[extensions]
CairoMakieExt = "CairoMakie"
PlotlyLightExt = "PlotlyLight"
WeaveExt = ["PlotlyLight", "Weave"]
WeaveExt = "Weave"

[compat]
CSV = "~0.9, 0.10"
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,26 @@ package extensions. Load the backend you want **before** (or alongside)
- [PlotlyLight](https://github.com/JuliaComputing/PlotlyLight.jl):
lightweight interactive HTML plots — `using PlotlyLight`

Every plot function takes a `backend` key word, defaulting to
`CairoMakieBackend()`:

```julia
using CairoMakie # or `using PlotlyLight`
using PowerGraphics
using PowerAnalytics

# where `res` is a PowerSimulations.SimulationResults object
gen = get_generation_data(res)
plot_powerdata(gen) # CairoMakie
# plot_powerdata_plotly(gen) # PlotlyLight (`_plotly`-suffixed API)
plot_fuel(res) # CairoMakie (default)
plot_fuel(res; backend = PlotlyLightBackend()) # PlotlyLight
```

The `_plotly`-suffixed functions (`plot_fuel_plotly`, `plot_dataframe_plotly`,
…) are deprecated: they still work but emit a warning. Replace them with the
un-suffixed function plus `backend = PlotlyLightBackend()`.

Every other public function returns a plot object. To get the demand *numbers*
behind `plot_demand` — as a `DataFrame` with a `DateTime` column — use
`get_demand_data(res)`.

If neither backend is loaded, `PowerGraphics.jl` prints a warning at load
time and the plotting functions throw an `ArgumentError` when called.

Expand Down
2 changes: 1 addition & 1 deletion docs/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pages = OrderedDict(
## TODO add additional pages here in the future and remove stubs
"Tutorials" => Any["Examples"=>"tutorials/examples.md"], # TODO: make examples page
"How to..." => Any["Change Backends"=>"how_to_guides/backends.md"],
# "Explanation" => Any["stub" => "explanation/stub.md"],
"Explanation" => Any["Backend Parity Contract"=>"explanation/backend_parity.md"],
"Reference" => Any[
"Public API"=>"reference/public.md",
"Developers"=>[
Expand Down
90 changes: 90 additions & 0 deletions docs/src/explanation/backend_parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Backend Parity Contract

```@meta
CurrentModule = PowerGraphics
```

`PowerGraphics.jl` renders through two plotting backends, and they are not pixel-identical.
Some of what differs is a promise the package intends to keep, and some of it is an
unavoidable consequence of what CairoMakie and PlotlyLight each can do. This page draws
that line explicitly, so that neither users nor maintainers have to guess which is which.

The distinction matters. When a divergence is undocumented, a bug fixed in one recipe
quietly stays broken in the other — which is exactly what happened to the bar-plot
stacking fix in
[PR #140](https://github.com/Sienna-Platform/PowerGraphics.jl/pull/140).

## Choosing a backend

Every `plot_*` function takes a `backend` key word:

```julia
backend::PlottingBackend = CairoMakieBackend()
```

- [`CairoMakieBackend`](@ref)`()` — the default. Static, publication-quality figures
written as `png`, `pdf`, or `svg`. Requires `using CairoMakie`.
- [`PlotlyLightBackend`](@ref)`()` — lightweight interactive figures written as `html`.
Requires `using PlotlyLight`.

The backend packages are weak dependencies loaded through Julia package extensions, so the
matching package must be `using`-loaded **before** any plot call. Otherwise the stubs in
`src/PowerGraphics.jl` throw an `ArgumentError` telling you which `using` is missing.

```julia
using CairoMakie # or PlotlyLight
using PowerGraphics

plot_fuel(res) # CairoMakie (default)
plot_fuel(res; backend = PlotlyLightBackend()) # PlotlyLight
```

## Guaranteed identical across backends

The behaviors below are resolved **once** in `src/call_plots.jl` (and, for colors,
`src/definitions.jl`) before either recipe is reached. The recipes in `ext/` consume
already-decided values; they do not re-derive them. Treat this list as a stability
promise: **a change to any of these is a change to both backends by construction.**

| Behavior | Where it is decided | The promise |
|:---------------------------- |:----------------------------------------- |:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Series draw order | `_series_draw_order` | On non-bar plots, series whose values sum to a net-negative total are drawn first, then the rest, each group keeping its original column order. Net-negative series (storage charging, source input) sit below the zero axis, so drawing them first leaves the positive bands on top. |
| Sign-aware stacking | `_series_is_negative` | A series is classified by the sign of its *total*, not per timestep, by the one helper that `_signed_stack_bounds`, `_series_draw_order` and the PlotlyLight `stackgroup` split all read. Positive-type series stack upward from 0; negative-type series stack downward from 0. A positive series keeps a zero-width band in place at timesteps where it is 0 (PV at night) rather than jumping to the negative baseline. |
| `nofill` default | `_PlotOptions` | `nofill = !bar && !stack`. A plain line plot draws no area fill; stacked and bar plots do. |
| `linestyle` / `linewidth` | `_resolve_linestyle`, `_PlotOptions` | `linestyle::Symbol` is the canonical spelling and defaults to `:solid`; the old PlotlyLight-only `line_dash` spelling is folded into it centrally. `linewidth` defaults to `1` and is converted to `Float64` once. |
| Title resolution | `_resolve_title` | `title` defaults to "no title"; the legacy `" "` (single-space) sentinel for "untitled" is normalized to `nothing` in one place. |
| Untitled-save filename | `_UNTITLED_SAVE_NAME` | A [`plot_dataframe`](@ref) save with no title lands at `dataframe.<format>`. |
| Empty-`DataFrame` handling | `_plot_dataframe!` | An empty input warns `"Plot dataframe empty: skipping plot creation"` and returns the plot handle unchanged. Neither recipe is entered, so no labels, legend, or file are produced. |
| Default series color palette | `_PlotOptions`, `get_palette_seriescolor` | Both backends receive a finished `seriescolor` vector, one entry per drawn series and continuing the cycle past series already on the plot. Both select the *same* colors — the whole palette from [`load_palette`](@ref), so more series get a distinct color before the cycle repeats. The two backends differ only in the representation each library wants (`Colors.RGBA` objects vs. `"rgba(...)"` strings). |
| Label handling / `label_fn` | `_PlotOptions` | `label_fn` defaults to [`label_short`](@ref) and is applied in core; the recipes receive the finished legend text as `column_labels`. |

## Deliberate, documented differences

These differences are intentional. Each one exists because of a constraint in the
underlying library, and the "Why" column is the reason not to "fix" it.

| Behavior | CairoMakie | PlotlyLight | Why the difference exists |
|:----------------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------------------------------------------------------- |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Save formats** | `png`, `pdf`, `svg` via `CairoMakie.save`, defaulting to `png`. A `.html` filename throws an `ArgumentError` pointing at `PlotlyLightBackend()`. | `html` only, and the default. Any other extension emits a warning and is rewritten to `.html`; the rewritten path is returned. | PlotlyLight has no built-in image export — it serializes a plot to an HTML/JS payload. Rasterizing would require Kaleido/PlotlyBase, which the package deliberately does not depend on. CairoMakie is a vector/raster renderer with no HTML target. `_default_save_format` is therefore dispatched on the backend; an explicit `format` key word still wins. |
| **Time axis** | `DateTime`s are converted to unix floats (`Dates.datetime2unix`) and only the first and last timestamps are drawn as ticks. | Timestamps are passed through as a native Plotly datetime axis with full automatic tick control. | `CairoMakie.band!` — the primitive behind stacked areas — cannot take a `DateTime` axis. Every CairoMakie plot therefore uses a float axis so that stacked and non-stacked layers can share one `Axis`. Float ticks would render as raw unix seconds, so the axis is labeled explicitly at the endpoints. |
| **Bar-plot x-axis** | Grouped bars (`stack = false`) get one tick per category with the label rotated 45° and right/top-anchored. Stacked bars get a single unlabeled tick and are identified by legend only. | Tick labels are hidden for all bar plots (`showticklabels = !bar`); bars are identified by legend only. | Long category labels such as `RenewableDispatch__Curtailment` overlap when drawn horizontally, hence the rotation. CairoMakie stacked bars all sit at one x position (a single `barplot!` call with per-element stack ids), so there is no per-category tick to label; Plotly's `barmode` handles positioning itself and its legend is interactive, so tick labels are redundant. |
| **Y-limit anchoring** | `reset_limits!` on the axis; zero is *not* forced into range. | `yaxis.rangemode = "tozero"`. | Plotly's `rangemode` is a layout flag with no exact Makie equivalent. Makie's autolimits keep a tight fit around the data, which is usually the better default for a static figure; Plotly's zoom/pan makes an anchored baseline cheap to escape. |
| **Stacked-area band outline** | In the non-stair stacked branch the per-band outline is deliberately **omitted** — only the filled band is drawn. The stair branch does draw a `stairs!` outline. | Every trace is a `scatter` with `mode = "lines"`, so the outline is always drawn alongside the fill. | For intermittent series (PV at night, idle storage) a CairoMakie outline jumps between the stacked position and the zero anchor, drawing near-vertical streaks across the stack. Plotly's `stackgroup` machinery interpolates the line along the stacked baseline instead, so the same artifact does not appear. |
| **`save_plot` key words** | Accepted and ignored. | Filtered to a supported set and forwarded to the HTML writer: `autoplay`, `post_script`, `full_html`, `animation_opts`, `default_width`, `default_height`. | These are `PlotlyLight`'s HTML-serialization options; `CairoMakie.save` has no analogue. Unrecognized key words are dropped rather than erroring so that a single `save_plot` call can be written backend-agnostically. |
| **Figure size** | Hardcoded `1280 × 720` (16:9). | Plotly's own default. | Makie's 800×600 (4:3) default deforms time-series stack plots badly enough to be worth overriding; Plotly's default is responsive in the browser. Neither backend honors a `size` key word — see [issue #77](https://github.com/Sienna-Platform/PowerGraphics.jl/issues/77). |

## Guidance for maintainers

The recipes in `ext/plot_recipes.jl` and `ext/plotly_recipes.jl` are **drawing layers
only**: each reads a fully-resolved `_PlotOptions` —
scaled data, legend labels, colors, net-sign classification — and turns it into library
calls. Neither reads the raw `kwargs`.

When you change plotting behavior, decide which kind of change it is: a guaranteed
behavior belongs in `src/`, once, and in the table above; a library-forced divergence
belongs in one recipe *and* in the differences table, naming the constraint. A
**user-visible rendering** difference that is in neither table is a bug, not a design
decision. Internal representation may differ freely and is deliberately not catalogued
here — the plot handle types and the mechanics of legend construction are two examples,
and neither changes what the reader sees. If a difference could be unified but is not,
unify it — the default answer is parity.
1 change: 0 additions & 1 deletion docs/src/explanation/stub.md

This file was deleted.

35 changes: 35 additions & 0 deletions docs/src/how_to_guides/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,40 @@ using CairoMakie # or PlotlyLight
using PowerGraphics
```

## Pick the backend per plot

The backend is a value, not a separate function: every `plot_*` function takes a
`backend` key word, defaulting to [`CairoMakieBackend`](@ref)`()`. Pass
[`PlotlyLightBackend`](@ref)`()` to render interactive HTML instead.

```julia
plot_fuel(res) # CairoMakie (default)
plot_fuel(res; backend = PlotlyLightBackend()) # PlotlyLight

# The same key word works for every family and its `!` form:
plot_demand(res; backend = PlotlyLightBackend())
plot_dataframe!(p, df, time_range; backend = PlotlyLightBackend())
```

`report` takes the same key word: `report(res, out_path, template; backend = PlotlyLightBackend())`.

!!! warning "Deprecated: the `_plotly` suffix"

The `_plotly`-suffixed functions — `plot_demand_plotly`,
`plot_dataframe_plotly`, `plot_results_plotly`, `plot_fuel_plotly`,
`plot_powerdata_plotly`, and their `!` forms — are deprecated. They still
work and forward to the un-suffixed function with
`backend = PlotlyLightBackend()`, but they emit a warning and will be
removed in a future breaking release. They do not accept a `backend` key
word; use the un-suffixed function if you need to choose the backend.

If neither backend is loaded, `PowerGraphics.jl` will print a warning and plotting
functions will not be available.

## Switching backends without surprises

The two backends do not render identically. Before you swap one for the other — or before
you change plotting behavior — check the [Backend Parity Contract](@ref), which lists what
is guaranteed to match across backends and which differences are deliberate (save formats,
time-axis ticks, bar-plot tick labels, y-limit anchoring, and the `save_plot` key words
each backend accepts).
Loading