Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 931a44e

Browse files
tacaswellmeeseeksmachine
authored andcommitted
Backport PR matplotlib#31673: DOC: Explain the technical background of autoscaling
1 parent 7c4bea5 commit 931a44e

1 file changed

Lines changed: 153 additions & 1 deletion

File tree

galleries/users_explain/axes/autoscale.py

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,13 @@
6262
ax.margins(y=-0.2)
6363

6464
# %%
65+
#
66+
# .. _autoscale_sticky_edges:
67+
#
6568
# Sticky edges
6669
# ------------
6770
# 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
6972
# considered in the margins calculation.
7073
#
7174

@@ -166,3 +169,152 @@
166169
ax.autoscale(enable=None, axis="x", tight=True)
167170

168171
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

Comments
 (0)