Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add popup position and anchor #6414

Merged
merged 21 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions examples/user_guide/13-Custom_Interactivity.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
Expand Down
186 changes: 120 additions & 66 deletions holoviews/plotting/bokeh/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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:
"""
Expand Down Expand Up @@ -611,9 +622,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"},
Expand All @@ -627,24 +639,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);
}}
}}""",
))

Expand Down Expand Up @@ -1173,61 +1217,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
} else {
while (ix.length && (typeof ix[0] !== 'number')) {
ix = ix[0]
iy = iy[0]
}
tx = Math.max(...ix)
ty = Math.max(...iy)
export default ({panel, renderer, source, selected, popup_position}, cb_obj, _) => {
const el = panel.elements[1];
if ((el && !el.visible) || !cb_obj.final) {
return;
}
if (!x || (tx > x)) {
x = tx
let x, y, xs, ys;
let indices = selected.indices;
if (cb_obj.geometry.type == 'point') {
indices = indices.slice(-1);
}
if (!y || (ty > y)) {
y = ty
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 (x && y) {
panel.position.setv({x, y})
}
}""",
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 {
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});
}
}
""",
))


def _get_position(self, event):
el = self.plot.current_frame
if isinstance(el, Dataset):
Expand Down
21 changes: 20 additions & 1 deletion holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
Loading
Loading