12def add_end_labels(ax_main, ax_annot, entries, x_pad=0.05, min_gap_px=25, fontsize=9):
14 Place non-overlapping value callouts in a dedicated annotation axes.
16 `ax_annot` must share its y-axis with `ax_main` (i.e. have been created
17 with ``sharey=ax_main``), so a "data" y-coordinate means the same thing
18 in either axes. Each callout is linked back to its line's last data
19 point in `ax_main` with a leader line.
23 ax_main : matplotlib.axes.Axes
24 The axes holding the plotted lines.
25 ax_annot : matplotlib.axes.Axes
26 The (typically narrow, spine-less) axes the callout text is drawn
27 into. Must share its y-axis with `ax_main`.
28 entries : list of (float, float, str, color)
29 One ``(x, y, text, color)`` tuple per callout, where ``(x, y)`` is
30 the data point the leader line points to.
31 x_pad : float, optional
32 Horizontal position of the callout text, in `ax_annot` axes-fraction
34 min_gap_px : float, optional
35 Minimum vertical spacing between callouts, in display pixels.
36 fontsize : float, optional
37 Font size of the callout text.
44 order = sorted(range(len(entries)), key=
lambda i: entries[i][1])
45 disp_y = [ax_main.transData.transform((0, entries[i][1]))[1]
for i
in order]
52 clusters.append({
"center": y,
"count": 1})
53 while len(clusters) >= 2:
54 a, b = clusters[-2], clusters[-1]
55 span_a = (a[
"count"] - 1) * min_gap_px
56 span_b = (b[
"count"] - 1) * min_gap_px
57 top_a = a[
"center"] + span_a / 2
58 bot_b = b[
"center"] - span_b / 2
59 if bot_b - top_a < min_gap_px:
60 count = a[
"count"] + b[
"count"]
61 center = (a[
"center"] * a[
"count"] + b[
"center"] * b[
"count"]) / count
62 clusters[-2:] = [{
"center": center,
"count": count}]
68 span = (c[
"count"] - 1) * min_gap_px
69 start = c[
"center"] - span / 2
70 disp_y.extend(start + k * min_gap_px
for k
in range(c[
"count"]))
74 inv = ax_main.transData.inverted()
75 for idx, y_disp
in zip(order, disp_y):
76 x_data, y_data, text, color = entries[idx]
77 label_y_data = inv.transform((0, y_disp))[1]
80 angle_b = 60
if label_y_data >= y_data
else -60
84 xycoords=ax_main.transData,
85 xytext=(x_pad, label_y_data),
86 textcoords=(
"axes fraction",
"data"),
91 annotation_clip=
False,
92 bbox=dict(boxstyle=
"round", fc=
"0.8"),
99 connectionstyle=f
"angle,angleA=0,angleB={angle_b},rad=10",
109 end_label_fmt=lambda y: f
"{y:.2f}",
110 before_plot_func=
None,
117 Build a standardized benchmark plot.
119 Layout: a log-log plot on the left (5/6 of the width), a value-callout
120 panel on the right (1/6), and a legend row spanning the full width
126 Maps a series key to a dict with keys ``x``, ``y``, ``color``,
127 ``label``, ``linestyle`` and ``marker``, one entry per line to plot.
128 xlabel, ylabel, title : str
129 Axis labels and title for the main plot.
130 end_label_fmt : callable, optional
131 Formats a series' last y-value into its callout text.
132 before_plot_func : callable, optional
133 Called as ``before_plot_func(ax_main)`` before the series in
134 `plot_data` are plotted, e.g. to draw reference lines underneath
136 dpi, figsize : optional
137 Passed to `matplotlib.pyplot.figure`.
138 min_gap_px : float, optional
139 Minimum vertical spacing between callouts, in display pixels; see
141 legend_ncol : int, optional
142 Number of columns in the legend row.
146 (matplotlib.figure.Figure, matplotlib.axes.Axes)
147 The created figure and its main (plot) axes.
151 fig = plt.figure(dpi=dpi, figsize=figsize)
152 gs = fig.add_gridspec(
153 2, 2, width_ratios=[5, 1], height_ratios=[5, 1.5], hspace=0.35, wspace=0.05
155 ax_main = fig.add_subplot(gs[0, 0])
156 ax_annot = fig.add_subplot(gs[0, 1], sharey=ax_main)
157 ax_legend = fig.add_subplot(gs[1, :])
159 ax_legend.axis(
"off")
163 fig.subplots_adjust(left=0.1, right=0.99, top=0.94, bottom=0.06)
165 if before_plot_func
is not None:
166 before_plot_func(ax_main)
169 for d
in plot_data.values():
178 end_labels.append((d[
"x"][-1], d[
"y"][-1], end_label_fmt(d[
"y"][-1]), d[
"color"]))
180 ax_main.set_xlabel(xlabel)
181 ax_main.set_ylabel(ylabel)
182 ax_main.set_title(title)
184 ax_main.set_xscale(
"log")
185 ax_main.set_yscale(
"log")
189 add_end_labels(ax_main, ax_annot, end_labels, min_gap_px=min_gap_px)
191 handles, labels = ax_main.get_legend_handles_labels()
192 ax_legend.legend(handles, labels, loc=
"center", ncol=legend_ncol, fontsize=10)
make_std_bench_plot(plot_data, xlabel, ylabel, title, end_label_fmt=lambda y:f"{y:.2f}", before_plot_func=None, dpi=250, figsize=(8, 6), min_gap_px=75, legend_ncol=2)