From f4cabe6ae8bb506b6233bd856c18fa65a715225d Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 16 Oct 2024 16:25:41 -0700 Subject: [PATCH 01/18] add popup position and anchor --- .../user_guide/13-Custom_Interactivity.ipynb | 49 +++++ holoviews/plotting/bokeh/callbacks.py | 186 +++++++++++------- holoviews/streams.py | 21 +- holoviews/tests/ui/bokeh/test_callback.py | 74 +++++++ 4 files changed, 263 insertions(+), 67 deletions(-) diff --git a/examples/user_guide/13-Custom_Interactivity.ipynb b/examples/user_guide/13-Custom_Interactivity.ipynb index a88758f47a..1c6133acbf 100644 --- a/examples/user_guide/13-Custom_Interactivity.ipynb +++ b/examples/user_guide/13-Custom_Interactivity.ipynb @@ -483,6 +483,55 @@ ")" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `popup_position` can be set to one of the following options:\n", + "\n", + "- `top_right` (the default)\n", + "- `top_left`\n", + "- `bottom_left`\n", + "- `bottom_right`\n", + "- `right`\n", + "- `left`\n", + "- `top`\n", + "- `bottom`\n", + "\n", + "The `popup_anchor` is automatically determined based on the `popup_position`, but can also be manually set to one of the following predefined positions:\n", + "\n", + "- `top_left`, `top_center`, `top_right`\n", + "- `center_left`, `center_center`, `center_right`\n", + "- `bottom_left`, `bottom_center`, `bottom_right`\n", + "- `top`, `left`, `center`, `right`, `bottom`\n", + "\n", + "Alternatively, the `popup_anchor` can be specified as a tuple, using a mix of `start`, `center`, `end`, like `(\"start\", \"center\")`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.streams.Selection1D(\n", + " source=points,\n", + " popup=popup_stats,\n", + " popup_position=\"left\",\n", + " popup_anchor=\"right\"\n", + ")\n", + "\n", + "points.opts(\n", + " tools=[\"box_select\", \"lasso_select\", \"tap\"],\n", + " active_tools=[\"lasso_select\"],\n", + " size=6,\n", + " color=\"black\",\n", + " fill_color=None,\n", + " width=500,\n", + " height=500\n", + ")" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 0ad8513561..bc97e6538d 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -74,6 +74,17 @@ from ...util.warnings import warn from .util import BOKEH_GE_3_3_0, convert_timestamp +POPUP_POSITION_ANCHOR = { + "top_right": "top_left", + "top_left": "top_right", + "bottom_left": "bottom_right", + "bottom_right": "bottom_left", + "right": "top_left", + "left": "top_right", + "top": "bottom", + "bottom": "top", +} + class Callback: """ @@ -610,9 +621,10 @@ def initialize(self, plot_id=None): } """], css_classes=["popup-close-btn"]) + self._popup_position = stream.popup_position self._panel = Panel( position=XY(x=np.nan, y=np.nan), - anchor="top_left", + anchor=stream.popup_anchor or POPUP_POSITION_ANCHOR.get(self._popup_position, 'top_left'), elements=[close_button], visible=False, styles={"zIndex": "1000"}, @@ -626,24 +638,56 @@ def _watch_position(self): geom_type = self.geom_type self.plot.state.on_event('selectiongeometry', self._update_selection_event) self.plot.state.js_on_event('selectiongeometry', CustomJS( - args=dict(panel=self._panel), + args=dict(panel=self._panel, popup_position=self.popup_position), code=f""" - export default ({{panel}}, cb_obj, _) => {{ - const el = panel.elements[1] - if ((el && !el.visible) || !cb_obj.final || ({geom_type!r} !== 'any' && cb_obj.geometry.type !== {geom_type!r})) {{ - return - }} - let pos; - if (cb_obj.geometry.type === 'point') {{ - pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}} - }} else if (cb_obj.geometry.type === 'rect') {{ - pos = {{x: cb_obj.geometry.x1, y: cb_obj.geometry.y1}} - }} else if (cb_obj.geometry.type === 'poly') {{ - pos = {{x: Math.max(...cb_obj.geometry.x), y: Math.max(...cb_obj.geometry.y)}} - }} - if (pos) {{ - panel.position.setv(pos) - }} + export default ({{panel, popup_position}}, cb_obj, _) => {{ + const el = panel.elements[1]; + if ((el && !el.visible) || !cb_obj.final || ({geom_type!r} !== 'any' && cb_obj.geometry.type !== {geom_type!r})) {{ + return; + }} + + let pos; + if (cb_obj.geometry.type === 'point') {{ + pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}}; + }} else if (cb_obj.geometry.type === 'rect') {{ + let x, y; + if (popup_position.includes('left')) {{ + x = cb_obj.geometry.x0; + }} else if (popup_position.includes('right')) {{ + x = cb_obj.geometry.x1; + }} else {{ + x = (cb_obj.geometry.x0 + cb_obj.geometry.x1) / 2; + }} + if (popup_position.includes('top')) {{ + y = cb_obj.geometry.y1; + }} else if (popup_position.includes('bottom')) {{ + y = cb_obj.geometry.y0; + }} else {{ + y = (cb_obj.geometry.y0 + cb_obj.geometry.y1) / 2; + }} + pos = {{x: x, y: y}}; + }} else if (cb_obj.geometry.type === 'poly') {{ + let x, y; + if (popup_position.includes('left')) {{ + x = Math.min(...cb_obj.geometry.x); + }} else if (popup_position.includes('right')) {{ + x = Math.max(...cb_obj.geometry.x); + }} else {{ + x = (Math.min(...cb_obj.geometry.x) + Math.max(...cb_obj.geometry.x)) / 2; + }} + if (popup_position.includes('top')) {{ + y = Math.max(...cb_obj.geometry.y); + }} else if (popup_position.includes('bottom')) {{ + y = Math.min(...cb_obj.geometry.y); + }} else {{ + y = (Math.min(...cb_obj.geometry.y) + Math.max(...cb_obj.geometry.y)) / 2; + }} + pos = {{x: x, y: y}}; + }} + + if (pos) {{ + panel.position.setv(pos); + }} }}""", )) @@ -1163,61 +1207,71 @@ def _watch_position(self): source = self.plot.handles['source'] renderer = self.plot.handles['glyph_renderer'] selected = self.plot.handles['selected'] + self.plot.state.js_on_event('selectiongeometry', CustomJS( - args=dict(panel=self._panel, renderer=renderer, source=source, selected=selected), + args=dict(panel=self._panel, renderer=renderer, source=source, selected=selected, popup_position=self._popup_position), code=""" - export default ({panel, renderer, source, selected}, cb_obj, _) => { - const el = panel.elements[1] - if ((el && !el.visible) || !cb_obj.final) { - return - } - let x, y, xs, ys; - let indices = selected.indices; - if (cb_obj.geometry.type == 'point') { - indices = indices.slice(-1) - } - if (renderer.glyph.x && renderer.glyph.y) { - xs = source.get_column(renderer.glyph.x.field) - ys = source.get_column(renderer.glyph.y.field) - } else if (renderer.glyph.right && renderer.glyph.top) { - xs = source.get_column(renderer.glyph.right.field) - ys = source.get_column(renderer.glyph.top.field) - } else if (renderer.glyph.x1 && renderer.glyph.y1) { - xs = source.get_column(renderer.glyph.x1.field) - ys = source.get_column(renderer.glyph.y1.field) - } else if (renderer.glyph.xs && renderer.glyph.ys) { - xs = source.get_column(renderer.glyph.xs.field) - ys = source.get_column(renderer.glyph.ys.field) - } - if (!xs || !ys) { return } - for (const i of indices) { - let ix = xs[i] - let iy = ys[i] - let tx, ty - if (typeof ix === 'number') { - tx = ix - ty = iy + export default ({panel, renderer, source, selected, popup_position}, cb_obj, _) => { + const el = panel.elements[1]; + if ((el && !el.visible) || !cb_obj.final) { + return; + } + let x, y, xs, ys; + let indices = selected.indices; + if (cb_obj.geometry.type == 'point') { + indices = indices.slice(-1); + } + if (renderer.glyph.x && renderer.glyph.y) { + xs = source.get_column(renderer.glyph.x.field); + ys = source.get_column(renderer.glyph.y.field); + } else if (renderer.glyph.right && renderer.glyph.top) { + xs = source.get_column(renderer.glyph.right.field); + ys = source.get_column(renderer.glyph.top.field); + } else if (renderer.glyph.x1 && renderer.glyph.y1) { + xs = source.get_column(renderer.glyph.x1.field); + ys = source.get_column(renderer.glyph.y1.field); + } else if (renderer.glyph.xs && renderer.glyph.ys) { + xs = source.get_column(renderer.glyph.xs.field); + ys = source.get_column(renderer.glyph.ys.field); + } + if (!xs || !ys) { return; } + + let minX = null, maxX = null, minY = null, maxY = null; + + for (const i of indices) { + const tx = xs[i]; + const ty = ys[i]; + + if (minX === null || tx < minX) { minX = tx; } + if (maxX === null || tx > maxX) { maxX = tx; } + if (minY === null || ty < minY) { minY = ty; } + if (maxY === null || ty > maxY) { maxY = ty; } + } + + if (minX !== null && maxX !== null && minY !== null && maxY !== null) { + if (popup_position.includes('left')) { + x = minX; + } else if (popup_position.includes('right')) { + x = maxX; } else { - while (ix.length && (typeof ix[0] !== 'number')) { - ix = ix[0] - iy = iy[0] - } - tx = Math.max(...ix) - ty = Math.max(...iy) - } - if (!x || (tx > x)) { - x = tx + x = (minX + maxX) / 2; } - if (!y || (ty > y)) { - y = ty + + if (popup_position.includes('top')) { + y = maxY; + } else if (popup_position.includes('bottom')) { + y = minY; + } else { + y = (minY + maxY) / 2; } - } - if (x && y) { - panel.position.setv({x, y}) - } - }""", + + panel.position.setv({x, y}); + } + } + """, )) + def _get_position(self, event): el = self.plot.current_frame if isinstance(el, Dataset): diff --git a/holoviews/streams.py b/holoviews/streams.py index e129c8b431..93e2c9bcaa 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -22,6 +22,17 @@ # Types supported by Pointer derived streams pointer_types = (Number, str, tuple)+util.datetime_types +POPUP_POSITIONS = [ + "top_right", + "top_left", + "bottom_left", + "bottom_right", + "right", + "left", + "top", + "bottom", +] + class _SkipTrigger: pass @@ -1255,9 +1266,17 @@ class LinkedStream(Stream): supplying stream data. """ - def __init__(self, linked=True, popup=None, **params): + def __init__(self, linked=True, popup=None, popup_position=POPUP_POSITIONS[0], popup_anchor=None, **params): + if popup_position not in POPUP_POSITIONS: + raise ValueError( + f"Invalid popup_position: {popup_position!r}; " + f"expect one of {POPUP_POSITIONS}" + ) + super().__init__(linked=linked, **params) self.popup = popup + self.popup_position = popup_position + self.popup_anchor = popup_anchor class PointerX(LinkedStream): diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index e2cb70a005..2a516276bd 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -509,6 +509,80 @@ def popup_form(index): expect(locator).not_to_have_text("lasso\n0") +@skip_popup +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_selection1d_box_select_right(serve_hv, points): + def popup_form(index): + if index: + return f"# lasso\n{len(index)}" + + hv.streams.Selection1D(source=points, popup=popup_form, popup_position="right", popup_anchor="left") + points.opts(tools=["box_select"], active_tools=["box_select"]) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + + box = hv_plot.bounding_box() + start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 + mid_x, mid_y = box['x'], box['y'] + end_x, end_y = box['x'], box['y'] + + # Perform lasso selection + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + + # Wait for popup to show + wait_until(lambda: expect(page.locator("#lasso")).to_have_count(1), page) + locator = page.locator("#lasso") + expect(locator).to_have_count(1) + expect(locator).not_to_have_text("lasso\n0") + + popup = locator.bounding_box() + assert popup['x'] + popup["width"] > mid_x # Should be towards the right + + +@skip_popup +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_selection1d_box_select_left(serve_hv, points): + def popup_form(index): + if index: + return f"# lasso\n{len(index)}" + + hv.streams.Selection1D(source=points, popup=popup_form, popup_position="left", popup_anchor="right") + points.opts(tools=["box_select"], active_tools=["box_select"]) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + + box = hv_plot.bounding_box() + start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 + mid_x, mid_y = box['x'], box['y'] + end_x, end_y = box['x'], box['y'] + + # Perform lasso selection + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + + # Wait for popup to show + wait_until(lambda: expect(page.locator("#lasso")).to_have_count(1), page) + locator = page.locator("#lasso") + expect(locator).to_have_count(1) + expect(locator).not_to_have_text("lasso\n0") + + popup = locator.bounding_box() + assert popup['x'] < mid_x # Should be towards the left + + @pytest.mark.usefixtures("bokeh_backend") def test_stream_subcoordinate_y_range(serve_hv, points): def cb(x_range, y_range): From f667fe4531acbf22252ba1167a5bd97143e122b9 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 16 Oct 2024 16:26:50 -0700 Subject: [PATCH 02/18] tab --- holoviews/plotting/bokeh/callbacks.py | 98 +++++++++++++-------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index bc97e6538d..d24e89d0dd 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -1212,61 +1212,61 @@ def _watch_position(self): args=dict(panel=self._panel, renderer=renderer, source=source, selected=selected, popup_position=self._popup_position), code=""" export default ({panel, renderer, source, selected, popup_position}, cb_obj, _) => { - const el = panel.elements[1]; - if ((el && !el.visible) || !cb_obj.final) { - return; - } - let x, y, xs, ys; - let indices = selected.indices; - if (cb_obj.geometry.type == 'point') { - indices = indices.slice(-1); - } - if (renderer.glyph.x && renderer.glyph.y) { - xs = source.get_column(renderer.glyph.x.field); - ys = source.get_column(renderer.glyph.y.field); - } else if (renderer.glyph.right && renderer.glyph.top) { - xs = source.get_column(renderer.glyph.right.field); - ys = source.get_column(renderer.glyph.top.field); - } else if (renderer.glyph.x1 && renderer.glyph.y1) { - xs = source.get_column(renderer.glyph.x1.field); - ys = source.get_column(renderer.glyph.y1.field); - } else if (renderer.glyph.xs && renderer.glyph.ys) { - xs = source.get_column(renderer.glyph.xs.field); - ys = source.get_column(renderer.glyph.ys.field); - } - if (!xs || !ys) { return; } - - let minX = null, maxX = null, minY = null, maxY = null; + const el = panel.elements[1]; + if ((el && !el.visible) || !cb_obj.final) { + return; + } + let x, y, xs, ys; + let indices = selected.indices; + if (cb_obj.geometry.type == 'point') { + indices = indices.slice(-1); + } + if (renderer.glyph.x && renderer.glyph.y) { + xs = source.get_column(renderer.glyph.x.field); + ys = source.get_column(renderer.glyph.y.field); + } else if (renderer.glyph.right && renderer.glyph.top) { + xs = source.get_column(renderer.glyph.right.field); + ys = source.get_column(renderer.glyph.top.field); + } else if (renderer.glyph.x1 && renderer.glyph.y1) { + xs = source.get_column(renderer.glyph.x1.field); + ys = source.get_column(renderer.glyph.y1.field); + } else if (renderer.glyph.xs && renderer.glyph.ys) { + xs = source.get_column(renderer.glyph.xs.field); + ys = source.get_column(renderer.glyph.ys.field); + } + if (!xs || !ys) { return; } - for (const i of indices) { - const tx = xs[i]; - const ty = ys[i]; + let minX = null, maxX = null, minY = null, maxY = null; - if (minX === null || tx < minX) { minX = tx; } - if (maxX === null || tx > maxX) { maxX = tx; } - if (minY === null || ty < minY) { minY = ty; } - if (maxY === null || ty > maxY) { maxY = ty; } - } + for (const i of indices) { + const tx = xs[i]; + const ty = ys[i]; - if (minX !== null && maxX !== null && minY !== null && maxY !== null) { - if (popup_position.includes('left')) { - x = minX; - } else if (popup_position.includes('right')) { - x = maxX; - } else { - x = (minX + maxX) / 2; + if (minX === null || tx < minX) { minX = tx; } + if (maxX === null || tx > maxX) { maxX = tx; } + if (minY === null || ty < minY) { minY = ty; } + if (maxY === null || ty > maxY) { maxY = ty; } } - if (popup_position.includes('top')) { - y = maxY; - } else if (popup_position.includes('bottom')) { - y = minY; - } else { - y = (minY + maxY) / 2; + if (minX !== null && maxX !== null && minY !== null && maxY !== null) { + if (popup_position.includes('left')) { + x = minX; + } else if (popup_position.includes('right')) { + x = maxX; + } else { + x = (minX + maxX) / 2; + } + + if (popup_position.includes('top')) { + y = maxY; + } else if (popup_position.includes('bottom')) { + y = minY; + } else { + y = (minY + maxY) / 2; + } + + panel.position.setv({x, y}); } - - panel.position.setv({x, y}); - } } """, )) From e3f320410a769b9060dae41068a8e73a6c4ba03a Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 16 Oct 2024 16:27:49 -0700 Subject: [PATCH 03/18] tab --- holoviews/plotting/bokeh/callbacks.py | 88 +++++++++++++-------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index d24e89d0dd..ad085940ff 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -641,53 +641,53 @@ def _watch_position(self): args=dict(panel=self._panel, popup_position=self.popup_position), code=f""" export default ({{panel, popup_position}}, cb_obj, _) => {{ - const el = panel.elements[1]; - if ((el && !el.visible) || !cb_obj.final || ({geom_type!r} !== 'any' && cb_obj.geometry.type !== {geom_type!r})) {{ - return; - }} - - let pos; - if (cb_obj.geometry.type === 'point') {{ - pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}}; - }} else if (cb_obj.geometry.type === 'rect') {{ - let x, y; - if (popup_position.includes('left')) {{ - x = cb_obj.geometry.x0; - }} else if (popup_position.includes('right')) {{ - x = cb_obj.geometry.x1; - }} else {{ - x = (cb_obj.geometry.x0 + cb_obj.geometry.x1) / 2; - }} - if (popup_position.includes('top')) {{ - y = cb_obj.geometry.y1; - }} else if (popup_position.includes('bottom')) {{ - y = cb_obj.geometry.y0; - }} else {{ - y = (cb_obj.geometry.y0 + cb_obj.geometry.y1) / 2; - }} - pos = {{x: x, y: y}}; - }} else if (cb_obj.geometry.type === 'poly') {{ - let x, y; - if (popup_position.includes('left')) {{ - x = Math.min(...cb_obj.geometry.x); - }} else if (popup_position.includes('right')) {{ - x = Math.max(...cb_obj.geometry.x); - }} else {{ - x = (Math.min(...cb_obj.geometry.x) + Math.max(...cb_obj.geometry.x)) / 2; + const el = panel.elements[1]; + if ((el && !el.visible) || !cb_obj.final || ({geom_type!r} !== 'any' && cb_obj.geometry.type !== {geom_type!r})) {{ + return; }} - if (popup_position.includes('top')) {{ - y = Math.max(...cb_obj.geometry.y); - }} else if (popup_position.includes('bottom')) {{ - y = Math.min(...cb_obj.geometry.y); - }} else {{ - y = (Math.min(...cb_obj.geometry.y) + Math.max(...cb_obj.geometry.y)) / 2; + + let pos; + if (cb_obj.geometry.type === 'point') {{ + pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}}; + }} else if (cb_obj.geometry.type === 'rect') {{ + let x, y; + if (popup_position.includes('left')) {{ + x = cb_obj.geometry.x0; + }} else if (popup_position.includes('right')) {{ + x = cb_obj.geometry.x1; + }} else {{ + x = (cb_obj.geometry.x0 + cb_obj.geometry.x1) / 2; + }} + if (popup_position.includes('top')) {{ + y = cb_obj.geometry.y1; + }} else if (popup_position.includes('bottom')) {{ + y = cb_obj.geometry.y0; + }} else {{ + y = (cb_obj.geometry.y0 + cb_obj.geometry.y1) / 2; + }} + pos = {{x: x, y: y}}; + }} else if (cb_obj.geometry.type === 'poly') {{ + let x, y; + if (popup_position.includes('left')) {{ + x = Math.min(...cb_obj.geometry.x); + }} else if (popup_position.includes('right')) {{ + x = Math.max(...cb_obj.geometry.x); + }} else {{ + x = (Math.min(...cb_obj.geometry.x) + Math.max(...cb_obj.geometry.x)) / 2; + }} + if (popup_position.includes('top')) {{ + y = Math.max(...cb_obj.geometry.y); + }} else if (popup_position.includes('bottom')) {{ + y = Math.min(...cb_obj.geometry.y); + }} else {{ + y = (Math.min(...cb_obj.geometry.y) + Math.max(...cb_obj.geometry.y)) / 2; + }} + pos = {{x: x, y: y}}; }} - pos = {{x: x, y: y}}; - }} - if (pos) {{ - panel.position.setv(pos); - }} + if (pos) {{ + panel.position.setv(pos); + }} }}""", )) From 026ae1b4986ef8c41b4d046fa154bfc521e7382d Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 16 Oct 2024 16:29:15 -0700 Subject: [PATCH 04/18] add more test --- holoviews/tests/ui/bokeh/test_callback.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 2a516276bd..97002b30be 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -543,7 +543,8 @@ def popup_form(index): expect(locator).not_to_have_text("lasso\n0") popup = locator.bounding_box() - assert popup['x'] + popup["width"] > mid_x # Should be towards the right + assert popup['x'] > mid_x # Should be towards the right + assert popup['y'] > mid_y # Should be towards the top @skip_popup @@ -581,6 +582,7 @@ def popup_form(index): popup = locator.bounding_box() assert popup['x'] < mid_x # Should be towards the left + assert popup['y'] > mid_y # Should be towards the top @pytest.mark.usefixtures("bokeh_backend") From 897cae2019469d01fa5a8797354b1abd396033b6 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Wed, 16 Oct 2024 16:35:24 -0700 Subject: [PATCH 05/18] add test and fix bug --- holoviews/plotting/bokeh/callbacks.py | 2 +- holoviews/tests/ui/bokeh/test_callback.py | 38 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index ad085940ff..aacbe8541f 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -638,7 +638,7 @@ def _watch_position(self): geom_type = self.geom_type self.plot.state.on_event('selectiongeometry', self._update_selection_event) self.plot.state.js_on_event('selectiongeometry', CustomJS( - args=dict(panel=self._panel, popup_position=self.popup_position), + args=dict(panel=self._panel, popup_position=self._popup_position), code=f""" export default ({{panel, popup_position}}, cb_obj, _) => {{ const el = panel.elements[1]; diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 97002b30be..4eb1b7153f 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -344,6 +344,44 @@ def popup_form(x, y): expect(locator).to_have_count(2) +@skip_popup +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_box_select_right(serve_hv, points): + def popup_form(bounds): + if bounds: + return f"# box\n{len(bounds)}" + + hv.streams.BoundsXY(source=points, popup=popup_form, popup_position="right", popup_anchor="left") + points.opts(tools=["box_select"], active_tools=["box_select"]) + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + + box = hv_plot.bounding_box() + start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 + mid_x, mid_y = box['x'], box['y'] + end_x, end_y = box['x'], box['y'] + + # Perform lasso selection + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + + # Wait for popup to show + wait_until(lambda: expect(page.locator("#box")).to_have_count(1), page) + locator = page.locator("#box") + expect(locator).to_have_count(1) + expect(locator).not_to_have_text("box\n0") + + popup = locator.bounding_box() + assert popup['x'] > mid_x # Should be towards the right + assert popup['y'] > mid_y # Should be towards the top + + @skip_popup @pytest.mark.usefixtures("bokeh_backend") def test_stream_popup_visible(serve_hv, points): From a75a43821997d7f8e6856274369b3f111a63e86b Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Mon, 21 Oct 2024 11:32:27 -0700 Subject: [PATCH 06/18] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commit suggestions Co-authored-by: Simon Høxbro Hansen --- holoviews/plotting/bokeh/callbacks.py | 4 ++-- holoviews/streams.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 049911a829..6d8e93729f 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -625,7 +625,7 @@ def initialize(self, plot_id=None): self._popup_position = stream.popup_position self._panel = Panel( position=XY(x=np.nan, y=np.nan), - anchor=stream.popup_anchor or POPUP_POSITION_ANCHOR.get(self._popup_position, 'top_left'), + anchor=stream.popup_anchor or POPUP_POSITION_ANCHOR[self._popup_position], elements=[close_button], visible=False, styles={"zIndex": "1000"}, @@ -1246,7 +1246,7 @@ def _watch_position(self): } if (!xs || !ys) { return; } - let minX = null, maxX = null, minY = null, maxY = null; + let minX, maxX, minY, maxY; for (const i of indices) { const tx = xs[i]; diff --git a/holoviews/streams.py b/holoviews/streams.py index 93e2c9bcaa..5cbbe41f7c 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -1266,7 +1266,7 @@ class LinkedStream(Stream): supplying stream data. """ - def __init__(self, linked=True, popup=None, popup_position=POPUP_POSITIONS[0], popup_anchor=None, **params): + def __init__(self, linked=True, popup=None, popup_position="top_right", popup_anchor=None, **params): if popup_position not in POPUP_POSITIONS: raise ValueError( f"Invalid popup_position: {popup_position!r}; " From 0f872962d602efaf97a6291719730fe4f9de8c52 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 21 Oct 2024 11:48:18 -0700 Subject: [PATCH 07/18] clean up logic --- holoviews/plotting/bokeh/callbacks.py | 48 ++++++++++++++------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 6d8e93729f..a28e65ec91 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -1244,7 +1244,11 @@ def _watch_position(self): xs = source.get_column(renderer.glyph.xs.field); ys = source.get_column(renderer.glyph.ys.field); } - if (!xs || !ys) { return; } + + if (!xs || !ys || indices.length == 0) { + panel.visible = false; + return; + } let minX, maxX, minY, maxY; @@ -1252,31 +1256,29 @@ def _watch_position(self): const tx = xs[i]; const ty = ys[i]; - if (minX === null || tx < minX) { minX = tx; } - if (maxX === null || tx > maxX) { maxX = tx; } - if (minY === null || ty < minY) { minY = ty; } - if (maxY === null || ty > maxY) { maxY = ty; } + if (minX === undefined || tx < minX) { minX = tx; } + if (maxX === undefined || tx > maxX) { maxX = tx; } + if (minY === undefined || ty < minY) { minY = ty; } + if (maxY === undefined || ty > maxY) { maxY = ty; } } - if (minX !== null && maxX !== null && minY !== null && maxY !== null) { - if (popup_position.includes('left')) { - x = minX; - } else if (popup_position.includes('right')) { - x = maxX; - } else { - x = (minX + maxX) / 2; - } - - if (popup_position.includes('top')) { - y = maxY; - } else if (popup_position.includes('bottom')) { - y = minY; - } else { - y = (minY + maxY) / 2; - } - - panel.position.setv({x, y}); + if (popup_position.includes('left')) { + x = minX; + } else if (popup_position.includes('right')) { + x = maxX; + } else { + x = (minX + maxX) / 2; } + + if (popup_position.includes('top')) { + y = maxY; + } else if (popup_position.includes('bottom')) { + y = minY; + } else { + y = (minY + maxY) / 2; + } + + panel.position.setv({x, y}); } """, )) From a38a81a6aaa42e895325994c9773fbe70f03c939 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 21 Oct 2024 14:36:32 -0700 Subject: [PATCH 08/18] change default anchors --- holoviews/plotting/bokeh/callbacks.py | 57 +++++++++++++++++++++------ 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index a28e65ec91..5cd0a92dd8 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -76,10 +76,10 @@ from .util import BOKEH_GE_3_3_0, convert_timestamp POPUP_POSITION_ANCHOR = { - "top_right": "top_left", - "top_left": "top_right", - "bottom_left": "bottom_right", - "bottom_right": "bottom_left", + "top_right": "bottom_left", + "top_left": "bottom_right", + "bottom_left": "top_right", + "bottom_right": "top_left", "right": "top_left", "left": "top_right", "top": "bottom", @@ -778,7 +778,7 @@ async def _process_selection_event(self): position = self._get_position(event) if event else None if position: self._panel.position = XY(**position) - if self.plot.comm: # update Jupyter Notebook + if self.plot.comm: # update Jupyter Notebooks push_on_root(self.plot.root.ref['id']) return @@ -1222,6 +1222,7 @@ def _watch_position(self): args=dict(panel=self._panel, renderer=renderer, source=source, selected=selected, popup_position=self._popup_position), code=""" export default ({panel, renderer, source, selected, popup_position}, cb_obj, _) => { + panel.visible = false; // Hide the popup panel so it doesn't show in previous location const el = panel.elements[1]; if ((el && !el.visible) || !cb_obj.final) { return; @@ -1231,6 +1232,7 @@ def _watch_position(self): if (cb_obj.geometry.type == 'point') { indices = indices.slice(-1); } + if (renderer.glyph.x && renderer.glyph.y) { xs = source.get_column(renderer.glyph.x.field); ys = source.get_column(renderer.glyph.y.field); @@ -1245,23 +1247,55 @@ def _watch_position(self): ys = source.get_column(renderer.glyph.ys.field); } - if (!xs || !ys || indices.length == 0) { - panel.visible = false; + if (!xs || !ys) { return; } let minX, maxX, minY, maxY; + // Loop over each index in the selection and find the corresponding polygon coordinates for (const i of indices) { - const tx = xs[i]; - const ty = ys[i]; - + let ix = xs[i]; + let iy = ys[i]; + let tx, ty; + + // Check if the values are numbers or nested arrays + if (typeof ix === 'number') { + tx = ix; + ty = iy; + } else { + // Drill down into nested arrays until we find the number values + while (ix.length && typeof ix[0] !== 'number') { + ix = ix[0]; + iy = iy[0]; + } + + // Set tx and ty based on the popup position preferences + if (popup_position.includes('left')) { + tx = Math.min(...ix); + } else if (popup_position.includes('right')) { + tx = Math.max(...ix); + } else { + tx = (Math.min(...ix) + Math.max(...ix)) / 2; + } + + if (popup_position.includes('top')) { + ty = Math.max(...iy); + } else if (popup_position.includes('bottom')) { + ty = Math.min(...iy); + } else { + ty = (Math.min(...iy) + Math.max(...iy)) / 2; + } + } + + // Update the min/max values for x and y if (minX === undefined || tx < minX) { minX = tx; } if (maxX === undefined || tx > maxX) { maxX = tx; } if (minY === undefined || ty < minY) { minY = ty; } if (maxY === undefined || ty > maxY) { maxY = ty; } } + // Set x and y based on popup_position preference if (popup_position.includes('left')) { x = minX; } else if (popup_position.includes('right')) { @@ -1278,12 +1312,13 @@ def _watch_position(self): y = (minY + maxY) / 2; } + // Set the popup position and make it visible panel.position.setv({x, y}); + panel.visible = true; } """, )) - def _get_position(self, event): el = self.plot.current_frame if isinstance(el, Dataset): From fecdf6b60af4d735f41e025b6fd5ce04f9b2662c Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 21 Oct 2024 14:36:50 -0700 Subject: [PATCH 09/18] add parametrized tests --- holoviews/tests/ui/bokeh/test_callback.py | 356 ++++++++++++++-------- 1 file changed, 228 insertions(+), 128 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index ea84805064..a73897b84e 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -6,7 +6,15 @@ import holoviews as hv from holoviews import Curve, DynamicMap, Scatter from holoviews.plotting.bokeh.util import BOKEH_GE_3_4_0 -from holoviews.streams import BoundsX, BoundsXY, BoundsY, Lasso, MultiAxisTap, RangeXY +from holoviews.streams import ( + BoundsX, + BoundsXY, + BoundsY, + Lasso, + MultiAxisTap, + RangeXY, + Tap, +) from .. import expect, wait_until @@ -268,39 +276,86 @@ def popup_form(name): @skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_polygons_tap(serve_hv): +@pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" +]) +def test_stream_popup_polygons_tap(serve_hv, popup_position): def popup_form(name): - return f"# {name}" + return "# selection" points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"]) - hv.streams.Tap(source=points, popup=popup_form("Tap")) + hv.streams.Tap(source=points, popup=popup_form("Tap"), popup_position=popup_position) page = serve_hv(points) hv_plot = page.locator('.bk-events') hv_plot.click() expect(hv_plot).to_have_count(1) - locator = page.locator(".markdown") + # Wait for popup to show + wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) + locator = page.locator("#selection") expect(locator).to_have_count(1) + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + + distance_to_left = abs(popup_box['x'] - box['x']) + distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) + distance_to_top = abs(popup_box['y'] - box['y']) + distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) + + if "right" in popup_position: + assert distance_to_right <= distance_to_left + elif "left" in popup_position: + assert distance_to_left <= distance_to_right + + if "top" in popup_position: + assert distance_to_top <= distance_to_bottom + elif "bottom" in popup_position: + assert distance_to_bottom <= distance_to_top + @skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_polygons_selection1d(serve_hv): +@pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" +]) +def test_stream_popup_polygons_selection1d(serve_hv, popup_position): def popup_form(name): return f"# {name}" points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"]) - hv.streams.Selection1D(source=points, popup=popup_form("Tap")) + hv.streams.Selection1D(source=points, popup=popup_form("Tap"), popup_position=popup_position) page = serve_hv(points) hv_plot = page.locator('.bk-events') hv_plot.click() expect(hv_plot).to_have_count(1) - locator = page.locator(".markdown") + # Wait for popup to show + wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) + locator = page.locator("#selection") expect(locator).to_have_count(1) + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + + distance_to_left = abs(popup_box['x'] - box['x']) + distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) + distance_to_top = abs(popup_box['y'] - box['y']) + distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) + + if "right" in popup_position: + assert distance_to_right <= distance_to_left + elif "left" in popup_position: + assert distance_to_left <= distance_to_right + + if "top" in popup_position: + assert distance_to_top <= distance_to_bottom + elif "bottom" in popup_position: + assert distance_to_bottom <= distance_to_top @skip_popup @pytest.mark.usefixtures("bokeh_backend") @@ -344,44 +399,6 @@ def popup_form(x, y): expect(locator).to_have_count(2) -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_box_select_right(serve_hv, points): - def popup_form(bounds): - if bounds: - return f"# box\n{len(bounds)}" - - hv.streams.BoundsXY(source=points, popup=popup_form, popup_position="right", popup_anchor="left") - points.opts(tools=["box_select"], active_tools=["box_select"]) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - - box = hv_plot.bounding_box() - start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 - mid_x, mid_y = box['x'], box['y'] - end_x, end_y = box['x'], box['y'] - - # Perform lasso selection - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() - - # Wait for popup to show - wait_until(lambda: expect(page.locator("#box")).to_have_count(1), page) - locator = page.locator("#box") - expect(locator).to_have_count(1) - expect(locator).not_to_have_text("box\n0") - - popup = locator.bounding_box() - assert popup['x'] > mid_x # Should be towards the right - assert popup['y'] > mid_y # Should be towards the top - - @skip_popup @pytest.mark.usefixtures("bokeh_backend") def test_stream_popup_async_callbacks(serve_hv): @@ -472,25 +489,6 @@ def test_stream_popup_selection1d_undefined(serve_hv, points): hv_plot.click() # should not raise any error; properly guarded -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_tap(serve_hv, points): - def popup_form(index): - return "# Tap" - - points = points.opts(hit_dilation=5) - hv.streams.Selection1D(source=points, popup=popup_form) - points.opts(tools=["tap"], active_tools=["tap"]) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - hv_plot.click() - - locator = page.locator("#tap") - expect(locator).to_have_count(1) - - @skip_popup @pytest.mark.usefixtures("bokeh_backend") def test_stream_popup_noncallable_reappear(serve_hv, points): @@ -533,111 +531,213 @@ def popup_form(name): @skip_popup +@pytest.mark.parametrize("tool", ["box_select", "lasso_select", "tap"]) +@pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" +]) @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_lasso_select(serve_hv, points): +def test_stream_popup_position_selection1d(serve_hv, points, tool, popup_position): def popup_form(index): if index: - return f"# lasso\n{len(index)}" + return f"# selection\n{len(index)} {popup_position}" - hv.streams.Selection1D(source=points, popup=popup_form) - points.opts(tools=["tap", "lasso_select"], active_tools=["lasso_select"]) + hv.streams.Selection1D(source=points, popup=popup_form, popup_position=popup_position) + points.opts(tools=[tool], active_tools=[tool]) page = serve_hv(points) hv_plot = page.locator('.bk-events') expect(hv_plot).to_have_count(1) box = hv_plot.bounding_box() - start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 - mid_x, mid_y = box['x'] + 1, box['y'] + 1 - end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 - - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() + if tool == "box_select": + # try to get it centered as possible + start_x, start_y = box['x'] + 90, box['y'] + 90 + end_x, end_y = box['x'] + 170, box['y'] + 125 + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "lasso_select": + start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 + mid_x, mid_y = box['x'] + 1, box['y'] + 1 + end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "tap": + hv_plot.click() - wait_until(lambda: expect(page.locator("#lasso")).to_have_count(1), page) - locator = page.locator("#lasso") + # Wait for popup to show + wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) + locator = page.locator("#selection") expect(locator).to_have_count(1) - expect(locator).not_to_have_text("lasso\n0") + expect(locator).not_to_have_text("selection\n0") + + popup_box = locator.bounding_box() + + distance_to_left = abs(popup_box['x'] - box['x']) + distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) + distance_to_top = abs(popup_box['y'] - box['y']) + distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) + + if "right" in popup_position: + assert distance_to_right <= distance_to_left + elif "left" in popup_position: + assert distance_to_left <= distance_to_right + + if "top" in popup_position: + assert distance_to_top <= distance_to_bottom + elif "bottom" in popup_position: + assert distance_to_bottom <= distance_to_top @skip_popup @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_box_select_right(serve_hv, points): +def test_stream_popup_anchor_selection1d(serve_hv, points): def popup_form(index): if index: - return f"# lasso\n{len(index)}" + return f"# selection\n{len(index)}" - hv.streams.Selection1D(source=points, popup=popup_form, popup_position="right", popup_anchor="left") - points.opts(tools=["box_select"], active_tools=["box_select"]) + hv.streams.Selection1D(source=points, popup=popup_form, popup_position="top", popup_anchor="top_right") + points.opts(tools=["tap"], active_tools=["tap"]) page = serve_hv(points) hv_plot = page.locator('.bk-events') expect(hv_plot).to_have_count(1) - - box = hv_plot.bounding_box() - start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 - mid_x, mid_y = box['x'], box['y'] - end_x, end_y = box['x'], box['y'] - - # Perform lasso selection - page.mouse.move(start_x, start_y) hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() # Wait for popup to show - wait_until(lambda: expect(page.locator("#lasso")).to_have_count(1), page) - locator = page.locator("#lasso") + wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) + locator = page.locator("#selection") expect(locator).to_have_count(1) - expect(locator).not_to_have_text("lasso\n0") + expect(locator).not_to_have_text("selection\n0") + + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() - popup = locator.bounding_box() - assert popup['x'] > mid_x # Should be towards the right - assert popup['y'] > mid_y # Should be towards the top + distance_to_left = abs(popup_box['x'] - box['x']) + distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) + distance_to_top = abs(popup_box['y'] - box['y']) + distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) + + assert distance_to_left <= distance_to_right + assert distance_to_bottom <= distance_to_top @skip_popup +@pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso), ("tap", Tap)]) +@pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" +]) @pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_box_select_left(serve_hv, points): - def popup_form(index): - if index: - return f"# lasso\n{len(index)}" +def test_stream_popup_position_streams(serve_hv, points, tool, tool_type, popup_position): + def popup_form(*args, **kwargs): + return "# selection" - hv.streams.Selection1D(source=points, popup=popup_form, popup_position="left", popup_anchor="right") - points.opts(tools=["box_select"], active_tools=["box_select"]) + points = points.opts(tools=[tool], active_tools=[tool]) + tool_type(source=points, popup=popup_form, popup_position=popup_position) page = serve_hv(points) hv_plot = page.locator('.bk-events') expect(hv_plot).to_have_count(1) + hv_plot.click() box = hv_plot.bounding_box() - start_x, start_y = box['x'] + 10, box['y'] + box['height'] - 10 - mid_x, mid_y = box['x'], box['y'] - end_x, end_y = box['x'], box['y'] + if tool == "box_select": + # try to get it centered as possible + start_x, start_y = box['x'] + 90, box['y'] + 90 + end_x, end_y = box['x'] + 170, box['y'] + 125 + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "lasso_select": + start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 + mid_x, mid_y = box['x'] + 1, box['y'] + 1 + end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "tap": + hv_plot.click() + + locator = page.locator("#selection") + popup_box = locator.bounding_box() + + distance_to_left = abs(popup_box['x'] - box['x']) + distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) + distance_to_top = abs(popup_box['y'] - box['y']) + distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) + + if "right" in popup_position: + assert distance_to_right <= distance_to_left + elif "left" in popup_position: + assert distance_to_left <= distance_to_right + + if "top" in popup_position: + assert distance_to_top <= distance_to_bottom + elif "bottom" in popup_position: + assert distance_to_bottom <= distance_to_top - # Perform lasso selection - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() - # Wait for popup to show - wait_until(lambda: expect(page.locator("#lasso")).to_have_count(1), page) - locator = page.locator("#lasso") - expect(locator).to_have_count(1) - expect(locator).not_to_have_text("lasso\n0") +@skip_popup +@pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso), ("tap", Tap)]) +@pytest.mark.usefixtures("bokeh_backend") +def test_stream_popup_anchor_streams(serve_hv, points, tool, tool_type): + def popup_form(*args, **kwargs): + return "# selection" + + points = points.opts(tools=[tool], active_tools=[tool]) + tool_type(source=points, popup=popup_form, popup_position="bottom", popup_anchor="bottom_right") + + page = serve_hv(points) + hv_plot = page.locator('.bk-events') + expect(hv_plot).to_have_count(1) + hv_plot.click() - popup = locator.bounding_box() - assert popup['x'] < mid_x # Should be towards the left - assert popup['y'] > mid_y # Should be towards the top + box = hv_plot.bounding_box() + if tool == "box_select": + # try to get it centered as possible + start_x, start_y = box['x'] + 90, box['y'] + 90 + end_x, end_y = box['x'] + 170, box['y'] + 125 + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "lasso_select": + start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 + mid_x, mid_y = box['x'] + 1, box['y'] + 1 + end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 + page.mouse.move(start_x, start_y) + hv_plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "tap": + hv_plot.click() + + locator = page.locator("#selection") + popup_box = locator.bounding_box() + + distance_to_left = abs(popup_box['x'] - box['x']) + distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) + distance_to_top = abs(popup_box['y'] - box['y']) + distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) + + assert distance_to_left <= distance_to_right + assert distance_to_bottom >= distance_to_top @pytest.mark.usefixtures("bokeh_backend") From 8c0bacc828d1516238510a5749e356e20093e536 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 21 Oct 2024 14:57:51 -0700 Subject: [PATCH 10/18] simplify tests and fix polygons --- holoviews/tests/ui/bokeh/test_callback.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index a73897b84e..49bc82ffbb 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -192,7 +192,7 @@ def test_multi_axis_tap(serve_hv): def test(): assert s.xs == {'x': 11.560240963855422} - assert s.ys == {'y1': 18.642857142857146, 'y2': 78.71428571428572} + assert s.ys == {'y1': 18, 'y2': 78.71428571428572} wait_until(test, page) @@ -324,9 +324,9 @@ def popup_form(name): ]) def test_stream_popup_polygons_selection1d(serve_hv, popup_position): def popup_form(name): - return f"# {name}" + return "# selection" - points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"]) + points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"], padding=1) hv.streams.Selection1D(source=points, popup=popup_form("Tap"), popup_position=popup_position) page = serve_hv(points) @@ -570,6 +570,8 @@ def popup_form(index): page.mouse.move(end_x, end_y) page.mouse.up() elif tool == "tap": + mid_x, mid_y = box['x'] + 1, box['y'] + 1 + page.mouse.move(mid_x, mid_y) hv_plot.click() # Wait for popup to show @@ -630,7 +632,7 @@ def popup_form(index): @skip_popup -@pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso), ("tap", Tap)]) +@pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso)]) @pytest.mark.parametrize("popup_position", [ "top_right", "top_left", "bottom_left", "bottom_right", "right", "left", "top", "bottom" @@ -673,6 +675,7 @@ def popup_form(*args, **kwargs): locator = page.locator("#selection") popup_box = locator.bounding_box() + expect(locator).to_have_count(1) distance_to_left = abs(popup_box['x'] - box['x']) distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) @@ -730,6 +733,7 @@ def popup_form(*args, **kwargs): locator = page.locator("#selection") popup_box = locator.bounding_box() + expect(locator).to_have_count(1) distance_to_left = abs(popup_box['x'] - box['x']) distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) From 6ae2d02372ab9cfea054df3975ec73d1060e20b5 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 21 Oct 2024 15:16:19 -0700 Subject: [PATCH 11/18] move expect before --- holoviews/tests/ui/bokeh/test_callback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 49bc82ffbb..2702c3022a 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -674,8 +674,8 @@ def popup_form(*args, **kwargs): hv_plot.click() locator = page.locator("#selection") - popup_box = locator.bounding_box() expect(locator).to_have_count(1) + popup_box = locator.bounding_box() distance_to_left = abs(popup_box['x'] - box['x']) distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) @@ -732,8 +732,8 @@ def popup_form(*args, **kwargs): hv_plot.click() locator = page.locator("#selection") - popup_box = locator.bounding_box() expect(locator).to_have_count(1) + popup_box = locator.bounding_box() distance_to_left = abs(popup_box['x'] - box['x']) distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) From 2488bb48b144c435fda6aeb932b20e0d9d9f39ad Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 21 Oct 2024 15:17:29 -0700 Subject: [PATCH 12/18] fix other test --- holoviews/tests/ui/bokeh/test_callback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 2702c3022a..60e2de9b9d 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -192,7 +192,7 @@ def test_multi_axis_tap(serve_hv): def test(): assert s.xs == {'x': 11.560240963855422} - assert s.ys == {'y1': 18, 'y2': 78.71428571428572} + assert s.ys == {'y1': 18.642857142857146, 'y2': np.float64(78.71428571428572)} wait_until(test, page) From 632d7ff966386065d1e34049ce0b4284344def31 Mon Sep 17 00:00:00 2001 From: Andrew <15331990+ahuang11@users.noreply.github.com> Date: Tue, 22 Oct 2024 07:46:52 -0700 Subject: [PATCH 13/18] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Simon Høxbro Hansen --- holoviews/tests/ui/bokeh/test_callback.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 60e2de9b9d..4aec6bd259 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -192,7 +192,9 @@ def test_multi_axis_tap(serve_hv): def test(): assert s.xs == {'x': 11.560240963855422} - assert s.ys == {'y1': 18.642857142857146, 'y2': np.float64(78.71428571428572)} + assert len(s.ys) == 2 + assert np.isclose(s.ys["y1"], 18.642857142857146) + assert np.isclose(s.ys["y2"], 78.71428571428572) wait_until(test, page) @@ -293,7 +295,7 @@ def popup_form(name): expect(hv_plot).to_have_count(1) # Wait for popup to show - wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) + expect(page.locator("#selection")).to_have_count(1) locator = page.locator("#selection") expect(locator).to_have_count(1) From 5612bde89a02fc8a2dc7a86c40dd842490062280 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 22 Oct 2024 08:22:55 -0700 Subject: [PATCH 14/18] into class --- holoviews/plotting/bokeh/callbacks.py | 2 +- holoviews/tests/ui/bokeh/test_callback.py | 730 +++++++--------------- 2 files changed, 243 insertions(+), 489 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 5cd0a92dd8..f58489be50 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -1247,7 +1247,7 @@ def _watch_position(self): ys = source.get_column(renderer.glyph.ys.field); } - if (!xs || !ys) { + if (!xs || !ys || !indices.length) { return; } diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 4aec6bd259..ad631d84a7 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -258,494 +258,6 @@ def range_function(x_range, y_range): assert BOUND_COUNT[0] == 1 -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup(serve_hv): - def popup_form(name): - return f"# {name}" - - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) - hv.streams.Tap(source=points, popup=popup_form("Tap")) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - hv_plot.click() - expect(hv_plot).to_have_count(1) - - locator = page.locator(".markdown") - expect(locator).to_have_count(1) - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -@pytest.mark.parametrize("popup_position", [ - "top_right", "top_left", "bottom_left", "bottom_right", - "right", "left", "top", "bottom" -]) -def test_stream_popup_polygons_tap(serve_hv, popup_position): - def popup_form(name): - return "# selection" - - points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"]) - hv.streams.Tap(source=points, popup=popup_form("Tap"), popup_position=popup_position) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - hv_plot.click() - expect(hv_plot).to_have_count(1) - - # Wait for popup to show - expect(page.locator("#selection")).to_have_count(1) - locator = page.locator("#selection") - expect(locator).to_have_count(1) - - box = hv_plot.bounding_box() - popup_box = locator.bounding_box() - - distance_to_left = abs(popup_box['x'] - box['x']) - distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) - distance_to_top = abs(popup_box['y'] - box['y']) - distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) - - if "right" in popup_position: - assert distance_to_right <= distance_to_left - elif "left" in popup_position: - assert distance_to_left <= distance_to_right - - if "top" in popup_position: - assert distance_to_top <= distance_to_bottom - elif "bottom" in popup_position: - assert distance_to_bottom <= distance_to_top - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -@pytest.mark.parametrize("popup_position", [ - "top_right", "top_left", "bottom_left", "bottom_right", - "right", "left", "top", "bottom" -]) -def test_stream_popup_polygons_selection1d(serve_hv, popup_position): - def popup_form(name): - return "# selection" - - points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"], padding=1) - hv.streams.Selection1D(source=points, popup=popup_form("Tap"), popup_position=popup_position) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - hv_plot.click() - expect(hv_plot).to_have_count(1) - - # Wait for popup to show - wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) - locator = page.locator("#selection") - expect(locator).to_have_count(1) - - box = hv_plot.bounding_box() - popup_box = locator.bounding_box() - - distance_to_left = abs(popup_box['x'] - box['x']) - distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) - distance_to_top = abs(popup_box['y'] - box['y']) - distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) - - if "right" in popup_position: - assert distance_to_right <= distance_to_left - elif "left" in popup_position: - assert distance_to_left <= distance_to_right - - if "top" in popup_position: - assert distance_to_top <= distance_to_bottom - elif "bottom" in popup_position: - assert distance_to_bottom <= distance_to_top - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_none(serve_hv, points): - def popup_form(name): - return - - hv.streams.Tap(source=points, popup=popup_form("Tap")) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - - bbox = hv_plot.bounding_box() - hv_plot.click() - - page.mouse.move(bbox['x']+100, bbox['y']+100) - page.mouse.down() - page.mouse.move(bbox['x']+150, bbox['y']+150, steps=5) - page.mouse.up() - - locator = page.locator("#tap") - expect(locator).to_have_count(0) - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_callbacks(serve_hv): - def popup_form(x, y): - return pn.widgets.Button(name=f"{x},{y}") - - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) - hv.streams.Tap(source=points, popup=popup_form) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - hv_plot.click() - expect(hv_plot).to_have_count(1) - - locator = page.locator(".bk-btn") - expect(locator).to_have_count(2) - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_async_callbacks(serve_hv): - async def popup_form(x, y): - return pn.widgets.Button(name=f"{x},{y}") - - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) - hv.streams.Tap(source=points, popup=popup_form) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - hv_plot.click() - expect(hv_plot).to_have_count(1) - - locator = page.locator(".bk-btn") - expect(locator).to_have_count(2) - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_visible(serve_hv, points): - def popup_form(x, y): - def hide(_): - col.visible = False - button = pn.widgets.Button( - name=f"{x},{y}", - on_click=hide, - css_classes=["custom-button"] - ) - col = pn.Column(button) - return col - - points = points.opts(tools=["tap"]) - hv.streams.Tap(source=points, popup=popup_form) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - hv_plot.click() - expect(hv_plot).to_have_count(1) - - # initial appearance - locator = page.locator(".bk-btn") - expect(locator).to_have_count(2) - expect(locator.first).to_be_visible() - - # click custom button to hide - locator = page.locator(".custom-button") - locator.click() - locator = page.locator(".bk-btn") - expect(locator.first).not_to_be_visible() - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_close_button(serve_hv, points): - def popup_form(x, y): - return "Hello" - - points = points.opts(tools=["tap", "box_select"]) - hv.streams.Tap(source=points, popup=popup_form) - hv.streams.BoundsXY(source=points, popup=popup_form) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - hv_plot.click() - - locator = page.locator(".bk-btn.bk-btn-default") - expect(locator).to_have_count(1) - expect(locator).to_be_visible() - page.click(".bk-btn.bk-btn-default") - expect(locator).not_to_be_visible() - - hv_plot.click() - locator = page.locator(".bk-btn.bk-btn-default") - expect(locator).to_have_count(1) - expect(locator).to_be_visible() - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_selection1d_undefined(serve_hv, points): - hv.streams.Selection1D(source=points) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - hv_plot.click() # should not raise any error; properly guarded - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_noncallable_reappear(serve_hv, points): - def popup_form(name): - text_input = pn.widgets.TextInput(name='Description') - button = pn.widgets.Button( - name='Save', - on_click=lambda _: layout.param.update(visible=False), - button_type="primary" - ) - layout = pn.Column(f'# {name}', text_input, button) - return layout - - points = points.opts(hit_dilation=5) - hv.streams.Tap(source=points, popup=popup_form('Tap')) - points.opts(tools=["tap"], active_tools=["tap"]) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - - hv_plot.click() - - locator = page.locator("#tap") - expect(locator).to_have_count(1) - locator = page.locator(".bk-btn.bk-btn-primary") - expect(locator).to_have_count(1) - expect(locator).to_be_visible() - - page.click(".bk-btn.bk-btn-primary") - expect(locator).not_to_be_visible() - - hv_plot.click() - - locator = page.locator("#tap") - expect(locator).to_have_count(1) - locator = page.locator(".bk-btn.bk-btn-primary") - expect(locator).to_have_count(1) - expect(locator).to_be_visible() - - -@skip_popup -@pytest.mark.parametrize("tool", ["box_select", "lasso_select", "tap"]) -@pytest.mark.parametrize("popup_position", [ - "top_right", "top_left", "bottom_left", "bottom_right", - "right", "left", "top", "bottom" -]) -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_position_selection1d(serve_hv, points, tool, popup_position): - def popup_form(index): - if index: - return f"# selection\n{len(index)} {popup_position}" - - hv.streams.Selection1D(source=points, popup=popup_form, popup_position=popup_position) - points.opts(tools=[tool], active_tools=[tool]) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - - box = hv_plot.bounding_box() - if tool == "box_select": - # try to get it centered as possible - start_x, start_y = box['x'] + 90, box['y'] + 90 - end_x, end_y = box['x'] + 170, box['y'] + 125 - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(end_x, end_y) - page.mouse.up() - elif tool == "lasso_select": - start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 - mid_x, mid_y = box['x'] + 1, box['y'] + 1 - end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() - elif tool == "tap": - mid_x, mid_y = box['x'] + 1, box['y'] + 1 - page.mouse.move(mid_x, mid_y) - hv_plot.click() - - # Wait for popup to show - wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) - locator = page.locator("#selection") - expect(locator).to_have_count(1) - expect(locator).not_to_have_text("selection\n0") - - popup_box = locator.bounding_box() - - distance_to_left = abs(popup_box['x'] - box['x']) - distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) - distance_to_top = abs(popup_box['y'] - box['y']) - distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) - - if "right" in popup_position: - assert distance_to_right <= distance_to_left - elif "left" in popup_position: - assert distance_to_left <= distance_to_right - - if "top" in popup_position: - assert distance_to_top <= distance_to_bottom - elif "bottom" in popup_position: - assert distance_to_bottom <= distance_to_top - - -@skip_popup -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_anchor_selection1d(serve_hv, points): - def popup_form(index): - if index: - return f"# selection\n{len(index)}" - - hv.streams.Selection1D(source=points, popup=popup_form, popup_position="top", popup_anchor="top_right") - points.opts(tools=["tap"], active_tools=["tap"]) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - hv_plot.click() - - # Wait for popup to show - wait_until(lambda: expect(page.locator("#selection")).to_have_count(1), page) - locator = page.locator("#selection") - expect(locator).to_have_count(1) - expect(locator).not_to_have_text("selection\n0") - - box = hv_plot.bounding_box() - popup_box = locator.bounding_box() - - distance_to_left = abs(popup_box['x'] - box['x']) - distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) - distance_to_top = abs(popup_box['y'] - box['y']) - distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) - - assert distance_to_left <= distance_to_right - assert distance_to_bottom <= distance_to_top - - -@skip_popup -@pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso)]) -@pytest.mark.parametrize("popup_position", [ - "top_right", "top_left", "bottom_left", "bottom_right", - "right", "left", "top", "bottom" -]) -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_position_streams(serve_hv, points, tool, tool_type, popup_position): - def popup_form(*args, **kwargs): - return "# selection" - - points = points.opts(tools=[tool], active_tools=[tool]) - tool_type(source=points, popup=popup_form, popup_position=popup_position) - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - hv_plot.click() - - box = hv_plot.bounding_box() - if tool == "box_select": - # try to get it centered as possible - start_x, start_y = box['x'] + 90, box['y'] + 90 - end_x, end_y = box['x'] + 170, box['y'] + 125 - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(end_x, end_y) - page.mouse.up() - elif tool == "lasso_select": - start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 - mid_x, mid_y = box['x'] + 1, box['y'] + 1 - end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() - elif tool == "tap": - hv_plot.click() - - locator = page.locator("#selection") - expect(locator).to_have_count(1) - popup_box = locator.bounding_box() - - distance_to_left = abs(popup_box['x'] - box['x']) - distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) - distance_to_top = abs(popup_box['y'] - box['y']) - distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) - - if "right" in popup_position: - assert distance_to_right <= distance_to_left - elif "left" in popup_position: - assert distance_to_left <= distance_to_right - - if "top" in popup_position: - assert distance_to_top <= distance_to_bottom - elif "bottom" in popup_position: - assert distance_to_bottom <= distance_to_top - - -@skip_popup -@pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso), ("tap", Tap)]) -@pytest.mark.usefixtures("bokeh_backend") -def test_stream_popup_anchor_streams(serve_hv, points, tool, tool_type): - def popup_form(*args, **kwargs): - return "# selection" - - points = points.opts(tools=[tool], active_tools=[tool]) - tool_type(source=points, popup=popup_form, popup_position="bottom", popup_anchor="bottom_right") - - page = serve_hv(points) - hv_plot = page.locator('.bk-events') - expect(hv_plot).to_have_count(1) - hv_plot.click() - - box = hv_plot.bounding_box() - if tool == "box_select": - # try to get it centered as possible - start_x, start_y = box['x'] + 90, box['y'] + 90 - end_x, end_y = box['x'] + 170, box['y'] + 125 - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(end_x, end_y) - page.mouse.up() - elif tool == "lasso_select": - start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 - mid_x, mid_y = box['x'] + 1, box['y'] + 1 - end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 - page.mouse.move(start_x, start_y) - hv_plot.click() - page.mouse.down() - page.mouse.move(mid_x, mid_y) - page.mouse.move(end_x, end_y) - page.mouse.up() - elif tool == "tap": - hv_plot.click() - - locator = page.locator("#selection") - expect(locator).to_have_count(1) - popup_box = locator.bounding_box() - - distance_to_left = abs(popup_box['x'] - box['x']) - distance_to_right = abs((popup_box['x'] + popup_box['width']) - (box['x'] + box['width'])) - distance_to_top = abs(popup_box['y'] - box['y']) - distance_to_bottom = abs((popup_box['y'] + popup_box['height']) - (box['y'] + box['height'])) - - assert distance_to_left <= distance_to_right - assert distance_to_bottom >= distance_to_top - - @pytest.mark.usefixtures("bokeh_backend") def test_stream_subcoordinate_y_range(serve_hv, points): def cb(x_range, y_range): @@ -774,3 +286,245 @@ def cb(x_range, y_range): expected_xrange = (7.008849557522124, 63.95575221238938) expected_yrange = (0.030612244897959183, 1.0918367346938775) wait_until(lambda: stream.x_range == expected_xrange and stream.y_range == expected_yrange, page) + + +@pytest.mark.usefixtures("bokeh_backend") +@skip_popup +class TestPopup: + @pytest.fixture + def points(self): + rng = np.random.default_rng(10) + return hv.Points(rng.normal(size=(1000, 2))).opts(padding=0.2) + + def _select_points_based_on_tool(self, tool, page, plot): + """Helper method to perform point selection based on tool type.""" + box = plot.bounding_box() + + if tool == "box_select": + start_x, start_y = box['x'] + 90, box['y'] + 90 + end_x, end_y = box['x'] + 170, box['y'] + 125 + page.mouse.move(start_x, start_y) + plot.click() + page.mouse.down() + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "lasso_select": + start_x, start_y = box['x'] + 1, box['y'] + box['height'] - 1 + mid_x, mid_y = box['x'] + 1, box['y'] + 1 + end_x, end_y = box['x'] + box['width'] - 1, box['y'] + 1 + page.mouse.move(start_x, start_y) + plot.click() + page.mouse.down() + page.mouse.move(mid_x, mid_y) + page.mouse.move(end_x, end_y) + page.mouse.up() + elif tool == "tap": + mid_x, mid_y = box['x'] + box['width']/2, box['y'] + box['height']/2 + page.mouse.move(mid_x, mid_y) + plot.click() + + def _get_popup_distances_relative_to_bbox(self, popup_box, plot_box): + return { + 'left': abs(popup_box['x'] - plot_box['x']), + 'right': abs((popup_box['x'] + popup_box['width']) - (plot_box['x'] + plot_box['width'])), + 'top': abs(popup_box['y'] - plot_box['y']), + 'bottom': abs((popup_box['y'] + popup_box['height']) - (plot_box['y'] + plot_box['height'])) + } + + def _verify_popup_position(self, distances, popup_position): + if "right" in popup_position: + assert distances['right'] <= distances['left'] + elif "left" in popup_position: + assert distances['left'] <= distances['right'] + + if "top" in popup_position: + assert distances['top'] <= distances['bottom'] + elif "bottom" in popup_position: + assert distances['bottom'] <= distances['top'] + + def _serve_plot_and_click(self, serve_hv, plot): + page = serve_hv(plot) + hv_plot = page.locator('.bk-events') + hv_plot.click() + expect(hv_plot).to_have_count(1) + return page, hv_plot + + def _locate_popup(self, page, count=1): + locator = page.locator(".markdown") + expect(locator).to_have_count(count) + return locator + + def test_basic(self, serve_hv): + def popup_form(name): + return f"# {name}" + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form("Tap")) + + page, _ = self._serve_plot_and_click(serve_hv, points) + self._locate_popup(page) + + @pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" + ]) + def test_polygons_tap(self, serve_hv, popup_position): + def popup_form(name): + return "# selection" + + points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form("Tap"), popup_position=popup_position) + + page, hv_plot = self._serve_plot_and_click(serve_hv, points) + locator = self._locate_popup(page) + + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + distances = self._get_popup_distances_relative_to_bbox(popup_box, box) + self._verify_popup_position(distances, popup_position) + + def test_return_none(self, serve_hv, points): + def popup_form(name): + return None + + hv.streams.Tap(source=points, popup=popup_form("Tap")) + + page, _ = self._serve_plot_and_click(serve_hv, points) + self._locate_popup(page, count=0) + + def test_callbacks(self, serve_hv): + def popup_form(x, y): + return pn.widgets.Button(name=f"{x},{y}") + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form) + + page, _ = self._serve_plot_and_click(serve_hv, points) + locator = page.locator(".bk-btn") + expect(locator).to_have_count(2) + + async def test_async_callbacks(self, serve_hv): + async def popup_form(x, y): + return pn.widgets.Button(name=f"{x},{y}") + + points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form) + + page, _ = self._serve_plot_and_click(serve_hv, points) + locator = page.locator(".bk-btn") + expect(locator).to_have_count(2) + + def test_callback_visible(self, serve_hv, points): + def popup_form(x, y): + def hide(_): + col.visible = False + button = pn.widgets.Button( + name=f"{x},{y}", + on_click=hide, + css_classes=["custom-button"] + ) + col = pn.Column(button) + return col + + points = points.opts(tools=["tap"]) + hv.streams.Tap(source=points, popup=popup_form) + + page, _ = self._serve_plot_and_click(serve_hv, points) + + locator = page.locator(".bk-btn") + expect(locator).to_have_count(2) + expect(locator.first).to_be_visible() + + locator = page.locator(".custom-button") + locator.click() + locator = page.locator(".bk-btn") + expect(locator.first).not_to_be_visible() + + @pytest.mark.parametrize("tool", ["box_select", "lasso_select", "tap"]) + @pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" + ]) + def test_position_selection1d(self, serve_hv, points, tool, popup_position): + def popup_form(index): + if index: + return f"# selection\n{len(index)} {popup_position}" + + hv.streams.Selection1D(source=points, popup=popup_form, popup_position=popup_position) + points.opts(tools=[tool], active_tools=[tool]) + + page, hv_plot = self._serve_plot_and_click(serve_hv, points) + self._select_points_based_on_tool(tool, page, hv_plot) + + locator = self._locate_popup(page) + expect(locator).not_to_have_text("selection\n0") + + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + distances = self._get_popup_distances_relative_to_bbox(popup_box, box) + self._verify_popup_position(distances, popup_position) + + def test_anchor_selection1d(self, serve_hv, points): + def popup_form(index): + if index: + return f"# selection\n{len(index)}" + + hv.streams.Selection1D(source=points, popup=popup_form, popup_position="top", popup_anchor="top_right") + points.opts(tools=["tap"], active_tools=["tap"]) + + page, hv_plot = self._serve_plot_and_click(serve_hv, points) + locator = self._locate_popup(page) + expect(locator).not_to_have_text("selection\n0") + + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + distances = self._get_popup_distances_relative_to_bbox(popup_box, box) + + assert distances['right'] >= distances['left'] + + + @pytest.mark.parametrize("tool, tool_type", [ + ("box_select", BoundsXY), + ("lasso_select", Lasso), + ("tap", Tap) + ]) + def test_anchor_tools(self, serve_hv, points, tool, tool_type): + def popup_form(*args, **kwargs): + return "# selection" + + points = points.opts(tools=[tool], active_tools=[tool]) + tool_type(source=points, popup=popup_form, popup_position="bottom", popup_anchor="bottom_right") + page, hv_plot = self._serve_plot_and_click(serve_hv, points) + + self._select_points_based_on_tool(tool, page, hv_plot) + locator = self._locate_popup(page) + + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + distances = self._get_popup_distances_relative_to_bbox(popup_box, box) + + assert distances['right'] >= distances['left'] + assert distances['bottom'] >= distances['top'] + + + @pytest.mark.parametrize("tool, tool_type", [("box_select", BoundsXY), ("lasso_select", Lasso)]) + @pytest.mark.parametrize("popup_position", [ + "top_right", "top_left", "bottom_left", "bottom_right", + "right", "left", "top", "bottom" + ]) + def test_position_tools(self, serve_hv, points, tool, tool_type, popup_position): + def popup_form(*args, **kwargs): + return "# selection" + + points = points.opts(tools=[tool], active_tools=[tool]) + tool_type(source=points, popup=popup_form, popup_position=popup_position) + + page, hv_plot = self._serve_plot_and_click(serve_hv, points) + self._select_points_based_on_tool(tool, page, hv_plot) + locator = self._locate_popup(page) + + box = hv_plot.bounding_box() + popup_box = locator.bounding_box() + distances = self._get_popup_distances_relative_to_bbox(popup_box, box) + + self._verify_popup_position(distances, popup_position) From 9cee665ee2bc50f01ceda9d29d80d1e231a4c972 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 22 Oct 2024 08:38:25 -0700 Subject: [PATCH 15/18] fix tests --- holoviews/tests/ui/bokeh/test_callback.py | 52 +++++++++++++---------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index ad631d84a7..4240f5adb4 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -218,7 +218,9 @@ def test_multi_axis_tap_datetime(serve_hv): def test(): assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} - assert s.ys == {'y1': 18.13070539419087, 'y2': 76.551867219917} + assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} + assert np.isclose(s.ys["y1"], 18) + assert np.isclose(s.ys["y2"], 76) wait_until(test, page) @@ -294,7 +296,7 @@ class TestPopup: @pytest.fixture def points(self): rng = np.random.default_rng(10) - return hv.Points(rng.normal(size=(1000, 2))).opts(padding=0.2) + return hv.Points(rng.normal(size=(1000, 2))) def _select_points_based_on_tool(self, tool, page, plot): """Helper method to perform point selection based on tool type.""" @@ -319,8 +321,6 @@ def _select_points_based_on_tool(self, tool, page, plot): page.mouse.move(end_x, end_y) page.mouse.up() elif tool == "tap": - mid_x, mid_y = box['x'] + box['width']/2, box['y'] + box['height']/2 - page.mouse.move(mid_x, mid_y) plot.click() def _get_popup_distances_relative_to_bbox(self, popup_box, plot_box): @@ -342,10 +342,9 @@ def _verify_popup_position(self, distances, popup_position): elif "bottom" in popup_position: assert distances['bottom'] <= distances['top'] - def _serve_plot_and_click(self, serve_hv, plot): + def _serve_plot(self, serve_hv, plot): page = serve_hv(plot) hv_plot = page.locator('.bk-events') - hv_plot.click() expect(hv_plot).to_have_count(1) return page, hv_plot @@ -354,14 +353,15 @@ def _locate_popup(self, page, count=1): expect(locator).to_have_count(count) return locator - def test_basic(self, serve_hv): + def test_basic(self, serve_hv, points): def popup_form(name): return f"# {name}" - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + points.opts(tools=["tap"]) hv.streams.Tap(source=points, popup=popup_form("Tap")) - page, _ = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) self._locate_popup(page) @pytest.mark.parametrize("popup_position", [ @@ -375,7 +375,8 @@ def popup_form(name): points = hv.Polygons([(0, 0), (0, 1), (1, 1), (1, 0)]).opts(tools=["tap"]) hv.streams.Tap(source=points, popup=popup_form("Tap"), popup_position=popup_position) - page, hv_plot = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) locator = self._locate_popup(page) box = hv_plot.bounding_box() @@ -389,28 +390,31 @@ def popup_form(name): hv.streams.Tap(source=points, popup=popup_form("Tap")) - page, _ = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) self._locate_popup(page, count=0) - def test_callbacks(self, serve_hv): + def test_callbacks(self, serve_hv, points): def popup_form(x, y): return pn.widgets.Button(name=f"{x},{y}") - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + points.opts(tools=["tap"]) hv.streams.Tap(source=points, popup=popup_form) - page, _ = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) locator = page.locator(".bk-btn") expect(locator).to_have_count(2) - async def test_async_callbacks(self, serve_hv): + def test_async_callbacks(self, serve_hv, points): async def popup_form(x, y): return pn.widgets.Button(name=f"{x},{y}") - points = hv.Points(np.random.randn(10, 2)).opts(tools=["tap"]) + points.opts(tools=["tap"]) hv.streams.Tap(source=points, popup=popup_form) - page, _ = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) locator = page.locator(".bk-btn") expect(locator).to_have_count(2) @@ -426,10 +430,11 @@ def hide(_): col = pn.Column(button) return col - points = points.opts(tools=["tap"]) + points.opts(tools=["tap"]) hv.streams.Tap(source=points, popup=popup_form) - page, _ = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) locator = page.locator(".bk-btn") expect(locator).to_have_count(2) @@ -453,7 +458,7 @@ def popup_form(index): hv.streams.Selection1D(source=points, popup=popup_form, popup_position=popup_position) points.opts(tools=[tool], active_tools=[tool]) - page, hv_plot = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) self._select_points_based_on_tool(tool, page, hv_plot) locator = self._locate_popup(page) @@ -472,7 +477,8 @@ def popup_form(index): hv.streams.Selection1D(source=points, popup=popup_form, popup_position="top", popup_anchor="top_right") points.opts(tools=["tap"], active_tools=["tap"]) - page, hv_plot = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) + self._select_points_based_on_tool("tap", page, hv_plot) locator = self._locate_popup(page) expect(locator).not_to_have_text("selection\n0") @@ -494,7 +500,7 @@ def popup_form(*args, **kwargs): points = points.opts(tools=[tool], active_tools=[tool]) tool_type(source=points, popup=popup_form, popup_position="bottom", popup_anchor="bottom_right") - page, hv_plot = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) self._select_points_based_on_tool(tool, page, hv_plot) locator = self._locate_popup(page) @@ -519,7 +525,7 @@ def popup_form(*args, **kwargs): points = points.opts(tools=[tool], active_tools=[tool]) tool_type(source=points, popup=popup_form, popup_position=popup_position) - page, hv_plot = self._serve_plot_and_click(serve_hv, points) + page, hv_plot = self._serve_plot(serve_hv, points) self._select_points_based_on_tool(tool, page, hv_plot) locator = self._locate_popup(page) From 2b90cfc74b60979db76633f288eedfdc08d1b71c Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 22 Oct 2024 08:56:29 -0700 Subject: [PATCH 16/18] add a buffer --- holoviews/tests/ui/bokeh/test_callback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 4240f5adb4..5227ee86d6 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -219,8 +219,8 @@ def test_multi_axis_tap_datetime(serve_hv): def test(): assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} - assert np.isclose(s.ys["y1"], 18) - assert np.isclose(s.ys["y2"], 76) + assert 18 <= s.ys["y1"] <= 18.5 + assert 76 <= s.ys["y2"] <= 76.5 wait_until(test, page) From 9844a3b665ce1f1cd4fcbd5ba4f8cc49bacee478 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 22 Oct 2024 10:01:06 -0700 Subject: [PATCH 17/18] add atol --- holoviews/tests/ui/bokeh/test_callback.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index 5227ee86d6..d8ccf67126 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -219,8 +219,8 @@ def test_multi_axis_tap_datetime(serve_hv): def test(): assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} - assert 18 <= s.ys["y1"] <= 18.5 - assert 76 <= s.ys["y2"] <= 76.5 + assert np.isclose(s.ys["y1"], 18.2, atol=0.5) + assert np.isclose(s.ys["y2"], 76.5, atol=0.5) wait_until(test, page) From ab043493c3c683edaa5ea259f8a375ecc1037443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 23 Oct 2024 08:55:13 +0200 Subject: [PATCH 18/18] small changes --- holoviews/tests/ui/bokeh/test_callback.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py index d8ccf67126..befa045480 100644 --- a/holoviews/tests/ui/bokeh/test_callback.py +++ b/holoviews/tests/ui/bokeh/test_callback.py @@ -219,8 +219,9 @@ def test_multi_axis_tap_datetime(serve_hv): def test(): assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} assert s.xs == {'x': np.datetime64('2024-01-12T13:26:44.819277')} - assert np.isclose(s.ys["y1"], 18.2, atol=0.5) - assert np.isclose(s.ys["y2"], 76.5, atol=0.5) + assert len(s.ys) == 2 + assert np.isclose(s.ys["y1"], 18.130705394191) + assert np.isclose(s.ys["y2"], 76.551867219917) wait_until(test, page) @@ -293,11 +294,6 @@ def cb(x_range, y_range): @pytest.mark.usefixtures("bokeh_backend") @skip_popup class TestPopup: - @pytest.fixture - def points(self): - rng = np.random.default_rng(10) - return hv.Points(rng.normal(size=(1000, 2))) - def _select_points_based_on_tool(self, tool, page, plot): """Helper method to perform point selection based on tool type.""" box = plot.bounding_box()