Skip to content

Commit b038042

Browse files
committed
Improve Timeline.plot
1 parent 37d185a commit b038042

2 files changed

Lines changed: 113 additions & 24 deletions

File tree

notebooks/mesonic-dev-timeline-plot.ipynb

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@
2020
"%autoreload 2"
2121
]
2222
},
23+
{
24+
"cell_type": "code",
25+
"execution_count": null,
26+
"id": "1b446b60",
27+
"metadata": {},
28+
"outputs": [],
29+
"source": [
30+
"%matplotlib widget"
31+
]
32+
},
2333
{
2434
"cell_type": "code",
2535
"execution_count": null,
@@ -141,14 +151,25 @@
141151
"context.timeline.plot_new()"
142152
]
143153
},
154+
{
155+
"cell_type": "code",
156+
"execution_count": null,
157+
"id": "ba90dcc6",
158+
"metadata": {},
159+
"outputs": [],
160+
"source": [
161+
"context.timeline"
162+
]
163+
},
144164
{
145165
"cell_type": "code",
146166
"execution_count": null,
147167
"id": "cddc6dfe",
148168
"metadata": {},
149169
"outputs": [],
150170
"source": [
151-
"context.timeline.plot_new(offset=\"amp\", width=\"amp\")"
171+
"context.timeline.plot_new(offset=\"amp\", width=\"amp\")\n",
172+
"#plt.semilogy()"
152173
]
153174
},
154175
{
@@ -158,7 +179,7 @@
158179
"metadata": {},
159180
"outputs": [],
160181
"source": [
161-
"context.timeline.plot_new(offset=\"amp\", width=\"pan\")"
182+
"context.timeline.plot_new(offset=\"freq\", width=\"pan\")"
162183
]
163184
},
164185
{
@@ -489,7 +510,7 @@
489510
"name": "python",
490511
"nbconvert_exporter": "python",
491512
"pygments_lexer": "ipython3",
492-
"version": "3.10.11"
513+
"version": "3.10.2"
493514
}
494515
},
495516
"nbformat": 4,

src/mesonic/timeline.py

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -428,9 +428,15 @@ def _check_consistency(self, duration_key="dur"):
428428
def _create_segments(self, offset_key="freq", width_key="amp"):
429429
splitted_synth_events = self._check_consistency()
430430
# for quiver X, U, Y (V=0), arrowwidths
431-
segments = {"begins": [], "ends": [], "offsets": [], "widths": []}
432-
# for scatter tuples (x,y)
433-
markers = {"start": [], "stop": [], "set": []}
431+
segments = {
432+
"begins": [],
433+
"ends": [],
434+
"offsets": [],
435+
"widths": [],
436+
"connections": [],
437+
}
438+
# start & stop tuples (x,y) for scatter, set events: (x,y,x,y2)
439+
markers = {"starts": [], "stops": [], "sets": []}
434440
synth_plot_info = {
435441
synth: {
436442
"events": events,
@@ -444,6 +450,7 @@ def _create_segments(self, offset_key="freq", width_key="amp"):
444450
ends = synth_plot_info[synth]["segments"]["ends"]
445451
offsets = synth_plot_info[synth]["segments"]["offsets"]
446452
widths = synth_plot_info[synth]["segments"]["widths"]
453+
connections = synth_plot_info[synth]["segments"]["connections"]
447454

448455
for time, event in events[SynthEventType.START]:
449456
begins.append(time)
@@ -458,9 +465,7 @@ def _create_segments(self, offset_key="freq", width_key="amp"):
458465
for set_time, set_event in events[SynthEventType.SET]:
459466
combined_set_events[set_time].append(set_event)
460467
insert_idx = 0
461-
# begins = begins
462468
for set_time, set_events in combined_set_events.items():
463-
# set search index back by one
464469
while insert_idx < len(begins) and begins[insert_idx] < set_time:
465470
insert_idx += 1
466471
# get properties for the event data
@@ -471,6 +476,12 @@ def _create_segments(self, offset_key="freq", width_key="amp"):
471476
offset = set_event.data["new_value"]
472477
offsets.insert(insert_idx, offset)
473478
found_offset = True
479+
connections.append(
480+
[
481+
(set_time, set_event.data["old_value"]),
482+
(set_time, offset),
483+
]
484+
)
474485
if set_event.data["name"] == width_key:
475486
width = set_event.data["new_value"]
476487
widths.insert(insert_idx, width)
@@ -493,9 +504,7 @@ def _create_segments(self, offset_key="freq", width_key="amp"):
493504
assert len(begins) == len(widths)
494505
return synth_plot_info
495506

496-
def plot_new(
497-
self, offset="freq", width="amp", duration_key="dur", default_duration=0.1
498-
):
507+
def plot_new(self, offset="freq", width="amp", scale=None):
499508
"""Plot the Timeline.
500509
501510
Parameters
@@ -516,7 +525,11 @@ def plot_new(
516525
If matplotlib cannot be imported.
517526
"""
518527
try:
528+
import matplotlib.patches as mpatches
519529
import matplotlib.pyplot as plt
530+
import matplotlib.ticker as ticker
531+
from matplotlib.collections import LineCollection
532+
520533
except ImportError as err:
521534
raise ImportError(
522535
"plotting the Timeline is only possible when matplotlib is installed."
@@ -560,14 +573,25 @@ def plot_new(
560573
# "color": None,
561574
# "y_offset": 0,
562575

563-
fig = plt.figure()
564-
axes = fig.add_subplot(1, 1, 1)
565-
from matplotlib.collections import LineCollection
576+
fig = plt.figure(figsize=(8, 2))
577+
ax = fig.add_subplot(1, 1, 1)
578+
cmap = plt.get_cmap("Set1")
566579

567580
x_min, x_max = 0, 0
568581
y_min, y_max = 0, 0
569582

570-
for synth in synth_plot_info.keys():
583+
patches = []
584+
synth_colors = {}
585+
synth_labels = {}
586+
for synth_idx, synth in enumerate(synth_plot_info.keys()):
587+
synth_colors[synth] = cmap(synth_idx)
588+
synth_labels[synth] = f"{synth.name}" + (
589+
" (mutable)" if synth.mutable else " (immutable)"
590+
)
591+
patches.append(
592+
mpatches.Patch(color=synth_colors[synth], label=synth_labels[synth])
593+
)
594+
571595
segments = synth_plot_info[synth]["segments"]
572596
begins = np.array(segments["begins"])
573597
ends = np.array(segments["ends"])
@@ -597,22 +621,66 @@ def warp_width(value, scale=None, width_min=2, width_max=4):
597621
)
598622
)
599623
)
600-
lc = LineCollection(
601-
lines,
602-
linewidth=widths,
624+
synth_segments_collection = LineCollection(
625+
lines, linewidth=widths, color=synth_colors[synth]
603626
)
604-
axes.add_collection(lc)
627+
ax.add_collection(synth_segments_collection)
628+
if synth.mutable:
629+
connections = synth_plot_info[synth]["segments"]["connections"]
630+
synth_connections_collection = LineCollection(
631+
connections, linewidth=0.5, color=synth_colors[synth]
632+
)
633+
ax.add_collection(synth_connections_collection)
605634

606635
x_min = min(begins.min(), x_min)
607636
x_max = max(ends.max(), x_max)
608637
y_min = min(offsets.min(), y_min)
609638
y_max = max(offsets.max(), y_max)
610639

611-
x_lim = (x_max - x_min) * 0.1
612-
axes.set_xlim(x_min - x_lim, x_max + x_lim)
613-
y_lim = (y_max - y_min) * 0.1
614-
axes.set_ylim(y_min - y_lim, y_max + y_lim)
615-
axes.grid()
640+
x_lim = (x_max - x_min) * 0.01
641+
ax.set_xlim(x_min - x_lim, x_max + x_lim)
642+
# y_lim = (y_max - y_min) * 0.1
643+
# axes.set_ylim(y_min - y_lim, y_max + y_lim)
644+
645+
# test
646+
ax.legend(handles=patches, loc="best")
647+
ax.grid()
648+
ax.set_title("Timeline")
649+
ax.set_xlabel("time [s]")
650+
651+
if offset == "freq":
652+
ax.set_yscale("log")
653+
ax.set_ylabel("frequency [Hz]")
654+
secax = ax.secondary_yaxis(
655+
location="right", functions=(pam.cps_to_midi, pam.midi_to_cps)
656+
)
657+
secax.set_ylabel("MIDI Note")
658+
secax.yaxis.set_major_locator(
659+
ticker.MaxNLocator(nbins="auto", steps=[1, 2, 4, 5, 10], integer=True)
660+
)
661+
secax.yaxis.set_major_formatter(lambda x, pos: str(int(x)))
662+
elif offset == "amp":
663+
ax.set_yscale("log")
664+
ax.set_ylabel("amplitude")
665+
666+
def safe_amp_to_db(amp):
667+
return pam.amp_to_db(amp) if amp > 0 else -90
668+
669+
secax = ax.secondary_yaxis(
670+
location="right", functions=(safe_amp_to_db, pam.db_to_amp)
671+
)
672+
secax.set_ylabel("level [dB]")
673+
secax.yaxis.set_major_locator(
674+
ticker.MaxNLocator(nbins="auto", steps=[1, 2, 4, 5, 10], integer=True)
675+
)
676+
secax.yaxis.set_major_formatter(lambda x, pos: str(int(x)))
677+
else:
678+
ax.set_ylabel(offset)
679+
680+
ax.autoscale(enable=True, axis=True, tight=True)
681+
# axes.margins(0.05)
682+
683+
fig.tight_layout()
616684

617685
# TODO Idea: convention to order the Synth Params by most probalbe usage
618686

0 commit comments

Comments
 (0)