Skip to content

Commit 4054829

Browse files
authored
Add a freeze scroll button (#21)
1 parent bb46dde commit 4054829

File tree

3 files changed

+52
-10
lines changed

3 files changed

+52
-10
lines changed

js/widget.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async function render({ model, el }) {
8484
renderQueue = renderQueue.then(() => {
8585
return new Promise((resolve) => {
8686
Logger.debug(`Widget ${widgetId}: Starting graph render`);
87-
const zoomEnabled = model.get('enable_zoom');
87+
const zoomEnabled = model.get("enable_zoom");
8888
d3graphvizInstance
8989
.engine("dot")
9090
.fade(false)
@@ -159,6 +159,27 @@ async function render({ model, el }) {
159159
await renderGraph(model.get("dot_source"));
160160
});
161161

162+
model.on("change:freeze_scroll", async () => {
163+
const freezeScroll = model.get("freeze_scroll");
164+
const svg = d3.select(`#${widgetId} svg`);
165+
const zoomEnabled = model.get("enable_zoom");
166+
167+
if (freezeScroll) {
168+
// Disable only scroll and zoom
169+
svg.on("wheel.zoom", null); // Disable scroll wheel zoom
170+
svg.on("mousedown.zoom", null); // Disable zoom on mousedown
171+
svg.on("touchstart.zoom", null); // Disable zoom on touchstart
172+
svg.on("touchmove.zoom", null); // Disable zoom on touchmove
173+
svg.on("touchend.zoom", null); // Disable zoom on touchend
174+
svg.on("touchcancel.zoom", null); // Disable zoom on touchcancel
175+
} else {
176+
// Re-enable zoom if not frozen and zoom is enabled
177+
if (zoomEnabled) {
178+
svg.call(d3graphvizInstance.zoomBehavior());
179+
}
180+
}
181+
});
182+
162183
model.on("msg:custom", (msg) => {
163184
if (msg.action === "reset_zoom") {
164185
resetGraph();

src/graphviz_anywidget/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class GraphvizAnyWidget(anywidget.AnyWidget):
3737
search_type = traitlets.Unicode("included").tag(sync=True)
3838
case_sensitive = traitlets.Bool(False).tag(sync=True) # noqa: FBT003
3939
enable_zoom = traitlets.Bool(True).tag(sync=True)
40+
freeze_scroll = traitlets.Bool(False).tag(sync=True) # noqa: FBT003
4041

4142

4243
def graphviz_widget(
@@ -83,10 +84,18 @@ def graphviz_widget(
8384
"""
8485
widget = GraphvizAnyWidget(dot_source=dot_source)
8586
reset_button = ipywidgets.Button(
86-
description="Reset Zoom",
87+
description="Reset",
8788
layout=ipywidgets.Layout(width="auto"),
89+
icon="refresh",
8890
button_style="warning",
8991
)
92+
freeze_toggle = ipywidgets.ToggleButton(
93+
value=False,
94+
description="Freeze Scroll",
95+
icon="snowflake-o",
96+
layout=ipywidgets.Layout(width="auto"),
97+
button_style="primary",
98+
)
9099
direction_selector = ipywidgets.Dropdown(
91100
options=["bidirectional", "downstream", "upstream", "single"],
92101
value="bidirectional",
@@ -107,7 +116,7 @@ def graphviz_widget(
107116
case_toggle = ipywidgets.ToggleButton(
108117
value=False,
109118
description="Case Sensitive",
110-
icon="check",
119+
icon="font",
111120
layout=ipywidgets.Layout(width="auto"),
112121
)
113122

@@ -127,18 +136,29 @@ def update_search_type(change: dict) -> None:
127136
def toggle_case_sensitive(change: dict) -> None:
128137
widget.case_sensitive = change["new"]
129138

139+
def toggle_freeze_scroll(change: dict) -> None:
140+
widget.freeze_scroll = change["new"]
141+
if widget.freeze_scroll:
142+
freeze_toggle.description = "Unfreeze Scroll"
143+
freeze_toggle.button_style = "danger"
144+
else:
145+
freeze_toggle.description = "Freeze Scroll"
146+
freeze_toggle.button_style = "primary"
147+
130148
reset_button.on_click(reset_graph)
131149
direction_selector.observe(update_direction, names="value")
132150
search_input.observe(perform_search, names="value")
133151
search_type_selector.observe(update_search_type, names="value")
134152
case_toggle.observe(toggle_case_sensitive, names="value")
153+
freeze_toggle.observe(toggle_freeze_scroll, names="value")
135154

136155
# Display ipywidgets
137156
return ipywidgets.VBox(
138157
[
139158
ipywidgets.HBox(
140159
[
141160
reset_button,
161+
freeze_toggle,
142162
direction_selector,
143163
search_input,
144164
search_type_selector,

tests/test_widget.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,13 @@ def test_graphviz_widget_full_structure() -> None:
6363
# Test control row 1 (reset and direction)
6464
control_row1 = widget.children[0]
6565
assert isinstance(control_row1, HBox)
66-
assert len(control_row1.children) == 5
66+
assert len(control_row1.children) == 6
6767
assert isinstance(control_row1.children[0], Button) # Reset button
68-
assert isinstance(control_row1.children[1], Dropdown) # Direction selector
69-
assert isinstance(control_row1.children[2], Text) # Search input
70-
assert isinstance(control_row1.children[3], Dropdown) # Search type
71-
assert isinstance(control_row1.children[4], ToggleButton) # Case sensitive
68+
assert isinstance(control_row1.children[1], ToggleButton) # Freeze scroll button
69+
assert isinstance(control_row1.children[2], Dropdown) # Direction selector
70+
assert isinstance(control_row1.children[3], Text) # Search input
71+
assert isinstance(control_row1.children[4], Dropdown) # Search type
72+
assert isinstance(control_row1.children[5], ToggleButton) # Case sensitive
7273

7374
# Test graph widget
7475
assert isinstance(widget.children[-1], GraphvizAnyWidget)
@@ -77,7 +78,7 @@ def test_graphviz_widget_full_structure() -> None:
7778
def test_graphviz_widget_direction_options() -> None:
7879
"""Test direction selector options in full widget."""
7980
widget = graphviz_widget()
80-
direction_selector = widget.children[0].children[1]
81+
direction_selector = widget.children[0].children[2]
8182
assert set(direction_selector.options) == {
8283
"bidirectional",
8384
"downstream",
@@ -89,7 +90,7 @@ def test_graphviz_widget_direction_options() -> None:
8990
def test_graphviz_widget_search_type_options() -> None:
9091
"""Test search type options in full widget."""
9192
widget = graphviz_widget()
92-
search_type_selector = widget.children[0].children[3]
93+
search_type_selector = widget.children[0].children[4]
9394
assert set(search_type_selector.options) == {"exact", "included", "regex"}
9495

9596

0 commit comments

Comments
 (0)