Skip to content

New C++ contour code with corner_mask kwarg #3874

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

Merged
merged 1 commit into from
Feb 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
2015-02-19 Rewrite of C++ code that calculates contours to add support for
corner masking. This is controlled by the 'corner_mask' keyword
in plotting commands 'contour' and 'contourf'. - IMT

2015-01-23 Text bounding boxes are now computed with advance width rather than
ink area. This may result in slightly different placement of text.
ink area. This may result in slightly different placement of text.

2014-10-27 Allowed selection of the backend using the `MPLBACKEND` environment
variable. Added documentation on backend selection methods.

2014-09-27 Overhauled `colors.LightSource`. Added `LightSource.hillshade` to
allow the independent generation of illumination maps. Added new
types of blending for creating more visually appealing shaded relief
plots (e.g. `blend_mode="overlay"`, etc, in addition to the legacy
"hsv" mode).
allow the independent generation of illumination maps. Added new
types of blending for creating more visually appealing shaded relief
plots (e.g. `blend_mode="overlay"`, etc, in addition to the legacy
"hsv" mode).

2014-06-10 Added Colorbar.remove()

Expand Down Expand Up @@ -64,7 +68,7 @@
of a generator at line 263.

2014-04-02 Added `clipon=False` to patch creation of wedges and shadows
in `pie`.
in `pie`.

2014-02-25 In backend_qt4agg changed from using update -> repaint under
windows. See comment in source near `self._priv_update` for
Expand Down
25 changes: 25 additions & 0 deletions doc/api/api_changes/2015-02-19-IMT.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
Changed behaviour of contour plots
``````````````````````````````````

The default behaviour of :func:`~matplotlib.pyplot.contour` and
:func:`~matplotlib.pyplot.contourf` when using a masked array is now determined
by the new keyword argument `corner_mask`, or if this is not specified then
the new rcParam `contour.corner_mask` instead. The new default behaviour is
equivalent to using `corner_mask=True`; the previous behaviour can be obtained
using `corner_mask=False` or by changing the rcParam. The example
http://matplotlib.org/examples/pylab_examples/contour_corner_mask.py
demonstrates the difference. Use of the old contouring algorithm, which is
obtained with `corner_mask='legacy'`, is now deprecated.

Contour labels may now appear in different places than in earlier versions of
matplotlib.

In addition, the keyword argument `nchunk` now applies to
:func:`~matplotlib.pyplot.contour` as well as
:func:`~matplotlib.pyplot.contourf`, and it subdivides the domain into
subdomains of exactly `nchunk` by `nchunk` quads, whereas previously it was
only roughly `nchunk` by `nchunk` quads.

The C/C++ object that performs contour calculations used to be stored in the
public attribute QuadContourSet.Cntr, but is now stored in a private attribute
and should not be accessed by end users.
16 changes: 16 additions & 0 deletions doc/users/whats_new/plotting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ Added center and frame kwargs to pie

These control where the center of the pie graph are and if
the Axes frame is shown.

Contour plot corner masking
```````````````````````````

Ian Thomas rewrote the C++ code that calculates contours to add support for
corner masking. This is controlled by a new keyword argument
``corner_mask`` in the functions :func:`~matplotlib.pyplot.contour` and
:func:`~matplotlib.pyplot.contourf`. The previous behaviour, which is now
obtained using ``corner_mask=False``, was for a single masked point to
completely mask out all four quads touching that point. The new behaviour,
obtained using ``corner_mask=True``, only masks the corners of those
quads touching the point; any triangular corners comprising three unmasked
points are contoured as usual. If the ``corner_mask`` keyword argument is not
specified, the default value is taken from rcParams.

.. plot:: mpl_examples/pylab_examples/contour_corner_mask.py
35 changes: 35 additions & 0 deletions examples/pylab_examples/contour_corner_mask.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/usr/bin/env python
"""
Illustrate the difference between corner_mask=False and corner_mask=True
for masked contour plots.
"""
import matplotlib.pyplot as plt
import numpy as np

# Data to plot.
x, y = np.meshgrid(np.arange(7), np.arange(10))
z = np.sin(0.5*x)*np.cos(0.52*y)

# Mask various z values.
mask = np.zeros_like(z, dtype=np.bool)
mask[2, 3:5] = True
mask[3:5, 4] = True
mask[7, 2] = True
mask[5, 0] = True
mask[0, 6] = True
z = np.ma.array(z, mask=mask)

corner_masks = [False, True]
for i, corner_mask in enumerate(corner_masks):
plt.subplot(1, 2, i+1)
cs = plt.contourf(x, y, z, corner_mask=corner_mask)
plt.contour(cs, colors='k')
plt.title('corner_mask = {}'.format(corner_mask))

# Plot grid.
plt.grid(c='k', ls='-', alpha=0.3)

# Indicate masked points with red circles.
plt.plot(np.ma.array(x, mask=~mask), y, 'ro')

plt.show()
95 changes: 71 additions & 24 deletions lib/matplotlib/contour.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import numpy as np
from numpy import ma
import matplotlib._cntr as _cntr
import matplotlib._contour as _contour
import matplotlib.path as mpath
import matplotlib.ticker as ticker
import matplotlib.cm as cm
Expand Down Expand Up @@ -989,9 +990,10 @@ def get_transform(self):

def __getstate__(self):
state = self.__dict__.copy()
# the C object Cntr cannot currently be pickled. This isn't a big issue
# as it is not actually used once the contour has been calculated
state['Cntr'] = None
# the C object _contour_generator cannot currently be pickled. This
# isn't a big issue as it is not actually used once the contour has
# been calculated.
state['_contour_generator'] = None
return state

def legend_elements(self, variable_name='x', str_format=str):
Expand Down Expand Up @@ -1433,18 +1435,34 @@ def _process_args(self, *args, **kwargs):
Process args and kwargs.
"""
if isinstance(args[0], QuadContourSet):
C = args[0].Cntr
if self.levels is None:
self.levels = args[0].levels
self.zmin = args[0].zmin
self.zmax = args[0].zmax
self._corner_mask = args[0]._corner_mask
if self._corner_mask == 'legacy':
contour_generator = args[0].Cntr
else:
contour_generator = args[0]._contour_generator
else:
x, y, z = self._contour_args(args, kwargs)

_mask = ma.getmask(z)
if _mask is ma.nomask:
if _mask is ma.nomask or not _mask.any():
_mask = None
C = _cntr.Cntr(x, y, z.filled(), _mask)

self._corner_mask = kwargs.get('corner_mask', None)
if self._corner_mask is None:
self._corner_mask = mpl.rcParams['contour.corner_mask']

if self._corner_mask == 'legacy':
cbook.warn_deprecated('1.5',
name="corner_mask='legacy'",
alternative='corner_mask=False or True')
contour_generator = _cntr.Cntr(x, y, z.filled(), _mask)
else:
contour_generator = _contour.QuadContourGenerator(
x, y, z.filled(), _mask, self._corner_mask, self.nchunk)

t = self.get_transform()

Expand All @@ -1465,7 +1483,10 @@ def _process_args(self, *args, **kwargs):
self.ax.update_datalim([(x0, y0), (x1, y1)])
self.ax.autoscale_view(tight=True)

self.Cntr = C
if self._corner_mask == 'legacy':
self.Cntr = contour_generator
else:
self._contour_generator = contour_generator

def _get_allsegs_and_allkinds(self):
"""
Expand All @@ -1476,20 +1497,28 @@ def _get_allsegs_and_allkinds(self):
lowers, uppers = self._get_lowers_and_uppers()
allkinds = []
for level, level_upper in zip(lowers, uppers):
nlist = self.Cntr.trace(level, level_upper,
nchunk=self.nchunk)
nseg = len(nlist) // 2
segs = nlist[:nseg]
kinds = nlist[nseg:]
allsegs.append(segs)
if self._corner_mask == 'legacy':
nlist = self.Cntr.trace(level, level_upper,
nchunk=self.nchunk)
nseg = len(nlist) // 2
vertices = nlist[:nseg]
kinds = nlist[nseg:]
else:
vertices, kinds = \
self._contour_generator.create_filled_contour(
level, level_upper)
allsegs.append(vertices)
allkinds.append(kinds)
else:
allkinds = None
for level in self.levels:
nlist = self.Cntr.trace(level)
nseg = len(nlist) // 2
segs = nlist[:nseg]
allsegs.append(segs)
if self._corner_mask == 'legacy':
nlist = self.Cntr.trace(level)
nseg = len(nlist) // 2
vertices = nlist[:nseg]
else:
vertices = self._contour_generator.create_contour(level)
allsegs.append(vertices)
return allsegs, allkinds

def _contour_args(self, args, kwargs):
Expand Down Expand Up @@ -1672,6 +1701,20 @@ def _initialize_x_y(self, z):

Optional keyword arguments:

*corner_mask*: [ *True* | *False* | 'legacy' ]
Enable/disable corner masking, which only has an effect if *Z* is
a masked array. If *False*, any quad touching a masked point is
masked out. If *True*, only the triangular corners of quads
nearest those points are always masked out, other triangular
corners comprising three unmasked points are contoured as usual.
If 'legacy', the old contouring algorithm is used, which is
equivalent to *False* and is deprecated, only remaining whilst the
new algorithm is tested fully.

If not specified, the default is taken from
rcParams['contour.corner_mask'], which is True unless it has
been modified.

*colors*: [ *None* | string | (mpl_colors) ]
If *None*, the colormap specified by cmap will be used.

Expand Down Expand Up @@ -1750,6 +1793,15 @@ def _initialize_x_y(self, z):
filled contours, the default is *True*. For line contours,
it is taken from rcParams['lines.antialiased'].

*nchunk*: [ 0 | integer ]
Copy link
Member

Choose a reason for hiding this comment

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

@ianthomas23 - does this actually get used? It is a complex piece of behaviour for not really that immediately obvious benefit (i.e. the rendering artifacts can really be quite prohibitive). Anyone else use 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.

I've never used chunking myself, and I don't recall any questions about it on the mailing lists in the last few years. I presume it was originally used to work around limitations in some backends which could only accept arrays of points of some maximum size, but I don't know what the status of such limitations are now.

Copy link
Member

Choose a reason for hiding this comment

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

We have the ability to simplify and split long paths before the render stage, so I'm not too sure how valuable the chunking actually is anymore. @WeatherGod do you use this? @mdboom - are my facts correct?

Copy link
Member

Choose a reason for hiding this comment

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

This discussion certainly makes sense to me -- but I've never used nchunk personally. I think most of the backends (excepting probably Gdk) can handle really long paths now and the implicit simplification should help a lot with path length anyways. Maybe we deprecate it now and remove it later if no one complains? (Though I suspect no one will complain until after it's been removed ;)

Copy link
Member

Choose a reason for hiding this comment

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

I've never used chunking; it was in the original algorithm, and when writing the wrapper I left it in in case it might be needed for very large, complex cases. I suspect that even now, some renderers (ps? pdf?) can bog down with extremely complex patches; but I don't have any examples.

Copy link
Member

Choose a reason for hiding this comment

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

correct me if I am wrong, but the whole chunking thing is only associated with the legacy mode. So, it gets deprecated with the mode.

Copy link
Member Author

Choose a reason for hiding this comment

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

@WeatherGod: you are wrong, consider yourself corrected!

Copy link
Member

Choose a reason for hiding this comment

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

@ianthomas23 Took another look, and I see what you mean, I was looking at the calls to trace() and filled_polygons() and saw nchunks used in the former but not the latter. I see now that the new generator takes the nchunk argument directly.

But, in some respects, the intent behind the question still stands. We don't have to have the new generator take that argument, and relegate it completely to the legacy mode.

If 0, no subdivision of the domain. Specify a positive integer to
divide the domain into subdomains of *nchunk* by *nchunk* quads.
Chunking reduces the maximum length of polygons generated by the
contouring algorithm which reduces the rendering workload passed
on to the backend and also requires slightly less RAM. It can
however introduce rendering artifacts at chunk boundaries depending
on the backend, the *antialiased* flag and value of *alpha*.

contour-only keyword arguments:

*linewidths*: [ *None* | number | tuple of numbers ]
Expand All @@ -1774,13 +1826,6 @@ def _initialize_x_y(self, z):

contourf-only keyword arguments:

*nchunk*: [ 0 | integer ]
If 0, no subdivision of the domain. Specify a positive integer to
divide the domain into subdomains of roughly *nchunk* by *nchunk*
points. This may never actually be advantageous, so this option may
be removed. Chunking introduces artifacts at the chunk boundaries
unless *antialiased* is *False*.

*hatches*:
A list of cross hatch patterns to use on the filled areas.
If None, no hatching will be added to the contour.
Expand All @@ -1802,4 +1847,6 @@ def _initialize_x_y(self, z):
.. plot:: mpl_examples/pylab_examples/contour_demo.py

.. plot:: mpl_examples/pylab_examples/contourf_demo.py

.. plot:: mpl_examples/pylab_examples/contour_corner_mask.py
"""
9 changes: 9 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,13 @@ def validate_negative_linestyle_legacy(s):
return (0, dashes) # (offset, (solid, blank))


def validate_corner_mask(s):
if s == 'legacy':
return s
else:
return validate_bool(s)


def validate_tkpythoninspect(s):
# Introduced 2010/07/05
warnings.warn("tk.pythoninspect is obsolete, and has no effect")
Expand Down Expand Up @@ -589,8 +596,10 @@ def __call__(self, s):
'image.origin': ['upper', six.text_type], # lookup table
'image.resample': [False, validate_bool],

# contour props
'contour.negative_linestyle': ['dashed',
validate_negative_linestyle_legacy],
'contour.corner_mask': [True, validate_corner_mask],

# axes props
'axes.axisbelow': [False, validate_bool],
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading