Skip to content

components.py

Composable building blocks for ESM chart classes.

Each class encapsulates a single cross-cutting concern (file I/O, layout styling, bar trace styling, total-sum annotation, or time-series styling) that was previously inherited from ESMChart. Chart classes instantiate the components they need rather than inheriting from a shared base.

BarTraceStyler

Apply consistent styling to bar traces in a Plotly figure.

Merges the formerly duplicated _style_bars (width=0.6) and _style_grouped_bars (width=0.8) methods into one class parameterised by bar width.

Parameters:

Name Type Description Default
width float

Bar width passed to update_traces. Use 0.6 for plain bar charts and 0.8 for grouped (faceted) bar charts.

required
Source code in evals/plots/components.py
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
class BarTraceStyler:
    """
    Apply consistent styling to bar traces in a Plotly figure.

    Merges the formerly duplicated ``_style_bars`` (width=0.6) and
    ``_style_grouped_bars`` (width=0.8) methods into one class
    parameterised by bar width.

    Parameters
    ----------
    width
        Bar width passed to ``update_traces``.  Use ``0.6`` for plain
        bar charts and ``0.8`` for grouped (faceted) bar charts.
    """

    def __init__(self, width: float) -> None:
        self.width = width

    def apply(self, fig: go.Figure, unit: str) -> None:
        """
        Apply bar-trace styling to all bar traces in *fig*.

        Parameters
        ----------
        fig
            Target figure.
        unit
            Unit string appended to the hover template.
        """
        fig.update_traces(
            selector={"type": "bar"},
            width=self.width,
            textposition="inside",
            insidetextanchor="middle",
            texttemplate="<b>%{customdata[1]}</b>",
            insidetextfont={"size": 16},
            textangle=0,
            hovertemplate="%{customdata[0]}: %{customdata[1]} " + unit,
            hoverlabel={"namelength": 0},
        )

apply(fig, unit)

Apply bar-trace styling to all bar traces in fig.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Target figure.

required
unit str

Unit string appended to the hover template.

required
Source code in evals/plots/components.py
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
def apply(self, fig: go.Figure, unit: str) -> None:
    """
    Apply bar-trace styling to all bar traces in *fig*.

    Parameters
    ----------
    fig
        Target figure.
    unit
        Unit string appended to the hover template.
    """
    fig.update_traces(
        selector={"type": "bar"},
        width=self.width,
        textposition="inside",
        insidetextanchor="middle",
        texttemplate="<b>%{customdata[1]}</b>",
        insidetextfont={"size": 16},
        textangle=0,
        hovertemplate="%{customdata[0]}: %{customdata[1]} " + unit,
        hoverlabel={"namelength": 0},
    )

FileExporter

Serialise a Plotly figure to HTML and JSON files.

Parameters:

Name Type Description Default
cfg typing.Any

Plot configuration object with file_name_template and database_* attributes.

required
metric_name str

Name of the metric being exported; used to fill the {metric} placeholder in file_name_template.

required
Source code in evals/plots/components.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
class FileExporter:
    """
    Serialise a Plotly figure to HTML and JSON files.

    Parameters
    ----------
    cfg
        Plot configuration object with ``file_name_template`` and
        ``database_*`` attributes.
    metric_name
        Name of the metric being exported; used to fill the
        ``{metric}`` placeholder in ``file_name_template``.
    """

    def __init__(self, cfg: typing.Any, metric_name: str) -> None:
        self.cfg = cfg
        self.metric_name = metric_name

    def construct_file_name(self, groupby: list[str], idx: typing.Hashable) -> str:
        """
        Construct the file name based on the provided template.

        Parameters
        ----------
        groupby
            List of groupby keys used to fill the template.
        idx
            The data-frame index from the groupby clause.  Scalars of
            any type (str, int, etc.) are wrapped in a tuple so they
            can be zipped with *groupby*.

        Returns
        -------
        :
            The constructed filename string (without extension).
        """
        if not isinstance(idx, (list, tuple)):
            idx = (idx,)
        resolved = {
            g: ALIAS_LOCATION_REV.get(i, i) for g, i in zip(groupby, idx, strict=True)
        }
        return self.cfg.file_name_template.format(metric=self.metric_name, **resolved)

    def to_html(
        self,
        fig: go.Figure,
        output_path: pathlib.Path,
        groupby: list[str],
        idx: typing.Hashable,
    ) -> pathlib.Path:
        """
        Serialise *fig* to an HTML file.

        Parameters
        ----------
        fig
            Plotly figure to serialise.
        output_path
            Folder to save the file under (the ``HTML/`` sub-folder is
            created by the caller).
        groupby
            Groupby keys needed to fill the file-name template.
        idx
            Data-frame index used to fill the file-name template.

        Returns
        -------
        :
            Path of the written file.
        """
        file_name = f"{self.construct_file_name(groupby, idx)}.html"
        file_path = output_path / "HTML" / file_name

        template_html = """\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="repo_name" content="{{ repo_name }}" />
<meta name="repo_branch" content="{{ repo_branch }}" />
<meta name="repo_hash" content="{{ repo_hash }}" />
</head>
<body>
    {{ fig }}
</body>
</html>"""

        div = fig.to_html(include_plotlyjs="directory", full_html=False)
        with file_path.open("w", encoding="utf-8") as fh:
            fh.write(Template(template_html).render(fig=div, **RUN_META_DATA))

        bundle_path = file_path.parent / "plotly.min.js"
        if not bundle_path.exists():
            bundle_path.write_text(get_plotlyjs(), encoding="utf-8")

        return file_path

    def to_json(
        self,
        fig: go.Figure,
        location: str,
        year: typing.Any,
        output_path: pathlib.Path,
        groupby: list[str],
        idx: typing.Hashable,
    ) -> pathlib.Path:
        """
        Serialise *fig* to a JSON file.

        Parameters
        ----------
        fig
            Plotly figure to serialise.
        location
            Geographic location label stored in the JSON payload.
        year
            Year label stored in the JSON payload (``None`` when the
            view does not group by year).
        output_path
            Folder to save the file under.
        groupby
            Groupby keys needed to fill the file-name template.
        idx
            Data-frame index used to fill the file-name template.

        Returns
        -------
        :
            Path of the written file.
        """
        file_name = f"{self.construct_file_name(groupby, idx)}.json"
        file_path = output_path / "JSON" / file_name

        with file_path.open("w", encoding="utf-8") as fh:
            json_content = {
                "location": location,
                "year": year,
                "plot_type": self.cfg.database_plot_type,
                "bus_carrier": self.cfg.database_bus_carrier,
                "specifier": self.cfg.database_specifier,
                "plotly_dict": fig.to_json(),
            }
            json.dump(json_content, fh)

        return file_path

construct_file_name(groupby, idx)

Construct the file name based on the provided template.

Parameters:

Name Type Description Default
groupby list[str]

List of groupby keys used to fill the template.

required
idx typing.Hashable

The data-frame index from the groupby clause. Scalars of any type (str, int, etc.) are wrapped in a tuple so they can be zipped with groupby.

required

Returns:

Type Description
str

The constructed filename string (without extension).

Source code in evals/plots/components.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def construct_file_name(self, groupby: list[str], idx: typing.Hashable) -> str:
    """
    Construct the file name based on the provided template.

    Parameters
    ----------
    groupby
        List of groupby keys used to fill the template.
    idx
        The data-frame index from the groupby clause.  Scalars of
        any type (str, int, etc.) are wrapped in a tuple so they
        can be zipped with *groupby*.

    Returns
    -------
    :
        The constructed filename string (without extension).
    """
    if not isinstance(idx, (list, tuple)):
        idx = (idx,)
    resolved = {
        g: ALIAS_LOCATION_REV.get(i, i) for g, i in zip(groupby, idx, strict=True)
    }
    return self.cfg.file_name_template.format(metric=self.metric_name, **resolved)

to_html(fig, output_path, groupby, idx)

Serialise fig to an HTML file.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Plotly figure to serialise.

required
output_path pathlib.Path

Folder to save the file under (the HTML/ sub-folder is created by the caller).

required
groupby list[str]

Groupby keys needed to fill the file-name template.

required
idx typing.Hashable

Data-frame index used to fill the file-name template.

required

Returns:

Type Description
pathlib.Path

Path of the written file.

Source code in evals/plots/components.py
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
    def to_html(
        self,
        fig: go.Figure,
        output_path: pathlib.Path,
        groupby: list[str],
        idx: typing.Hashable,
    ) -> pathlib.Path:
        """
        Serialise *fig* to an HTML file.

        Parameters
        ----------
        fig
            Plotly figure to serialise.
        output_path
            Folder to save the file under (the ``HTML/`` sub-folder is
            created by the caller).
        groupby
            Groupby keys needed to fill the file-name template.
        idx
            Data-frame index used to fill the file-name template.

        Returns
        -------
        :
            Path of the written file.
        """
        file_name = f"{self.construct_file_name(groupby, idx)}.html"
        file_path = output_path / "HTML" / file_name

        template_html = """\
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="repo_name" content="{{ repo_name }}" />
<meta name="repo_branch" content="{{ repo_branch }}" />
<meta name="repo_hash" content="{{ repo_hash }}" />
</head>
<body>
    {{ fig }}
</body>
</html>"""

        div = fig.to_html(include_plotlyjs="directory", full_html=False)
        with file_path.open("w", encoding="utf-8") as fh:
            fh.write(Template(template_html).render(fig=div, **RUN_META_DATA))

        bundle_path = file_path.parent / "plotly.min.js"
        if not bundle_path.exists():
            bundle_path.write_text(get_plotlyjs(), encoding="utf-8")

        return file_path

to_json(fig, location, year, output_path, groupby, idx)

Serialise fig to a JSON file.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Plotly figure to serialise.

required
location str

Geographic location label stored in the JSON payload.

required
year typing.Any

Year label stored in the JSON payload (None when the view does not group by year).

required
output_path pathlib.Path

Folder to save the file under.

required
groupby list[str]

Groupby keys needed to fill the file-name template.

required
idx typing.Hashable

Data-frame index used to fill the file-name template.

required

Returns:

Type Description
pathlib.Path

Path of the written file.

Source code in evals/plots/components.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
def to_json(
    self,
    fig: go.Figure,
    location: str,
    year: typing.Any,
    output_path: pathlib.Path,
    groupby: list[str],
    idx: typing.Hashable,
) -> pathlib.Path:
    """
    Serialise *fig* to a JSON file.

    Parameters
    ----------
    fig
        Plotly figure to serialise.
    location
        Geographic location label stored in the JSON payload.
    year
        Year label stored in the JSON payload (``None`` when the
        view does not group by year).
    output_path
        Folder to save the file under.
    groupby
        Groupby keys needed to fill the file-name template.
    idx
        Data-frame index used to fill the file-name template.

    Returns
    -------
    :
        Path of the written file.
    """
    file_name = f"{self.construct_file_name(groupby, idx)}.json"
    file_path = output_path / "JSON" / file_name

    with file_path.open("w", encoding="utf-8") as fh:
        json_content = {
            "location": location,
            "year": year,
            "plot_type": self.cfg.database_plot_type,
            "bus_carrier": self.cfg.database_bus_carrier,
            "specifier": self.cfg.database_specifier,
            "plotly_dict": fig.to_json(),
        }
        json.dump(json_content, fh)

    return file_path

LayoutStyler

Apply common Plotly layout, title, legend, and footnote styling.

Parameters:

Name Type Description Default
cfg typing.Any

Plot configuration object with legend_header, title_font_size, font_size, legend_font_size, xaxis_title, yaxes_showgrid, yaxes_visible, and footnotes attributes.

required
Source code in evals/plots/components.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
class LayoutStyler:
    """
    Apply common Plotly layout, title, legend, and footnote styling.

    Parameters
    ----------
    cfg
        Plot configuration object with ``legend_header``,
        ``title_font_size``, ``font_size``, ``legend_font_size``,
        ``xaxis_title``, ``yaxes_showgrid``, ``yaxes_visible``, and
        ``footnotes`` attributes.
    """

    def __init__(self, cfg: typing.Any) -> None:
        self.cfg = cfg

    def set_base_layout(self, fig: go.Figure) -> None:
        """Apply base figure properties (size, fonts, grid, zero-line)."""
        fig.update_layout(
            height=800,
            font_family="Calibri",
            plot_bgcolor="#ffffff",
            legend_title_text=self.cfg.legend_header,
        )
        fig.update_yaxes(
            showgrid=self.cfg.yaxes_showgrid, visible=self.cfg.yaxes_visible
        )
        fig.add_hline(y=0.0)
        fig.update_xaxes(
            showgrid=False,
            tickprefix="<b>",
            ticksuffix="</b>",
            tickfont_size=20,
            title_font={"size": 20},
        )
        fig.update_layout(
            xaxis={"categoryorder": "category ascending"},
            hovermode="x",
        )
        fig.update_layout(legend={"traceorder": "reversed"})

    def style_title_and_legend_and_xaxis_label(self, fig: go.Figure) -> None:
        """Update figure title font, legend position, and x-axis label."""
        fig.update_layout(
            title_font_size=self.cfg.title_font_size,
            font_size=self.cfg.font_size,
            legend={
                "x": 1,
                "y": 1,
                "font": {"size": self.cfg.legend_font_size},
            },
        )
        if self.cfg.xaxis_title:
            fig.update_layout(xaxis_title=self.cfg.xaxis_title)

    def count_footnote_lines(self) -> int:
        """Return the number of ``<br>`` line-breaks across all footnote texts."""
        return "".join(self.cfg.footnotes).count("<br>")

    def append_footnote(
        self,
        fig: go.Figure,
        footnote_text: str,
        y: float = -0.17,
        align: str = None,
    ) -> None:
        """
        Append a single footnote annotation at the bottom of *fig*.

        Parameters
        ----------
        fig
            Target figure.
        footnote_text
            Text to display.
        y
            Vertical position (negative values move the footnote down).
        align
            Text alignment mode.
        """
        if footnote_text:
            fig.add_annotation(
                text=footnote_text,
                xref="paper",
                yref="paper",
                xanchor="left",
                yanchor="top",
                x=0,
                y=y,
                showarrow=False,
                font={"size": 15},
                align=align,
            )

    def append_footnotes(self, fig: go.Figure) -> None:
        """Append both configured footnotes and adjust the bottom margin."""
        self.append_footnote(fig, self.cfg.footnotes[0], align="left")
        self.append_footnote(fig, self.cfg.footnotes[1], y=-0.2)
        if lines := self.count_footnote_lines():
            fig.update_layout(margin={"b": 125 + 50 * lines})

    def apply(self, fig: go.Figure, unit: str = "", location: str = "") -> None:
        """
        Apply the full standard layout sequence to *fig*.

        Calls :meth:`set_base_layout`,
        :meth:`style_title_and_legend_and_xaxis_label`, and
        :meth:`append_footnotes` in order.

        Parameters
        ----------
        fig
            Target figure.
        unit
            Display unit (currently unused here; available for subclasses).
        location
            Geographic location (currently unused here; available for
            subclasses).
        """
        self.set_base_layout(fig)
        self.style_title_and_legend_and_xaxis_label(fig)
        self.append_footnotes(fig)

append_footnote(fig, footnote_text, y=-0.17, align=None)

Append a single footnote annotation at the bottom of fig.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Target figure.

required
footnote_text str

Text to display.

required
y float

Vertical position (negative values move the footnote down).

-0.17
align str

Text alignment mode.

None
Source code in evals/plots/components.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
def append_footnote(
    self,
    fig: go.Figure,
    footnote_text: str,
    y: float = -0.17,
    align: str = None,
) -> None:
    """
    Append a single footnote annotation at the bottom of *fig*.

    Parameters
    ----------
    fig
        Target figure.
    footnote_text
        Text to display.
    y
        Vertical position (negative values move the footnote down).
    align
        Text alignment mode.
    """
    if footnote_text:
        fig.add_annotation(
            text=footnote_text,
            xref="paper",
            yref="paper",
            xanchor="left",
            yanchor="top",
            x=0,
            y=y,
            showarrow=False,
            font={"size": 15},
            align=align,
        )

append_footnotes(fig)

Append both configured footnotes and adjust the bottom margin.

Source code in evals/plots/components.py
289
290
291
292
293
294
def append_footnotes(self, fig: go.Figure) -> None:
    """Append both configured footnotes and adjust the bottom margin."""
    self.append_footnote(fig, self.cfg.footnotes[0], align="left")
    self.append_footnote(fig, self.cfg.footnotes[1], y=-0.2)
    if lines := self.count_footnote_lines():
        fig.update_layout(margin={"b": 125 + 50 * lines})

apply(fig, unit='', location='')

Apply the full standard layout sequence to fig.

Calls :meth:set_base_layout, :meth:style_title_and_legend_and_xaxis_label, and :meth:append_footnotes in order.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Target figure.

required
unit str

Display unit (currently unused here; available for subclasses).

''
location str

Geographic location (currently unused here; available for subclasses).

''
Source code in evals/plots/components.py
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
def apply(self, fig: go.Figure, unit: str = "", location: str = "") -> None:
    """
    Apply the full standard layout sequence to *fig*.

    Calls :meth:`set_base_layout`,
    :meth:`style_title_and_legend_and_xaxis_label`, and
    :meth:`append_footnotes` in order.

    Parameters
    ----------
    fig
        Target figure.
    unit
        Display unit (currently unused here; available for subclasses).
    location
        Geographic location (currently unused here; available for
        subclasses).
    """
    self.set_base_layout(fig)
    self.style_title_and_legend_and_xaxis_label(fig)
    self.append_footnotes(fig)

count_footnote_lines()

Return the number of <br> line-breaks across all footnote texts.

Source code in evals/plots/components.py
250
251
252
def count_footnote_lines(self) -> int:
    """Return the number of ``<br>`` line-breaks across all footnote texts."""
    return "".join(self.cfg.footnotes).count("<br>")

set_base_layout(fig)

Apply base figure properties (size, fonts, grid, zero-line).

Source code in evals/plots/components.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
def set_base_layout(self, fig: go.Figure) -> None:
    """Apply base figure properties (size, fonts, grid, zero-line)."""
    fig.update_layout(
        height=800,
        font_family="Calibri",
        plot_bgcolor="#ffffff",
        legend_title_text=self.cfg.legend_header,
    )
    fig.update_yaxes(
        showgrid=self.cfg.yaxes_showgrid, visible=self.cfg.yaxes_visible
    )
    fig.add_hline(y=0.0)
    fig.update_xaxes(
        showgrid=False,
        tickprefix="<b>",
        ticksuffix="</b>",
        tickfont_size=20,
        title_font={"size": 20},
    )
    fig.update_layout(
        xaxis={"categoryorder": "category ascending"},
        hovermode="x",
    )
    fig.update_layout(legend={"traceorder": "reversed"})

style_title_and_legend_and_xaxis_label(fig)

Update figure title font, legend position, and x-axis label.

Source code in evals/plots/components.py
236
237
238
239
240
241
242
243
244
245
246
247
248
def style_title_and_legend_and_xaxis_label(self, fig: go.Figure) -> None:
    """Update figure title font, legend position, and x-axis label."""
    fig.update_layout(
        title_font_size=self.cfg.title_font_size,
        font_size=self.cfg.font_size,
        legend={
            "x": 1,
            "y": 1,
            "font": {"size": self.cfg.legend_font_size},
        },
    )
    if self.cfg.xaxis_title:
        fig.update_layout(xaxis_title=self.cfg.xaxis_title)

TimeSeriesStyler

Apply time-series-specific Plotly styling.

Parameters:

Name Type Description Default
cfg typing.Any

Plot configuration object with yaxis_color attribute.

required
Source code in evals/plots/components.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
class TimeSeriesStyler:
    """
    Apply time-series-specific Plotly styling.

    Parameters
    ----------
    cfg
        Plot configuration object with ``yaxis_color`` attribute.
    """

    def __init__(self, cfg: typing.Any) -> None:
        self.cfg = cfg

    def style_inflexible_demand(self, fig: go.Figure) -> None:
        """Override trace style for the 'Inflexible Demand' series."""
        fig.update_traces(
            selector={"name": "Inflexible Demand"},
            fillcolor=None,
            fill=None,
            stackgroup=None,
            legendrank=2000,
        )

    def style_axes_and_layout(self, fig: go.Figure, title: str, unit: str) -> None:
        """
        Update axes and layout for time-series figures.

        Parameters
        ----------
        fig
            Target figure.
        title
            Figure title string.
        unit
            Unit label for the y-axis title.
        """
        fig.update_yaxes(
            tickprefix="<b>",
            ticksuffix="</b>",
            tickfont_size=15,
            color=self.cfg.yaxis_color,
            title_font_size=15,
            tickformat=".0f",
            gridwidth=1,
            gridcolor="gainsboro",
        )
        fig.update_xaxes(ticklabelmode="period")
        fig.update_layout(
            title=title,
            yaxis_title=unit,
            hovermode="x",
        )

style_axes_and_layout(fig, title, unit)

Update axes and layout for time-series figures.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Target figure.

required
title str

Figure title string.

required
unit str

Unit label for the y-axis title.

required
Source code in evals/plots/components.py
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
def style_axes_and_layout(self, fig: go.Figure, title: str, unit: str) -> None:
    """
    Update axes and layout for time-series figures.

    Parameters
    ----------
    fig
        Target figure.
    title
        Figure title string.
    unit
        Unit label for the y-axis title.
    """
    fig.update_yaxes(
        tickprefix="<b>",
        ticksuffix="</b>",
        tickfont_size=15,
        color=self.cfg.yaxis_color,
        title_font_size=15,
        tickformat=".0f",
        gridwidth=1,
        gridcolor="gainsboro",
    )
    fig.update_xaxes(ticklabelmode="period")
    fig.update_layout(
        title=title,
        yaxis_title=unit,
        hovermode="x",
    )

style_inflexible_demand(fig)

Override trace style for the 'Inflexible Demand' series.

Source code in evals/plots/components.py
530
531
532
533
534
535
536
537
538
def style_inflexible_demand(self, fig: go.Figure) -> None:
    """Override trace style for the 'Inflexible Demand' series."""
    fig.update_traces(
        selector={"name": "Inflexible Demand"},
        fillcolor=None,
        fill=None,
        stackgroup=None,
        legendrank=2000,
    )

TotalSumRenderer

Add total-sum scatter annotations on top of stacked bar charts.

Parameters:

Name Type Description Default
col_values str

Name of the column containing numeric values (e.g. the metric column returned by the chart's df property).

required
plot_xaxis str

Name of the column used as the x-axis (typically DataModel.YEAR).

required
unit str

Unit string appended to the text template.

required
Source code in evals/plots/components.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
class TotalSumRenderer:
    """
    Add total-sum scatter annotations on top of stacked bar charts.

    Parameters
    ----------
    col_values
        Name of the column containing numeric values (e.g. the metric
        column returned by the chart's ``df`` property).
    plot_xaxis
        Name of the column used as the x-axis (typically ``DataModel.YEAR``).
    unit
        Unit string appended to the text template.
    """

    def __init__(self, col_values: str, plot_xaxis: str, unit: str) -> None:
        self.col_values = col_values
        self.plot_xaxis = plot_xaxis
        self.unit = unit

    def add_sum_trace(
        self,
        fig: go.Figure,
        df: pd.DataFrame,
        orientation: str = None,
        name_trace: str = "Sum",
    ) -> None:
        """
        Add a scatter trace for total-sum labels on a plain bar chart.

        Parameters
        ----------
        fig
            Target figure.
        df
            Data frame in the same format as ``ESMBarChart.df``.
        orientation
            ``"up"`` for positive values, ``"down"`` for negative
            values, ``None`` for stacked (all values).
        name_trace
            Trace name (used for identification in the JSON output).
        """
        sign = 1
        if orientation == "up":
            values = df[df[self.col_values].gt(0)]
        elif orientation == "down":
            sign = -1
            values = df[df[self.col_values].le(0)]
        else:
            values = df

        totals = values.groupby(self.plot_xaxis).sum(numeric_only=True)
        totals["display_value"] = totals[self.col_values].apply(prettify_number)
        y_offset = totals[self.col_values].abs().max() / 100 * sign

        scatter = go.Scatter(
            x=totals.index,
            y=totals[self.col_values] + y_offset,
            text=totals["display_value"],
            texttemplate="<b> %{text} " + self.unit + "</b>",
            mode="text",
            textposition=f"{'bottom' if orientation == 'down' else 'top'} center",
            showlegend=False,
            name=name_trace,
            textfont={"size": 18},
            hoverinfo="skip",
        )
        fig.add_trace(scatter)

    def add_subplot_traces(
        self,
        fig: go.Figure,
        df: pd.DataFrame,
        facet_column: str,
    ) -> None:
        """
        Add total-sum scatter traces for every subplot in a faceted bar chart.

        Parameters
        ----------
        fig
            Target figure (must have been built with ``make_subplots``).
        df
            Data frame in the same format as ``ESMGroupedBarChart.df``.
        facet_column
            Column name used for faceting (e.g. ``DataModel.BUS_CARRIER``).
        """

        def _add_for_xaxis(xaxis: go.layout.XAxis) -> None:
            idx = xaxis["anchor"].lstrip("y")
            sector = xaxis["title"]["text"].lstrip("<b>")
            values = df[df[facet_column] == sector].copy()

            values["pos"] = values[self.col_values].where(values[self.col_values].gt(0))
            values["neg"] = values[self.col_values].where(values[self.col_values].le(0))

            totals = values.groupby(self.plot_xaxis).sum(numeric_only=True)
            totals["pos_display"] = totals["pos"].apply(prettify_number)
            totals["neg_display"] = totals["neg"].apply(prettify_number)

            col = int(idx) if idx else 1

            if totals["pos"].sum() > 0:
                fig.add_trace(
                    go.Scatter(
                        x=totals.index,
                        y=totals["pos"] + totals["pos"].abs().max() / 100,
                        text=totals["pos_display"],
                        texttemplate="<b>%{text}</b>",
                        mode="text",
                        textposition="top center",
                        showlegend=False,
                        name="Sum",
                        textfont={"size": 18},
                        hoverinfo="skip",
                    ),
                    col=col,
                    row=1,
                )

            if totals["neg"].sum() < 0:
                fig.add_trace(
                    go.Scatter(
                        x=totals.index,
                        y=totals["neg"] - totals["neg"].abs().max() / 100,
                        text=totals["neg_display"],
                        texttemplate="<b>%{text}</b>",
                        mode="text",
                        textposition="bottom center",
                        showlegend=False,
                        name="Sum",
                        textfont={"size": 18},
                        hoverinfo="skip",
                    ),
                    col=col,
                    row=1,
                )

        fig.for_each_xaxis(_add_for_xaxis)

add_subplot_traces(fig, df, facet_column)

Add total-sum scatter traces for every subplot in a faceted bar chart.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Target figure (must have been built with make_subplots).

required
df pandas.DataFrame

Data frame in the same format as ESMGroupedBarChart.df.

required
facet_column str

Column name used for faceting (e.g. DataModel.BUS_CARRIER).

required
Source code in evals/plots/components.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
def add_subplot_traces(
    self,
    fig: go.Figure,
    df: pd.DataFrame,
    facet_column: str,
) -> None:
    """
    Add total-sum scatter traces for every subplot in a faceted bar chart.

    Parameters
    ----------
    fig
        Target figure (must have been built with ``make_subplots``).
    df
        Data frame in the same format as ``ESMGroupedBarChart.df``.
    facet_column
        Column name used for faceting (e.g. ``DataModel.BUS_CARRIER``).
    """

    def _add_for_xaxis(xaxis: go.layout.XAxis) -> None:
        idx = xaxis["anchor"].lstrip("y")
        sector = xaxis["title"]["text"].lstrip("<b>")
        values = df[df[facet_column] == sector].copy()

        values["pos"] = values[self.col_values].where(values[self.col_values].gt(0))
        values["neg"] = values[self.col_values].where(values[self.col_values].le(0))

        totals = values.groupby(self.plot_xaxis).sum(numeric_only=True)
        totals["pos_display"] = totals["pos"].apply(prettify_number)
        totals["neg_display"] = totals["neg"].apply(prettify_number)

        col = int(idx) if idx else 1

        if totals["pos"].sum() > 0:
            fig.add_trace(
                go.Scatter(
                    x=totals.index,
                    y=totals["pos"] + totals["pos"].abs().max() / 100,
                    text=totals["pos_display"],
                    texttemplate="<b>%{text}</b>",
                    mode="text",
                    textposition="top center",
                    showlegend=False,
                    name="Sum",
                    textfont={"size": 18},
                    hoverinfo="skip",
                ),
                col=col,
                row=1,
            )

        if totals["neg"].sum() < 0:
            fig.add_trace(
                go.Scatter(
                    x=totals.index,
                    y=totals["neg"] - totals["neg"].abs().max() / 100,
                    text=totals["neg_display"],
                    texttemplate="<b>%{text}</b>",
                    mode="text",
                    textposition="bottom center",
                    showlegend=False,
                    name="Sum",
                    textfont={"size": 18},
                    hoverinfo="skip",
                ),
                col=col,
                row=1,
            )

    fig.for_each_xaxis(_add_for_xaxis)

add_sum_trace(fig, df, orientation=None, name_trace='Sum')

Add a scatter trace for total-sum labels on a plain bar chart.

Parameters:

Name Type Description Default
fig plotly.graph_objects.Figure

Target figure.

required
df pandas.DataFrame

Data frame in the same format as ESMBarChart.df.

required
orientation str

"up" for positive values, "down" for negative values, None for stacked (all values).

None
name_trace str

Trace name (used for identification in the JSON output).

'Sum'
Source code in evals/plots/components.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
def add_sum_trace(
    self,
    fig: go.Figure,
    df: pd.DataFrame,
    orientation: str = None,
    name_trace: str = "Sum",
) -> None:
    """
    Add a scatter trace for total-sum labels on a plain bar chart.

    Parameters
    ----------
    fig
        Target figure.
    df
        Data frame in the same format as ``ESMBarChart.df``.
    orientation
        ``"up"`` for positive values, ``"down"`` for negative
        values, ``None`` for stacked (all values).
    name_trace
        Trace name (used for identification in the JSON output).
    """
    sign = 1
    if orientation == "up":
        values = df[df[self.col_values].gt(0)]
    elif orientation == "down":
        sign = -1
        values = df[df[self.col_values].le(0)]
    else:
        values = df

    totals = values.groupby(self.plot_xaxis).sum(numeric_only=True)
    totals["display_value"] = totals[self.col_values].apply(prettify_number)
    y_offset = totals[self.col_values].abs().max() / 100 * sign

    scatter = go.Scatter(
        x=totals.index,
        y=totals[self.col_values] + y_offset,
        text=totals["display_value"],
        texttemplate="<b> %{text} " + self.unit + "</b>",
        mode="text",
        textposition=f"{'bottom' if orientation == 'down' else 'top'} center",
        showlegend=False,
        name=name_trace,
        textfont={"size": 18},
        hoverinfo="skip",
    )
    fig.add_trace(scatter)

empty_figure(title)

Return an empty graph with explanation text.

Parameters:

Name Type Description Default
title str

The figure title displayed at the top of the graph.

required

Returns:

Type Description
plotly.graph_objects.Figure

The plotly figure with a text that explains that there is no data available for this view.

Source code in evals/plots/components.py
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
def empty_figure(title: str) -> go.Figure:
    """
    Return an empty graph with explanation text.

    Parameters
    ----------
    title
        The figure title displayed at the top of the graph.

    Returns
    -------
    :
        The plotly figure with a text that explains that there is no
        data available for this view.
    """
    fig = px.bar(pd.DataFrame(), title=title)
    fig.add_annotation(
        text="No Values to be displayed",
        xref="paper",
        yref="paper",
        xanchor="center",
        yanchor="middle",
        x=0.5,
        y=0.5,
        showarrow=False,
        font={"size": 20},
    )
    fig.update_xaxes(showgrid=False, showticklabels=False)
    fig.update_yaxes(showgrid=False, showticklabels=False)
    fig.update_layout(xaxis_title="", yaxis_title="", plot_bgcolor="white")
    fig.update_layout(meta=[RUN_META_DATA])
    return fig

empty_input(df)

Return True if df is empty or contains only NaN values.

Source code in evals/plots/components.py
32
33
34
def empty_input(df: pd.DataFrame) -> bool:
    """Return True if *df* is empty or contains only NaN values."""
    return bool(df.empty or df.isna().all().all())