From 322734834526945c9ec94856e7fafe2d6561707a Mon Sep 17 00:00:00 2001 From: "Maarten A. Breddels" Date: Fri, 5 Feb 2021 14:40:46 +0100 Subject: [PATCH] feat: mouse move events exposed --- bqplot_image_gl/interacts.py | 4 +- examples/mouse.ipynb | 171 +++++++++++++++++++++++++++++++++++ js/lib/MouseInteraction.js | 22 ++++- 3 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 examples/mouse.ipynb diff --git a/bqplot_image_gl/interacts.py b/bqplot_image_gl/interacts.py index 4647420..6e8f818 100644 --- a/bqplot_image_gl/interacts.py +++ b/bqplot_image_gl/interacts.py @@ -1,6 +1,6 @@ from bqplot.interacts import BrushSelector, Interaction from bqplot.scales import Scale -from traitlets import Float, Unicode, Dict, Instance +from traitlets import Float, Unicode, Dict, Instance, Int from ipywidgets.widgets.widget import widget_serialization from bqplot_image_gl._version import __version__ @@ -79,6 +79,7 @@ class MouseInteraction(Interaction): y_scale: An instance of Scale This is the scale which is used for inversion from the pixels to data co-ordinates in the y-direction. + move_throttle: Send mouse move events only every specified milliseconds. """ _view_module = Unicode('bqplot-image-gl').tag(sync=True) _model_module = Unicode('bqplot-image-gl').tag(sync=True) @@ -91,3 +92,4 @@ class MouseInteraction(Interaction): y_scale = Instance(Scale, allow_none=True, default_value=None)\ .tag(sync=True, dimension='y', **widget_serialization) cursor = Unicode('auto').tag(sync=True) + move_throttle = Int(50).tag(sync=True) diff --git a/examples/mouse.ipynb b/examples/mouse.ipynb new file mode 100644 index 0000000..22e36a7 --- /dev/null +++ b/examples/mouse.ipynb @@ -0,0 +1,171 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "discrete-retention", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import math\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ethical-posting", + "metadata": {}, + "outputs": [], + "source": [ + "with open('./data.json') as f:\n", + " data = json.load(f)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "emerging-marking", + "metadata": {}, + "outputs": [], + "source": [ + "values = np.array(data['values'], dtype='float32')\n", + "values = values.reshape((data['height'], data['width']))[:10,]\n", + "values.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "twelve-cabinet", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from bqplot import Figure, LinearScale, Axis, ColorScale\n", + "from bqplot_image_gl import ImageGL, Contour\n", + "import ipywidgets as widgets\n", + "scale_x = LinearScale(min=-1, max=4, allow_padding=False)\n", + "scale_y = LinearScale(min=-1, max=4, allow_padding=False)\n", + "scales = {'x': scale_x, 'y': scale_y}\n", + "axis_x = Axis(scale=scale_x, label='x')\n", + "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", + "scales_image = {'x': scale_x, 'y': scale_y, 'image': ColorScale(min=np.min(values).item(), max=np.max(values).item())}\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "appreciated-transformation", + "metadata": {}, + "outputs": [], + "source": [ + "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", + "image = ImageGL(image=values, scales=scales_image, x=[0, 2], y=[0, 2])\n", + "figure.marks = (image, )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "buried-cooperation", + "metadata": {}, + "outputs": [], + "source": [ + "figure" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "sufficient-shirt", + "metadata": {}, + "outputs": [], + "source": [ + "from bqplot_image_gl.interacts import MouseInteraction" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "empirical-model", + "metadata": {}, + "outputs": [], + "source": [ + "widget_label = widgets.Label(value=\"move cursor for information\")\n", + "widget_label" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "known-possession", + "metadata": {}, + "outputs": [], + "source": [ + "interaction = MouseInteraction(x_scale=scales_image['x'], y_scale=scales_image['y'], move_throttle=70)\n", + "figure.interaction = interaction\n", + "def on_mouse_msg(interaction, data, buffers):\n", + " # it might be a good idea to throttle on the Python side as well, for instance when many computations\n", + " # happen, we can effectively ignore the queue of messages\n", + " if data['event'] == 'mousemove':\n", + " domain_x = data['domain']['x']\n", + " domain_y = data['domain']['y']\n", + " normalized_x = (domain_x - image.x[0]) / (image.x[1] - image.x[0])\n", + " normalized_y = (domain_y - image.y[0]) / (image.y[1] - image.y[0])\n", + " # TODO: think about +/-1 and pixel edges\n", + " pixel_x = int(math.floor(normalized_x * image.image.shape[1]))\n", + " pixel_y = int(math.floor(normalized_y * image.image.shape[0]))\n", + " if pixel_x >= 0 and pixel_x < image.image.shape[1] and pixel_y >= 0 and pixel_y < image.image.shape[0]:\n", + " value = str(image.image[pixel_y, pixel_x])\n", + " else:\n", + " value = \"out of range\"\n", + " msg = f\"x={pixel_x} y={pixel_y} value={value} (nx={normalized_x} ny={normalized_y})\"\n", + " widget_label.value = msg\n", + " elif data['event'] == 'mouseleave':\n", + " widget_label.value = \"Bye!\"\n", + " elif data['event'] == 'mouseenter':\n", + " widget_label.value = \"Almost there...\" # this is is not visible because mousemove overwrites the msg\n", + " \n", + "interaction.on_msg(on_mouse_msg)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "federal-domestic", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.6" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "state": {}, + "version_major": 2, + "version_minor": 0 + } + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/js/lib/MouseInteraction.js b/js/lib/MouseInteraction.js index 865c20e..5b86da6 100644 --- a/js/lib/MouseInteraction.js +++ b/js/lib/MouseInteraction.js @@ -7,6 +7,7 @@ const Interaction_1 = require("bqplot"); const base_1 = require("@jupyter-widgets/base"); const d3 = require("d3"); const d3_drag_1 = require("d3-drag"); +const _ = require("lodash"); const d3_selection_1 = require("d3-selection"); const d3GetEvent = function () { return require("d3-selection").event; }.bind(this); @@ -21,6 +22,8 @@ class MouseInteractionModel extends base_1.WidgetModel { _view_module_version: version, scale_x: null, scale_y: null, + scale_y: null, + move_throttle: 50, cursor: 'auto' }); } } @@ -41,6 +44,12 @@ class MouseInteraction extends Interaction_1.Interaction { }; this.listenTo(this.model, "change:cursor", updateCursor); updateCursor(); + const updateThrottle = () => { + this._emitThrottled = _.throttle(this._emit, this.model.get('move_throttle')); + } + updateThrottle(); + this.listenTo(this.model, 'change:move_throttle', updateThrottle); + eventElement.call(d3_drag_1.drag().on("start", () => { const e = d3GetEvent(); this._emit('dragstart', { x: e.x, y: e.y }); @@ -52,14 +61,24 @@ class MouseInteraction extends Interaction_1.Interaction { this._emit('dragend', { x: e.x, y: e.y }); })); // and click events - ['click', 'dblclick'].forEach(eventName => { + ['click', 'dblclick', 'mouseenter', 'mouseleave'].forEach(eventName => { eventElement.on(eventName, () => { + this._emitThrottled.flush(); // we don't want mousemove events to come after enter/leave const e = d3GetEvent(); // to be consistent with drag events, we need to user clientPoint const [x, y] = d3_selection_1.clientPoint(eventElement.node(), e); this._emit(eventName, { x, y }); }); }); + // throttled events + ['mousemove'].forEach(eventName => { + eventElement.on(eventName, () => { + const e = d3GetEvent(); + // to be consistent with drag events, we need to user clientPoint + const [x, y] = d3_selection_1.clientPoint(eventElement.node(), e); + this._emitThrottled(eventName, { x, y }); + }); + }); } updateScaleRanges() { this.x_scale.set_range(this.parent.padded_range("x", this.x_scale.model)); @@ -75,4 +94,3 @@ class MouseInteraction extends Interaction_1.Interaction { } } exports.MouseInteraction = MouseInteraction; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTW91c2VJbnRlcmFjdGlvbi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9Nb3VzZUludGVyYWN0aW9uLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBQUEsK0NBQTRDO0FBQzVDLGdEQUFtRTtBQUNuRSxxQ0FBK0I7QUFDL0IsK0NBQTJDO0FBQzNDLE1BQU0sVUFBVSxHQUFHLGNBQVcsT0FBTyxPQUFPLENBQUMsY0FBYyxDQUFDLENBQUMsS0FBSyxDQUFBLENBQUEsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsQ0FBQztBQUMvRSx1Q0FBeUM7QUFFekMsTUFBYSxxQkFBc0IsU0FBUSxrQkFBVztJQUNsRCxRQUFRO1FBQ0oseUJBQVcsa0JBQVcsQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLElBQ3ZDLFdBQVcsRUFBRSx1QkFBdUIsRUFDcEMsVUFBVSxFQUFFLGtCQUFrQixFQUM5QixhQUFhLEVBQUUsUUFBUSxFQUN2QixZQUFZLEVBQUUsUUFBUSxFQUN0QixxQkFBcUIsRUFBRSxzQkFBWSxFQUNuQyxvQkFBb0IsRUFBRSxzQkFBWSxFQUNsQyxPQUFPLEVBQUUsSUFBSSxFQUNiLE9BQU8sRUFBRSxJQUFJLEVBQ2IsTUFBTSxFQUFFLE1BQU0sSUFDaEI7SUFDTixDQUFDOztBQUVNLGlDQUFXLHFCQUNYLGtCQUFXLENBQUMsV0FBVyxJQUMxQixPQUFPLEVBQUUsRUFBRSxXQUFXLEVBQUUsb0JBQWEsRUFBRSxFQUN2QyxPQUFPLEVBQUUsRUFBRSxXQUFXLEVBQUUsb0JBQWEsRUFBRSxJQUMxQztBQW5CTCxzREFvQkM7QUFFRCxNQUFhLGdCQUFpQixTQUFRLHlCQUFXO0lBRzdDLEtBQUssQ0FBQyxNQUFNO1FBQ1IsMEJBQTBCO1FBQzFCLE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUM7UUFDL0IsS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ2YsSUFBSSxDQUFDLE9BQU8sR0FBRyxNQUFNLElBQUksQ0FBQyxp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