yongrenjie / penguins

A Python 3 package for analysing and plotting NMR spectra.
https://yongrenjie.github.io/penguins
MIT License
5 stars 2 forks source link

Allow 2D spectra to be plotted with accompanying projections #5

Closed yongrenjie closed 5 months ago

yongrenjie commented 4 years ago

So far I've figured out how to do this when there's only one spectrum on an entire Figure, by passing gridspec_kw to subplots(). However, I think we can do this more generally (and with fewer inconsistencies with the existing API) using the axes_divider module, something like this: https://matplotlib.org/3.1.1/gallery/axes_grid1/demo_colorbar_with_axes_divider.html

The API should be kwarg(s) for _stage2d(), perhaps a dictionary projection_options, with keys f1 (bool), f2 (bool), f1_dataset (optional Dataset1DProj, defaults to ds.f1projp()), f2_dataset (optional Dataset1DProj, defaults to ds.f2projp()), ratio (float, ratio of main spectrum width vs projection), and maybe more keys to control geometry (whether f1proj appears on the left or the right...?).

How do we pass plot options, e.g. the color of projections? Can't have a nested dictionary can we...

yongrenjie commented 4 years ago

FWIW here's the existing code using gridspec

import penguins as pg
clipc = pg.read("./nmr/200804-6a-sc", 10, 1)

# make 2D spectrum 3x3 and projections 1x3, 3x1
gridspec_kw = {"width_ratios": [1, 3], "height_ratios": [1, 3]}
fig, axs = pg.subplots(2, 2, gridspec_kw=gridspec_kw)
# assign them to easy-to-remember variables
axblank, axf2, axf1, axmain = list(axs.flat)

# plot the 2D spectrum
clipc.stage(levels=2e5)
pg.mkplot(axmain, autolabel="nucl")

# get rid of the top-left empty space
axblank.remove()

# 1D projections
f2projp = clipc.f2projp()
f2projp.stage(color="blue")
pg.mkplot(axf2, xlabel="")
# the xlim should be set to the maximum and minimum chemical shift, i.e.
# we don't want overhang at the ends
p = f2projp.ppm_scale()
axf2.set_xlim((max(p), min(p)))
# get rid of x-axis
axf2.xaxis.set_visible(False)
axf2.spines["bottom"].set_visible(False)

# magic
from matplotlib import transforms
base_trfm = axf1.transData
rot_trfm = transforms.Affine2D().rotate_deg(90)
scale_trfm = transforms.Affine2D().scale(sx=1, sy=-1)
f1projp = clipc.f1projp()
f1projp.stage(color="blue", plot_options={"transform": scale_trfm + rot_trfm + base_trfm})
pg.mkplot(ax=axf1)
p = f1projp.ppm_scale()
axf1.set_ylim(max(p), min(p))  # this automatically inverts the y-axis
# Don't need to invert xaxis because mkplot() does it for us.
# axf1.invert_xaxis()
# However, we do need to disable the xaxis.
axf1.xaxis.set_visible(False)
axf1.spines["bottom"].set_visible(False)

# Let's put the y-axis ticks on the right.
axmain.yaxis.tick_right()

# And change the y-label position.
axmain.yaxis.label.set_rotation(0)
axmain.yaxis.label.set_horizontalalignment("left")
axmain.yaxis.label.set_verticalalignment("top")
axmain.yaxis.set_label_coords(1.02, 1)

# show the plot
pg.tight_layout()
pg.show()
Screenshot 2020-08-10 at 3 39 56 AM
yongrenjie commented 4 years ago

Here's the code using axes_divider. This allows us to use subplots() to generate series of axes and then manipulate the individual axes after that.

fig, ax = pg.subplots(1, 1, figsize=(7, 7))

clipc.stage(levels=2e5)
pg.mkplot(ax=ax, autolabel="nucl")

### The only difference from before is that we use ax_divider instead of gridspec.
from mpl_toolkits.axes_grid1 import make_axes_locatable
ax_divider = make_axes_locatable(ax)
axf2 = ax_divider.append_axes("top", size="20%", pad="2%")
axf1 = ax_divider.append_axes("left", size="20%", pad="2%")

### Same code as before.
# 1D projections
f2projp = clipc.f2projp()
f2projp.stage(color="blue")
pg.mkplot(axf2, xlabel="")
# the xlim should be set to the maximum and minimum chemical shift, i.e.
# we don't want overhang at the ends
p = f2projp.ppm_scale()
axf2.set_xlim((max(p), min(p)))
# get rid of x-axis
axf2.xaxis.set_visible(False)
axf2.spines["bottom"].set_visible(False)

# magic
from matplotlib import transforms
base_trfm = axf1.transData
rot_trfm = transforms.Affine2D().rotate_deg(90)
scale_trfm = transforms.Affine2D().scale(sx=1, sy=-1)
f1projp = clipc.f1projp()
f1projp.stage(color="blue", plot_options={"transform": scale_trfm + rot_trfm + base_trfm})
pg.mkplot(ax=axf1)
p = f1projp.ppm_scale()
axf1.set_ylim(max(p), min(p))  # this automatically inverts the y-axis
# Don't need to invert xaxis because mkplot() does it for us.
# axf1.invert_xaxis()
# However, we do need to disable the xaxis.
axf1.xaxis.set_visible(False)
axf1.spines["bottom"].set_visible(False)

ax.yaxis.tick_right()
ax.yaxis.label.set_rotation(0)
ax.yaxis.label.set_horizontalalignment("left")
ax.yaxis.label.set_verticalalignment("top")
ax.yaxis.set_label_coords(1.02, 1)

pg.show()
Screenshot 2020-08-10 at 3 51 19 AM
yongrenjie commented 3 years ago

There's also https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.axes.Axes.autoscale.html

set tight=True to remove the padding added on either edge of the spectrum.