-
Notifications
You must be signed in to change notification settings - Fork 300
Added logarithmic anomaly contouring example. #1048
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
Changes from all commits
d781df8
889836e
d8d65c0
f652666
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: |
||
| 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`. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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*. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| # 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] | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
|
||
|
|
||
| # 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() | ||
| 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() |
There was a problem hiding this comment.
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.:
Something along these lines?