Skip to content

Commit

Permalink
Merge pull request #15 from khaeru/compat-plotnine
Browse files Browse the repository at this point in the history
Add .compat.plotnine
  • Loading branch information
khaeru committed Feb 5, 2021
2 parents 607039d + a2f06cd commit c5f5744
Show file tree
Hide file tree
Showing 14 changed files with 270 additions and 47 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ jobs:
name: ${{ matrix.os }}-py${{ matrix.python-version }}

steps:
- name: Cancel previous runs that have not completed
uses: styfle/cancel-workflow-action@0.7.0
with:
access_token: ${{ github.token }}

- uses: actions/checkout@v2
with:
path: genno
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ htmlcov
# mypy
.mypy_cache

# sphinx
# Sphinx and related generated files
doc/*.svg
doc/.ipynb_checkpoints
doc/_build
18 changes: 18 additions & 0 deletions doc/compat-ixmp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
:mod:`ixmp`
***********

:doc:`Package documentation <ixmp:index>`

.. currentmodule:: genno.compat.ixmp

.. automodule:: genno.compat.ixmp
:members:

.. automodule:: genno.compat.ixmp.computations
:members:

.. automodule:: genno.compat.ixmp.reporter
:members:

.. automodule:: genno.compat.ixmp.util
:members:
64 changes: 64 additions & 0 deletions doc/compat-plotnine.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
:mod:`plotnine`
***************

:doc:`Package documentation <plotnine:index>`

.. currentmodule:: genno.compat.plotnine

To use :class:`.Plot`:

.. ipython::

In [1]: from pathlib import Path
...:
...: import xarray as xr
...: import plotnine as p9
...:
...: from genno import Computer, Quantity
...: from genno.compat.plotnine import Plot


1. Create a subclass that overrides :meth:`Plot.generate`, :attr:`Plot.basename`, and optionally :attr:`Plot.inputs`.

.. ipython::

In [1]: class DemoPlot(Plot):
...: basename = "plotnine-demo"
...: suffix = ".svg"
...:
...: def generate(self, x, y):
...: data = x.merge(y, on="t")
...: return (
...: p9.ggplot(data, p9.aes(x="x", y="y"))
...: + p9.geom_line(color="red")
...: + p9.geom_point(color="blue")
...: )

2. Call :meth:`.make_task` to get a task tuple suitable for adding to a :class:`.Computer`:

.. ipython:: python
# Set up a Computer, including the output path and some data
c = Computer(output_dir=Path("."))
t = [("t", [-1, 0, 1])]
c.add("x:t", Quantity(xr.DataArray([1.0, 2, 3], coords=t), name="x"))
c.add("y:t", Quantity(xr.DataArray([1.0, 4, 9], coords=t), name="y"))
# Add the plot to the Computer
c.add("plot", DemoPlot.make_task("x:t", "y:t"))
# Show the task that was added
c.graph["plot"]
3. :meth:`.get` the node. The result is the path the the saved plot(s).

.. ipython::

In [1]: c.get("plot")
Out[1]: ./test.svg

.. image:: ./plotnine-demo.svg
:alt: Demonstration output from genno.compat.plotnine.

.. automodule:: genno.compat.plotnine
:members:
15 changes: 15 additions & 0 deletions doc/compat-pyam.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
:mod:`pyam`
***********

:doc:`Package documentation <pyam:index>`

.. currentmodule:: genno.compat.pyam

.. automodule:: genno.compat.pyam
:members:

.. automodule:: genno.compat.pyam.computations
:members:

.. automodule:: genno.compat.pyam.util
:members:
39 changes: 0 additions & 39 deletions doc/compat.rst

This file was deleted.

1 change: 1 addition & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"ixmp": ("https://docs.messageix.org/projects/ixmp/en/latest", None),
"message_ix": ("https://docs.messageix.org/en/latest", None),
"pint": ("https://pint.readthedocs.io/en/stable/", None),
"plotnine": ("https://plotnine.readthedocs.io/en/stable/", None),
"pyam": ("https://pyam-iamc.readthedocs.io/en/stable/", None),
"python": ("https://docs.python.org/3/", None),
}
16 changes: 14 additions & 2 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,24 @@ genno is built on high-quality Python data packages including ``dask``, ``xarray

.. toctree::
:maxdepth: 2
:caption: Contents
:caption: User guide

usage
api
compat
whatsnew

.. toctree::
:maxdepth: 1
:caption: Interoperability

compat-ixmp
compat-plotnine
compat-pyam

.. toctree::
:maxdepth: 2
:caption: Development

releasing

License
Expand Down
8 changes: 3 additions & 5 deletions doc/releasing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Before releasing, check:

Address any failures before releasing.

1. Edit :file:`doc/whatsnew.rst` to replace "Next release" with the version number and date.
1. Edit :file:`doc/whatsnew.rst`.
Comment the heading "Next release", then insert another heading below it, at the same level, with the version number and date.
Make a commit with a message like "Mark vX.Y.Z in whatsnew.rst".

2. Tag the version, e.g.::
Expand Down Expand Up @@ -40,10 +41,7 @@ Address any failures before releasing.

$ twine upload dist/*

7. Edit :file:`doc/whatsnew.rst` to add a new heading for the next release.
Make a commit with a message like "Reset whatsnew.rst to development state".

8. Push the commits and tag to GitHub::
7. Push the commits and tag to GitHub::

$ git push --tags

Expand Down
2 changes: 2 additions & 0 deletions doc/whatsnew.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ What's new
Next release
============

- Add :doc:`compat-plotnine` compatibility (:pull:`15`).
- Add a :doc:`usage` overview to the documentation (:pull:`13`).

v0.2.0 (2021-01-18)
===================
Expand Down
10 changes: 10 additions & 0 deletions genno/compat/plotnine/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
try:
import plotnine # noqa: F401
except ModuleNotFoundError: # pragma: no cover
HAS_PLOTNINE = False
else:
HAS_PLOTNINE = True

from .plot import Plot

__all__ = ["Plot"]
73 changes: 73 additions & 0 deletions genno/compat/plotnine/plot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import logging
from abc import ABC, abstractmethod
from typing import Hashable, Sequence

import plotnine as p9

log = logging.getLogger(__name__)


class Plot(ABC):
"""Class for plotting using :mod:`plotnine`."""

#: Filename base for saving the plot.
basename = ""
#: File extension; determines file format.
suffix = ".pdf"
#: Keys for quantities needed by :meth:`generate`.
inputs: Sequence[Hashable] = []
#: Keyword arguments for :meth:`plotnine.ggplot.save`.
save_args = dict(verbose=False)

# TODO add static geoms automatically in generate()
__static: Sequence = []

def save(self, config, *args, **kwargs):
path = config["output_dir"] / f"{self.basename}{self.suffix}"

log.info(f"Save to {path}")

args = map(lambda qty: qty.to_series().rename(qty.name).reset_index(), args)

plot_or_plots = self.generate(*args, **kwargs)

try:
# Single plot
plot_or_plots.save(path, **self.save_args)
except AttributeError:
# Iterator containing multiple plots
p9.save_as_pdf_pages(plot_or_plots, path, **self.save_args)

return path

@classmethod
def make_task(cls, *inputs):
"""Return a task :class:`tuple` to add to a Computer.
Parameters
----------
inputs : sequence of :class:`.Key`, :class:`str`, or other hashable, optional
If provided, overrides the :attr:`inputs` property of the class.
Returns
-------
tuple
- The first, callable element of the task is :meth:`save`.
- The second element is ``"config"``, to access the configuration of the
Computer.
- The third and following elements are the `inputs`.
"""
return tuple([cls().save, "config"] + (list(inputs) if inputs else cls.inputs))

@abstractmethod
def generate(self, *args, **kwargs):
"""Generate and return the plot.
Must be implemented by subclasses.
Parameters
----------
args : sequence of :class:`pandas.DataFrame`
Because :mod:`plotnine` operates on pandas data structures, :obj:`Quantity`
are automatically converted before being provided to :meth:`generate`.
"""
58 changes: 58 additions & 0 deletions genno/tests/compat/test_plotnine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from pathlib import Path

import plotnine as p9
import pytest
import xarray as xr

from genno import Computer, Quantity
from genno.compat.plotnine import Plot


def test_Plot(tmp_path):
c = Computer(output_dir=tmp_path)
t = [("t", [-1, 0, 1])]
c.add("x:t", Quantity(xr.DataArray([1.0, 2, 3], coords=t), name="x"))
c.add("y:t", Quantity(xr.DataArray([1.0, 2, 3], coords=t), name="y"))

# Exception raised when the class is incomplete
with pytest.raises(
TypeError,
match=("Can't instantiate abstract class Plot1 with abstract methods generate"),
):

class Plot1(Plot):
inputs = ["x:t", "y:t"]

c.add("plot", Plot1.make_task())

class Plot2(Plot):
basename = "test"
suffix = ".svg"

def generate(self, x, y):
return p9.ggplot(x.merge(y, on="t"), p9.aes(x="x", y="y")) + p9.geom_point()

c.add("plot", Plot2.make_task("x:t", "y:t"))

# Graph contains the task. Don't compare the callable
assert ("config", "x:t", "y:t") == c.graph["plot"][1:]
assert callable(c.graph["plot"][0])

# Plot can be generated
result = c.get("plot")

# Result is the path to the file
assert isinstance(result, Path)

# Concrete Plot subclasses can be further subclassed
class Plot3(Plot2):
suffix = ".pdf"
inputs = ["x:t", "y:t"]

def generate(self, x, y):
# Return an iterable of 2 plots
return (super().generate(x, y), super().generate(x, y))

# Multi-page PDFs can be saved
c.add("plot", Plot3.make_task())
c.get("plot")
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,14 @@ docs =
# Specific packages for which compatibility is provided
ixmp =
ixmp >= 3.2.0
plotnine =
plotnine
pyam =
pyam-iamc
# All compat packages
compat =
%(ixmp)s
%(plotnine)s
%(pyam)s
tests =
%(compat)s
Expand Down Expand Up @@ -81,6 +84,8 @@ ignore_missing_imports = True
ignore_missing_imports = True
[mypy-pint.*]
ignore_missing_imports = True
[mypy-plotnine.*]
ignore_missing_imports = True
[mypy-pyam.*]
ignore_missing_imports = True
[mypy-setuptools.*]
Expand Down

0 comments on commit c5f5744

Please sign in to comment.