|
| 1 | +""" |
| 2 | +Demo of the `matplotlib.ticker.TransformFormatter` class. |
| 3 | +
|
| 4 | +This code demonstrates two features: |
| 5 | +
|
| 6 | + 1. A linear transformation of the input values. A callable class for |
| 7 | + doing the transformation is presented as a recipe here. The data |
| 8 | + type of the inputs does not change. |
| 9 | + 2. A transformation of the input type. The example here allows |
| 10 | + `matplotlib.ticker.StrMethodFormatter` to handle integer formats |
| 11 | + ('b', 'o', 'd', 'n', 'x', 'X'), which will normally raise an error |
| 12 | + if used directly. This transformation is associated with a |
| 13 | + `matplotlib.ticker.MaxNLocator` which has `integer` set to True to |
| 14 | + ensure that the inputs are indeed integers. |
| 15 | +
|
| 16 | +The same histogram is plotted in two sub-plots with a shared x-axis. |
| 17 | +Each axis shows a different temperature scale: one in degrees Celsius, |
| 18 | +one in degrees Rankine (the Fahrenheit analogue of Kelvins). This is one |
| 19 | +of the few examples of recognized scientific units that have both a |
| 20 | +scale and an offset relative to each other. |
| 21 | +""" |
| 22 | + |
| 23 | +import numpy as np |
| 24 | +from matplotlib import pyplot as plt |
| 25 | +from matplotlib.axis import Ticker |
| 26 | +from matplotlib.ticker import ( |
| 27 | + TransformFormatter, StrMethodFormatter, MaxNLocator |
| 28 | +) |
| 29 | + |
| 30 | + |
| 31 | +class LinearTransform: |
| 32 | + """ |
| 33 | + A callable class that transforms input values to output according to |
| 34 | + a linear transformation. |
| 35 | + """ |
| 36 | + |
| 37 | + def __init__(self, in_start=None, in_end=None, out_start=None, out_end=None): |
| 38 | + """ |
| 39 | + Sets up the transformation such that `in_start` gets mapped to |
| 40 | + `out_start` and `in_end` gets mapped to `out_end`. The following |
| 41 | + shortcuts apply when only some of the inputs are specified: |
| 42 | +
|
| 43 | + - none: no-op |
| 44 | + - in_start: translation to zero |
| 45 | + - out_start: translation from zero |
| 46 | + - in_end: scaling to one (divide input by in_end) |
| 47 | + - out_end: scaling from one (multiply input by in_end) |
| 48 | + - in_start, out_start: translation |
| 49 | + - in_end, out_end: scaling (in_start and out_start zero) |
| 50 | + - in_start, out_end: in_end=out_end, out_start=0 |
| 51 | + - in_end, out_start: in_start=0, out_end=in_end |
| 52 | +
|
| 53 | + Based on the following rules: |
| 54 | +
|
| 55 | + - start missing: set start to zero |
| 56 | + - both ends are missing: set ranges to 1.0 |
| 57 | + - one end is missing: set it to the other end |
| 58 | + """ |
| 59 | + self._in_offset = 0.0 if in_start is None else in_start |
| 60 | + self._out_offset = 0.0 if out_start is None else out_start |
| 61 | + |
| 62 | + if in_end is None: |
| 63 | + if out_end is None: |
| 64 | + self._in_scale = 1.0 |
| 65 | + else: |
| 66 | + self._in_scale = out_end - self._in_offset |
| 67 | + else: |
| 68 | + self._in_scale = in_end - self._in_offset |
| 69 | + |
| 70 | + if out_end is None: |
| 71 | + if in_end is None: |
| 72 | + self._out_scale = 1.0 |
| 73 | + else: |
| 74 | + self._out_scale = in_end - self._out_offset |
| 75 | + else: |
| 76 | + self._out_scale = out_end - self._out_offset |
| 77 | + |
| 78 | + def __call__(self, x): |
| 79 | + """ |
| 80 | + Transforms the input value `x` according to the rule set up in |
| 81 | + `__init__`. |
| 82 | + """ |
| 83 | + return ((x - self._in_offset) * self._out_scale / self._in_scale + |
| 84 | + self._out_offset) |
| 85 | + |
| 86 | +# X-data |
| 87 | +temp_C = np.arange(-5.0, 5.1, 0.25) |
| 88 | +# Y-data |
| 89 | +counts = 15.0 * np.exp(-temp_C**2 / 25) |
| 90 | +# Add some noise |
| 91 | +counts += np.random.normal(scale=4.0, size=counts.shape) |
| 92 | +if counts.min() < 0: |
| 93 | + counts += counts.min() |
| 94 | + |
| 95 | +fig = plt.figure() |
| 96 | +ax1 = fig.add_subplot(111) |
| 97 | +ax2 = fig.add_subplot(111, sharex=ax1, sharey=ax1, frameon=False) |
| 98 | + |
| 99 | +ax1.plot(temp_C, counts, drawstyle='steps-mid') |
| 100 | + |
| 101 | +ax1.xaxis.set_major_formatter(StrMethodFormatter('{x:0.2f}')) |
| 102 | + |
| 103 | +# This step is necessary to allow the shared x-axes to have different |
| 104 | +# Formatter and Locator objects. |
| 105 | +ax2.xaxis.major = Ticker() |
| 106 | +# 0C -> 491.67R (definition), -273.15C (0K)->0R (-491.67F)(definition) |
| 107 | +ax2.xaxis.set_major_locator(ax1.xaxis.get_major_locator()) |
| 108 | +ax2.xaxis.set_major_formatter( |
| 109 | + TransformFormatter(LinearTransform(in_start=-273.15, in_end=0, |
| 110 | + out_end=491.67), |
| 111 | + StrMethodFormatter('{x:0.2f}'))) |
| 112 | + |
| 113 | +# The y-axes share their locators and formatters, so only one needs to |
| 114 | +# be set |
| 115 | +ax1.yaxis.set_major_locator(MaxNLocator(integer=True)) |
| 116 | +# Setting the transfrom to `int` will only alter the type, not the |
| 117 | +# actual value of the ticks |
| 118 | +ax1.yaxis.set_major_formatter( |
| 119 | + TransformFormatter(int, StrMethodFormatter('{x:02X}'))) |
| 120 | + |
| 121 | +ax1.set_xlabel('Temperature (\u00B0C)') |
| 122 | +ax1.set_ylabel('Samples (Hex)') |
| 123 | +ax2.set_xlabel('Temperature (\u00B0R)') |
| 124 | + |
| 125 | +ax1.xaxis.tick_top() |
| 126 | +ax1.xaxis.set_label_position('top') |
| 127 | + |
| 128 | +plt.show() |
0 commit comments