# --- # jupyter: # jupytext: # text_representation: # extension: .py # format_name: percent # format_version: '1.3' # jupytext_version: 1.11.4 # kernelspec: # display_name: Python 3 # language: python # name: python3 # --- # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_guides: # # Colorbars and legends # ===================== # # UltraPlot includes some useful changes to the matplotlib API that make # working with colorbars and legends :ref:`easier `. # Notable features include "inset" colorbars, "outer" legends, # on-the-fly colorbars and legends, colorbars built from artists, # and row-major and centered-row legends. # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_guides_loc: # # Outer and inset locations # ------------------------- # # Matplotlib supports drawing "inset" legends and "outer" colorbars using the `loc` # and `location` keyword arguments. However, "outer" legends are only # posssible using the somewhat opaque `bbox_to_anchor` keyword (see `here # `__) # and "inset" colorbars are not possible without manually creating and positioning # the associated axes. UltraPlot tries to improve this behavior: # # * :meth:`~ultraplot.axes.Axes.legend` can draw both "inset" legends when you request an inset # location (e.g., ``loc='upper right'`` or the shorthand ``loc='ur'``) and "outer" # legends along a subplot edge when you request a :ref:`side location ` # (e.g., ``loc='right'`` or the shorthand ``loc='r'``). If you draw multiple legends # or colorbars on one side, they are "stacked" on top of each other. Unlike using # `bbox_to_anchor`, the "outer" legend position is adjusted automatically when the # :ref:`tight layout algorithm ` is active. # * UltraPlot adds the axes command `ultraplot.axes.Axes.colorbar`, # analogous to :meth:`~ultraplot.axes.Axes.legend` and equivalent to # calling :func:`~ultraplot.figure.Figure.colorbar` with an `ax` keyword. # :func:`~ultraplot.axes.Axes.colorbar` can draw both "outer" colorbars when you request # a side location (e.g., ``loc='right'`` or the shorthand ``loc='r'``) and "inset" # colorbars when you request an :ref:`inset location ` # (e.g., ``loc='upper right'`` or the shorthand ``loc='ur'``). Inset # colorbars have optional background "frames" that can be configured # with various :func:`~ultraplot.axes.Axes.colorbar` keywords. # :func:`~ultraplot.axes.Axes.colorbar` and :meth:`~ultraplot.axes.Axes.legend` also both accept # `space` and `pad` keywords. `space` controls the absolute separation of the # "outer" colorbar or legend from the parent subplot edge and `pad` controls the # :ref:`tight layout ` padding relative to the subplot's tick and axis labels # (or, for "inset" locations, the padding between the subplot edge and the inset frame). # The below example shows a variety of arrangements of "outer" and "inset" # colorbars and legends. # # .. important:: # # Unlike matplotlib, UltraPlot adds "outer" colorbars and legends by allocating # new rows and columns in the :class:`~ultraplot.gridspec.GridSpec` rather than # "stealing" space from the parent subplot (note that subsequently indexing # the :class:`~ultraplot.gridspec.GridSpec` will ignore the slots allocated for # colorbars and legends). This approach means that "outer" colorbars and # legends :ref:`do not affect subplot aspect ratios ` # and :ref:`do not affect subplot spacing `, which lets # UltraPlot avoid relying on complicated `"constrained layout" algorithms # `__ # and tends to improve the appearance of figures with even the most # complex arrangements of subplots, colorbars, and legends. # %% import numpy as np import ultraplot as uplt state = np.random.RandomState(51423) fig = uplt.figure(share=False, refwidth=2.3) # Colorbars ax = fig.subplot(121, title="Axes colorbars") data = state.rand(10, 10) m = ax.heatmap(data, cmap="dusk") ax.colorbar(m, loc="r") ax.colorbar(m, loc="t") # title is automatically adjusted ax.colorbar(m, loc="ll", label="colorbar label") # inset colorbar demonstration # Legends ax = fig.subplot(122, title="Axes legends", titlepad="0em") data = (state.rand(10, 5) - 0.5).cumsum(axis=0) hs = ax.plot(data, lw=3, cycle="ggplot", labels=list("abcde")) ax.legend(loc="ll", label="legend label") # automatically infer handles and labels ax.legend(hs, loc="t", ncols=5, frame=False) # automatically infer labels from handles ax.legend(hs, list("jklmn"), loc="r", ncols=1, frame=False) # manually override labels fig.format( abc=True, xlabel="xlabel", ylabel="ylabel", suptitle="Colorbar and legend location demo", ) # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_guides_plot: # # On-the-fly colorbars and legends # -------------------------------- # # In UltraPlot, you can add colorbars and legends on-the-fly by supplying keyword # arguments to various :class:`~ultraplot.axes.PlotAxes` commands. To plot data and # draw a colorbar or legend in one go, pass a location (e.g., ``colorbar='r'`` # or ``legend='b'``) to the plotting command (e.g., :func:`~ultraplot.axes.PlotAxes.plot` # or :func:`~ultraplot.axes.PlotAxes.contour`). To pass keyword arguments to the colorbar # and legend commands, use the `legend_kw` and `colorbar_kw` arguments (e.g., # ``legend_kw={'ncol': 3}``). Note that :func:`~ultraplot.axes.Axes.colorbar` can also # build colorbars from lists of arbitrary matplotlib artists, for example the # lines generated by :func:`~ultraplot.axes.PlotAxes.plot` or :func:`~ultraplot.axes.PlotAxes.line` # (see :ref:`below `). # # .. note:: # # Specifying the same `colorbar` location with multiple plotting calls will have # a different effect depending on the plotting command. For :ref:`1D commands # `, this will add each item to a "queue" used to build colorbars # from a list of artists. For :ref:`2D commands `, this will "stack" # colorbars in outer locations, or replace existing colorbars in inset locations. # By contrast, specifying the same `legend` location will always add items to # the same legend rather than creating "stacks". # %% import ultraplot as uplt labels = list("xyzpq") state = np.random.RandomState(51423) fig = uplt.figure(share=0, refwidth=2.3, suptitle="On-the-fly colorbar and legend demo") # Legends data = (state.rand(30, 10) - 0.5).cumsum(axis=0) ax = fig.subplot(121, title="On-the-fly legend") ax.plot( # add all at once data[:, :5], lw=2, cycle="Reds1", cycle_kw={"ls": ("-", "--"), "left": 0.1}, labels=labels, legend="b", legend_kw={"title": "legend title"}, ) for i in range(5): ax.plot( # add one-by-one data[:, 5 + i], label=labels[i], linewidth=2, cycle="Blues1", cycle_kw={"N": 5, "ls": ("-", "--"), "left": 0.1}, colorbar="ul", colorbar_kw={"label": "colorbar from lines"}, ) # Colorbars ax = fig.subplot(122, title="On-the-fly colorbar") data = state.rand(8, 8) ax.contourf( data, cmap="Reds1", extend="both", colorbar="b", colorbar_kw={"length": 0.8, "label": "colorbar label"}, ) ax.contour( data, color="gray7", lw=1.5, label="contour", legend="ul", legend_kw={"label": "legend from contours"}, ) # %% import numpy as np import ultraplot as uplt N = 10 state = np.random.RandomState(51423) fig, axs = uplt.subplots( nrows=2, share=False, refwidth="55mm", panelpad="1em", suptitle="Stacked colorbars demo", ) # Repeat for both axes args1 = (0, 0.5, 1, 1, "grays", 0.5) args2 = (0, 0, 0.5, 0.5, "reds", 1) args3 = (0.5, 0, 1, 0.5, "blues", 2) for j, ax in enumerate(axs): ax.format(xlabel="data", xlocator=np.linspace(0, 0.8, 5), title=f"Subplot #{j+1}") for i, (x0, y0, x1, y1, cmap, scale) in enumerate((args1, args2, args3)): if j == 1 and i == 0: continue data = state.rand(N, N) * scale x, y = np.linspace(x0, x1, N + 1), np.linspace(y0, y1, N + 1) m = ax.pcolormesh(x, y, data, cmap=cmap, levels=np.linspace(0, scale, 11)) ax.colorbar(m, loc="l", label=f"dataset #{i + 1}") # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_guides_multi: # # Figure-wide colorbars and legends # --------------------------------- # # In UltraPlot, colorbars and legends can be added to the edge of figures using the # figure methods `ultraplot.figure.Figure.colorbar` and :class:`ultraplot.figure.Figure.legend`. # These methods align colorbars and legends between the edges # of the :func:`~ultraplot.figure.Figure.gridspec` rather than the figure. # As with :ref:`axes colorbars and legends `, if you # draw multiple colorbars or legends on the same side, they are stacked on # top of each other. To draw a colorbar or legend alongside particular row(s) or # column(s) of the subplot grid, use the `row`, `rows`, `col`, or `cols` keyword # arguments. You can pass an integer to draw the colorbar or legend beside a # single row or column (e.g., ``fig.colorbar(m, row=1)``), or pass a tuple to # draw the colorbar or legend along a range of rows or columns # (e.g., ``fig.colorbar(m, rows=(1, 2))``). The space separation between the subplot # grid edge and the colorbars or legends can be controlled with the `space` keyword, # and the tight layout padding can be controlled with the `pad` keyword. # %% import numpy as np import ultraplot as uplt state = np.random.RandomState(51423) fig, axs = uplt.subplots(ncols=3, nrows=3, refwidth=1.4) for ax in axs: m = ax.pcolormesh( state.rand(20, 20), cmap="grays", levels=np.linspace(0, 1, 11), extend="both" ) fig.format( suptitle="Figure colorbars and legends demo", abc="a.", abcloc="l", xlabel="xlabel", ylabel="ylabel", ) fig.colorbar(m, label="column 1", ticks=0.5, loc="b", col=1) fig.colorbar(m, label="columns 2 and 3", ticks=0.2, loc="b", cols=(2, 3)) fig.colorbar(m, label="stacked colorbar", ticks=0.1, loc="b", minorticks=0.05) fig.colorbar(m, label="colorbar with length <1", ticks=0.1, loc="r", length=0.7) # %% import numpy as np import ultraplot as uplt state = np.random.RandomState(51423) fig, axs = uplt.subplots( ncols=2, nrows=2, order="F", refwidth=1.7, wspace=2.5, share=False ) # Plot data data = (state.rand(50, 50) - 0.1).cumsum(axis=0) for ax in axs[:2]: m = ax.contourf(data, cmap="grays", extend="both") hs = [] colors = uplt.get_colors("grays", 5) for abc, color in zip("ABCDEF", colors): data = state.rand(10) for ax in axs[2:]: (h,) = ax.plot(data, color=color, lw=3, label=f"line {abc}") hs.append(h) # Add colorbars and legends fig.colorbar(m, length=0.8, label="colorbar label", loc="b", col=1, locator=5) fig.colorbar(m, label="colorbar label", loc="l") fig.legend(hs, ncols=2, center=True, frame=False, loc="b", col=2) fig.legend(hs, ncols=1, label="legend label", frame=False, loc="r") fig.format(abc="A", abcloc="ul", suptitle="Figure colorbars and legends demo") for ax, title in zip(axs, ("2D {} #1", "2D {} #2", "Line {} #1", "Line {} #2")): ax.format(xlabel="xlabel", title=title.format("dataset")) # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_colorbars: # # Added colorbar features # ----------------------- # # The `ultraplot.axes.Axes.colorbar` and `ultraplot.figure.Figure.colorbar` commands are # somehwat more flexible than their matplotlib counterparts. The following core # features are unique to UltraPlot: # * Calling ``colorbar`` with a list of :class:`~matplotlib.artist.Artist`\ s, # a :class:`~matplotlib.colors.Colormap` name or object, or a list of colors # will build the required `~matplotlib.cm.ScalarMappable` on-the-fly. Lists # of :class:`~matplotlib.artist.Artists`\ s are used when you use the `colorbar` # keyword with :ref:`1D commands ` like :func:`~ultraplot.axes.PlotAxes.plot`. # * The associated :ref:`colormap normalizer ` can be specified with the # `vmin`, `vmax`, `norm`, and `norm_kw` keywords. The `~ultraplot.colors.DiscreteNorm` # levels can be specified with `values`, or UltraPlot will infer them from the # :class:`~matplotlib.artist.Artist` labels (non-numeric labels will be applied to # the colorbar as tick labels). This can be useful for labeling discrete plot # elements that bear some numeric relationship to each other. # # UltraPlot also includes improvements for adding ticks and tick labels to colorbars. # Similar to :func:`ultraplot.axes.CartesianAxes.format`, you can flexibly specify # major tick locations, minor tick locations, and major tick labels using the # `locator`, `minorlocator`, `formatter`, `ticks`, `minorticks`, and `ticklabels` # keywords. These arguments are passed through the :class:`~ultraplot.constructor.Locator` and # :class:`~ultraplot.constructor.Formatter` :ref:`constructor functions `. # Unlike matplotlib, the default ticks for :ref:`discrete colormaps ` # are restricted based on the axis length using `~ultraplot.ticker.DiscreteLocator`. # You can easily toggle minor ticks using ``tickminor=True``. # # Similar to :ref:`axes panels `, the geometry of UltraPlot colorbars is # specified with :ref:`physical units ` (this helps avoid the common issue # where colorbars appear "too skinny" or "too fat" and preserves their appearance # when the figure size changes). You can specify the colorbar width locally using the # `width` keyword or globally using the :rcraw:`colorbar.width` setting (for outer # colorbars) and the :rcraw:`colorbar.insetwidth` setting (for inset colorbars). # Similarly, you can specify the colorbar length locally with the `length` keyword or # globally using the :rcraw:`colorbar.insetlength` setting. The outer colorbar length # is always relative to the subplot grid and always has a default of ``1``. You # can also specify the size of the colorbar "extensions" in physical units rather # than relative units using the `extendsize` keyword rather than matplotlib's # `extendfrac`. The default `extendsize` values are :rcraw:`colorbar.extend` (for # outer colorbars) and :rcraw:`colorbar.insetextend` (for inset colorbars). # See :func:`~ultraplot.axes.Axes.colorbar` for details. # %% import numpy as np import ultraplot as uplt fig = uplt.figure(share=False, refwidth=2) # Colorbars from lines ax = fig.subplot(121) state = np.random.RandomState(51423) data = 1 + (state.rand(12, 10) - 0.45).cumsum(axis=0) cycle = uplt.Cycle("algae") hs = ax.line( data, lw=4, cycle=cycle, colorbar="lr", colorbar_kw={"length": "8em", "label": "line colorbar"}, ) ax.colorbar(hs, loc="t", values=np.arange(0, 10), label="line colorbar", ticks=2) # Colorbars from a mappable ax = fig.subplot(122) m = ax.contourf(data.T, extend="both", cmap="algae", levels=uplt.arange(0, 3, 0.5)) fig.colorbar( m, loc="r", length=1, label="interior ticks", tickloc="left" # length is relative ) ax.colorbar( m, loc="ul", length=6, # length is em widths label="inset colorbar", tickminor=True, alpha=0.5, ) fig.format( suptitle="Colorbar formatting demo", xlabel="xlabel", ylabel="ylabel", titleabove=False, ) # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_legends: # # Added legend features # --------------------- # # The :meth:`~ultraplot.axes.Axes.legend` and :meth:`~ultraplot.figure.Figure.legend`` commands are # somewhat more flexible than their matplotlib counterparts. The following core # features are the same as matplotlib: # * Calling ``legend`` without positional arguments will # automatically fill the legend with the labeled artist in the # the parent axes (when using :meth:`~ultraplot.axes.Axes.legend`) or # or the parent figure (when using :meth:`~ultraplot.figure.Figure.legend``). # * Legend labels can be assigned early by calling plotting comamnds with # the `label` keyword (e.g., ``ax.plot(..., label='label')``) or on-the-fly by # passing two positional arguments to ``legend`` (where the first argument is the # "handle" list and the second is the "label" list). # The following core features are unique to UltraPlot: # * Legend labels can be assigned for each column of a # :ref:`2D array passed to a 1D plotting command ` # using the `labels` keyword (e.g., ``labels=['label1', 'label2', ...]``). # * Legend labels can be assigned to `~matplotlib.contour.ContourSet`\ s by passing # the `label` keyword to a contouring command (e.g., :func:`~ultraplot.axes.PlotAxes.contour` # or :func:`~ultraplot.axes.PlotAxes.contourf`). # * A "handle" list can be passed to ``legend`` as the sole # positional argument and the labels will be automatically inferred # using `~matplotlib.artist.Artist.get_label`. Valid "handles" include # `~matplotlib.lines.Line2D`\ s returned by :func:`~ultraplot.axes.PlotAxes.plot`, # :class:`~matplotlib.container.BarContainer`\ s returned by :func:`~ultraplot.axes.PlotAxes.bar`, # and :class:`~matplotlib.collections.PolyCollection`\ s # returned by :func:`~ultraplot.axes.PlotAxes.fill_between`. # * A composite handle can be created by grouping the "handle" # list objects into tuples (see this `matplotlib guide # `__ # for more on tuple groups). The associated label will be automatically # inferred from the objects in the group. If multiple distinct # labels are found then the group is automatically expanded. # # :meth:`~ultraplot.axes.Axes.legend` and :func:`ultraplot.figure.Figure.legend` include a few other # useful features. To draw legends with centered rows, pass ``center=True`` or # a list of lists of "handles" to ``legend`` (this stacks several single-row, # horizontally centered legends and adds an encompassing frame behind them). # To switch between row-major and column-major order for legend entries, # use the `order` keyword (the default ``order='C'`` is row-major, # unlike matplotlib's column-major ``order='F'``). To alphabetize the legend # entries, pass ``alphabetize=True`` to ``legend``. To modify the legend handles # (e.g., :func:`~ultraplot.axes.PlotAxes.plot` or :func:`~ultraplot.axes.PlotAxes.scatter` handles) # pass the relevant properties like `color`, `linewidth`, or `markersize` to ``legend`` # (or use the `handle_kw` keyword). See `ultraplot.axes.Axes.legend` for details. # %% import numpy as np import ultraplot as uplt uplt.rc.cycle = "538" fig, axs = uplt.subplots(ncols=2, span=False, share="labels", refwidth=2.3) labels = ["a", "bb", "ccc", "dddd", "eeeee"] hs1, hs2 = [], [] # On-the-fly legends state = np.random.RandomState(51423) for i, label in enumerate(labels): data = (state.rand(20) - 0.45).cumsum(axis=0) h1 = axs[0].plot( data, lw=4, label=label, legend="ul", legend_kw={"order": "F", "title": "column major"}, ) hs1.extend(h1) h2 = axs[1].plot( data, lw=4, cycle="Set3", label=label, legend="r", legend_kw={"lw": 8, "ncols": 1, "frame": False, "title": "modified\n handles"}, ) hs2.extend(h2) # Outer legends ax = axs[0] ax.legend(hs1, loc="b", ncols=3, title="row major", order="C", facecolor="gray2") ax = axs[1] ax.legend(hs2, loc="b", ncols=3, center=True, title="centered rows") axs.format(xlabel="xlabel", ylabel="ylabel", suptitle="Legend formatting demo") # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_guides_decouple: # # Decoupling legend content and location # -------------------------------------- # # Sometimes you may want to generate a legend using handles from specific axes # but place it relative to other axes. In UltraPlot, you can achieve this by passing # both the `ax` and `ref` keywords to :func:`~ultraplot.figure.Figure.legend` # (or :func:`~ultraplot.figure.Figure.colorbar`). The `ax` keyword specifies the # axes used to generate the legend handles, while the `ref` keyword specifies the # reference axes used to determine the legend location. # # For example, to draw a legend based on the handles in the second row of subplots # but place it below the first row of subplots, you can use # ``fig.legend(ax=axs[1, :], ref=axs[0, :], loc='bottom')``. If ``ref`` is a list # of axes, UltraPlot intelligently infers the span (width or height) and anchors # the legend to the appropriate outer edge (e.g., the bottom-most axis for ``loc='bottom'`` # or the right-most axis for ``loc='right'``). # %% import numpy as np import ultraplot as uplt fig, axs = uplt.subplots(nrows=2, ncols=2, refwidth=2, share=False) axs.format(abc="A.", suptitle="Decoupled legend location demo") # Plot data on all axes state = np.random.RandomState(51423) data = (state.rand(20, 4) - 0.5).cumsum(axis=0) axs[0, :].plot(data, cycle="538", labels=list("abcd")) axs[1, :].plot(data, cycle="accent", labels=list("abcd")) # Legend 1: Content from Row 2 (ax=axs[1, :]), Location below Row 1 (ref=axs[0, :]) # This places a legend describing the bottom row data underneath the top row. fig.legend(ax=axs[1, :], ref=axs[0, :], loc="bottom", title="Data from Row 2") # Legend 2: Content from Row 1 (ax=axs[0, :]), Location below Row 2 (ref=axs[1, :]) # This places a legend describing the top row data underneath the bottom row. fig.legend(ax=axs[0, :], ref=axs[1, :], loc="bottom", title="Data from Row 1")