Skip to content

Commit 93b4aca

Browse files
Optimize interactive editor startup: lazy tabs + fast preview
- Lazy tab loading: Style/Layout/Theme/Export tabs show placeholder until user clicks them, deferring ~140 widget constructions - Preview always renders at preview_dpi (100) for speed, regardless of figure DPI setting. Export DPI shown in status bar only. - Defer _rebuild_style_widgets() until Style tab first opened - Guard axis-change style rebuild behind tab-loaded check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e3ede50 commit 93b4aca

1 file changed

Lines changed: 55 additions & 10 deletions

File tree

src/statspai/plots/_jupyter_editor.py

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,19 @@ def _render_fig_to_png(figure, dpi=preview_dpi):
112112

113113
def _do_render(figure):
114114
"""Actually render the figure to the preview widget."""
115-
render_dpi = int(figure.dpi)
116-
fig_image.value = _render_fig_to_png(figure, dpi=render_dpi)
115+
# Always use preview_dpi for the live preview (fast).
116+
# The figure's own DPI is preserved for export/save.
117+
fig_image.value = _render_fig_to_png(figure, dpi=preview_dpi)
117118
n = len(editor.edits)
118119
w_in, h_in = figure.get_size_inches()
119-
dpi = int(figure.dpi)
120-
px_w, px_h = int(w_in * dpi), int(h_in * dpi)
120+
export_dpi = int(figure.dpi)
121+
px_w, px_h = int(w_in * export_dpi), int(h_in * export_dpi)
121122
_render_state['pending'] = 0
122123
apply_btn.description = 'Apply'
123124
status_bar.value = (
124125
f'<span style="font-size:11px; color:#2ECC71">'
125-
f'{n} edit(s) — DPI: {dpi} → export size: {px_w}×{px_h}px</span>'
126+
f'{n} edit(s) — DPI: {export_dpi} → '
127+
f'export size: {px_w}\u00d7{px_h}px</span>'
126128
)
127129

128130
def _live_refresh(figure):
@@ -272,8 +274,9 @@ def _on_ax_change(change):
272274
ylim_range.max = yl[1] + yr * 0.5
273275
ylim_range.step = yr / 50
274276
ylim_range.value = [yl[0], yl[1]]
275-
# Rebuild style tab content
276-
_rebuild_style_widgets()
277+
# Rebuild style tab content (only if already loaded)
278+
if _tab_loaded.get(1, False):
279+
_rebuild_style_widgets()
277280

278281
ax_selector.observe(_on_ax_change, names='value')
279282

@@ -608,7 +611,8 @@ def _cb(change):
608611

609612
style_container.children = children
610613

611-
_rebuild_style_widgets()
614+
# NOTE: _rebuild_style_widgets() is deferred — called on first
615+
# tab switch to Style tab (see _on_tab_switch).
612616

613617
# ==================================================================
614618
# Tab 3: Layout (spines, grid, figsize, legend, axes limits, etc.)
@@ -1216,17 +1220,58 @@ def _live_code_update(figure):
12161220
editor.on_refresh(_live_code_update)
12171221

12181222
# ==================================================================
1219-
# Assemble tabs
1223+
# Assemble tabs (with lazy loading for non-default tabs)
12201224
# ==================================================================
1225+
_loading_msg = widgets.HTML(
1226+
'<div style="padding:20px; color:#999; text-align:center">'
1227+
'Loading...</div>'
1228+
)
1229+
_lazy_placeholders = {
1230+
1: widgets.VBox([_loading_msg]),
1231+
2: widgets.VBox([widgets.HTML(
1232+
'<div style="padding:20px; color:#999; text-align:center">'
1233+
'Loading...</div>')]),
1234+
3: widgets.VBox([widgets.HTML(
1235+
'<div style="padding:20px; color:#999; text-align:center">'
1236+
'Loading...</div>')]),
1237+
4: widgets.VBox([widgets.HTML(
1238+
'<div style="padding:20px; color:#999; text-align:center">'
1239+
'Loading...</div>')]),
1240+
}
1241+
_tab_loaded = {0: True, 1: False, 2: False, 3: False, 4: False}
1242+
_real_tabs = {
1243+
1: style_container,
1244+
2: layout_tab,
1245+
3: theme_tab,
1246+
4: code_tab,
1247+
}
1248+
12211249
tabs = widgets.Tab(children=[
1222-
text_tab, style_container, layout_tab, theme_tab, code_tab,
1250+
text_tab,
1251+
_lazy_placeholders[1],
1252+
_lazy_placeholders[2],
1253+
_lazy_placeholders[3],
1254+
_lazy_placeholders[4],
12231255
])
12241256
tabs.set_title(0, 'Text')
12251257
tabs.set_title(1, 'Style')
12261258
tabs.set_title(2, 'Layout')
12271259
tabs.set_title(3, 'Theme')
12281260
tabs.set_title(4, 'Export')
12291261

1262+
def _on_tab_switch(change):
1263+
idx = change['new']
1264+
if idx is not None and not _tab_loaded.get(idx, True):
1265+
_tab_loaded[idx] = True
1266+
children = list(tabs.children)
1267+
children[idx] = _real_tabs[idx]
1268+
tabs.children = children
1269+
# Build style widgets on first visit
1270+
if idx == 1:
1271+
_rebuild_style_widgets()
1272+
1273+
tabs.observe(_on_tab_switch, names='selected_index')
1274+
12301275
panel_header = widgets.HBox(
12311276
[
12321277
widgets.HTML(

0 commit comments

Comments
 (0)