|
|
- # Copyright 2015 Bloomberg Finance L.P.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
-
- r"""
-
- =========
- Interacts
- =========
-
- .. currentmodule:: bqplot.interacts
-
- .. autosummary::
- :toctree: _generate/
-
- BrushIntervalSelector
- BrushSelector
- HandDraw
- IndexSelector
- FastIntervalSelector
- MultiSelector
- OneDSelector
- Interaction
- PanZoom
- Selector
- TwoDSelector
- """
-
- from traitlets import (Bool, Int, Float, Unicode, Dict,
- Instance, List, Enum, observe)
- from traittypes import Array
- from ipywidgets import Widget, Color, widget_serialization, register
-
- from .scales import Scale, DateScale
- from .traits import Date, array_serialization, _array_equal
- from .marks import Lines
- from ._version import __frontend_version__
- import numpy as np
-
-
- def register_interaction(key=None):
- """Decorator registering an interaction class in the registry.
-
- If no key is provided, the class name is used as a key. A key is provided
- for each core bqplot interaction type so that the frontend can use this
- key regardless of the kernal language.
- """
- def wrap(interaction):
- name = key if key is not None else interaction.__module__ + \
- interaction.__name__
- interaction.types[name] = interaction
- return interaction
- return wrap
-
-
- class Interaction(Widget):
-
- """The base interaction class.
-
- An interaction is a mouse interaction layer for a figure that requires the
- capture of all mouse events on the plot area. A consequence is that one can
- allow only one interaction at any time on a figure.
-
- An interaction can be associated with features such as selection or
- manual change of specific mark. Although, they differ from the so called
- 'mark interactions' in that they do not rely on knowing whether a specific
- element of the mark are hovered by the mouse.
-
- Attributes
- ----------
- types: dict (class-level attribute) representing interaction types
- A registry of existing interaction types.
- """
- types = {}
-
- _view_name = Unicode('Interaction').tag(sync=True)
- _model_name = Unicode('BaseModel').tag(sync=True)
-
- _view_module = Unicode('bqplot').tag(sync=True)
- _model_module = Unicode('bqplot').tag(sync=True)
- _view_module_version = Unicode(__frontend_version__).tag(sync=True)
- _model_module_version = Unicode(__frontend_version__).tag(sync=True)
- # We cannot display an interaction outside of a figure
- _ipython_display_ = None
-
-
- @register_interaction('bqplot.HandDraw')
- class HandDraw(Interaction):
-
- """A hand-draw interaction.
-
- This can be used to edit the 'y' value of an existing line using the mouse.
- The minimum and maximum x values of the line which can be edited may be
- passed as parameters.
- The y-values for any part of the line can be edited by drawing the desired
- path while holding the mouse-down.
- y-values corresponding to x-values smaller than min_x or greater than max_x
- cannot be edited by HandDraw.
-
- Attributes
- ----------
- lines: an instance Lines mark or None (default: None)
- The instance of Lines which is edited using the hand-draw interaction.
- The 'y' values of the line are changed according to the path of the
- mouse. If the lines has multi dimensional 'y', then the 'line_index'
- attribute is used to selected the 'y' to be edited.
- line_index: nonnegative integer (default: 0)
- For a line with multi-dimensional 'y', this indicates the index of the
- 'y' to be edited by the handdraw.
- min_x: float or Date or None (default: None)
- The minimum value of 'x' which should be edited via the handdraw.
- max_x: float or Date or None (default: None)
- The maximum value of 'x' which should be edited via the handdraw.
- """
- lines = Instance(Lines, allow_none=True, default_value=None)\
- .tag(sync=True, **widget_serialization)
- line_index = Int().tag(sync=True)
- # TODO: Handle infinity in a meaningful way (json does not)
- min_x = (Float(None, allow_none=True) | Date(None, allow_none=True))\
- .tag(sync=True)
- max_x = (Float(None, allow_none=True) | Date(None, allow_none=True))\
- .tag(sync=True)
-
- _view_name = Unicode('HandDraw').tag(sync=True)
- _model_name = Unicode('HandDrawModel').tag(sync=True)
-
-
- @register_interaction('bqplot.PanZoom')
- @register
- class PanZoom(Interaction):
-
- """An interaction to pan and zoom wrt scales.
-
- Attributes
- ----------
- allow_pan: bool (default: True)
- Toggle the ability to pan.
- allow_zoom: bool (default: True)
- Toggle the ability to zoom.
- scales: Dictionary of lists of Scales (default: {})
- Dictionary with keys such as 'x' and 'y' and values being the scales in
- the corresponding direction (dimensions) which should be panned or
- zoomed.
- """
- allow_pan = Bool(True).tag(sync=True)
- allow_zoom = Bool(True).tag(sync=True)
- scales = Dict(trait=List(trait=Instance(Scale)))\
- .tag(sync=True, **widget_serialization)
-
- _view_name = Unicode('PanZoom').tag(sync=True)
- _model_name = Unicode('PanZoomModel').tag(sync=True)
-
-
- def panzoom(marks):
- """Helper function for panning and zooming over a set of marks.
-
- Creates and returns a panzoom interaction with the 'x' and 'y' dimension
- scales of the specified marks.
- """
- return PanZoom(scales={
- 'x': sum([mark._get_dimension_scales('x', preserve_domain=True) for mark in marks], []),
- 'y': sum([mark._get_dimension_scales('y', preserve_domain=True) for mark in marks], [])
- })
-
-
- class Selector(Interaction):
-
- """Selector interaction. A selector can be used to select a subset of data
-
- Base class for all the selectors.
-
- Attributes
- ----------
- marks: list (default: [])
- list of marks for which the `selected` attribute is updated based on
- the data selected by the selector.
- """
- marks = List().tag(sync=True, **widget_serialization)
-
- def reset(self):
- self.send({"type": "reset"})
-
-
- class OneDSelector(Selector):
-
- """One-dimensional selector interaction
-
- Base class for all selectors which select data in one dimension, i.e.,
- either the x or the y direction. The ``scale`` attribute should
- be provided.
-
- Attributes
- ----------
- scale: An instance of Scale
- This is the scale which is used for inversion from the pixels to data
- co-ordinates. This scale is used for setting the selected attribute for
- the selector.
- """
- scale = Instance(Scale, allow_none=True, default_value=None)\
- .tag(sync=True, dimension='x', **widget_serialization)
- _model_name = Unicode('OneDSelectorModel').tag(sync=True)
-
-
- class TwoDSelector(Selector):
-
- """Two-dimensional selector interaction.
-
- Base class for all selectors which select data in both the x and y
- dimensions. The attributes 'x_scale' and 'y_scale' should be provided.
-
- Attributes
- ----------
- x_scale: An instance of Scale
- This is the scale which is used for inversion from the pixels to data
- co-ordinates in the x-direction. This scale is used for setting the
- selected attribute for the selector along with ``y_scale``.
- 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. This scale is used for setting the
- selected attribute for the selector along with ``x_scale``.
- """
- x_scale = Instance(Scale, allow_none=True, default_value=None)\
- .tag(sync=True, dimension='x', **widget_serialization)
- y_scale = Instance(Scale, allow_none=True, default_value=None)\
- .tag(sync=True, dimension='y', **widget_serialization)
- _model_name = Unicode('TwoDSelectorModel').tag(sync=True)
-
-
- @register_interaction('bqplot.FastIntervalSelector')
- class FastIntervalSelector(OneDSelector):
-
- """Fast interval selector interaction.
-
- This 1-D selector is used to select an interval on the x-scale
- by just moving the mouse (without clicking or dragging). The
- x-coordinate of the mouse controls the mid point of the interval selected
- while the y-coordinate of the mouse controls the the width of the interval.
- The larger the y-coordinate, the wider the interval selected.
-
- Interval selector has three modes:
- 1. default mode: This is the default mode in which the mouse controls
- the location and width of the interval.
- 2. fixed-width mode: In this mode the width of the interval is frozen
- and only the location of the interval is controlled with the
- mouse.
- A single click from the default mode takes you to this mode.
- Another single click takes you back to the default mode.
- 3. frozen mode: In this mode the selected interval is frozen and the
- selector does not respond to mouse move.
- A double click from the default mode takes you to this mode.
- Another double click takes you back to the default mode.
-
- Attributes
- ----------
- selected: numpy.ndarray
- Two-element array containing the start and end of the interval selected
- in terms of the scale of the selector.
- color: Color or None (default: None)
- color of the rectangle representing the interval selector
- size: Float or None (default: None)
- if not None, this is the fixed pixel-width of the interval selector
- """
- selected = Array(None, allow_none=True)\
- .tag(sync=True, **array_serialization)
- color = Color(None, allow_none=True).tag(sync=True)
- size = Float(None, allow_none=True).tag(sync=True)
-
- _view_name = Unicode('FastIntervalSelector').tag(sync=True)
- _model_name = Unicode('FastIntervalSelectorModel').tag(sync=True)
-
-
- @register_interaction('bqplot.IndexSelector')
- class IndexSelector(OneDSelector):
-
- """Index selector interaction.
-
- This 1-D selector interaction uses the mouse x-coordinate to select the
- corresponding point in terms of the selector scale.
-
- Index Selector has two modes:
- 1. default mode: The mouse controls the x-position of the selector.
- 2. frozen mode: In this mode, the selector is frozen at a point and
- does not respond to mouse events.
-
- A single click switches between the two modes.
-
- Attributes
- ----------
- selected: numpy.ndarray
- A single element array containing the point corresponding the
- x-position of the mouse. This attribute is updated as you move the
- mouse along the x-direction on the figure.
- color: Color or None (default: None)
- Color of the line representing the index selector.
- line_width: nonnegative integer (default: 0)
- Width of the line represetning the index selector.
- """
- selected = Array(None, allow_none=True)\
- .tag(sync=True, **array_serialization)
- line_width = Int(2).tag(sync=True)
- color = Color(None, allow_none=True).tag(sync=True)
-
- _view_name = Unicode('IndexSelector').tag(sync=True)
- _model_name = Unicode('IndexSelectorModel').tag(sync=True)
-
-
- @register_interaction('bqplot.BrushIntervalSelector')
- class BrushIntervalSelector(OneDSelector):
-
- """Brush interval selector interaction.
-
- This 1-D selector interaction enables the user to select an interval using
- the brushing action of the mouse. A mouse-down marks the start of the
- interval. The drag after the mouse down in the x-direction selects the
- extent and a mouse-up signifies the end of the interval.
-
- Once an interval is drawn, the selector can be moved to a new interval by
- dragging the selector to the new interval.
-
- A double click at the same point without moving the mouse in the
- x-direction will result in the entire interval being selected.
-
- Attributes
- ----------
- selected: numpy.ndarray
- Two element array containing the start and end of the interval selected
- in terms of the scale of the selector.
- This attribute changes while the selection is being made with the
- ``BrushIntervalSelector``.
- brushing: bool
- Boolean attribute to indicate if the selector is being dragged.
- It is True when the selector is being moved and False when it is not.
- This attribute can be used to trigger computationally intensive code
- which should be run only on the interval selection being completed as
- opposed to code which should be run whenever selected is changing.
- orientation: {'horizontal', 'vertical'}
- The orientation of the interval, either vertical or horizontal
- color: Color or None (default: None)
- Color of the rectangle representing the brush selector.
- """
- brushing = Bool().tag(sync=True)
- selected = Array(None, allow_none=True)\
- .tag(sync=True, **array_serialization)
- orientation = Enum(['horizontal', 'vertical'],
- default_value='horizontal').tag(sync=True)
- color = Color(None, allow_none=True).tag(sync=True)
-
- _view_name = Unicode('BrushIntervalSelector').tag(sync=True)
- _model_name = Unicode('BrushIntervalSelectorModel').tag(sync=True)
-
-
- @register_interaction('bqplot.BrushSelector')
- class BrushSelector(TwoDSelector):
-
- """Brush interval selector interaction.
-
- This 2-D selector interaction enables the user to select a rectangular
- region using the brushing action of the mouse. A mouse-down marks the
- starting point of the interval. The drag after the mouse down selects the
- rectangle of interest and a mouse-up signifies the end point of
- the interval.
-
- Once an interval is drawn, the selector can be moved to a new interval by
- dragging the selector to the new interval.
-
- A double click at the same point without moving the mouse will result in
- the entire interval being selected.
-
- Attributes
- ----------
- selected_x: numpy.ndarray
- Two element array containing the start and end of the interval selected
- in terms of the x_scale of the selector.
- This attribute changes while the selection is being made with the
- ``BrushSelector``.
- selected_y: numpy.ndarray
- Two element array containing the start and end of the interval selected
- in terms of the y_scale of the selector.
- This attribute changes while the selection is being made with the
- ``BrushSelector``.
- selected: numpy.ndarray
- A 2x2 array containing the coordinates ::
-
- [[selected_x[0], selected_y[0]],
- [selected_x[1], selected_y[1]]]
- brushing: bool (default: False)
- boolean attribute to indicate if the selector is being dragged.
- It is True when the selector is being moved and False when it is not.
- This attribute can be used to trigger computationally intensive code
- which should be run only on the interval selection being completed as
- opposed to code which should be run whenever selected is changing.
- color: Color or None (default: None)
- Color of the rectangle representing the brush selector.
- """
- clear = Bool().tag(sync=True)
- brushing = Bool().tag(sync=True)
- selected_x = Array(None, allow_none=True).tag(sync=True, **array_serialization)
- selected_y = Array(None, allow_none=True).tag(sync=True, **array_serialization)
- selected = Array(None, allow_none=True)
- color = Color(None, allow_none=True).tag(sync=True)
-
- # This is for backward compatibility for code that relied on selected
- # instead of select_x and selected_y
- @observe('selected_x', 'selected_y')
- def _set_selected(self, change):
- if self.selected_x is None or len(self.selected_x) == 0 or \
- self.selected_y is None or len(self.selected_y) == 0:
- self.selected = None
- else:
- self.selected = np.array([[self.selected_x[0], self.selected_y[0]],
- [self.selected_x[1], self.selected_y[1]]])
-
- @observe('selected')
- def _set_selected_xy(self, change):
- value = self.selected
- if self.selected is None or len(self.selected) == 0:
- # if we set either selected_x OR selected_y to None
- # we don't want to set the other to None as well
- if not (self.selected_x is None or len(self.selected_x) == 0 or
- self.selected_y is None or len(self.selected_y) == 0):
- self.selected_x = None
- self.selected_y = None
- else:
- (x0, y0), (x1, y1) = value
- x = [x0, x1]
- y = [y0, y1]
-
- with self.hold_sync():
- if not _array_equal(self.selected_x, x):
- self.selected_x = x
- if not _array_equal(self.selected_y, y):
- self.selected_y = y
-
- _view_name = Unicode('BrushSelector').tag(sync=True)
- _model_name = Unicode('BrushSelectorModel').tag(sync=True)
-
-
- @register_interaction('bqplot.MultiSelector')
- class MultiSelector(BrushIntervalSelector):
-
- """Multi selector interaction.
-
- This 1-D selector interaction enables the user to select multiple intervals
- using the mouse. A mouse-down marks the start of the interval. The drag
- after the mouse down in the x-direction selects the extent and a mouse-up
- signifies the end of the interval.
-
- The current selector is highlighted with a green border and the inactive
- selectors are highlighted with a red border.
-
- The multi selector has three modes:
- 1. default mode: In this mode the interaction behaves exactly as the
- brush selector interaction with the current selector.
- 2. add mode: In this mode a new selector can be added by clicking at
- a point and dragging over the interval of interest. Once a new
- selector has been added, the multi selector is back in the
- default mode.
- From the default mode, ctrl+click switches to the add mode.
- 3. choose mode: In this mode, any of the existing inactive selectors
- can be set as the active selector. When an inactive selector is
- selected by clicking, the multi selector goes back to the
- default mode.
- From the default mode, shift+click switches to the choose mode.
-
- A double click at the same point without moving the mouse in the
- x-direction will result in the entire interval being selected for the
- current selector.
-
- Attributes
- ----------
- selected: dict
- A dictionary with keys being the names of the intervals and values
- being the two element arrays containing the start and end of the
- interval selected by that particular selector in terms of the scale of
- the selector.
- This is a read-only attribute.
- This attribute changes while the selection is being made with the
- MultiSelectorinteraction.
- brushing: bool (default: False)
- A boolean attribute to indicate if the selector is being dragged.
- It is True when the selector is being moved and false when it is not.
- This attribute can be used to trigger computationally intensive code
- which should be run only on the interval selection being completed as
- opposed to code which should be run whenever selected is changing.
- names: list
- A list of strings indicating the keys of the different intervals.
- Default values are 'int1', 'int2', 'int3' and so on.
- show_names: bool (default: True)
- Attribute to indicate if the names of the intervals are to be displayed
- along with the interval.
- """
- names = List().tag(sync=True)
- selected = Dict().tag(sync=True)
- _selected = Dict().tag(sync=True) # TODO: UglyHack. Hidden variable to get
- # around the even more ugly hack to have a trait which converts dates,
- # if present, into strings and send it across. It means writing a trait
- # which does that on top of a dictionary. I don't like that
-
- # TODO: Not a trait. The value has to be set at declaration time.
- show_names = Bool(True).tag(sync=True)
-
- def __init__(self, **kwargs):
- try:
- self.read_json = kwargs.get('scale').domain_class.from_json
- except AttributeError:
- self.read_json = None
- super(MultiSelector, self).__init__(**kwargs)
- self.on_trait_change(self.hidden_selected_changed, '_selected')
-
- def hidden_selected_changed(self, name, selected):
- actual_selected = {}
- if(self.read_json is None):
- self.selected = self._selected
- else:
- for key in self._selected:
- actual_selected[key] = [self.read_json(elem)
- for elem in self._selected[key]]
- self.selected = actual_selected
-
- _view_name = Unicode('MultiSelector').tag(sync=True)
- _model_name = Unicode('MultiSelectorModel').tag(sync=True)
-
-
- @register_interaction('bqplot.LassoSelector')
- class LassoSelector(TwoDSelector):
-
- """Lasso selector interaction.
-
- This 2-D selector enables the user to select multiple sets of data points
- by drawing lassos on the figure. A mouse-down starts drawing the lasso and
- after the mouse-up the lasso is closed and the `selected` attribute of each
- mark gets updated with the data in the lasso.
-
- The user can select (de-select) by clicking on lassos and can delete them
- (and their associated data) by pressing the 'Delete' button.
-
- Attributes
- ----------
- marks: List of marks which are instances of {Lines, Scatter} (default: [])
- List of marks on which lasso selector will be applied.
- color: Color (default: None)
- Color of the lasso.
- """
- color = Color(None, allow_none=True).tag(sync=True)
-
- _view_name = Unicode('LassoSelector').tag(sync=True)
- _model_name = Unicode('LassoSelectorModel').tag(sync=True)
|