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

Provide projection-aware tick formatters. #401

Merged
merged 12 commits into from
Jun 17, 2014

Conversation

ajdawson
Copy link
Member

This PR adds projection-aware tick formatters for latitude and longitude axes. These fit with the current tick labelling capabilities, designed only for rectangular projections. They work by transforming the tick values to the PlateCarree projections before doing the formatting, resulting in meaningful tick values. The intention here is to provide something that "just works" for the most common cases (see #204), but future efforts may be better off going in the direction of improving the grid line labelling abilities of cartopy.

from cartopy.mpl.geoaxes import GeoAxes


class _GeoFormatter(Formatter):
Copy link
Member

Choose a reason for hiding this comment

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

Why did you decide to make this private? I can imagine users wanting to subclass it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I made the methods public but I forgot to do the same to the class...

@pelson
Copy link
Member

pelson commented Mar 25, 2014

@ajdawson - just wondered if you could comment on the existing Formatters ~ https://github.com/SciTools/cartopy/blob/master/lib/cartopy/mpl/gridliner.py#L83 which produce a similar result (to the test images). I've not had much of a chance to look at this code yet, so please forgive the naive question.

@pelson
Copy link
Member

pelson commented Mar 25, 2014

Given how flaky image testing of text is with matplotlib, I'm keen to avoid testing these with visual tests. There is a method on a formatter (format_data) which might help with making the tests non-visual.

@ajdawson
Copy link
Member Author

@ajdawson - just wondered if you could comment on the existing Formatters ~ https://github.com/SciTools/cartopy/blob/master/lib/cartopy/mpl/gridliner.py#L83 which produce a similar result (to the test images). I've not had much of a chance to look at this code yet, so please forgive the naive question.

These new formatters will transform the tick locations from native plot projection coordinates to PlateCarree() in order to make formatted labels, which helps with situations like labelling plots with a non-zero central longitude.

These new formatters are formatter classes in their own right, as opposed to instances of FuncFormatter which allows more flexibility and tailoring them to the particular needs of lat/lon labelling such as controlling E/W direction labels on longitudes of 0 and 180, as well as more generic options like the choice of format string and control over the degree symbol being given to the user.

@ajdawson
Copy link
Member Author

Given how flaky image testing of text is with matplotlib, I'm keen to avoid testing these with visual tests. There is a method on a formatter ( format_data ) which might help with making the tests non-visual.

I wanted to do this, but I found it tricky at first and ended up re-implementing much of the workings in the tests which is obviously not desirable. I think perhaps there is an opportunity to use mock axes objects to avoid some of this hassle (an axes object is required even for the format_data()/__call__() method). I felt it was simpler and neater to produce a few images, but I hadn't considered the flakiness of image comparisons of text objects when making this decision. I'll attempt to revise the testing.

@ajdawson
Copy link
Member Author

I've added a new commit that revises the testing procedure and makes the base GeoFormatter class public. I haven't implemented GeoFormatter as an abstract class at this stage.

These commits will need squashing before eventual merge (just a reminder to myself).

from cartopy.mpl.geoaxes import GeoAxes


class GeoFormatter(Formatter):
Copy link
Member

Choose a reason for hiding this comment

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

@ajdawson - just wondering if you have more of these subclasses up your sleeve? If not, I'm inclined to suggest this becomes PlateCarreeFormatter - essentially I currently believe the GeoFormatter is an attempt to generalise a problem which only needs two solutions (Latitude & Longitude) and as a result has interfaces which make the implementation more complex than they (currently) need to be (e.g. make_transform_args and extract_transform_result). Is that an unfair observation? I'm happy to accept this complexity if there are potentially good cases for which this interface is useful, but I think it is worth knowing what they are even if we don't implement them just yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd agree that currently this is just for Plate Carree and that I can't see a situation where it would be anything else. I decided that the added complexity (make_transform_args and extract_transform_args) were better than having repeated code in each of a latitude and longitude formatter, which do pretty much the same thing but in subtly different ways. That is in fact why I originally made this class private, it was not designed for other people to subclass, but simply to support common functionality in LatitudeFormatter and LongitudeFormatter classes. In my original implementation I was going to make _make_transform_args and _extract_transform_result private methods but I thought that was not right since they are required to implement the (then private) _GeoFormatter class.

This seems to be in need of a refactor, suggestions very welcome.

Copy link
Member Author

Choose a reason for hiding this comment

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

This seems to be in need of a refactor, suggestions very welcome.

[Mostly note to self:] Perhaps a private fuinction to handle the common stuff instead of a shared parent class? One which accepts a function that transforms a single value, rather than two functions that tell it what arguments to use and how to get the value.

@ajdawson
Copy link
Member Author

I've added another commit. This simplifies the code somewhat, but it retains its previous structure. I've flip-flopped and made the shared parent class private again (renamed as _PlateCarreeFormatter). The intention of this class was only ever to prevent duplicating large parts of the __call__ method in the two public classes LatitudeFormatter and LongitudeFormatter. As such the _PlateCarreeFormatter class should be considered an implementation detail rather than part of the interface. If users want to make subclasses they can subclass either LatitudeFormatter or LongitudeFormatter.

Considering _PlateCarreeFormatter as an implementation detail makes the concerns about metaclasses etc less important, in fact I probably could have got rid of the stub methods all together.

@esc24
Copy link
Member

esc24 commented Apr 1, 2014

I've had a look and the refactor looks good. However I have an issue with the axis attribute. You use it in __call__() assuming it has been set. I'm ok with that as you'll get a sensible attribute error if it isn't set, but it did make me curious where/how it is set. It turns out it is set by the call to axis.set_major_fomatter(). The problem I have (and it's not your fault) is that you cannot reuse these formatters for multiple plots, or rather you can, but you'll get unwanted results. I had a couple of figures with different projections (a mercator and a platecarree with non zero central longitude). I used the same lat and lon formatters as I wanted them to share the same formatting (i.e. deg symbol etc.), but this caused the tick numbers in the first plot to change when they were redrawn as the projection the formatters use had changed from Mercator to PlateCarree(central_longitude=180).

@ajdawson
Copy link
Member Author

ajdawson commented Apr 1, 2014

However I have an issue with the axis attribute. You use it in call() assuming it has been set. I'm ok with that as you'll get a sensible attribute error if it isn't set, but it did make me curious where/how it is set.

I'm just taking this behaviour from the Formatter class from matplotlib.

The problem I have (and it's not your fault) is that you cannot reuse these formatters for multiple plots, or rather you can, but you'll get unwanted results.

I can't see an easy way around this issue. There labels are only formatted as they are drawn so if you want to use the same formatter for 3 different plots (on different projections) you are asking the formatter to know which one is being drawn. This is not an obvious point, but it seems like it is asking too much of the formatter. Perhaps a note needs to go into the docstring to explain this issue.

"""
raise NotImplementedError("A subclass must implement this method.")

def hemisphere(self, value, value_source_crs):
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason why this is public? It would make the docs a little odd.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is public in the derived classes because it is the thing people will likely modify (if they modify anything at all). Will this class show up in the docs if it is private?

I'm half tempted to not bother including these in the base class at all, since it is meant to be a private implementation detail...

@ajdawson
Copy link
Member Author

I've made slight modifications to this again. The parent class is now totally private (think of it only as an implementation detail, users shouldn't know or care that it exists). I've also added a note to the docstrings to indicate the limitations of use.

I don't necessarily think the way this is implemented is ideal, but it fills an important gap. Users are increasingly asking how to label their plots, particularly Plate Carree plots since these are very common and users expect it to "just work".

# Round the transformed values to the nearest 0.1 degree for display
# purposes (transforms can introduce minor rounding errors that make
# the tick values look bad).
projected_value = round(10 * projected_value) / 10
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if a significant figure approach might be better here, I'm bothered about this when we zoom in a long way...

@pelson
Copy link
Member

pelson commented Jun 2, 2014

Would you mind putting a what's new entry in (that could also be the place for the example if you don't want to put something in the gallery/a documentation page for ticker)

@ajdawson
Copy link
Member Author

ajdawson commented Jun 9, 2014

I have added (many) more commits which should bring this up to scratch. I think I've addressed all the previously raised points. The test failure is the problem fixed by #414 I think.

.. plot:: examples/tick_labels.py
:width: 300pt:

.. plot:: examples/tick_labels.py
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason for doing this twice?

Copy link
Member Author

Choose a reason for hiding this comment

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

No... spurious copy/paste I guess.

@pelson
Copy link
Member

pelson commented Jun 10, 2014

In much the same way that I don't think the built-in matplotlib tick functionality is the way the final solution is going to go, this does provide a stop-gap which makes some frequently needed functionality easier. As a result, I'd be prepared to merge this with a few tweaks. @esc24 - since you originally exposed the set_ticks placeholder functionality, would you like to comment on the approach (rather than any low level detail for now) before we go on to iterate and ultimately merge?

@esc24
Copy link
Member

esc24 commented Jun 16, 2014

I'm 👍 on the approach. I just had a go with it and I like it.

ccrs.Mercator)):
raise TypeError("This formatter cannot be used with "
"non-rectangular projections.")
target = ccrs.PlateCarree()
Copy link
Member

Choose a reason for hiding this comment

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

I'd consider doing this in __init__ and assigning to self._target_projection or similar to save instantiating a new object on every call.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea. I'll implement this and ping you back when done.

@esc24
Copy link
Member

esc24 commented Jun 16, 2014

@ajdawson - A minor comment to consider and a fix to a conflict in what's new and I'll merge.

ajdawson added 6 commits June 16, 2014 18:15
Also added notes to docstrings about reuse of the formatter
classes.
Tick locations are rounded after transformation to prevent spurious
values from making the plot look bad. The precision to which they
are rounded is increased, and can be overridden by the user.
@ajdawson
Copy link
Member Author

@esc24 - I've made the target projection a class variable, it never changes so it seems like a good option. The merge conflict should be eliminated now too.

@cpelley cpelley mentioned this pull request Jun 17, 2014
16 tasks
esc24 added a commit that referenced this pull request Jun 17, 2014
Provide projection-aware tick formatters.
@esc24 esc24 merged commit 28ef382 into SciTools:master Jun 17, 2014
@pelson
Copy link
Member

pelson commented Jun 17, 2014

Thanks @ajdawson & thanks @esc24 for reviewing. Nice feature to have when using the mpl built-in ticks. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants