Add ability to decode x,y location of PlotWidget clicks.

This commit is contained in:
David Bau
2022-07-21 12:56:06 -04:00
parent 4209d99941
commit 5423dc8245
5 changed files with 103 additions and 15 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
from .labwidget import Model, Widget, Trigger, Property, Event from .labwidget import Model, Widget, Trigger, Property, Event
from .labwidget import Button, Label, Textbox, Numberbox, Range, ColorPicker from .labwidget import Button, Label, Textbox, Numberbox, Range, ColorPicker
from .labwidget import Choice, Menu, Datalist, Div, ClickDiv, Image from .labwidget import Choice, Menu, Datalist, Div, ClickDiv, Img
from .labwidget import Textarea from .labwidget import Textarea
from .paintwidget import PaintWidget from .paintwidget import PaintWidget
from .plotwidget import PlotWidget from .plotwidget import PlotWidget
+24 -4
View File
@@ -481,6 +481,9 @@ class Event(object):
self.name = name self.name = name
self.target = target self.target = target
def __repr__(self):
return f'Event({self.value}, {self.name})'
entered_handler_stack = [] entered_handler_stack = []
@@ -986,7 +989,7 @@ class ClickDiv(Div):
''') ''')
class Image(Widget): class Img(Widget):
""" """
Just a IMG element. Use the src property to change its contents by url, Just a IMG element. Use the src property to change its contents by url,
or use the clear() and render(imgdata) methods to convert PIL or or use the clear() and render(imgdata) methods to convert PIL or
@@ -995,7 +998,11 @@ class Image(Widget):
def __init__(self, src='', style=None, **kwargs): def __init__(self, src='', style=None, **kwargs):
super().__init__(style=defaulted(style, margin=0), **kwargs) super().__init__(style=defaulted(style, margin=0), **kwargs)
self.src = Property(src) if (hasattr(src, 'save') or hasattr(src, 'savefig')):
self.src = Property('')
self.render(src)
else:
self.src = Property(src)
self.click = Trigger() self.click = Trigger()
def clear(self): def clear(self):
@@ -1018,10 +1025,21 @@ class Image(Widget):
buf.close() buf.close()
def widget_js(self): def widget_js(self):
# The click event has four properties that indicate the pixel
# location within the image that was clicked: imgx, imgy measure
# from the top-left. imgyb measures from the bottom, and imgxr
# measures from the right.
return minify(''' return minify('''
model.on('src', (ev) => { element.src = ev.value; }); model.on('src', (ev) => { element.src = ev.value; });
element.addEventListener('click', (ev) => { element.addEventListener('click', (ev) => {
model.trigger('click'); var b=element.getBoundingClientRect();
console.log(ev.pageX, ev.pageY)
model.trigger('click', {
x: (ev.pageX-b.left)*element.naturalWidth/element.clientWidth,
y: (ev.pageY-b.top)*element.naturalHeight/element.clientHeight,
width: element.naturalWidth,
height: element.naturalHeight
});
}); });
''') ''')
@@ -1107,9 +1125,11 @@ if WIDGET_ENV is None:
if WIDGET_ENV is None: if WIDGET_ENV is None:
try: try:
from ipykernel.comm import Comm as jupyter_comm from ipykernel.comm import Comm as jupyter_comm
COMM_MANAGER = get_ipython().kernel.comm_manager from IPython import get_ipython as ipython_get_ipython
COMM_MANAGER = ipython_get_ipython().kernel.comm_manager
WIDGET_ENV = 'jupyter' WIDGET_ENV = 'jupyter'
except Exception as e: except Exception as e:
print(e)
pass pass
def no_env_warning(): def no_env_warning():
+2 -2
View File
@@ -1,8 +1,8 @@
from .labwidget import Widget, Property, Image, minify from .labwidget import Widget, Property, Img, minify
from . import show from . import show
class PaintWidget(Image): class PaintWidget(Img):
def __init__(self, def __init__(self,
width=256, height=256, width=256, height=256,
src='', mask='', brushsize=10.0, oneshot=False, disabled=False, src='', mask='', brushsize=10.0, oneshot=False, disabled=False,
+46 -5
View File
@@ -1,7 +1,7 @@
from .labwidget import Image, Property from .labwidget import Img, Property
import inspect import inspect
class PlotWidget(Image): class PlotWidget(Img):
""" """
A widget to create interactive matplotlib plots by defining a simple function. A widget to create interactive matplotlib plots by defining a simple function.
Example of usage: Example of usage:
@@ -27,7 +27,7 @@ class PlotWidget(Image):
import matplotlib, matplotlib.pyplot import matplotlib, matplotlib.pyplot
super().__init__() super().__init__()
init_args = dict(kwargs) init_args = dict(kwargs)
render_args = dict(format='svg') self.render_args = dict()
if rc is None: if rc is None:
rc = {} rc = {}
@@ -47,7 +47,7 @@ class PlotWidget(Image):
for name in ['format', 'metadata', 'bbox_inches', 'pad_inches', for name in ['format', 'metadata', 'bbox_inches', 'pad_inches',
'facecolor', 'edgecolor', 'backend']: 'facecolor', 'edgecolor', 'backend']:
if name in init_args: if name in init_args:
render_args[name] = init_args.pop(name) self.render_args[name] = init_args.pop(name)
for default_arg, default_value in [('figsize', (5, 3.5))]: for default_arg, default_value in [('figsize', (5, 3.5))]:
if default_arg not in init_args: if default_arg not in init_args:
@@ -68,6 +68,47 @@ class PlotWidget(Image):
for name in all_names: for name in all_names:
args.append(getattr(self, name)) args.append(getattr(self, name))
redraw_rule(*args) redraw_rule(*args)
self.render(self.fig, **render_args) self.render(self.fig, **self.render_args)
self.on(' '.join(all_names), invoke_redraw) self.on(' '.join(all_names), invoke_redraw)
invoke_redraw() invoke_redraw()
def event_location(self, event):
'''
Transform a click event from pixel coordinates to plot data coordinates.
'''
# Image natural size and image-relative pixel location.
w = event.value['width']
h = event.value['height']
px = event.value['x']
py = event.value['y']
# To convert from image to display coords, we need the bbox.
# https://stackoverflow.com/questions/28692981
if self.render_args.get('bbox_inches', None) == 'tight':
bbox = self.fig.get_tightbbox(self.fig.canvas.get_renderer()).padded(0)
bbox.set_points(bbox.get_points() * self.fig.dpi)
else:
bbox = self.fig.bbox
# Matplotlib display-coordinate location
dx = (bbox.x1 - bbox.x0) * px / w + bbox.x0
dy = (bbox.y1 - bbox.y0) * (h - py) / h + bbox.y0
# Identify the first axis that contains the point
for inside in self.fig.axes:
if inside.get_window_extent().contains(dx, dy):
break
else:
inside = None
ax = self.fig.axes[0] if (len(self.fig.axes) and inside is None) else inside
# Axis-data-relative coordinate location.
# https://stackoverflow.com/questions/59794014
if ax is not None:
x, y = ax.transData.inverted().transform((dx, dy))
else:
x, y = None, None
class PlotLocation():
def __init__(self, x, y, axis, inside):
self.x = x
self.y = y
self.axis = axis
self.inside = inside
return PlotLocation(x, y, ax, inside is not None)
+30 -3
View File
@@ -476,20 +476,47 @@
"cell_type": "code", "cell_type": "code",
"execution_count": null, "execution_count": null,
"id": "8ed28ab5", "id": "8ed28ab5",
"metadata": {}, "metadata": {
"scrolled": true
},
"outputs": [], "outputs": [],
"source": [ "source": [
"\n",
"freq_ra = Range(min=0.1, max=5.0, step=0.01, value=1.0)\n", "freq_ra = Range(min=0.1, max=5.0, step=0.01, value=1.0)\n",
"freq_nb = Numberbox(freq_ra.prop('value'))\n", "freq_nb = Numberbox(freq_ra.prop('value'))\n",
"amp_ra = Range(min=0.1, max=5.0, step=0.01, value=1.0)\n", "amp_ra = Range(min=0.1, max=5.0, step=0.01, value=1.0)\n",
"amp_nb = Numberbox(amp_ra.prop('value'))\n", "amp_nb = Numberbox(amp_ra.prop('value'))\n",
"pw = PlotWidget(myplot, frequency=freq_ra.prop('value'), amplitude=amp_ra.prop('value'))\n", "pw = PlotWidget(myplot, frequency=freq_ra.prop('value'), amplitude=amp_ra.prop('value'), format='svg')\n",
"show(show.TIGHT, [[pw],\n", "show(show.TIGHT, [[pw],\n",
" ['frequency', show.style(flex=10), freq_ra, freq_nb],\n", " ['frequency', show.style(flex=10), freq_ra, freq_nb],\n",
" ['amplitude', show.style(flex=10), amp_ra, amp_nb]])" " ['amplitude', show.style(flex=10), amp_ra, amp_nb]])"
] ]
}, },
{
"cell_type": "markdown",
"id": "d58975ba",
"metadata": {},
"source": [
"## Handling PlotWidget clicks\n",
"\n",
"When an image or PlotWidget is clicked, you can get the coordinates by listening to the click event."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ba3cbe25",
"metadata": {},
"outputs": [],
"source": [
"from baukit import Textbox\n",
"coord_b = Textbox()\n",
"def handle_click(e):\n",
" loc = pw.event_location(e)\n",
" coord_b.value = f'x,y=({loc.x:.2f}, {loc.y:.2f}) inside={loc.inside}'\n",
"pw.on('click', handle_click)\n",
"show(show.TIGHT, [['Click the previous plot to see coordinates here:', coord_b]])"
]
},
{ {
"cell_type": "markdown", "cell_type": "markdown",
"id": "e60ab5fa", "id": "e60ab5fa",