Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add _repr_html_ to Projection class #951

Merged
merged 3 commits into from
Nov 16, 2018
Merged

Conversation

DPeterK
Copy link
Member

@DPeterK DPeterK commented Nov 14, 2017

Provides a visual repr (!) of any cartopy Projection subclass in jupyter notebook.

For example:

screen shot 2017-11-14 at 14 12 05

Also works interactively:

screen shot 2017-11-14 at 14 15 50

@corinnebosley
Copy link
Member

@dkillick We seem to be having some shapereader issues here. I've seen this before in the last few days but I can't remember what the problem was. Has the testing version changed or anything?

@QuLogic
Copy link
Member

QuLogic commented Nov 29, 2017

Needs a rebase.

Copy link
Member

@corinnebosley corinnebosley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dkillick I forgot, I've already had a look and it looks good to me. I'm happy if you are.

That doesn't really matter of course, what matters is that @pp-mo is happy because he's got the big green button...

@pp-mo
Copy link
Member

pp-mo commented Nov 29, 2017

@pp-mo ... he's got the big green button...

I wasn't actively looking at this.
@QuLogic can merge, no ?

@@ -146,6 +146,13 @@ def domain(self):
domain = self._domain = sgeom.Polygon(self.boundary)
return domain

def _repr_html_(self):
import matplotlib.pyplot as plt
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a comment to explain context.
? something like ? "enables self-display in a Jupyter notebook"

@pp-mo
Copy link
Member

pp-mo commented Nov 30, 2017

Other than missing explanatory comment,
looks fine to me + ready to go 👍

@QuLogic
Copy link
Member

QuLogic commented Dec 1, 2017

I don't think this is correct. You are a) not actually returning any HTML for the repr, b) assuming the user is using an HTML-type backend for Matplotlib (i.e., inline, nbagg, etc.) and c) trying to modify the current figure.

@ajdawson
Copy link
Member

ajdawson commented Dec 1, 2017

I don't think this is correct.

I have to agree here. This seems like an abuse of the _repr_html_ method. I'm not sure the end result is that worthwhile either, saving 3 lines that will inevitably need to be written anyway.

@pp-mo
Copy link
Member

pp-mo commented Dec 1, 2017

not actually returning any HTML
seems like an abuse of the repr_html method.

Apologies I've clearly missed the point here.

What was the scope of this intended to be, then ?

@ajdawson
Copy link
Member

ajdawson commented Dec 1, 2017

_repr_html_ is supposed to return some html that represents your object. This implementation doesn't do that, instead it returns None and operates by modifying the state of pyplot (by drawing on the current figure).

@DPeterK
Copy link
Member Author

DPeterK commented Dec 1, 2017

@ajdawson @QuLogic I hope you're both satisfied, it's taken all afternoon to get this returning html.

I'm not sure the end result is that worthwhile either

Of course the end result is worthwhile. You can now see what the Projection looks like, which is the most obviously useful way to represent a cartopy Projection ever, especially when we're operating in such an always-visual environment as a jupyter notebook.

@ajdawson
Copy link
Member

ajdawson commented Dec 2, 2017

I hope you're both satisfied

Not yet I'm afraid. The problem as I see it is that a repr of any kind shouldn't interfere with the state of a program. Your implementation modifies the state of pyplot by overwriting the current figure.

To illustrate, let's say I'm working in a notebook with interactive plotting, I make a plot of something I'm interested in. I then construct a crs and look at its repr as in your example, now my figure is gone, replaced with the repr.

This functionality really needs to be implemented in a way that is side-effect free, because users should not have to care what state their notebook is in before they use it.

Of course the end result is worthwhile.

I'm not disputing that it isn't a useful way to see what a projection looks like. I'm suggesting that the implicit and potentially destructive modification of pyplot state is not worth it over the alternative which is simply:

crs = ccrs.Stereographic()
ax = plt.axes(projection=crs)
ax.coastlines()

This is both totally explicit (user knows what is going to happen) and is only a small amount of extra typing. If you want to push on and implement this feature in a side-effect free way then that is cool by me, but you'll have to consider if it is worth spending the extra time.

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should work fine with a small tweak to creating the Matplotlib side of things.

But on a side note, isn't there a _repr_png_ as well that could be used to avoid needing to wrap with <img>?

from io import BytesIO
import matplotlib.pyplot as plt
# Produce a visual repr of the Projection instance.
ax = plt.axes(projection=self)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fig, ax = plt.subplots(subplot_kw={'projection': self}) to avoid working on the current figure.

@pelson
Copy link
Member

pelson commented Jan 3, 2018

But on a side note, isn't there a repr_png as well that could be used to avoid needing to wrap with ?

I believe so. There is also a _repr_svg_ which is what shapely uses. Given we are producing a line drawing (coastlines and boundary), perhaps SVG is the better option.

@dopplershift
Copy link
Contributor

Given that we’re talking pretty detailed coastlines and the raster image is mostly white, I’d not expect SVG to be much, if any, smaller. I can live with either option.

@pelson
Copy link
Member

pelson commented Jan 11, 2018

Yes to doing this kind of thing. It is a hugely beneficial change.

This ties in closely with some work on improving the rendering of the projection list (which currently has some ugly rendering, particularly of the UTC projection). It like the projections used in the list to be the same as the projection used in the repr. The relevant doc code is currently at https://github.com/SciTools/cartopy/blob/master/docs/make_projection.py#L93.

Happy to take a look at bringing the two things together, but just wanted to share my hopes for what the __repr__ shows.

# "Save" to a bytestring.
fmt = 'png'
buf = BytesIO()
plt.savefig(buf, format=fmt)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the comment above, can use fig.savefig() and fig.close()

@dopplershift dopplershift added this to the 0.17 milestone Nov 1, 2018
@dopplershift
Copy link
Contributor

Any chance you can get this cleaned up and rebased in time for Friday’s release?

@DPeterK
Copy link
Member Author

DPeterK commented Nov 15, 2018

@dopplershift thanks for the poke! This is now rebased and all good suchlike.

I've also moved to _repr_svg_, as it really is a much better solution for richly representing a projection (I didn't know this existed when I started out, otherwise I would have gone straight for SVG over html).

@DPeterK
Copy link
Member Author

DPeterK commented Nov 15, 2018

Here's some examples of it functioning:

Basic SVG repr
screenshot 2018-11-15 at 12 25 10

From variable assignment
screenshot 2018-11-15 at 12 25 28

Fallback to default repr
screenshot 2018-11-15 at 12 26 28

# "Rewind" the buffer to the start and return it as an svg string.
buf.seek(0)
return buf.read()
finally:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you take out the finally clause? Because it’s in the finally, that code runs for every invocation of _repr_svg. Also, just calling repr wouldn’t be enough, you need to return it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, returning a string as an SVG isn't going to end well I don't think.

Copy link
Member

@pelson pelson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm inclined to include the repr(projection) in there too. We can do this pretty easily with a _repr_html_ (sorry for flip-flopping since my comment last year!).

fig, ax = plt.subplots(figsize=(5, 3),
subplot_kw={'projection': self})
ax.set_global()
ax.coastlines('110m')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could try out the 'auto' scale... might produce a more desirable result for limited area maps.

# "Rewind" the buffer to the start and return it as an svg string.
buf.seek(0)
return buf.read()
finally:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, returning a string as an SVG isn't going to end well I don't think.

pass
else:
if six.PY2:
from StringIO import StringIO
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just use six here. six.StringIO - might actually make migration to a python 3 only codebase easier too.

@pelson
Copy link
Member

pelson commented Nov 16, 2018

I tried pushing to this PR, but alas don't have the foo. Instead, here is my proposed repr implementation:

    def _repr_html_(self):
        import cgi
        try:
            # As matplotlib is not a core cartopy dependency, don't error
            # if it's not available.
            import matplotlib.pyplot as plt
        except ImportError:
            svg = None
        else:
            # Produce a visual repr of the Projection instance.
            fig, ax = plt.subplots(figsize=(5, 3),
                                   subplot_kw={'projection': self})
            ax.set_global()
            ax.coastlines('auto')
            ax.gridlines()
            buf = six.StringIO()
            fig.savefig(buf, format='svg', bbox_inches='tight')
            plt.close(fig)
            # "Rewind" the buffer to the start and return it as an svg string.
            buf.seek(0)
            svg = buf.read()
        return '<div>{}</div><pre>{}</pre>'.format(
                    svg or '', cgi.escape(repr(self)))

Result:

screenshot 2018-11-16 at 04 35 04

@QuLogic
Copy link
Member

QuLogic commented Nov 16, 2018

I'm inclined to include the repr(projection) in there too. We can do this pretty easily with a _repr_html_ (sorry for flip-flopping since my comment last year!).

That doesn't seem necessary. If you return None, then you get the default repr, which is what the finally above does, since it's missing a return.

screenshot_2018-11-15 untitled 1

@dopplershift
Copy link
Contributor

But having a return in the finally for any repr will result in unexpected behavior:

def foo():
    try:
        return 5
    finally:
        return 6
print(foo())

prints 6.

@QuLogic
Copy link
Member

QuLogic commented Nov 16, 2018

Yes, drop the finally; return None outside the else: will be fine.

@QuLogic
Copy link
Member

QuLogic commented Nov 16, 2018

Docs are hard to find, but there is this comment:

If you need a repr*_ method which may not be able to return something, you can have it return None, IIRC.

so return None is the solution here, no need for _repr_html_.

@pelson
Copy link
Member

pelson commented Nov 16, 2018

Confirmed that you are both spot on. Have implemented in #1196.

@QuLogic QuLogic merged commit 61e3173 into SciTools:master Nov 16, 2018
@pelson
Copy link
Member

pelson commented Nov 16, 2018

Awesome! Great stuff @dkillick. Thanks for persevering!

@DPeterK DPeterK deleted the proj_repr_html branch November 16, 2018 11:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants