You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

557 lines
22 KiB

4 years ago
  1. # Copyright 2015 Bloomberg Finance L.P.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. r"""
  15. =========
  16. Interacts
  17. =========
  18. .. currentmodule:: bqplot.interacts
  19. .. autosummary::
  20. :toctree: _generate/
  21. BrushIntervalSelector
  22. BrushSelector
  23. HandDraw
  24. IndexSelector
  25. FastIntervalSelector
  26. MultiSelector
  27. OneDSelector
  28. Interaction
  29. PanZoom
  30. Selector
  31. TwoDSelector
  32. """
  33. from traitlets import (Bool, Int, Float, Unicode, Dict,
  34. Instance, List, Enum, observe)
  35. from traittypes import Array
  36. from ipywidgets import Widget, Color, widget_serialization, register
  37. from .scales import Scale, DateScale
  38. from .traits import Date, array_serialization, _array_equal
  39. from .marks import Lines
  40. from ._version import __frontend_version__
  41. import numpy as np
  42. def register_interaction(key=None):
  43. """Decorator registering an interaction class in the registry.
  44. If no key is provided, the class name is used as a key. A key is provided
  45. for each core bqplot interaction type so that the frontend can use this
  46. key regardless of the kernal language.
  47. """
  48. def wrap(interaction):
  49. name = key if key is not None else interaction.__module__ + \
  50. interaction.__name__
  51. interaction.types[name] = interaction
  52. return interaction
  53. return wrap
  54. class Interaction(Widget):
  55. """The base interaction class.
  56. An interaction is a mouse interaction layer for a figure that requires the
  57. capture of all mouse events on the plot area. A consequence is that one can
  58. allow only one interaction at any time on a figure.
  59. An interaction can be associated with features such as selection or
  60. manual change of specific mark. Although, they differ from the so called
  61. 'mark interactions' in that they do not rely on knowing whether a specific
  62. element of the mark are hovered by the mouse.
  63. Attributes
  64. ----------
  65. types: dict (class-level attribute) representing interaction types
  66. A registry of existing interaction types.
  67. """
  68. types = {}
  69. _view_name = Unicode('Interaction').tag(sync=True)
  70. _model_name = Unicode('BaseModel').tag(sync=True)
  71. _view_module = Unicode('bqplot').tag(sync=True)
  72. _model_module = Unicode('bqplot').tag(sync=True)
  73. _view_module_version = Unicode(__frontend_version__).tag(sync=True)
  74. _model_module_version = Unicode(__frontend_version__).tag(sync=True)
  75. # We cannot display an interaction outside of a figure
  76. _ipython_display_ = None
  77. @register_interaction('bqplot.HandDraw')
  78. class HandDraw(Interaction):
  79. """A hand-draw interaction.
  80. This can be used to edit the 'y' value of an existing line using the mouse.
  81. The minimum and maximum x values of the line which can be edited may be
  82. passed as parameters.
  83. The y-values for any part of the line can be edited by drawing the desired
  84. path while holding the mouse-down.
  85. y-values corresponding to x-values smaller than min_x or greater than max_x
  86. cannot be edited by HandDraw.
  87. Attributes
  88. ----------
  89. lines: an instance Lines mark or None (default: None)
  90. The instance of Lines which is edited using the hand-draw interaction.
  91. The 'y' values of the line are changed according to the path of the
  92. mouse. If the lines has multi dimensional 'y', then the 'line_index'
  93. attribute is used to selected the 'y' to be edited.
  94. line_index: nonnegative integer (default: 0)
  95. For a line with multi-dimensional 'y', this indicates the index of the
  96. 'y' to be edited by the handdraw.
  97. min_x: float or Date or None (default: None)
  98. The minimum value of 'x' which should be edited via the handdraw.
  99. max_x: float or Date or None (default: None)
  100. The maximum value of 'x' which should be edited via the handdraw.
  101. """
  102. lines = Instance(Lines, allow_none=True, default_value=None)\
  103. .tag(sync=True, **widget_serialization)
  104. line_index = Int().tag(sync=True)
  105. # TODO: Handle infinity in a meaningful way (json does not)
  106. min_x = (Float(None, allow_none=True) | Date(None, allow_none=True))\
  107. .tag(sync=True)
  108. max_x = (Float(None, allow_none=True) | Date(None, allow_none=True))\
  109. .tag(sync=True)
  110. _view_name = Unicode('HandDraw').tag(sync=True)
  111. _model_name = Unicode('HandDrawModel').tag(sync=True)
  112. @register_interaction('bqplot.PanZoom')
  113. @register
  114. class PanZoom(Interaction):
  115. """An interaction to pan and zoom wrt scales.
  116. Attributes
  117. ----------
  118. allow_pan: bool (default: True)
  119. Toggle the ability to pan.
  120. allow_zoom: bool (default: True)
  121. Toggle the ability to zoom.
  122. scales: Dictionary of lists of Scales (default: {})
  123. Dictionary with keys such as 'x' and 'y' and values being the scales in
  124. the corresponding direction (dimensions) which should be panned or
  125. zoomed.
  126. """
  127. allow_pan = Bool(True).tag(sync=True)
  128. allow_zoom = Bool(True).tag(sync=True)
  129. scales = Dict(trait=List(trait=Instance(Scale)))\
  130. .tag(sync=True, **widget_serialization)
  131. _view_name = Unicode('PanZoom').tag(sync=True)
  132. _model_name = Unicode('PanZoomModel').tag(sync=True)
  133. def panzoom(marks):
  134. """Helper function for panning and zooming over a set of marks.
  135. Creates and returns a panzoom interaction with the 'x' and 'y' dimension
  136. scales of the specified marks.
  137. """
  138. return PanZoom(scales={
  139. 'x': sum([mark._get_dimension_scales('x', preserve_domain=True) for mark in marks], []),
  140. 'y': sum([mark._get_dimension_scales('y', preserve_domain=True) for mark in marks], [])
  141. })
  142. class Selector(Interaction):
  143. """Selector interaction. A selector can be used to select a subset of data
  144. Base class for all the selectors.
  145. Attributes
  146. ----------
  147. marks: list (default: [])
  148. list of marks for which the `selected` attribute is updated based on
  149. the data selected by the selector.
  150. """
  151. marks = List().tag(sync=True, **widget_serialization)
  152. def reset(self):
  153. self.send({"type": "reset"})
  154. class OneDSelector(Selector):
  155. """One-dimensional selector interaction
  156. Base class for all selectors which select data in one dimension, i.e.,
  157. either the x or the y direction. The ``scale`` attribute should
  158. be provided.
  159. Attributes
  160. ----------
  161. scale: An instance of Scale
  162. This is the scale which is used for inversion from the pixels to data
  163. co-ordinates. This scale is used for setting the selected attribute for
  164. the selector.
  165. """
  166. scale = Instance(Scale, allow_none=True, default_value=None)\
  167. .tag(sync=True, dimension='x', **widget_serialization)
  168. _model_name = Unicode('OneDSelectorModel').tag(sync=True)
  169. class TwoDSelector(Selector):
  170. """Two-dimensional selector interaction.
  171. Base class for all selectors which select data in both the x and y
  172. dimensions. The attributes 'x_scale' and 'y_scale' should be provided.
  173. Attributes
  174. ----------
  175. x_scale: An instance of Scale
  176. This is the scale which is used for inversion from the pixels to data
  177. co-ordinates in the x-direction. This scale is used for setting the
  178. selected attribute for the selector along with ``y_scale``.
  179. y_scale: An instance of Scale
  180. This is the scale which is used for inversion from the pixels to data
  181. co-ordinates in the y-direction. This scale is used for setting the
  182. selected attribute for the selector along with ``x_scale``.
  183. """
  184. x_scale = Instance(Scale, allow_none=True, default_value=None)\
  185. .tag(sync=True, dimension='x', **widget_serialization)
  186. y_scale = Instance(Scale, allow_none=True, default_value=None)\
  187. .tag(sync=True, dimension='y', **widget_serialization)
  188. _model_name = Unicode('TwoDSelectorModel').tag(sync=True)
  189. @register_interaction('bqplot.FastIntervalSelector')
  190. class FastIntervalSelector(OneDSelector):
  191. """Fast interval selector interaction.
  192. This 1-D selector is used to select an interval on the x-scale
  193. by just moving the mouse (without clicking or dragging). The
  194. x-coordinate of the mouse controls the mid point of the interval selected
  195. while the y-coordinate of the mouse controls the the width of the interval.
  196. The larger the y-coordinate, the wider the interval selected.
  197. Interval selector has three modes:
  198. 1. default mode: This is the default mode in which the mouse controls
  199. the location and width of the interval.
  200. 2. fixed-width mode: In this mode the width of the interval is frozen
  201. and only the location of the interval is controlled with the
  202. mouse.
  203. A single click from the default mode takes you to this mode.
  204. Another single click takes you back to the default mode.
  205. 3. frozen mode: In this mode the selected interval is frozen and the
  206. selector does not respond to mouse move.
  207. A double click from the default mode takes you to this mode.
  208. Another double click takes you back to the default mode.
  209. Attributes
  210. ----------
  211. selected: numpy.ndarray
  212. Two-element array containing the start and end of the interval selected
  213. in terms of the scale of the selector.
  214. color: Color or None (default: None)
  215. color of the rectangle representing the interval selector
  216. size: Float or None (default: None)
  217. if not None, this is the fixed pixel-width of the interval selector
  218. """
  219. selected = Array(None, allow_none=True)\
  220. .tag(sync=True, **array_serialization)
  221. color = Color(None, allow_none=True).tag(sync=True)
  222. size = Float(None, allow_none=True).tag(sync=True)
  223. _view_name = Unicode('FastIntervalSelector').tag(sync=True)
  224. _model_name = Unicode('FastIntervalSelectorModel').tag(sync=True)
  225. @register_interaction('bqplot.IndexSelector')
  226. class IndexSelector(OneDSelector):
  227. """Index selector interaction.
  228. This 1-D selector interaction uses the mouse x-coordinate to select the
  229. corresponding point in terms of the selector scale.
  230. Index Selector has two modes:
  231. 1. default mode: The mouse controls the x-position of the selector.
  232. 2. frozen mode: In this mode, the selector is frozen at a point and
  233. does not respond to mouse events.
  234. A single click switches between the two modes.
  235. Attributes
  236. ----------
  237. selected: numpy.ndarray
  238. A single element array containing the point corresponding the
  239. x-position of the mouse. This attribute is updated as you move the
  240. mouse along the x-direction on the figure.
  241. color: Color or None (default: None)
  242. Color of the line representing the index selector.
  243. line_width: nonnegative integer (default: 0)
  244. Width of the line represetning the index selector.
  245. """
  246. selected = Array(None, allow_none=True)\
  247. .tag(sync=True, **array_serialization)
  248. line_width = Int(2).tag(sync=True)
  249. color = Color(None, allow_none=True).tag(sync=True)
  250. _view_name = Unicode('IndexSelector').tag(sync=True)
  251. _model_name = Unicode('IndexSelectorModel').tag(sync=True)
  252. @register_interaction('bqplot.BrushIntervalSelector')
  253. class BrushIntervalSelector(OneDSelector):
  254. """Brush interval selector interaction.
  255. This 1-D selector interaction enables the user to select an interval using
  256. the brushing action of the mouse. A mouse-down marks the start of the
  257. interval. The drag after the mouse down in the x-direction selects the
  258. extent and a mouse-up signifies the end of the interval.
  259. Once an interval is drawn, the selector can be moved to a new interval by
  260. dragging the selector to the new interval.
  261. A double click at the same point without moving the mouse in the
  262. x-direction will result in the entire interval being selected.
  263. Attributes
  264. ----------
  265. selected: numpy.ndarray
  266. Two element array containing the start and end of the interval selected
  267. in terms of the scale of the selector.
  268. This attribute changes while the selection is being made with the
  269. ``BrushIntervalSelector``.
  270. brushing: bool
  271. Boolean attribute to indicate if the selector is being dragged.
  272. It is True when the selector is being moved and False when it is not.
  273. This attribute can be used to trigger computationally intensive code
  274. which should be run only on the interval selection being completed as
  275. opposed to code which should be run whenever selected is changing.
  276. orientation: {'horizontal', 'vertical'}
  277. The orientation of the interval, either vertical or horizontal
  278. color: Color or None (default: None)
  279. Color of the rectangle representing the brush selector.
  280. """
  281. brushing = Bool().tag(sync=True)
  282. selected = Array(None, allow_none=True)\
  283. .tag(sync=True, **array_serialization)
  284. orientation = Enum(['horizontal', 'vertical'],
  285. default_value='horizontal').tag(sync=True)
  286. color = Color(None, allow_none=True).tag(sync=True)
  287. _view_name = Unicode('BrushIntervalSelector').tag(sync=True)
  288. _model_name = Unicode('BrushIntervalSelectorModel').tag(sync=True)
  289. @register_interaction('bqplot.BrushSelector')
  290. class BrushSelector(TwoDSelector):
  291. """Brush interval selector interaction.
  292. This 2-D selector interaction enables the user to select a rectangular
  293. region using the brushing action of the mouse. A mouse-down marks the
  294. starting point of the interval. The drag after the mouse down selects the
  295. rectangle of interest and a mouse-up signifies the end point of
  296. the interval.
  297. Once an interval is drawn, the selector can be moved to a new interval by
  298. dragging the selector to the new interval.
  299. A double click at the same point without moving the mouse will result in
  300. the entire interval being selected.
  301. Attributes
  302. ----------
  303. selected_x: numpy.ndarray
  304. Two element array containing the start and end of the interval selected
  305. in terms of the x_scale of the selector.
  306. This attribute changes while the selection is being made with the
  307. ``BrushSelector``.
  308. selected_y: numpy.ndarray
  309. Two element array containing the start and end of the interval selected
  310. in terms of the y_scale of the selector.
  311. This attribute changes while the selection is being made with the
  312. ``BrushSelector``.
  313. selected: numpy.ndarray
  314. A 2x2 array containing the coordinates ::
  315. [[selected_x[0], selected_y[0]],
  316. [selected_x[1], selected_y[1]]]
  317. brushing: bool (default: False)
  318. boolean attribute to indicate if the selector is being dragged.
  319. It is True when the selector is being moved and False when it is not.
  320. This attribute can be used to trigger computationally intensive code
  321. which should be run only on the interval selection being completed as
  322. opposed to code which should be run whenever selected is changing.
  323. color: Color or None (default: None)
  324. Color of the rectangle representing the brush selector.
  325. """
  326. clear = Bool().tag(sync=True)
  327. brushing = Bool().tag(sync=True)
  328. selected_x = Array(None, allow_none=True).tag(sync=True, **array_serialization)
  329. selected_y = Array(None, allow_none=True).tag(sync=True, **array_serialization)
  330. selected = Array(None, allow_none=True)
  331. color = Color(None, allow_none=True).tag(sync=True)
  332. # This is for backward compatibility for code that relied on selected
  333. # instead of select_x and selected_y
  334. @observe('selected_x', 'selected_y')
  335. def _set_selected(self, change):
  336. if self.selected_x is None or len(self.selected_x) == 0 or \
  337. self.selected_y is None or len(self.selected_y) == 0:
  338. self.selected = None
  339. else:
  340. self.selected = np.array([[self.selected_x[0], self.selected_y[0]],
  341. [self.selected_x[1], self.selected_y[1]]])
  342. @observe('selected')
  343. def _set_selected_xy(self, change):
  344. value = self.selected
  345. if self.selected is None or len(self.selected) == 0:
  346. # if we set either selected_x OR selected_y to None
  347. # we don't want to set the other to None as well
  348. if not (self.selected_x is None or len(self.selected_x) == 0 or
  349. self.selected_y is None or len(self.selected_y) == 0):
  350. self.selected_x = None
  351. self.selected_y = None
  352. else:
  353. (x0, y0), (x1, y1) = value
  354. x = [x0, x1]
  355. y = [y0, y1]
  356. with self.hold_sync():
  357. if not _array_equal(self.selected_x, x):
  358. self.selected_x = x
  359. if not _array_equal(self.selected_y, y):
  360. self.selected_y = y
  361. _view_name = Unicode('BrushSelector').tag(sync=True)
  362. _model_name = Unicode('BrushSelectorModel').tag(sync=True)
  363. @register_interaction('bqplot.MultiSelector')
  364. class MultiSelector(BrushIntervalSelector):
  365. """Multi selector interaction.
  366. This 1-D selector interaction enables the user to select multiple intervals
  367. using the mouse. A mouse-down marks the start of the interval. The drag
  368. after the mouse down in the x-direction selects the extent and a mouse-up
  369. signifies the end of the interval.
  370. The current selector is highlighted with a green border and the inactive
  371. selectors are highlighted with a red border.
  372. The multi selector has three modes:
  373. 1. default mode: In this mode the interaction behaves exactly as the
  374. brush selector interaction with the current selector.
  375. 2. add mode: In this mode a new selector can be added by clicking at
  376. a point and dragging over the interval of interest. Once a new
  377. selector has been added, the multi selector is back in the
  378. default mode.
  379. From the default mode, ctrl+click switches to the add mode.
  380. 3. choose mode: In this mode, any of the existing inactive selectors
  381. can be set as the active selector. When an inactive selector is
  382. selected by clicking, the multi selector goes back to the
  383. default mode.
  384. From the default mode, shift+click switches to the choose mode.
  385. A double click at the same point without moving the mouse in the
  386. x-direction will result in the entire interval being selected for the
  387. current selector.
  388. Attributes
  389. ----------
  390. selected: dict
  391. A dictionary with keys being the names of the intervals and values
  392. being the two element arrays containing the start and end of the
  393. interval selected by that particular selector in terms of the scale of
  394. the selector.
  395. This is a read-only attribute.
  396. This attribute changes while the selection is being made with the
  397. MultiSelectorinteraction.
  398. brushing: bool (default: False)
  399. A boolean attribute to indicate if the selector is being dragged.
  400. It is True when the selector is being moved and false when it is not.
  401. This attribute can be used to trigger computationally intensive code
  402. which should be run only on the interval selection being completed as
  403. opposed to code which should be run whenever selected is changing.
  404. names: list
  405. A list of strings indicating the keys of the different intervals.
  406. Default values are 'int1', 'int2', 'int3' and so on.
  407. show_names: bool (default: True)
  408. Attribute to indicate if the names of the intervals are to be displayed
  409. along with the interval.
  410. """
  411. names = List().tag(sync=True)
  412. selected = Dict().tag(sync=True)
  413. _selected = Dict().tag(sync=True) # TODO: UglyHack. Hidden variable to get
  414. # around the even more ugly hack to have a trait which converts dates,
  415. # if present, into strings and send it across. It means writing a trait
  416. # which does that on top of a dictionary. I don't like that
  417. # TODO: Not a trait. The value has to be set at declaration time.
  418. show_names = Bool(True).tag(sync=True)
  419. def __init__(self, **kwargs):
  420. try:
  421. self.read_json = kwargs.get('scale').domain_class.from_json
  422. except AttributeError:
  423. self.read_json = None
  424. super(MultiSelector, self).__init__(**kwargs)
  425. self.on_trait_change(self.hidden_selected_changed, '_selected')
  426. def hidden_selected_changed(self, name, selected):
  427. actual_selected = {}
  428. if(self.read_json is None):
  429. self.selected = self._selected
  430. else:
  431. for key in self._selected:
  432. actual_selected[key] = [self.read_json(elem)
  433. for elem in self._selected[key]]
  434. self.selected = actual_selected
  435. _view_name = Unicode('MultiSelector').tag(sync=True)
  436. _model_name = Unicode('MultiSelectorModel').tag(sync=True)
  437. @register_interaction('bqplot.LassoSelector')
  438. class LassoSelector(TwoDSelector):
  439. """Lasso selector interaction.
  440. This 2-D selector enables the user to select multiple sets of data points
  441. by drawing lassos on the figure. A mouse-down starts drawing the lasso and
  442. after the mouse-up the lasso is closed and the `selected` attribute of each
  443. mark gets updated with the data in the lasso.
  444. The user can select (de-select) by clicking on lassos and can delete them
  445. (and their associated data) by pressing the 'Delete' button.
  446. Attributes
  447. ----------
  448. marks: List of marks which are instances of {Lines, Scatter} (default: [])
  449. List of marks on which lasso selector will be applied.
  450. color: Color (default: None)
  451. Color of the lasso.
  452. """
  453. color = Color(None, allow_none=True).tag(sync=True)
  454. _view_name = Unicode('LassoSelector').tag(sync=True)
  455. _model_name = Unicode('LassoSelectorModel').tag(sync=True)