diff --git a/docs/iris/example_code/graphics/anomaly_log_colouring.py b/docs/iris/example_code/graphics/anomaly_log_colouring.py index bf8e8fe058..0d0db4fad9 100644 --- a/docs/iris/example_code/graphics/anomaly_log_colouring.py +++ b/docs/iris/example_code/graphics/anomaly_log_colouring.py @@ -1,4 +1,6 @@ """ +.. _anomaly_pseudocolour: + Colouring anomaly data with logarithmic scaling =============================================== @@ -26,6 +28,12 @@ `_). 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 diff --git a/docs/iris/example_code/graphics/anomaly_log_contours.py b/docs/iris/example_code/graphics/anomaly_log_contours.py new file mode 100644 index 0000000000..c4a684cff9 --- /dev/null +++ b/docs/iris/example_code/graphics/anomaly_log_contours.py @@ -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 +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: +``_). + +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 +`_ (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 +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`. + +""" +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*. + + # 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] + + # 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() diff --git a/docs/iris/example_tests/test_anomaly_log_contours.py b/docs/iris/example_tests/test_anomaly_log_contours.py new file mode 100644 index 0000000000..e984069a37 --- /dev/null +++ b/docs/iris/example_tests/test_anomaly_log_contours.py @@ -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 . + + +# 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() diff --git a/lib/iris/tests/results/visual_tests/test_anomaly_log_contours.TestAnomalyLogContours.test_anomaly_log_contours.0.png b/lib/iris/tests/results/visual_tests/test_anomaly_log_contours.TestAnomalyLogContours.test_anomaly_log_contours.0.png new file mode 100644 index 0000000000..6337ef27ed Binary files /dev/null and b/lib/iris/tests/results/visual_tests/test_anomaly_log_contours.TestAnomalyLogContours.test_anomaly_log_contours.0.png differ