Skip to content
Closed
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
8 changes: 8 additions & 0 deletions docs/iris/example_code/graphics/anomaly_log_colouring.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""
.. _anomaly_pseudocolour:

Colouring anomaly data with logarithmic scaling
===============================================

Expand Down Expand Up @@ -26,6 +28,12 @@
<http://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.pcolormesh>`_).
See also: http://en.wikipedia.org/wiki/False_color#Pseudocolor.


.. seealso::

A related example shows how to make a contour plot with the same data and
scaling requirements. See : :ref:`anomaly_contours`.

"""
import cartopy.crs as ccrs
import iris
Expand Down
146 changes: 146 additions & 0 deletions docs/iris/example_code/graphics/anomaly_log_contours.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""
.. _anomaly_contours:

Contouring anomaly data with logarithmic levels
===============================================

In this example, we need to plot anomaly data where the values have a
"logarithmic" significance -- i.e. we want to give approximately equal ranges
of colour between data values of, say, 1 and 10 as between 10 and 100.

In many similar cases, as long as the required data range is symmetrical about
Copy link
Member

Choose a reason for hiding this comment

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

This reads like the "similar cases" are logarithmic scaling, but you are referring to linear colour scaling aren't you? I don't think it reads well. You should be specific about this e.g.:

For linear colour scaling it is perfectly practical to select a suitable colormap and allow
contour to pick the level colours automatically from that, provided that the data range is
symmetric about zero, and that the colormap is diverging (see the 'Diverging colormaps'
section in: `<http://matplotlib.org/examples/color/colormaps_reference.html>`_).

Something along these lines?

zero, it is perfectly practical to simply select a suitable colormap and allow
'contourf' to pick the level colours automatically from that.
(The colormap needs to be of the "diverging" style:
for suitable options, see the 'Diverging colormaps' section in:
`<http://matplotlib.org/examples/color/colormaps_reference.html>`_).

In this case, however, that approach would not allow for our log-scaling
requirement. This can be overcome with an alternative type of
`matplotlib.colors.Normalize
<http://matplotlib.org/api/colors_api.html#matplotlib.colors.Normalize>`_ (see
:ref:`anomaly_pseudocolour` for an example of this). However the resulting
code can become rather complex and also, perhaps more importantly, it turns out
that the resulting colours are not precisely what one might expect (or want).

Therefore in this example we have chosen instead to define the contour layer
colours *explicitly*, using a helper function to calculate the shading tones.
This is an approach with a very much more general applicability: In this case,
for instance, it is much more easily adapted to slightly altered requirements
Copy link
Member

Choose a reason for hiding this comment

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

"it is" - what is adapted, it isn't clear? Perhaps just say that the method is flexible and can accommodate changes such as unequal positive and negative scales:

This approach is more general and flexible, accommodating changes such as unequal positive and negative scales. Producing logarithmic shading only requires an appropriate choice of contouring levels.

such as unequal positive and negative scales.
Using this method, the "logarithmic" shading requirement is provided simply by
the appropriate choice of contouring levels.

.. seealso::

A related example shows how to make a pseudocolour plot with the same data
and scaling requirements. See : :ref:`anomaly_pseudocolour`.
Copy link
Member

Choose a reason for hiding this comment

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

Same comments as the other example file apply here.


"""
import cartopy.crs as ccrs
import iris
import iris.coord_categorisation
import iris.plot as iplt
import matplotlib.pyplot as plt
import matplotlib.colors as mcols
import numpy as np


# Define a function to construct suitable graduated colours in two shades.
def make_anomaly_colours(n_layers, colour_minus='blue', colour_plus='red',
colour_zero='white'):
# Calculate colours for anomaly plot contours, interpolated from a central
# 'colour_zero' to extremes of 'colour_minus' and 'colour_plus'.
#
# Returns an iterable of 'n_layers' colours, consisting of equal numbers of
# negative and positive tones, plus a central value of 'colour_zero'.
# That is, both upper and lower portions contain (n_layers - 1)/2 colours.
# Thus, 'n_layers' should always be *odd*.
Copy link
Member

Choose a reason for hiding this comment

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

It would seem more natural to specify the number of colours on a single side. That would get rid of the "must be odd" requirement, and the undefined behaviour when even. (Failing that, the function should at least check the value and throw an error if even.)

Currently the usage is n_layers=len(contour_levels) - 1 which is not particularly clean. Replacing it with n_layers=4 would be no worse.


# Convert the three keypoint colour specifications into RGBA value arrays.
key_colours = (colour_minus, colour_zero, colour_plus)
key_colours = [mcols.colorConverter.to_rgba(colour)
for colour in key_colours]
key_colours = [np.array(colour) for colour in key_colours]

# Calculate the blending step fractions.
n_steps_oneside = (n_layers - 1)/2
layer_fractions = np.linspace(0.0, 1.0, n_steps_oneside, endpoint=False)

# Add extra *1 dimensions so we can multiply the colours by the fractions.
colour_minus, colour_zero, colour_plus = [colour.reshape((1, 4))
for colour in key_colours]
layer_fractions = layer_fractions.reshape((n_steps_oneside, 1))

# Calculate the upper and lower colour value sequences.
colours_low = colour_minus + layer_fractions*(colour_zero - colour_minus)
# NOTE: low colours from exactly colour_minus to "nearly" colour_zero.
colours_high = colour_plus + layer_fractions*(colour_zero - colour_plus)
# NOTE: high colours from exactly colour_plus to "nearly" colour_zero -- so
# these ones are 'upside down' relative to the intended result order.

# Join the two ends with the middle, and return this as the result.
layer_colours = np.concatenate((colours_low,
colour_zero,
colours_high[::-1]))
return layer_colours


def main():
# Load a sample air temperatures sequence.
file_path = iris.sample_data_path('E1_north_america.nc')
temperatures = iris.load_cube(file_path)

# Create a year-number coordinate from the time information.
iris.coord_categorisation.add_year(temperatures, 'time')

# Create a sample anomaly field for one chosen year, by extracting that
# year and subtracting the time mean.
sample_year = 1982
year_temperature = temperatures.extract(iris.Constraint(year=sample_year))
time_mean = temperatures.collapsed('time', iris.analysis.MEAN)
anomaly = year_temperature - time_mean

# Construct a plot title string explaining which years are involved.
years = temperatures.coord('year').points
plot_title = 'Temperature anomaly'
plot_title += '\n{} differences from {}-{} average.'.format(
sample_year, years[0], years[-1])

# Define the levels we want to contour with.
# NOTE: these will also appear as the colorbar ticks.
contour_levels = [-3.0, -1, -0.3, -0.1, 0.1, 0.3, 1, 3]
Copy link
Member

Choose a reason for hiding this comment

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

It'd be nice to be consistent and symmetric with the use of the decimal point, i.e. one of:

  • [-3.0, -1.0, -0.3, -0.1, 0.1, 0.3, 1.0, 3.0],
  • [-3, -1, -0.3, -0.1, 0.1, 0.3, 1, 3],
  • or perhaps even [-3, -1, -.3, -.1, .1, .3, 1, 3].


# calculate a suitable set of graduated colour values.
layer_colours = make_anomaly_colours(n_layers=len(contour_levels) - 1,
colour_minus='#0040c0',
colour_plus='darkred')

# Create an Axes, specifying the map projection.
plt.axes(projection=ccrs.LambertConformal())

# Make a contour plot with these levels and colours.
contours = iplt.contourf(anomaly, contour_levels,
colors=layer_colours,
extend='both')
# NOTE: Setting "extend=both" means that out-of-range values are coloured
# with the min and max colours.

# Add a colourbar.
bar = plt.colorbar(contours, orientation='horizontal')
# NOTE: This picks up the 'extend=both' from the plot, automatically
# showing how out-of-range values are handled.

# Label the colourbar to show the units.
bar.set_label('[{}, log scale]'.format(anomaly.units))

# Add coastlines and a title.
plt.gca().coastlines()
plt.title(plot_title)

# Display the result.
plt.show()


if __name__ == '__main__':
main()
37 changes: 37 additions & 0 deletions docs/iris/example_tests/test_anomaly_log_contours.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# (C) British Crown Copyright 2014, Met Office
#
# This file is part of Iris.
#
# Iris is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Iris is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Iris. If not, see <http://www.gnu.org/licenses/>.


# Import Iris tests first so that some things can be initialised before
# importing anything else.
import iris.tests as tests

import extest_util

with extest_util.add_examples_to_path():
import anomaly_log_contours


class TestAnomalyLogContours(tests.GraphicsTest):
"""Test the anomaly contouring example code."""
def test_anomaly_log_contours(self):
with extest_util.show_replaced_by_check_graphic(self):
anomaly_log_contours.main()


if __name__ == '__main__':
tests.main()
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.