Plots¶
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
Matplotlib is building the font cache; this may take a moment.
from pyabc2.sources import load_example
tune = load_example("For the Love of Music")
tune
Tune(title='For The Love Of Music', key=Gmaj, type='slip jig')
Trajectory¶
Something simple we can do is plot the trajectory, as a sort of time series. Ignoring note duration, that looks like this:
y = np.array([n.value for n in tune.iter_notes()])
x = np.arange(len(y)) * 1/8
plt.figure(figsize=(7, 3), layout="constrained")
plt.axis("off")
plt.title(tune.title)
plt.plot(x, y);
Or, considering duration:
plt.figure(figsize=(7, 3), layout="constrained")
y = np.array([n.value for n in tune.iter_notes()])
x = np.arange(1, len(y) + 1) * 1/8 # shift for consistency
plt.plot(x, y, label="ignored")
data = np.array(
[
[n.value, float(n.duration)]
for n in tune.iter_notes()
]
)
x = data[:,1].cumsum() # ends of notes
y = data[:,0]
assert len(x) == len(y)
plt.plot(x, y, label="considered")
plt.axis("off")
plt.title(tune.title)
plt.legend(title="duration", loc="upper left");
The divergence occurs due to the 16th notes in the B part.
Histogram¶
We can make a histogram of the notes, again considering duration or not.
data = [
[n.value, float(n.duration), n.to_pitch().unicode()]
for n in tune.iter_notes()
]
df = pd.DataFrame(data, columns=["value", "duration", "pitch"])
_, ax = plt.subplots(figsize=(6, 3.5), layout="constrained")
count = (
df.groupby("value")
.aggregate({"pitch": "first", "duration": "size"})
.rename(columns={"duration": "unweighted"})
.assign(
weighted=(
df.assign(w=df["duration"] * 8)
.groupby("value")["w"].sum()
)
)
)
count.plot.bar(
x="pitch",
rot=0,
xlabel="Pitch",
ylabel="Count",
title=tune.title,
ax=ax,
);
Note
F♯₄ is skipped in this plot, which is a bit misleading. It would be better to have an empty place for it.
Polar¶
We can combine the two (trajectory and histogram) in a polar plot (featured in the readme).
from collections import Counter
from pyabc2 import Pitch
ref = Pitch.from_name("G4")
# Compute relative pitch values
v = [n.value - ref.value for n in tune.iter_notes()]
v = np.array(v)
# Set up ax
ax = get_polar_ax(tune.key)
ax.set_title(tune.title)
# Compute and plot trajectory
r0, v0 = np.divmod(v, 12)
# TODO: try spiral-y version with continuous r (v/12)
r = r0 - r0.min() + 1
t = v0 * 2 * np.pi / 12
cmap = plt.get_cmap("plasma")
colors = [cmap(x) for x in np.linspace(0, 1, len(r) - 1)]
traj_count = Counter()
for c, t1, r1, t2, r2 in zip(colors[:], t[:-1], r[:-1], t[1:], r[1:]):
p1, p3 = (r1*np.cos(t1), r1*np.sin(t1)), (r2*np.cos(t2), r2*np.sin(t2))
if p1 == p3:
continue
traj = ((t1, r1), (t2, r2))
traj_count[traj] += 1
# Calculate isos triangle vertex point
# (to move the traj line out of the way of previous same motions)
mx = (p1[0] + p3[0]) / 2
my = (p1[1] + p3[1]) / 2
mt = np.arctan2(p3[1] - p1[1], p3[0] - p1[0])
h = 0.05 + 0.07 * (traj_count[traj] - 1)
# TODO: ^ would be nice to know total count beforehand, so to set a max for h
p2 = (mx + h*np.cos(mt - np.pi/2), my + h*np.sin(mt - np.pi/2))
# Plot Bezier curve
xb, yb = quadratic_bezier(p1, p2, p3)
rb = np.sqrt(xb**2 + yb**2)
tb = np.arctan2(yb, xb)
ax.plot(tb, rb, c=c, lw=2, alpha=0.4)
# Compute and plot histogram data
# TODO: optionally weight with duration
vc = Counter(v)
rc0, vc0 = np.divmod(list(vc), 12)
rc = rc0 - rc0.min() + 1
tc = vc0 * 2 * np.pi / 12
s = np.array(list(vc.values())) * 50
ax.scatter(
tc,
rc,
s=s,
marker="o",
zorder=10,
alpha=0.4,
);