75
75
from ...util .warnings import warn
76
76
from .util import BOKEH_GE_3_3_0 , convert_timestamp
77
77
78
+ POPUP_POSITION_ANCHOR = {
79
+ "top_right" : "bottom_left" ,
80
+ "top_left" : "bottom_right" ,
81
+ "bottom_left" : "top_right" ,
82
+ "bottom_right" : "top_left" ,
83
+ "right" : "top_left" ,
84
+ "left" : "top_right" ,
85
+ "top" : "bottom" ,
86
+ "bottom" : "top" ,
87
+ }
88
+
78
89
79
90
class Callback :
80
91
"""
@@ -611,9 +622,10 @@ def initialize(self, plot_id=None):
611
622
}
612
623
""" ],
613
624
css_classes = ["popup-close-btn" ])
625
+ self ._popup_position = stream .popup_position
614
626
self ._panel = Panel (
615
627
position = XY (x = np .nan , y = np .nan ),
616
- anchor = "top_left" ,
628
+ anchor = stream . popup_anchor or POPUP_POSITION_ANCHOR [ self . _popup_position ] ,
617
629
elements = [close_button ],
618
630
visible = False ,
619
631
styles = {"zIndex" : "1000" },
@@ -627,24 +639,56 @@ def _watch_position(self):
627
639
geom_type = self .geom_type
628
640
self .plot .state .on_event ('selectiongeometry' , self ._update_selection_event )
629
641
self .plot .state .js_on_event ('selectiongeometry' , CustomJS (
630
- args = dict (panel = self ._panel ),
642
+ args = dict (panel = self ._panel , popup_position = self . _popup_position ),
631
643
code = f"""
632
- export default ({{panel}}, cb_obj, _) => {{
633
- const el = panel.elements[1]
634
- if ((el && !el.visible) || !cb_obj.final || ({ geom_type !r} !== 'any' && cb_obj.geometry.type !== { geom_type !r} )) {{
635
- return
636
- }}
637
- let pos;
638
- if (cb_obj.geometry.type === 'point') {{
639
- pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}}
640
- }} else if (cb_obj.geometry.type === 'rect') {{
641
- pos = {{x: cb_obj.geometry.x1, y: cb_obj.geometry.y1}}
642
- }} else if (cb_obj.geometry.type === 'poly') {{
643
- pos = {{x: Math.max(...cb_obj.geometry.x), y: Math.max(...cb_obj.geometry.y)}}
644
- }}
645
- if (pos) {{
646
- panel.position.setv(pos)
647
- }}
644
+ export default ({{panel, popup_position}}, cb_obj, _) => {{
645
+ const el = panel.elements[1];
646
+ if ((el && !el.visible) || !cb_obj.final || ({ geom_type !r} !== 'any' && cb_obj.geometry.type !== { geom_type !r} )) {{
647
+ return;
648
+ }}
649
+
650
+ let pos;
651
+ if (cb_obj.geometry.type === 'point') {{
652
+ pos = {{x: cb_obj.geometry.x, y: cb_obj.geometry.y}};
653
+ }} else if (cb_obj.geometry.type === 'rect') {{
654
+ let x, y;
655
+ if (popup_position.includes('left')) {{
656
+ x = cb_obj.geometry.x0;
657
+ }} else if (popup_position.includes('right')) {{
658
+ x = cb_obj.geometry.x1;
659
+ }} else {{
660
+ x = (cb_obj.geometry.x0 + cb_obj.geometry.x1) / 2;
661
+ }}
662
+ if (popup_position.includes('top')) {{
663
+ y = cb_obj.geometry.y1;
664
+ }} else if (popup_position.includes('bottom')) {{
665
+ y = cb_obj.geometry.y0;
666
+ }} else {{
667
+ y = (cb_obj.geometry.y0 + cb_obj.geometry.y1) / 2;
668
+ }}
669
+ pos = {{x: x, y: y}};
670
+ }} else if (cb_obj.geometry.type === 'poly') {{
671
+ let x, y;
672
+ if (popup_position.includes('left')) {{
673
+ x = Math.min(...cb_obj.geometry.x);
674
+ }} else if (popup_position.includes('right')) {{
675
+ x = Math.max(...cb_obj.geometry.x);
676
+ }} else {{
677
+ x = (Math.min(...cb_obj.geometry.x) + Math.max(...cb_obj.geometry.x)) / 2;
678
+ }}
679
+ if (popup_position.includes('top')) {{
680
+ y = Math.max(...cb_obj.geometry.y);
681
+ }} else if (popup_position.includes('bottom')) {{
682
+ y = Math.min(...cb_obj.geometry.y);
683
+ }} else {{
684
+ y = (Math.min(...cb_obj.geometry.y) + Math.max(...cb_obj.geometry.y)) / 2;
685
+ }}
686
+ pos = {{x: x, y: y}};
687
+ }}
688
+
689
+ if (pos) {{
690
+ panel.position.setv(pos);
691
+ }}
648
692
}}""" ,
649
693
))
650
694
@@ -734,7 +778,7 @@ async def _process_selection_event(self):
734
778
position = self ._get_position (event ) if event else None
735
779
if position :
736
780
self ._panel .position = XY (** position )
737
- if self .plot .comm : # update Jupyter Notebook
781
+ if self .plot .comm : # update Jupyter Notebooks
738
782
push_on_root (self .plot .root .ref ['id' ])
739
783
return
740
784
@@ -1173,59 +1217,106 @@ def _watch_position(self):
1173
1217
source = self .plot .handles ['source' ]
1174
1218
renderer = self .plot .handles ['glyph_renderer' ]
1175
1219
selected = self .plot .handles ['selected' ]
1220
+
1176
1221
self .plot .state .js_on_event ('selectiongeometry' , CustomJS (
1177
- args = dict (panel = self ._panel , renderer = renderer , source = source , selected = selected ),
1222
+ args = dict (panel = self ._panel , renderer = renderer , source = source , selected = selected , popup_position = self . _popup_position ),
1178
1223
code = """
1179
- export default ({panel, renderer, source, selected}, cb_obj, _) => {
1180
- const el = panel.elements[1]
1181
- if ((el && !el.visible) || !cb_obj.final) {
1182
- return
1183
- }
1184
- let x, y, xs, ys;
1185
- let indices = selected.indices;
1186
- if (cb_obj.geometry.type == 'point') {
1187
- indices = indices.slice(-1)
1188
- }
1189
- if (renderer.glyph.x && renderer.glyph.y) {
1190
- xs = source.get_column(renderer.glyph.x.field)
1191
- ys = source.get_column(renderer.glyph.y.field)
1192
- } else if (renderer.glyph.right && renderer.glyph.top) {
1193
- xs = source.get_column(renderer.glyph.right.field)
1194
- ys = source.get_column(renderer.glyph.top.field)
1195
- } else if (renderer.glyph.x1 && renderer.glyph.y1) {
1196
- xs = source.get_column(renderer.glyph.x1.field)
1197
- ys = source.get_column(renderer.glyph.y1.field)
1198
- } else if (renderer.glyph.xs && renderer.glyph.ys) {
1199
- xs = source.get_column(renderer.glyph.xs.field)
1200
- ys = source.get_column(renderer.glyph.ys.field)
1201
- }
1202
- if (!xs || !ys) { return }
1203
- for (const i of indices) {
1204
- let ix = xs[i]
1205
- let iy = ys[i]
1206
- let tx, ty
1207
- if (typeof ix === 'number') {
1208
- tx = ix
1209
- ty = iy
1210
- } else {
1211
- while (ix.length && (typeof ix[0] !== 'number')) {
1212
- ix = ix[0]
1213
- iy = iy[0]
1214
- }
1215
- tx = Math.max(...ix)
1216
- ty = Math.max(...iy)
1224
+ export default ({panel, renderer, source, selected, popup_position}, cb_obj, _) => {
1225
+ panel.visible = false; // Hide the popup panel so it doesn't show in previous location
1226
+ const el = panel.elements[1];
1227
+ if ((el && !el.visible) || !cb_obj.final) {
1228
+ return;
1217
1229
}
1218
- if (!x || (tx > x)) {
1219
- x = tx
1230
+ let x, y, xs, ys;
1231
+ let indices = selected.indices;
1232
+ if (cb_obj.geometry.type == 'point') {
1233
+ indices = indices.slice(-1);
1220
1234
}
1221
- if (!y || (ty > y)) {
1222
- y = ty
1235
+
1236
+ if (renderer.glyph.x && renderer.glyph.y) {
1237
+ xs = source.get_column(renderer.glyph.x.field);
1238
+ ys = source.get_column(renderer.glyph.y.field);
1239
+ } else if (renderer.glyph.right && renderer.glyph.top) {
1240
+ xs = source.get_column(renderer.glyph.right.field);
1241
+ ys = source.get_column(renderer.glyph.top.field);
1242
+ } else if (renderer.glyph.x1 && renderer.glyph.y1) {
1243
+ xs = source.get_column(renderer.glyph.x1.field);
1244
+ ys = source.get_column(renderer.glyph.y1.field);
1245
+ } else if (renderer.glyph.xs && renderer.glyph.ys) {
1246
+ xs = source.get_column(renderer.glyph.xs.field);
1247
+ ys = source.get_column(renderer.glyph.ys.field);
1223
1248
}
1224
- }
1225
- if (x && y) {
1226
- panel.position.setv({x, y})
1227
- }
1228
- }""" ,
1249
+
1250
+ if (!xs || !ys || !indices.length) {
1251
+ return;
1252
+ }
1253
+
1254
+ let minX, maxX, minY, maxY;
1255
+
1256
+ // Loop over each index in the selection and find the corresponding polygon coordinates
1257
+ for (const i of indices) {
1258
+ let ix = xs[i];
1259
+ let iy = ys[i];
1260
+ let tx, ty;
1261
+
1262
+ // Check if the values are numbers or nested arrays
1263
+ if (typeof ix === 'number') {
1264
+ tx = ix;
1265
+ ty = iy;
1266
+ } else {
1267
+ // Drill down into nested arrays until we find the number values
1268
+ while (ix.length && typeof ix[0] !== 'number') {
1269
+ ix = ix[0];
1270
+ iy = iy[0];
1271
+ }
1272
+
1273
+ // Set tx and ty based on the popup position preferences
1274
+ if (popup_position.includes('left')) {
1275
+ tx = Math.min(...ix);
1276
+ } else if (popup_position.includes('right')) {
1277
+ tx = Math.max(...ix);
1278
+ } else {
1279
+ tx = (Math.min(...ix) + Math.max(...ix)) / 2;
1280
+ }
1281
+
1282
+ if (popup_position.includes('top')) {
1283
+ ty = Math.max(...iy);
1284
+ } else if (popup_position.includes('bottom')) {
1285
+ ty = Math.min(...iy);
1286
+ } else {
1287
+ ty = (Math.min(...iy) + Math.max(...iy)) / 2;
1288
+ }
1289
+ }
1290
+
1291
+ // Update the min/max values for x and y
1292
+ if (minX === undefined || tx < minX) { minX = tx; }
1293
+ if (maxX === undefined || tx > maxX) { maxX = tx; }
1294
+ if (minY === undefined || ty < minY) { minY = ty; }
1295
+ if (maxY === undefined || ty > maxY) { maxY = ty; }
1296
+ }
1297
+
1298
+ // Set x and y based on popup_position preference
1299
+ if (popup_position.includes('left')) {
1300
+ x = minX;
1301
+ } else if (popup_position.includes('right')) {
1302
+ x = maxX;
1303
+ } else {
1304
+ x = (minX + maxX) / 2;
1305
+ }
1306
+
1307
+ if (popup_position.includes('top')) {
1308
+ y = maxY;
1309
+ } else if (popup_position.includes('bottom')) {
1310
+ y = minY;
1311
+ } else {
1312
+ y = (minY + maxY) / 2;
1313
+ }
1314
+
1315
+ // Set the popup position and make it visible
1316
+ panel.position.setv({x, y});
1317
+ panel.visible = true;
1318
+ }
1319
+ """ ,
1229
1320
))
1230
1321
1231
1322
def _get_position (self , event ):
0 commit comments