|
62 | 62 | ax.margins(y=-0.2) |
63 | 63 |
|
64 | 64 | # %% |
| 65 | +# |
| 66 | +# .. _autoscale_sticky_edges: |
| 67 | +# |
65 | 68 | # Sticky edges |
66 | 69 | # ------------ |
67 | 70 | # There are plot elements (`.Artist`\s) that are usually used without margins. |
68 | | -# For example false-color images (e.g. created with `.Axes.imshow`) are not |
| 71 | +# For example, false-color images (e.g. created with `.Axes.imshow`) are not |
69 | 72 | # considered in the margins calculation. |
70 | 73 | # |
71 | 74 |
|
|
166 | 169 | ax.autoscale(enable=None, axis="x", tight=True) |
167 | 170 |
|
168 | 171 | print(ax.margins()) |
| 172 | + |
| 173 | +# %% |
| 174 | +# Technical background |
| 175 | +# -------------------- |
| 176 | +# |
| 177 | +# This section explains the internal pipeline that runs when autoscaling |
| 178 | +# computes axis limits from data. Understanding the mechanics helps when |
| 179 | +# you encounter surprising behaviour or need to update limits manually. |
| 180 | +# |
| 181 | +# Data limits and view limits |
| 182 | +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
| 183 | +# |
| 184 | +# Matplotlib maintains two sets of limits: |
| 185 | +# |
| 186 | +# - **Data limits** (`.Axes.dataLim`): the tight bounding box of the raw data. |
| 187 | +# - **View limits** (`.Axes.viewLim`): the displayed axis limits. By default, |
| 188 | +# computed from the data limits through the autoscaling mechanism outlined |
| 189 | +# below, but they can be set independently. View limits can alternatively |
| 190 | +# be set explicitly through `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`, |
| 191 | +# which also disables autoscaling so that the set limits remain fixed. |
| 192 | +# |
| 193 | +# The following shows the input and output of this process — ``dataLim`` holds |
| 194 | +# the raw data bounds, ``viewLim`` the final displayed axis limits. |
| 195 | + |
| 196 | + |
| 197 | +fig, ax = plt.subplots() |
| 198 | +x = np.linspace(-6, 6, 201) |
| 199 | +y = np.sin(x) |
| 200 | +ax.plot(x, y) |
| 201 | +print(f"dataLim x: ({ax.dataLim.x0:.3f}, {ax.dataLim.x1:.3f})") |
| 202 | +print(f"dataLim y: ({ax.dataLim.y0:.3f}, {ax.dataLim.y1:.3f})") |
| 203 | +print(f"viewLim x: ({ax.viewLim.x0:.3f}, {ax.viewLim.x1:.3f})") |
| 204 | +print(f"viewLim y: ({ax.viewLim.y0:.3f}, {ax.viewLim.y1:.3f})") |
| 205 | + |
| 206 | +# %% |
| 207 | +# The x data range is [-6, 6] and the default 5% margin adds roughly 0.6 on |
| 208 | +# each side, widening the view to about [-6.6, 6.6]. The same applies to the |
| 209 | +# y axis. |
| 210 | +# |
| 211 | +# Update logic |
| 212 | +# ~~~~~~~~~~~~ |
| 213 | +# |
| 214 | +# Data and view limit updates are handled as separate stages. |
| 215 | +# |
| 216 | +# **Data limits**: When an artist is added to an Axes through one of the |
| 217 | +# plotting methods, the data limits are updated through `.Axes.update_datalim` |
| 218 | +# to include the new data. This only ever increases the data limits. It is |
| 219 | +# also possible to update `.Axes.dataLim` manually, but this is not common. |
| 220 | +# Removal of an artist or change of its data does not trigger any update of |
| 221 | +# the data limits, so they can become out of date. In such cases, it is |
| 222 | +# necessary to explicitly recompute the data limit through `.Axes.relim`. |
| 223 | +# |
| 224 | +# **View limits**: When autoscaling is enabled, the view limits are |
| 225 | +# automatically computed from the data limit. This update is lazy and only |
| 226 | +# triggered when the view limits are queried or drawn, so that they don't have |
| 227 | +# to be recomputed for every added artist. This is transparent to the user. |
| 228 | +# Explicit changes of the data limits through `.Axes.dataLim` or `.Axes.relim` |
| 229 | +# do not trigger an update of the view limits, so they can also become out of |
| 230 | +# date. In such cases, it is necessary to explicitly recompute the view limits |
| 231 | +# through `.Axes.autoscale_view`. |
| 232 | +# |
| 233 | +# View limit calculation |
| 234 | +# ~~~~~~~~~~~~~~~~~~~~~~ |
| 235 | +# |
| 236 | +# Given the data limits, the view limits are derived through these steps: |
| 237 | +# |
| 238 | +# - scale domain clamping |
| 239 | +# - margin expansion |
| 240 | +# - sticky edge clamping |
| 241 | +# - optional limit rounding |
| 242 | +# |
| 243 | +# Scale domain clamping |
| 244 | +# ~~~~~~~~~~~~~~~~~~~~~ |
| 245 | +# |
| 246 | +# Before margins are applied, the data limits are clipped to the valid domain |
| 247 | +# of the axis scale. This matters for scales like log (positive values only) |
| 248 | +# and logit (values strictly between 0 and 1): if a bound lies outside the |
| 249 | +# domain, it is replaced with a value at the domain boundary. |
| 250 | +# |
| 251 | +# For this purpose, `.Axes.dataLim` tracks not just the ordinary min/max of |
| 252 | +# the data but also ``minpos`` — the smallest strictly positive value seen. |
| 253 | +# A log-scale lower bound of zero or less is replaced with ``minpos`` rather |
| 254 | +# than the actual minimum, because only positive values can be displayed. |
| 255 | +# |
| 256 | +# For a logit scale, the upper bound is approximated as ``1 - minpos``, since |
| 257 | +# the largest data value below 1 is not tracked separately. This means the |
| 258 | +# autoscaled upper limit may include slightly more headroom than necessary |
| 259 | +# when the data maximum is well below 1. |
| 260 | +# |
| 261 | +# Margin expansion |
| 262 | +# ~~~~~~~~~~~~~~~~ |
| 263 | +# |
| 264 | +# The first step is to apply the margins, i.e. widen the view limits beyond the |
| 265 | +# data limits so that data is not at the very edge of the plot. Margins are |
| 266 | +# specified as a fraction of the data span in screen coordinates so that |
| 267 | +# the data-free border area always has the same visual size, irrespective of |
| 268 | +# data ranges or axis scales. The margin is applied symmetrically to both sides |
| 269 | +# of the data limits, so the view is expanded equally in both directions. |
| 270 | +# |
| 271 | +# This is illustrated in the following example, where the data limits and |
| 272 | +# axis scales are different, but the visual margin is the same in both cases. |
| 273 | + |
| 274 | +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4)) |
| 275 | +fig.suptitle("Margins are visually constant, " |
| 276 | + "even with different data limits and axis scales") |
| 277 | + |
| 278 | +ax1.plot([0, 10], [0, 1]) |
| 279 | +ax1.margins(0.2) |
| 280 | + |
| 281 | +x = np.linspace(1, 20) |
| 282 | +ax2.semilogy(x, np.exp(x)) |
| 283 | +ax2.margins(0.2) |
| 284 | + |
| 285 | +# %% |
| 286 | +# Sticky edges clamping |
| 287 | +# ~~~~~~~~~~~~~~~~~~~~~ |
| 288 | +# |
| 289 | +# Sticky edges are axis values at which margin expansion is clamped. After |
| 290 | +# computing the margin-expanded limits, if an expanded limit would extend |
| 291 | +# beyond a sticky edge, it is pulled back to that edge instead. |
| 292 | +# |
| 293 | +# Artists register sticky edges to prevent blank margins at natural data |
| 294 | +# boundaries. `~.Axes.imshow`, for example, registers sticky edges at its |
| 295 | +# four pixel boundaries, which is why images fill the Axes by default without |
| 296 | +# any surrounding margin (as shown in the :ref:`autoscale_sticky_edges` |
| 297 | +# section above). Sticky edges only suppress *outward expansion past the data |
| 298 | +# boundary* — they never shrink limits into the data, and negative margins |
| 299 | +# are not affected. Setting ``Axes.use_sticky_edges = False`` disables sticky |
| 300 | +# edge clamping on that Axes. |
| 301 | +# |
| 302 | +# Limit rounding |
| 303 | +# ~~~~~~~~~~~~~~ |
| 304 | +# |
| 305 | +# As a final step, the view limits can optionally be expanded outward to the |
| 306 | +# nearest "nice" tick position, so that the axis edges coincide with tick |
| 307 | +# marks. This is disabled by default, but can be turned on with the |
| 308 | +# "round_numbers" mode of :rc:`axes.autolimit_mode`: |
| 309 | +# |
| 310 | +# - ``'data'`` (default): keep the limits at the margin-expanded values. |
| 311 | +# - ``'round_numbers'``: expand the limits outward to the nearest "nice" tick |
| 312 | +# position, so the axis edges coincide with tick marks. |
| 313 | + |
| 314 | +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) |
| 315 | +ax1.plot([0.3, 4.7], [0.3, 4.7]) |
| 316 | +ax1.set_title("autolimit_mode='data' (default)") |
| 317 | +with plt.rc_context({'axes.autolimit_mode': 'round_numbers'}): |
| 318 | + ax2.plot([0.3, 4.7], [0.3, 4.7]) |
| 319 | + ax2.set_title("autolimit_mode='round_numbers'") |
| 320 | + ax2.autoscale_view() # force autoscale while round_numbers is active |
0 commit comments