1111
1212matplotlib .use ('Agg' )
1313import matplotlib .pyplot as plt # noqa: E402
14+ from matplotlib import font_manager # noqa: E402
1415
1516HERE : str = os .path .dirname (os .path .abspath (__file__ ))
1617SUBJECT : str = 'progressbar2'
17- HIGHLIGHT : str = '#d62728' # progressbar2 bars
18- OTHER : str = '#7f8fa6' # everyone else
18+
19+ # Our two variants share one brand hue so they read as siblings: the fast
20+ # default pops at full saturation, the full (widget) mode is a muted shade.
21+ # Every other library is a flat neutral grey.
22+ FAST : str = 'fast'
23+ FULL : str = 'full'
24+ OTHER : str = 'other'
25+ THEME : dict [str , typing .Any ] = {
26+ 'bg' : '#ffffff' ,
27+ 'fast' : '#4f46e5' , # bold indigo -> progressbar2 (fast), the default
28+ 'full' : '#c4b5fd' , # muted indigo -> progressbar2 (full), widgets mode
29+ 'other' : '#d8dde6' , # neutral grey -> every other library
30+ 'text' : '#1e2430' ,
31+ 'subtext' : '#5b6472' ,
32+ 'grid' : '#e7eaf0' ,
33+ 'title' : '#11151c' ,
34+ 'bar_height' : 0.6 ,
35+ }
36+
37+
38+ def _font () -> str :
39+ """Prefer a clean sans; degrade gracefully where it is not installed."""
40+ have = {f .name for f in font_manager .fontManager .ttflist }
41+ for name in ('Helvetica Neue' , 'Helvetica' , 'Arial' , 'DejaVu Sans' ):
42+ if name in have :
43+ return name
44+ return 'sans-serif'
45+
46+
47+ def _classify (panel : str , key : str ) -> str :
48+ """Tag a library as our fast default, our full mode, or another lib."""
49+ if key == 'progressbar2-fast' :
50+ return FAST
51+ if key == SUBJECT :
52+ # Panels A/C only benchmark the fast default; B splits fast vs full.
53+ return FAST if panel in ('A' , 'C' ) else FULL
54+ return OTHER
55+
56+
57+ def _relabel (panel : str , key : str ) -> str :
58+ """Display name: spell out fast/full for our bars, raw key otherwise."""
59+ cls = _classify (panel , key )
60+ if cls == FAST :
61+ return 'progressbar2 (fast)'
62+ if cls == FULL :
63+ return 'progressbar2 (full)'
64+ return key
1965
2066
2167def load () -> dict [str , typing .Any ]:
@@ -32,62 +78,103 @@ def make_chart(data: dict[str, typing.Any]) -> str:
3278 b = data ['scenario_b_forced_render' ]['libs' ]
3379 c = data ['scenario_c_import_time' ]['libs' ]
3480
35- panels : list [tuple [str , str , list [tuple [str , float ]], bool ]] = [
81+ panels : list [tuple [str , str , str , list [tuple [str , float ]], bool ]] = [
3682 (
37- 'A. Default iterator-wrap overhead\n (lower is faster)' ,
83+ 'A' ,
84+ 'Default iterator-wrap overhead' ,
3885 'nanoseconds added per iteration' ,
3986 _sorted ({k : v ['overhead_ns_per_iter' ] for k , v in a .items ()}),
4087 True ,
4188 ),
4289 (
43- 'B. Forced per-update render cost\n (lower is faster)' ,
90+ 'B' ,
91+ 'Forced per-update render cost' ,
4492 'microseconds per rendered update' ,
4593 _sorted ({k : v ['per_update_us' ] for k , v in b .items ()}),
4694 True ,
4795 ),
4896 (
49- 'C. Cold import time\n (lower is lighter)' ,
50- 'milliseconds (net of interpreter startup)' ,
97+ 'C' ,
98+ 'Cold import time' ,
99+ 'milliseconds (net of startup)' ,
51100 _sorted ({k : v ['net_ms' ] for k , v in c .items ()}),
52101 False ,
53102 ),
54103 ]
55104
56- fig , axes = plt .subplots (1 , 3 , figsize = (16 , 5 ))
57- for ax , (title , xlabel , pairs , logx ) in zip (axes , panels ):
58- labels = [k for k , _ in pairs ]
105+ color = {FAST : THEME ['fast' ], FULL : THEME ['full' ], OTHER : THEME ['other' ]}
106+
107+ plt .rcParams ['font.family' ] = _font ()
108+ fig , axes = plt .subplots (1 , 3 , figsize = (15.5 , 5.2 ))
109+ fig .patch .set_facecolor (THEME ['bg' ])
110+
111+ for ax , (pid , ptitle , xlabel , pairs , logx ) in zip (axes , panels ):
112+ ax .set_facecolor (THEME ['bg' ])
113+ classes = [_classify (pid , k ) for k , _ in pairs ]
114+ labels = [_relabel (pid , k ) for k , _ in pairs ]
59115 values = [v for _ , v in pairs ]
60- colors = [HIGHLIGHT if k == SUBJECT else OTHER for k in labels ]
61- ypos = range (len (labels ))
62- ax .barh (list (ypos ), values , color = colors )
63- ax .set_yticks (list (ypos ))
64- ax .set_yticklabels (labels )
116+ ypos = list (range (len (labels )))
117+
118+ ax .barh (
119+ ypos ,
120+ values ,
121+ height = THEME ['bar_height' ],
122+ color = [color [cls ] for cls in classes ],
123+ )
124+ ax .set_yticks (ypos )
125+ ax .set_yticklabels (labels , color = THEME ['text' ], fontsize = 10 )
65126 ax .invert_yaxis () # fastest at top
66- ax .set_xlabel (xlabel )
67- ax .set_title (title , fontsize = 11 , fontweight = 'bold' )
127+ ax .set_xlabel (xlabel , color = THEME ['subtext' ], fontsize = 9.5 )
128+ ax .set_title (
129+ f'{ pid } . { ptitle } ' ,
130+ loc = 'left' ,
131+ fontsize = 11.5 ,
132+ fontweight = 'bold' ,
133+ color = THEME ['title' ],
134+ pad = 12 ,
135+ )
68136 if logx :
69137 ax .set_xscale ('log' )
70- ax .grid (axis = 'x' , linestyle = ':' , alpha = 0.4 )
138+ ax .grid (axis = 'x' , color = THEME ['grid' ], linewidth = 1 )
139+ ax .set_axisbelow (True )
140+ for spine in ('top' , 'right' , 'left' ):
141+ ax .spines [spine ].set_visible (False )
142+ ax .spines ['bottom' ].set_color (THEME ['grid' ])
143+ ax .tick_params (colors = THEME ['subtext' ], length = 0 )
144+ ax .margins (x = 0.2 )
145+
71146 xmax = max (values )
72- for y , val in zip (ypos , values ):
147+ for y , val , cls in zip (ypos , values , classes ):
73148 label = f'{ val :.1f} ' if val >= 1 else f'{ val :.2f} '
74149 ax .text (
75- val * 1.05 if logx else val + xmax * 0.01 ,
150+ val * 1.08 if logx else val + xmax * 0.015 ,
76151 y ,
77152 label ,
78153 va = 'center' ,
79- fontsize = 9 ,
154+ ha = 'left' ,
155+ fontsize = 9.5 ,
156+ fontweight = 'normal' if cls == OTHER else 'bold' ,
157+ color = THEME ['text' ] if cls == OTHER else color [cls ],
80158 )
81- ax .margins (x = 0.18 )
82159
83160 fig .suptitle (
84161 'progressbar2 vs common Python progress-bar libraries' ,
85- fontsize = 14 ,
162+ fontsize = 15 ,
86163 fontweight = 'bold' ,
164+ color = THEME ['title' ],
165+ y = 0.975 ,
166+ )
167+ fig .text (
168+ 0.5 ,
169+ 0.915 ,
170+ 'lower is faster / lighter — fastest at top' ,
171+ ha = 'center' ,
172+ fontsize = 9.5 ,
173+ color = THEME ['subtext' ],
87174 )
88- fig .tight_layout (rect = (0 , 0 , 1 , 0.96 ))
175+ fig .tight_layout (rect = (0 , 0 , 1 , 0.88 ))
89176 out = os .path .join (HERE , 'chart.png' )
90- fig .savefig (out , dpi = 130 )
177+ fig .savefig (out , dpi = 140 , facecolor = THEME [ 'bg' ] )
91178 plt .close (fig )
92179 return out
93180
@@ -172,7 +259,7 @@ def make_report(data: dict[str, typing.Any], chart_name: str) -> str:
172259 {k : vv ['overhead_ns_per_iter' ] for k , vv in a ['libs' ].items ()}
173260 ):
174261 vv = a ['libs' ][name ]
175- bold = '**' if name == SUBJECT else ''
262+ bold = '**' if name in ( SUBJECT , 'progressbar2-fast' ) else ''
176263 w (
177264 f'| { bold } { name } { bold } | { vv ["total_min_s" ] * 1e3 :.1f} ms '
178265 f'| { vv ["overhead_ns_per_iter" ]:.1f} ns '
@@ -195,7 +282,7 @@ def make_report(data: dict[str, typing.Any], chart_name: str) -> str:
195282 {k : vv ['per_update_us' ] for k , vv in b ['libs' ].items ()}
196283 ):
197284 vv = b ['libs' ][name ]
198- bold = '**' if name == SUBJECT else ''
285+ bold = '**' if name in ( SUBJECT , 'progressbar2-fast' ) else ''
199286 w (
200287 f'| { bold } { name } { bold } | { vv ["total_min_s" ] * 1e3 :.1f} ms '
201288 f'| { vv ["per_update_us" ]:.2f} us '
@@ -221,7 +308,7 @@ def make_report(data: dict[str, typing.Any], chart_name: str) -> str:
221308 w ('|---|--:|' )
222309 for name , v in _sorted ({k : vv ['net_ms' ] for k , vv in c ['libs' ].items ()}):
223310 vv = c ['libs' ][name ]
224- bold = '**' if name == SUBJECT else ''
311+ bold = '**' if name in ( SUBJECT , 'progressbar2-fast' ) else ''
225312 w (f'| { bold } { name } { bold } | { vv ["net_ms" ]:.1f} ms |' )
226313 w ('' )
227314
0 commit comments