Skip to content

Commit b577a6e

Browse files
Add render mode toggle (Auto/Manual) and DPI UX improvements to plot editor
- Add Auto/Manual render mode toggle below preview: Manual mode batches edits and shows pending count, Apply button triggers re-render - Add DPI hint explaining preview scaling vs export resolution - Show pixel dimensions in status bar (e.g. 1920×1440px) - Sync Figure DPI slider to Save DPI slider - Rename save DPI label for clarity Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 74f6698 commit b577a6e

2 files changed

Lines changed: 94 additions & 14 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,4 +165,5 @@ Thumbs.db
165165
Start Superpower for R.md
166166

167167
test-notebooks/
168-
StatsPAI/statspai-推文-agent时代的统计包生态.md
168+
StatsPAI/statspai-推文-agent时代的统计包生态.md
169+
社媒文档/

src/statspai/plots/_jupyter_editor.py

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,26 +89,93 @@ def _render_fig_to_png(figure, dpi=preview_dpi):
8989
'Ready — make edits on the right panel</span>'
9090
)
9191

92+
# ---- Render mode: Auto vs Manual ----
93+
_render_state = {'pending': 0}
94+
95+
render_mode = widgets.ToggleButtons(
96+
options=['Auto', 'Manual'],
97+
value='Auto',
98+
description='',
99+
tooltips=[
100+
'Re-render preview after every edit',
101+
'Batch edits, then click Apply to re-render',
102+
],
103+
button_style='',
104+
layout=widgets.Layout(width='auto'),
105+
)
106+
apply_btn = widgets.Button(
107+
description=f'Apply',
108+
button_style='warning',
109+
icon='refresh',
110+
layout=widgets.Layout(width='auto', display='none'),
111+
)
112+
113+
def _do_render(figure):
114+
"""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)
117+
n = len(editor.edits)
118+
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)
121+
_render_state['pending'] = 0
122+
apply_btn.description = 'Apply'
123+
status_bar.value = (
124+
f'<span style="font-size:11px; color:#2ECC71">'
125+
f'{n} edit(s) — DPI: {dpi} → export size: {px_w}×{px_h}px</span>'
126+
)
127+
92128
def _live_refresh(figure):
93-
"""Callback: re-render the figure into the Image widget."""
129+
"""Callback: re-render or queue depending on render mode."""
94130
try:
95-
# Use figure's actual DPI for preview (matches user setting)
96-
render_dpi = int(figure.dpi)
97-
fig_image.value = _render_fig_to_png(figure, dpi=render_dpi)
98-
n = len(editor.edits)
99-
status_bar.value = (
100-
f'<span style="font-size:11px; color:#2ECC71">'
101-
f'{n} edit(s) — preview updated (DPI: {int(figure.dpi)})</span>'
102-
)
131+
if render_mode.value == 'Auto':
132+
_do_render(figure)
133+
else:
134+
# Manual mode: count pending edits, don't re-render
135+
_render_state['pending'] += 1
136+
p = _render_state['pending']
137+
n = len(editor.edits)
138+
apply_btn.description = f'Apply ({p})'
139+
status_bar.value = (
140+
f'<span style="font-size:11px; color:#F39C12">'
141+
f'{n} edit(s) — {p} pending re-render '
142+
f'(click Apply)</span>'
143+
)
103144
except Exception:
104145
pass
105146

147+
def _on_apply(btn):
148+
_do_render(editor.fig)
149+
150+
apply_btn.on_click(_on_apply)
151+
152+
def _on_render_mode(change):
153+
if change['new'] == 'Manual':
154+
apply_btn.layout.display = ''
155+
else:
156+
apply_btn.layout.display = 'none'
157+
# Switching back to Auto: render immediately if pending
158+
if _render_state['pending'] > 0:
159+
_do_render(editor.fig)
160+
161+
render_mode.observe(_on_render_mode, names='value')
162+
106163
# Register the live refresh callback
107164
editor.on_refresh(_live_refresh)
108165

109-
# Preview container (figure + status bar)
166+
render_bar = widgets.HBox(
167+
[render_mode, apply_btn],
168+
layout=widgets.Layout(
169+
justify_content='flex-start',
170+
align_items='center',
171+
gap='8px',
172+
),
173+
)
174+
175+
# Preview container (figure + render controls + status bar)
110176
fig_container = widgets.VBox([
111177
fig_image,
178+
render_bar,
112179
status_bar,
113180
], layout=widgets.Layout(
114181
flex='1 1 auto',
@@ -631,6 +698,11 @@ def _cb(change):
631698
layout=widgets.Layout(width='95%'),
632699
continuous_update=False,
633700
)
701+
dpi_hint = widgets.HTML(
702+
'<span style="font-size:11px; color:#888">'
703+
'DPI affects export/save resolution. '
704+
'Preview is scaled to fit — visual size won\'t change.</span>'
705+
)
634706

635707
# ---- Legend controls ----
636708
legend_visible = widgets.Checkbox(
@@ -810,6 +882,8 @@ def _on_figsize_h(change):
810882

811883
def _on_dpi(change):
812884
editor.set_dpi(change['new'])
885+
# Sync save DPI to match figure DPI
886+
save_dpi.value = change['new']
813887

814888
fig_dpi.observe(_on_dpi, names='value')
815889

@@ -911,7 +985,7 @@ def _on_ylim(change):
911985
widgets.HTML('<hr style="margin:4px 0">'),
912986
widgets.HTML('<b>Figure Size</b>'),
913987
size_preset, fig_width, fig_height,
914-
fig_dpi,
988+
fig_dpi, dpi_hint,
915989
widgets.HTML('<hr style="margin:4px 0">'),
916990
widgets.HTML('<b>Background</b>'),
917991
fig_bg_color, ax_bg_color,
@@ -1024,7 +1098,12 @@ def _on_theme(change):
10241098
)
10251099
save_dpi = widgets.IntSlider(
10261100
value=300, min=72, max=600, step=50,
1027-
description='DPI:', layout=widgets.Layout(width='95%'),
1101+
description='Save DPI:', layout=widgets.Layout(width='95%'),
1102+
)
1103+
save_dpi_hint = widgets.HTML(
1104+
'<span style="font-size:11px; color:#888">'
1105+
'Resolution for exported file. '
1106+
'Synced from Figure DPI in Layout tab.</span>'
10281107
)
10291108
save_btn = widgets.Button(
10301109
description='Save Figure',
@@ -1109,7 +1188,7 @@ def _on_copy(btn):
11091188
code_tab = widgets.VBox([
11101189
widgets.HTML('<b>Export & Code</b>'),
11111190
widgets.HBox([save_filename, save_btn]),
1112-
save_dpi,
1191+
save_dpi, save_dpi_hint,
11131192
widgets.HTML('<hr style="margin:4px 0">'),
11141193
widgets.HTML('<b>Reproducible Code</b>'),
11151194
widgets.HBox([generate_btn, undo_btn, reset_btn]),

0 commit comments

Comments
 (0)