Source code for calliope.postprocess.plotting.plotting

"""
Copyright (C) 2013-2019 Calliope contributors listed in AUTHORS.
Licensed under the Apache 2.0 License (see LICENSE file).

plotting.py
~~~~~~~~~~~

Functionality to plot model data.

"""

import os
import re
import warnings

import plotly.offline as pltly
import jinja2

from calliope.exceptions import warn
from calliope.postprocess.plotting.capacity import plot_capacity
from calliope.postprocess.plotting.timeseries import plot_timeseries
from calliope.postprocess.plotting.transmission import plot_transmission
from calliope.postprocess.plotting.flows import plot_flows
from calliope.postprocess.plotting.util import type_of_script


def plot_summary(model, to_file=False, mapbox_access_token=None):
    """
    Plot a summary containing timeseries, installed capacities, and
    transmission plots. Returns a HTML string by default, returns None if
    ``to_file`` given (and saves the HTML string to file).

    Parameters
    ----------
    to_file : str, optional
        Path to output file to save HTML to.
    mapbox_access_token : str, optional
        (passed to plot_transmission) If given and a valid Mapbox API
        key, a Mapbox map is drawn for lat-lon coordinates, else
        (by default), a more simple built-in map.

    """
    subset = {"costs": ["monetary"]}

    timeseries = _plot(*plot_timeseries(model, subset=subset), html_only=True)
    capacity = _plot(*plot_capacity(model, subset=subset), html_only=True)
    if "loc_coordinates" in model._model_data:
        transmission = _plot(
            *plot_transmission(
                model, html_only=True, mapbox_access_token=mapbox_access_token
            ),
            html_only=True,
        )
    else:
        transmission = "<br><br><p>No location coordinates defined -<br>not plotting transmission.</p>"

    template_path = os.path.join(
        os.path.dirname(__file__), "..", "..", "config", "plots_template.html"
    )
    with open(template_path, "r") as f:
        html_template = jinja2.Template(f.read())

    if "solution_time" in model._model_data.attrs:
        solution_time = model._model_data.attrs["solution_time"] / 60
        time_finished = model._model_data.attrs["time_finished"]
        result_stats = "taking {:.2f} minutes to solve, completed at {}".format(
            solution_time, time_finished
        )
    else:
        result_stats = "inputs only"

    html = html_template.render(
        model_name=model.model_config["name"],
        calliope_version=model._model_data.attrs["calliope_version"],
        result_stats=result_stats,
        top=timeseries,
        bottom_left=capacity,
        bottom_right=transmission,
    )

    # Strip plotly-inserted style="..." attributes
    html = re.sub(r'style=".+?"', "", html)

    if to_file:
        with open(to_file, "w") as f:
            f.write(html)
    else:
        return html


def _plot(
    data,
    layout,
    html_only=False,
    to_file=False,
    layout_updates=None,
    plotly_kwarg_updates=None,
    # kwargs are included as they get passed through from the
    # plotting accessor method, but are not actually used
    **kwargs,
):

    # We will bee moving plotting out of Calliope core code (and a method of the model object)
    # in 0.7.0; current implementation is now untested.
    warnings.warn(
        "Plotting will no longer be available as a method of the Calliope model object in"
        "future versions of Calliope. In the meantime, as of v0.6.6, plotting is untested; this functionality should now be used with caution. We expect to reintroduce it as a seperate module in v0.7.0.",
        FutureWarning,
    )

    plotly_kwargs = dict(
        show_link=False,
        config={
            "displaylogo": False,
            "modeBarButtonsToRemove": ["sendDataToCloud"],
        },
    )

    if type_of_script() == "jupyter":
        plotter = pltly.iplot
        plotly_filename_key = "filename"
        pltly.init_notebook_mode(connected=False)
    else:
        plotter = pltly.plot
        plotly_filename_key = "image_filename"
        plotly_kwargs["auto_open"] = True
        plotly_kwargs["filename"] = "temp_plot.html"

    if layout_updates:
        layout = {**layout, **layout_updates}

    if plotly_kwarg_updates:
        plotly_kwargs = {**plotly_kwargs, **plotly_kwarg_updates}

    if to_file:
        filename, image_type = to_file.rsplit(".", 1)
        # Plotly can only save certain file types
        if image_type not in ["png", "jpeg", "svg", "webp"]:
            raise TypeError(
                "Unable to save plot as `{}`, choose from "
                "[`png`, `jpeg`, `svg`, `webp`]".format(image_type)
            )
        if "updatemenus" in layout:
            raise ValueError(
                "Unable to save multiple arrays to SVG, pick one array only"
            )
        else:
            plotly_kwargs.update(image=image_type)
            plotly_kwargs[plotly_filename_key] = filename

    if data:
        if html_only:
            return pltly.plot(
                {"data": data, "layout": layout},
                include_plotlyjs=False,
                output_type="div",
                **plotly_kwargs,
            )
        else:
            plotter({"data": data, "layout": layout}, **plotly_kwargs)
    else:
        raise ValueError("No data to plot.")


[docs]class ModelPlotMethods: def __init__(self, model): self._model = model _docstring_additions = """ html_only : bool, optional; default = False Returns a html string for embedding the plot in a webpage to_file : False or str, optional; default = False Will save plot to file with the given name and extension. `to_file='plot.svg'` to save to SVG, `to_file='plot.png'` for a static PNG image. Allowed file extensions are: ['png', 'jpeg', 'svg', 'webp']. layout_updates : dict, optional The given dict will be merged with the Plotly layout dict generated by the Calliope plotting function, overwriting keys that already exist. plotly_kwarg_updates : dict, optional The given dict will be merged with the Plotly plot function's keyword arguments generated by the Calliope plotting function, overwriting keys that already exist. """ def check_optimality(self): termination = self._model._model_data.attrs.get( "termination_condition", "did_not_yet_run" ) # a MILP model which optimises to within the MIP gap, but does not fully # converge on the LP relaxation, may return as 'feasible', not 'optimal' if termination not in ["optimal", "did_not_yet_run", "feasible"]: warn("Model termination condition was not optimal. Plotting may fail!")
[docs] def timeseries(self, **kwargs): self.check_optimality() data, layout = plot_timeseries(self._model, **kwargs) return _plot(data, layout, **kwargs)
timeseries.__doc__ = plot_timeseries.__doc__.rstrip() + _docstring_additions
[docs] def capacity(self, **kwargs): self.check_optimality() data, layout = plot_capacity(self._model, **kwargs) return _plot(data, layout, **kwargs)
capacity.__doc__ = plot_capacity.__doc__.rstrip() + _docstring_additions
[docs] def transmission(self, **kwargs): self.check_optimality() data, layout = plot_transmission(self._model, **kwargs) return _plot(data, layout, **kwargs)
transmission.__doc__ = plot_transmission.__doc__.rstrip() + _docstring_additions def flows(self, **kwargs): self.check_optimality() data, layout = plot_flows(self._model, **kwargs) return _plot(data, layout, **kwargs) flows.__doc__ = plot_flows.__doc__.rstrip() + _docstring_additions
[docs] def summary(self, **kwargs): self.check_optimality() return plot_summary(self._model, **kwargs)
summary.__doc__ = plot_summary.__doc__