The resonance peak¶
Ticks can sit where the reader is looking, and then the axis annotates the figure.
Jean-luc Doumont's Trees, maps and theorems redraws a resonance curve three times, from the software's default to a version where every mark on the page earns its place. This lesson follows his sequence with vanzelfsprekend, one call per step, so you can see what each one buys. The curve is a Lorentzian with sampled points around it, a construction rather than a measurement, and the script is examples/tutorial_resonance.py.
The data and the default figure¶
rng = np.random.default_rng(0)
frequency = np.linspace(15.6, 19.4, 500)
sampled = np.linspace(16, 19, 61)
def lorentzian(f: np.ndarray) -> np.ndarray:
return 650 / (1 + ((f - 17.2) / 0.35) ** 2)
calculated = lorentzian(frequency)
random_sampled = sampled + rng.normal(0, 0.04, sampled.size)
measured = lorentzian(sampled) + rng.normal(0, 12, sampled.size)
fig, ax = plt.subplots(figsize=(5, 4))
ax.plot(
frequency, calculated, color="tol:orange", linewidth=1.2, label="calculated"
)
ax.scatter(
random_sampled,
measured,
s=10,
color=vzs.palettes.DATA_INK,
zorder=3,
label="measured",
)
Step one. The frame reaches round numbers the data never touches, and the legend sends the eye to the corner and back.
Trim the frame¶
vzs.apply(ax, frame="loose", offset=(24, -6))
Step two. loose ends each spine at the round number just past the data, and the offset stands the spines off the plot to say they are a reference scale, not the data's edge.
The bottom spine stands 24 points off and the left spine sits 6 points inside, which is where Doumont puts them: the frequency axis is a scale read against, the power axis is a baseline the curve rises from.
Move the labels to the spine ends¶
vzs.xlabel(ax, "frequency (GHz)")
vzs.ylabel(ax, "output power (mW)")
Step three. Two calls, no arguments. The x label ends where the tick labels end, and the y label stands horizontal above the top tick, Doumont's raised label from his "better" graph.
Put the ticks where the reader looks¶
ax.xaxis.set_major_locator(
vzs.FeatureLocator(sampled, measured, [16, lambda x, y: x[np.argmax(y)], 19])
)
ax.yaxis.set_major_locator(
vzs.FeatureLocator(frequency, calculated, [0, lambda x, y: y.max()])
)
ax.yaxis.set_minor_locator(
vzs.SummaryLocator(calculated, reducers=[lambda y: y.max() / 2])
)
ax.xaxis.set_major_formatter("{x:g}")
ax.yaxis.set_major_formatter("{x:.0f}")
vzs.tick_direction(ax, "in")
Step four. The x ticks mark the band edges and the peak's own frequency. The y ticks mark zero and the maximum, with a minor tick at half power, where the linewidth is read.
FeatureLocator takes fixed numbers and callables in one list. The 16 and 19 are the band edges; the callable finds the frequency of the highest measured point, 17.2 GHz here. A tick at the peak is worth more than a tick at 17 because the peak is what the reader came to find. SummaryLocator is the same idea over one axis's own values, and it sets the half-power minor tick. Inward ticks keep the marks off the labels now that the labels do the talking.
Replace the legend with labels¶
vzs.label(ax, "measured", x=lambda x, y: x[np.argmax(y)])
vzs.label(ax, "calculated", x=17.5)
Step five. Each label anchors where Doumont puts it, the summit for the points and the flank for the curve, and slides just far enough to clear the ink.
label takes the same features as FeatureLocator, so "measured" anchors at the summit with the peak's own x[argmax(y)] and "calculated" at a fixed frequency on the flank. You name each label and choose its anchor; the library decides only how far the text moves.
Next: the grand tours, where the ticks summarise a century of data and three panels tell three stories.