Plotting¶
ERLabPy provides a number of plotting functions to help visualize data and create publication quality figures.
Importing¶
The key module to plotting is erlab.plotting, which contains various plotting functions. To import it, use the following code:
import matplotlib.pyplot as plt
import erlab.plotting as eplt
import erlab
First, let us generate some example data from a simple tight binding model of graphene. A rigid shift of 200 meV has been applied so that the Dirac cone is visible.
from erlab.io.exampledata import generate_data
dat = generate_data(bandshift=-0.2, seed=1).T
dat
<xarray.DataArray (eV: 300, ky: 250, kx: 250)> Size: 150MB 3.17 3.71 2.969 4.094 4.071 2.702 ... 0.02378 0.1156 0.1156 0.0235 0.0007165 Coordinates: * eV (eV) float64 2kB -0.45 -0.4482 -0.4464 ... 0.08639 0.08819 0.09 * ky (ky) float64 2kB -0.89 -0.8829 -0.8757 ... 0.8757 0.8829 0.89 * kx (kx) float64 2kB -0.89 -0.8829 -0.8757 ... 0.8757 0.8829 0.89
We can see that the generated data is a three-dimensional xarray.DataArray. Now, let’s extract a cut along \(k_y = 0.3\).
cut = dat.qsel(ky=0.3)
cut
<xarray.DataArray (eV: 300, kx: 250)> Size: 600kB
3.52 2.469 3.126 3.968 2.827 2.746 ... 0.1384 0.05118 0.09941 0.1574 0.5755
Coordinates:
* eV (eV) float64 2kB -0.45 -0.4482 -0.4464 ... 0.08639 0.08819 0.09
* kx (kx) float64 2kB -0.89 -0.8829 -0.8757 ... 0.8757 0.8829 0.89
ky float64 8B 0.2967Plotting 2D data¶
The fastest way to plot a 2D array like this is to use plot_array. Each axis is automatically labeled.
eplt.plot_array(cut)
<matplotlib.image.AxesImage at 0x7b116e921be0>
plot_array takes many arguments that can customize the look of your plot. The following is an example of some of the functionality provided. For all arguments, see the API reference.
eplt.plot_array(
cut, cmap="Greys", gamma=0.5, colorbar=True, colorbar_kw=dict(width=10, ticks=[])
)
<matplotlib.image.AxesImage at 0x7b116dce7250>
plot_array can also be accessed (for 2D data) through xarray.DataArray.qplot().
cut.qplot(cmap="Greys", gamma=0.5)
<matplotlib.image.AxesImage at 0x7b116dbcead0>
Next, let’s add some annotations! The following code adds a line indicating the Fermi level and adds a colorbar. Here, unlike the previous example, the colorbar was added after plotting. Like this, adding elements separately instead of using keyword arguments can make the code more readable in complex plots.
eplt.plot_array(cut, cmap="Greys", gamma=0.5)
eplt.fermiline(linewidth=1, linestyle="--")
eplt.nice_colorbar(width=10, ticks=[])
<matplotlib.colorbar.Colorbar at 0x7b116c226c10>
You will read more about various annotations later in this document.
Slices of higher-dimensional data¶
What if we want to plot multiple slices at once? We should create subplots to place the
slices. plt.subplots is very useful in managing multiple axes and figures. If you
are unfamiliar with the syntax, visit the relevant matplotlib
documentation.
Suppose we want to plot constant energy surfaces at specific binding energies, say, at [-0.4, -0.2, 0.0]. We could create three subplots and iterate over the axes.
energies = [-0.4, -0.2, 0.0]
fig, axs = plt.subplots(1, 3, layout="compressed", sharey=True)
for energy, ax in zip(energies, axs):
const_energy_surface = dat.qsel(eV=energy)
eplt.plot_array(const_energy_surface, ax=ax, gamma=0.5, aspect="equal")
Here, we plotted each constant energy surface with plot_array.
To remove the duplicated y axis labels and add some annotations, we can use clean_labels and label_subplot_properties:
fig, axs = plt.subplots(1, 3, layout="compressed", sharey=True)
for energy, ax in zip(energies, axs):
const_energy_surface = dat.qsel(eV=energy)
eplt.plot_array(const_energy_surface, ax=ax, gamma=0.5, aspect="equal")
eplt.clean_labels(axs) # removes shared y labels
eplt.label_subplot_properties(axs, values={"Eb": energies}) # annotates energy
Not bad. However, when it gets to multiple slices along multiple datasets, it gets cumbersome.
Luckily, ERLabPy provides a function that automates the subplot creation, slicing, and annotation for you: plot_slices, which reduces the same code to a one-liner.
fig, axs = eplt.plot_slices([dat], eV=[-0.4, -0.2, 0.0], gamma=0.5, axis="image")
We can also plot the data integrated over an energy window, in this case with a width of 200 meV by adding the eV_width argument:
fig, axs = eplt.plot_slices(
[dat], eV=[-0.4, -0.2, 0.0], eV_width=0.2, gamma=0.5, axis="image"
)
Cuts along constant \(k_y\) can be plotted analogously.
fig, axs = eplt.plot_slices([dat], ky=[0.0, 0.1, 0.3], gamma=0.5, figsize=(6, 2))
Here, we notice that the first two plots slices through regions with less spectral weight, so the color across the three subplots are not on the same scale. This may be misleading in some occasions where intensity across different slices are important. Luckily, we have a function that can unify the color limits across multiple axes.
Note
The same effect can be achieved by passing on same_limits=True to plot_slices.
fig, axs = eplt.plot_slices([dat], ky=[0.0, 0.1, 0.3], gamma=0.5, figsize=(6, 2))
eplt.unify_clim(axs)
We can also choose a reference axis to get the color limits from.
fig, axs = eplt.plot_slices([dat], ky=[0.0, 0.1, 0.3], gamma=0.5, figsize=(6, 2))
eplt.unify_clim(axs, target=axs.flat[1])
What if we want to plot constant energy surfaces and cuts in the same figure? We can create the subplots first and then utilize the axes argument of plot_slices.
fig, axs = plt.subplots(2, 3, layout="compressed", sharex=True, sharey="row")
eplt.plot_slices([dat], eV=[-0.4, -0.2, 0.0], gamma=0.5, axes=axs[0, :], axis="image")
eplt.plot_slices([dat], ky=[0.0, 0.1, 0.3], gamma=0.5, axes=axs[1, :])
eplt.clean_labels(axs)
Annotations¶
ERLabPy provides a number of functions to annotate your plots. Some of the most frequently used functions were already used in the previous examples, such as fermiline
and label_subplot_properties. Here, we will go through some of the other functions that can be used to annotate your plots.
Labeling subplots¶
You can give an alphanumeric label to each subplot using label_subplots:
fig, axs = eplt.plot_slices(
[dat], ky=[0.0, 0.1, 0.3], figsize=(6, 2), cmap="Greys", annotate=False
)
eplt.label_subplots(axs, prefix="(", suffix=")")
The labels can be customized with different arguments, such as prefix, suffix, fontsize, pad, and more. See label_subplots for all available arguments.
Labeling high symmetry points¶
There are many ways to label high symmetry points along cuts. The simplest way is to use mark_points:
fig, ax = plt.subplots(figsize=(3.4, 2.1))
dat.qsel(kx=0).qplot(ax=ax)
eplt.mark_points([-0.6, 0, 0.6], ["K", "G", "K"], y=0.02)
Providing a y value larger than the maximum of the data will place the labels above the plot. If you want to place the labels at a specific position, you can provide a sequence of y values.
fig, ax = plt.subplots(figsize=(3.4, 2.1))
dat.qsel(kx=0).qplot(ax=ax)
eplt.mark_points([-0.6, 0, 0.6], ["K", "G", "K"], y=[0.12, 0.12, 0.02])
Note how the label colors are automatically set to stand out against the background. This is done by default, but you can also set the colors manually with the color argument.
You can also add an arrow between the points with matplotlib.patches.FancyArrowPatch:
import matplotlib.patches as mpatches
fig, ax = plt.subplots(figsize=(3.4, 2.1))
dat.qsel(kx=0).qplot(ax=ax)
eplt.mark_points([-0.6, 0, 0.6], ["K", "G", "K"], y=0.02)
ax.add_patch(
mpatches.FancyArrowPatch(
(0, 0.045),
(0.6, 0.045),
arrowstyle=mpatches.ArrowStyle(
"Simple", head_length=4, head_width=2, tail_width=0.25
),
shrinkA=5,
shrinkB=5,
color="w",
lw=0.0,
clip_on=False,
)
)
<matplotlib.patches.FancyArrowPatch at 0x7b116ddf9d30>
Setting multiple titles and axis labels¶
You can set titles and axis labels for multiple axes at once using set_titles, set_xlabels, and set_ylabels. These functions take a list of strings as input, which will be applied to the respective axes.
fig, axs = plt.subplots(1, 2, figsize=(3.4, 2.1), layout="compressed")
eplt.set_titles(axs, ["Left title", "Right title"])
eplt.set_xlabels(axs, ["Left x label", "Right x label"])
eplt.set_ylabels(axs, ["Left y label", "Right y label"])
Scaling axis units¶
Consider a 2D cut of a 3D dataset, where the energy axis is in eV. Suppose you want to convert the energy axis from eV to meV. You can use scale_units to scale the axis units:
fig, ax = plt.subplots(figsize=(3.4, 2.1))
cut.qplot(ax=ax)
eplt.scale_units(ax, "y", si=-3)
An exponent of -3 has been used to convert eV to meV, and the label has been updated accordingly with the corresponding SI prefix.
Brillouin zones¶
You can plot Brillouin zones (BZs) sliced along arbitrary planes in k-space using the helper functions in erlab.lattice.
import matplotlib.pyplot as plt
import numpy as np
import erlab
import erlab.plotting as eplt
Plotting a 2D Brillouin zone¶
The most simple task is to plot a 2D in-plane BZ slice. Here, we define a hexagonal lattice with lattice parameters \(a = b = 3.0\) Å, \(c = 5.0\) Å, and angles \(\alpha = 90^\circ\), \(\beta = 90^\circ\), and \(\gamma = 120^\circ\). For this, we can use eplt.plot_bz, which takes the upper 2 × 2 part of the lattice vectors and plots the in-plane BZ as a polygon.
avec = erlab.lattice.abc2avec(3.0, 3.0, 5.0, 90.0, 90.0, 120.0)
fig, ax = plt.subplots(figsize=(2, 2))
eplt.plot_bz(avec, ax=ax)
ax.set(
xlabel=r"$k_x$ (Å$^{-1}$)",
ylabel=r"$k_y$ (Å$^{-1}$)",
xlim=(-1.5, 1.5),
ylim=(-1.5, 1.5),
aspect="equal",
)
[Text(0.5, 0, '$k_x$ (Å$^{-1}$)'),
Text(0, 0.5, '$k_y$ (Å$^{-1}$)'),
(-1.5, 1.5),
(-1.5, 1.5),
None]
Plotting arbitrary Brillouin zone slices¶
Three dimensional Brillouin zones often need more general slicing.
The most general function is erlab.lattice.get_bz_slice(), which takes the full 3 × 3 reciprocal lattice vectors and a plane definition in k-space and returns the BZ edges sliced by that plane.
However, we usually slice along constant \(k_z\) or \(k_x\) (or \(k_y\)) planes. For these common cases, erlab.lattice.get_in_plane_bz() and erlab.lattice.get_out_of_plane_bz() are provided for convenience.
Let’s consider a face-centered orthorhombic (\(Fmmm\)) crystal with lattice parameters \(a = 6.0\) Å, \(b = 10.0\) Å, and \(c = 25.0\) Å. The lattice vectors are given by:
avec = erlab.lattice.abc2avec(6.0, 10.0, 25.0, 90.0, 90.0, 90.0)
Next, we convert the lattice vectors to primitive ones for the face-centered orthorhombic lattice and obtain the reciprocal lattice vectors:
avec_p = erlab.lattice.to_primitive(avec, centering_type="F")
bvec = erlab.lattice.to_reciprocal(avec_p)
We plot the in-plane BZ for \(k_z=0.2~\mathrm{Å}^{-1}\) by using erlab.lattice.get_in_plane_bz():
segments, vertices = erlab.lattice.get_in_plane_bz(
bvec, kz=0.2, angle=60.0, bounds=(-1.5, 1.5, -1.5, 1.5)
)
fig, ax = plt.subplots(figsize=(2.5, 2.5))
for seg in segments:
ax.plot(seg[:, 0], seg[:, 1], color="tab:purple", lw=1.5)
ax.scatter(vertices[:, 0], vertices[:, 1], s=20, c="tab:purple")
ax.set(xlabel=r"$k_x$ (Å$^{-1}$)", ylabel=r"$k_y$ (Å$^{-1}$)", aspect="equal")
[Text(0.5, 0, '$k_x$ (Å$^{-1}$)'), Text(0, 0.5, '$k_y$ (Å$^{-1}$)'), None]
We can also plot the out-of-plane BZ along \(k_y = 0\) using erlab.lattice.get_out_of_plane_bz():
segments, vertices = erlab.lattice.get_out_of_plane_bz(
bvec, k_parallel=0.0, angle=60.0, bounds=(-1.5, 1.5, -1.5, 1.5)
)
fig, ax = plt.subplots(figsize=(2.5, 2.5))
for seg in segments:
ax.plot(seg[:, 0], seg[:, 1], color="tab:purple", lw=1.5)
ax.scatter(vertices[:, 0], vertices[:, 1], s=20, c="tab:purple")
ax.set(xlabel=r"$k_x$ (Å$^{-1}$)", ylabel=r"$k_z$ (Å$^{-1}$)", aspect="equal")
[Text(0.5, 0, '$k_x$ (Å$^{-1}$)'), Text(0, 0.5, '$k_z$ (Å$^{-1}$)'), None]
2D colormaps¶
2D colormaps are a method to visualize two data with a single image by mapping one of the data to the lightness of the color and the other to the hue. This is useful when visualizing dichroic or spin-resolved ARPES data[Tusche et al., 2015].
Let us begin with the simulated constant energy contours of Graphene, 0.3 eV below and above the Fermi level.
dat0, dat1 = generate_data(
shape=(250, 250, 2), Erange=(-0.3, 0.3), temp=0.0, seed=1, count=1e6
).T
_, axs = eplt.plot_slices(
[dat0, dat1],
order="F",
subplot_kw={"layout": "compressed", "sharey": "row"},
axis="scaled",
label=True,
)
Suppose we want to visualize the sum and the normalized difference between the two. The simplest way is to plot them side by side.
dat_sum = dat0 + dat1
dat_ndiff = (dat0 - dat1) / dat_sum
fig, axs = eplt.plot_slices(
[dat_sum, dat_ndiff],
order="F",
subplot_kw={"layout": "compressed", "sharey": "row"},
cmap=["viridis", "bwr"],
axis="scaled",
)
eplt.proportional_colorbar()
eplt.set_titles(axs, ["Sum", "Normalized difference"])
The difference array is noisy for small values of the sum. We can plot using a 2D
colomap, where dat_ndiff is mapped to the color along the colormap and dat_sum is
mapped to the lightness of the colormap.
eplt.plot_array_2d(dat_sum, dat_ndiff)
(<matplotlib.image.AxesImage at 0x7b1166e22d50>,
<matplotlib.colorbar.Colorbar at 0x7b1166e22710>)
The color normalization for each axis can be set independently with lnorm and cnorm.
The appearance of the colorbar axes can be customized with the returned Colorbar
object.
_, cb = eplt.plot_array_2d(
dat_sum,
dat_ndiff,
lnorm=eplt.InversePowerNorm(0.5),
cnorm=eplt.CenteredInversePowerNorm(0.7, vcenter=0.0, halfrange=1.0),
)
cb.ax.set_xticks(cb.ax.get_xlim())
cb.ax.set_xticklabels(["Min", "Max"])
[Text(0.0, 0, 'Min'), Text(41.33577630506462, 0, 'Max')]
Styling figures¶
You can control the look and feel of matplotlib figures with style sheets and rcParams. In addition to the options provided by matplotlib, ERLabPy provides some style sheets that are listed below. Note that style sheets that change the default font requires the font to be installed on the system. To see how each one looks, try running the code provided by matplotlib.
Style Name |
Description |
|---|---|
khan |
Personal preferences of the package author. |
fira |
Changes the default font to Fira Sans. |
firalight |
Changes the default font to Fira Sans Light. |
times |
Changes the default font to Times New Roman. |
nature |
Changes the default font to Arial, and tweaks some aspects such as padding and default figure size. |
with plt.style.context(["nature"]):
eplt.plot_array(cut, cmap="Greys", gamma=0.5)
Tips¶
In the python ecosystem, there are some libraries that provide great colormaps, such as cmasher, cmocean, colorcet, and cmcrameri.
Although matplotlib is a powerful library, it is heavy and slow, and better suited for static plots. For interactive plots, libraries such as Plotly or Bokeh are popular.
The hvplot library is a high-level plotting library that provides a simple interface to Bokeh, Plotly, and Matplotlib. It is particularly useful for interactive plots and can be used with xarray objects. Here are some examples that uses the Bokeh backend:
import hvplot.xarray
cut.hvplot(x="kx", y="eV", cmap="Greys", aspect=1.5)
dat.hvplot(x="kx", y="ky", cmap="Greys", aspect="equal", widget_location="bottom")
Note
If you are viewing this documentation online, the slider above will not work. To see the interactive plot, you can run the notebook locally after installing hvplot.
For more information, see the hvplot documentation.
Miscellaneous¶
Some functions that do not fit into the above categories, but are useful nonetheless.
Copying equations as svg files¶
Matplotlib includes its own text parser to plot equations and text. By exploiting this, you can copy equations and text as svg files and paste them into vector graphics applications such as Adobe Illustrator or Inkscape. This is done with copy_mathtext:
eplt.copy_mathtext(r"$e^{i\pi} + 1 = 0$")
'<?xml version="1.0" encoding="utf-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="51pt" height="11pt" viewBox="0 0 51 11" xmlns="http://www.w3.org/2000/svg" version="1.1">\n <metadata>\n <rdf:RDF xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">\n <cc:Work>\n <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>\n <dc:date>2026-05-31T23:10:16.216360</dc:date>\n <dc:format>image/svg+xml</dc:format>\n <dc:creator>\n <cc:Agent>\n <dc:title>Matplotlib v3.10.9, https://matplotlib.org/</dc:title>\n </cc:Agent>\n </dc:creator>\n </cc:Work>\n </rdf:RDF>\n </metadata>\n <defs>\n <style type="text/css">*{stroke-linejoin: round; stroke-linecap: butt}</style>\n </defs>\n <g id="figure_1">\n <g id="patch_1">\n <path d="M 0 11 \nL 51 11 \nL 51 0 \nL 0 0 \nL 0 11 \nz\n" style="fill: none"/>\n </g>\n <g id="text_1">\n <!-- $e^{i\\pi} + 1 = 0$ -->\n <g transform="translate(0 10)">\n <text>\n <tspan x="0" y="-0.85625" style="font-style: oblique; font-size: 10px; font-family: \'DejaVu Sans\'">e</tspan>\n <tspan x="6.616458" y="-4.684375" style="font-style: oblique; font-size: 7px; font-family: \'DejaVu Sans\'">i</tspan>\n <tspan x="8.561283" y="-4.684375" style="font-style: oblique; font-size: 7px; font-family: \'DejaVu Sans\'">π</tspan>\n <tspan x="14.997318" y="-0.85625" style="font-size: 10px; font-family: \'DejaVu Sans\'">+</tspan>\n <tspan x="25.324466" y="-0.85625" style="font-size: 10px; font-family: \'DejaVu Sans\'">1</tspan>\n <tspan x="33.635013" y="-0.85625" style="font-size: 10px; font-family: \'DejaVu Sans\'">=</tspan>\n <tspan x="43.962161" y="-0.85625" style="font-size: 10px; font-family: \'DejaVu Sans\'">0</tspan>\n </text>\n </g>\n </g>\n </g>\n</svg>\n'