From 74acb8094969286c575d70c7370929c7a0b33765 Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Wed, 22 Apr 2026 21:02:41 +0530 Subject: [PATCH 1/2] Fix ScatterWidget circle_brush initialization in non-Jupyter runtimes Signed-off-by: Mridankan Mandal --- drawdata/static/scatter_widget.js | 7 ++++-- js/scatter_widget.js | 15 +++++++------ tests/test_bar_widget_scoping.py | 36 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 tests/test_bar_widget_scoping.py diff --git a/drawdata/static/scatter_widget.js b/drawdata/static/scatter_widget.js index d3bbeac..adf10c8 100644 --- a/drawdata/static/scatter_widget.js +++ b/drawdata/static/scatter_widget.js @@ -9567,6 +9567,7 @@ function render({ model, el }) { container.appendChild(controls); let selectedClassButton = null; let selectedColor = colors[0]; + let circle_brush; let classButtonsContainer = document.createElement("div"); classButtonsContainer.style.display = n_classes === 1 ? "none" : "flex"; classButtonsContainer.style.alignItems = "center"; @@ -9608,7 +9609,9 @@ function render({ model, el }) { button.style.backgroundColor = hexToRgba(colors[i], 0.25); button.style.borderColor = colors[i]; button.style.color = "var(--dd-text-color)"; - circle_brush.style("fill", selectedColor).style("fill-opacity", 0.3).style("stroke", selectedColor).style("stroke-width", 2); + if (circle_brush) { + circle_brush.style("fill", selectedColor).style("fill-opacity", 0.3).style("stroke", selectedColor).style("stroke-width", 2); + } }; classButtonsContainer.appendChild(button); if (i === 0) { @@ -9729,7 +9732,7 @@ function render({ model, el }) { redraw_from_scratch(); } } - let circle_brush = svg.append("circle").attr("cx", width / 2).attr("cy", height / 2).attr("r", model.get("brushsize") * brushScale).style("fill", selectedColor).style("fill-opacity", 0.3).style("stroke", selectedColor).style("stroke-width", 2).style("stroke-opacity", 0.9).attr("class", "brush-indicator"); + circle_brush = svg.append("circle").attr("cx", width / 2).attr("cy", height / 2).attr("r", model.get("brushsize") * brushScale).style("fill", selectedColor).style("fill-opacity", 0.3).style("stroke", selectedColor).style("stroke-width", 2).style("stroke-opacity", 0.9).attr("class", "brush-indicator"); function drag_start(event) { isDragging = false; } diff --git a/js/scatter_widget.js b/js/scatter_widget.js index 97ddc54..f7dbe63 100644 --- a/js/scatter_widget.js +++ b/js/scatter_widget.js @@ -54,6 +54,7 @@ function render({ model, el }) { let selectedClassButton = null; let selectedColor = colors[0]; + let circle_brush; // Container for class buttons (hidden when n_classes === 1) let classButtonsContainer = document.createElement("div"); @@ -105,11 +106,13 @@ function render({ model, el }) { button.style.borderColor = colors[i]; button.style.color = "var(--dd-text-color)"; - circle_brush - .style("fill", selectedColor) - .style("fill-opacity", 0.3) - .style("stroke", selectedColor) - .style("stroke-width", 2); + if (circle_brush) { + circle_brush + .style("fill", selectedColor) + .style("fill-opacity", 0.3) + .style("stroke", selectedColor) + .style("stroke-width", 2); + } }; classButtonsContainer.appendChild(button); @@ -321,7 +324,7 @@ function render({ model, el }) { } // Visual brush indicator with higher visibility - let circle_brush = svg + circle_brush = svg .append("circle") .attr("cx", width / 2) .attr("cy", height / 2) diff --git a/tests/test_bar_widget_scoping.py b/tests/test_bar_widget_scoping.py new file mode 100644 index 0000000..9fe5b45 --- /dev/null +++ b/tests/test_bar_widget_scoping.py @@ -0,0 +1,36 @@ +from pathlib import Path +import re + + +def test_bar_widget_css_is_scoped_to_widget_container(): + css = Path("drawdata/static/bar_widget.css").read_text(encoding="utf-8") + + # Ensure we expose a widget-specific root class for all styling. + assert ".dd-bar-container {" in css + + leaked_global_selectors = [ + r"^\.container\s*\{", + r"^\.controls\s*\{", + r"^\.active\s*\{", + r"^label\s*\{", + r"^input\[type=\"number\"\]\s*\{", + ] + + for pattern in leaked_global_selectors: + assert re.search(pattern, css, flags=re.MULTILINE) is None + + +def test_bar_widget_button_selection_is_container_scoped(): + js = Path("drawdata/static/bar_widget.js").read_text(encoding="utf-8") + + assert 'container.querySelectorAll("button.dd-bar-control")' in js + assert 'container.querySelector("button.dd-bar-control")' in js + assert 'document.querySelectorAll("button.control")' not in js + assert 'document.querySelector("button.control")' not in js + + +def test_scatter_widget_does_not_use_circle_brush_before_initialization(): + js = Path("drawdata/static/scatter_widget.js").read_text(encoding="utf-8") + + assert "let circle_brush;" in js + assert "if (circle_brush)" in js From f71c025bb987f4f5ea7de41d846aeb4b9b45719d Mon Sep 17 00:00:00 2001 From: Mridankan Mandal Date: Wed, 22 Apr 2026 21:07:20 +0530 Subject: [PATCH 2/2] Add focused regression test for ScatterWidget init order Signed-off-by: Mridankan Mandal --- tests/test_bar_widget_scoping.py | 36 ------------------------- tests/test_scatter_widget_init_order.py | 8 ++++++ 2 files changed, 8 insertions(+), 36 deletions(-) delete mode 100644 tests/test_bar_widget_scoping.py create mode 100644 tests/test_scatter_widget_init_order.py diff --git a/tests/test_bar_widget_scoping.py b/tests/test_bar_widget_scoping.py deleted file mode 100644 index 9fe5b45..0000000 --- a/tests/test_bar_widget_scoping.py +++ /dev/null @@ -1,36 +0,0 @@ -from pathlib import Path -import re - - -def test_bar_widget_css_is_scoped_to_widget_container(): - css = Path("drawdata/static/bar_widget.css").read_text(encoding="utf-8") - - # Ensure we expose a widget-specific root class for all styling. - assert ".dd-bar-container {" in css - - leaked_global_selectors = [ - r"^\.container\s*\{", - r"^\.controls\s*\{", - r"^\.active\s*\{", - r"^label\s*\{", - r"^input\[type=\"number\"\]\s*\{", - ] - - for pattern in leaked_global_selectors: - assert re.search(pattern, css, flags=re.MULTILINE) is None - - -def test_bar_widget_button_selection_is_container_scoped(): - js = Path("drawdata/static/bar_widget.js").read_text(encoding="utf-8") - - assert 'container.querySelectorAll("button.dd-bar-control")' in js - assert 'container.querySelector("button.dd-bar-control")' in js - assert 'document.querySelectorAll("button.control")' not in js - assert 'document.querySelector("button.control")' not in js - - -def test_scatter_widget_does_not_use_circle_brush_before_initialization(): - js = Path("drawdata/static/scatter_widget.js").read_text(encoding="utf-8") - - assert "let circle_brush;" in js - assert "if (circle_brush)" in js diff --git a/tests/test_scatter_widget_init_order.py b/tests/test_scatter_widget_init_order.py new file mode 100644 index 0000000..b83a30b --- /dev/null +++ b/tests/test_scatter_widget_init_order.py @@ -0,0 +1,8 @@ +from pathlib import Path + + +def test_scatter_widget_does_not_use_circle_brush_before_initialization(): + js = Path("drawdata/static/scatter_widget.js").read_text(encoding="utf-8") + + assert "let circle_brush;" in js + assert "if (circle_brush)" in js