Permalink
Browse files

Refactor wheel_zoom_tool. Add zoom button tools #916 (#4841)

* Refactor wheel_zoom_tool. Add zoom button tools #916

Refactors models/tools/gestures/wheel_zoom_tool into
a scale_range() helper provided by models/tools/zoom_util.

scale_range() is then used to add zoom_in_tool and zoom_out_tool
to models/tools/actions, implementing toolbar buttons that will
zoom

Remaining work on #916 includes:
* actual design done for icons/Zoom{In,Out}.png.  I slapped something
  together for initial concept from the wheel-zoom icon.

* ensure zoom-in and zoom-out are "bundled".  It doesn't make sense
  to have one and not the other.

  Can this be done in helpers.py and plotting.coffee helpers
  or should the object hierarchy have a zoom-buttons layer?

* add 'factor' as a parameter on the zoom buttons.  Currently, it
  is hardcoded to 10%

* review user-guide for documentation needs

* testing. scale_range() should probably have some unit tests and
  I've thought through how to do view testing of the zoom buttons

* possibly rebase this into multiple commits so that git (and github?)
  correctly display the history via praise:
  1. code movement from wheel_zoom_tool to zoom_utils
  2. code reorg and usage of locals to enable sharing with zoom buttons
  3. implement zoom buttons

  I'm not sure how picky you guys are about having clean, easy
  to follow history but I don't mind rebasing this into smaller commits.

Possible work for future (or already existing) tickets:
* Does it make sense to expose zoom-only mode on wheel-zoom? I
  personally find it awkward to use wheel-zoom when it pans and zooms
  at the same time.  This could be implemented easily now by
  programmatically eliding the 'center' parameter to scale_range().
  Perhaps it could be implemented as wheel-zoom button having a dropdown
  radio-button list like tap tool that lets the user

* add zoom buttons to user_guide/tools.rst #916

* add factor property to new zoom buttons #916

The zoom button tools will now take a Percent property named factor to control
how much they zoom in or out per button click

* mv src/coffee/models/tools/zoom_util.coffee -> src/coffee/util/zoom.coffee

As a part of #916 based on feedback from @bryevdv, move scale_range into
toplevel src/coffee/util/zoom.coffee

Also added missing require for wheel_zoom_tool.coffee. I didn't test
wheel zoom for breakage, and/or there isn't a test that caught it.

* use coffee named params in util/zoom::scale_range #916

* util/zoom::scale_range allows v_axis_only && h_axis_only

remove a TODO about possibly checking for v_axis_only && h_axis_only in util/zoom::scale_range.
The logic inside scale_range() will work (doing nothing) if both are true.  There is already
a use-case where the value of center can cause both to be true (if it is outside of the frame
boundary).  Seems like making that a hard error is unnecessary.

* add typings for new zoom buttons #925

* rm superfluous get() and mget() per bev in #4841

Removing uses of get() and mget() that aren't needed now
that @define properties can be accessed via '.'

* ignore c9.io IDE files

* use getter and not computed property to calculate tooltip

* Add unit tests for ZoomInTool and ZoomOutTool

* Add single dimension tests

* Add wheel_zoom_tool tests and remove rogue debugger statement

* Remove debugger and add zoom in/out to color_scatter example
  • Loading branch information...
1 parent 60b1775 commit c0a42f9864aaec5330858076dfa4f89e0dfb83ba @timsnyder timsnyder committed with bryevdv Sep 22, 2016
View
@@ -97,6 +97,7 @@ bokeh.sets.*
remotedata
target/
/.idea/
+/.c9/
osx-64
linux-64
View
@@ -23,8 +23,8 @@
from __future__ import absolute_import
from ..model import Model
-from ..core.properties import abstract, Float, Color
from ..core.properties import (
+ abstract, Float, Color, Percent,
Any, Auto, Bool, String, Enum, Instance, Either, List, Dict, Tuple, Override
)
from ..core.enums import Dimension, Location, Anchor
@@ -409,6 +409,49 @@ class BoxZoomTool(Drag):
""")
+class ZoomInTool(Action):
+ """ *toolbar icon*: |zoom_in_icon|
+
+ The zoom-in tool allows users to click a button to zoom in
+ by a fixed amount.
+
+ .. |zoom_in_icon| image:: /_images/icons/ZoomIn.png
+ :height: 18pt
+
+ """
+ # TODO ZoomInTool dimensions should probably be constrained to be the same as ZoomOutTool
+ dimensions = List(Enum(Dimension), default=["width", "height"], help="""
+ Which dimensions the zoom-in tool is constrained to act in. By
+ default the zoom-in zoom tool will zoom in any dimension, but can be
+ configured to only zoom horizontally across the width of the plot, or
+ vertically across the height of the plot.
+ """)
+
+ factor = Percent(default=0.1, help="""
+ Percentage to zoom for each click of the zoom-in tool.
+ """)
+
+class ZoomOutTool(Action):
+ """ *toolbar icon*: |zoom_out_icon|
+
+ The zoom-out tool allows users to click a button to zoom out
+ by a fixed amount.
+
+ .. |zoom_out_icon| image:: /_images/icons/ZoomOut.png
+ :height: 18pt
+
+ """
+ dimensions = List(Enum(Dimension), default=["width", "height"], help="""
+ Which dimensions the zoom-out tool is constrained to act in. By
+ default the zoom-out tool will zoom in any dimension, but can be
+ configured to only zoom horizontally across the width of the plot, or
+ vertically across the height of the plot.
+ """)
+
+ factor = Percent(default=0.1, help="""
+ Percentage to zoom for each click of the zoom-in tool.
+ """)
+
class BoxSelectTool(Drag):
""" *toolbar icon*: |box_select_icon|
@@ -13,7 +13,7 @@
BoxSelectTool, BoxZoomTool, CategoricalAxis,
TapTool, CrosshairTool, DataRange1d, DatetimeAxis,
FactorRange, Grid, HelpTool, HoverTool, LassoSelectTool, Legend, LinearAxis,
- LogAxis, PanTool, PolySelectTool, ContinuousTicker,
+ LogAxis, PanTool, ZoomInTool, ZoomOutTool, PolySelectTool, ContinuousTicker,
SaveTool, Range, Range1d, UndoTool, RedoTool, ResetTool, ResizeTool, Tool,
WheelPanTool, WheelZoomTool, ColumnDataSource, GlyphRenderer)
@@ -229,6 +229,12 @@ def _get_num_minor_ticks(axis_class, num_minor_ticks):
"wheel_zoom": lambda: WheelZoomTool(dimensions=["width", "height"]),
"xwheel_zoom": lambda: WheelZoomTool(dimensions=["width"]),
"ywheel_zoom": lambda: WheelZoomTool(dimensions=["height"]),
+ "zoom_in": lambda: ZoomInTool(dimensions=["width", "height"]),
+ "xzoom_in": lambda: ZoomInTool(dimensions=["width"]),
+ "yzoom_in": lambda: ZoomInTool(dimensions=["height"]),
+ "zoom_out": lambda: ZoomOutTool(dimensions=["width", "height"]),
+ "xzoom_out": lambda: ZoomOutTool(dimensions=["width"]),
+ "yzoom_out": lambda: ZoomOutTool(dimensions=["height"]),
"xwheel_pan": lambda: WheelPanTool(dimension="width"),
"ywheel_pan": lambda: WheelPanTool(dimension="height"),
"resize": lambda: ResizeTool(),
@@ -13,7 +13,7 @@
def large_plot(n):
from bokeh.models import (
Plot, LinearAxis, Grid, GlyphRenderer,
- ColumnDataSource, DataRange1d, PanTool, WheelZoomTool, BoxZoomTool,
+ ColumnDataSource, DataRange1d, PanTool, ZoomInTool, ZoomOutTool, WheelZoomTool, BoxZoomTool,
BoxSelectTool, ResizeTool, SaveTool, ResetTool
)
from bokeh.models.layouts import VBox
@@ -36,13 +36,15 @@ def large_plot(n):
renderer = GlyphRenderer(data_source=source, glyph=glyph)
plot.renderers.append(renderer)
pan = PanTool()
+ zoom_in = ZoomInTool()
+ zoom_out = ZoomOutTool()
wheel_zoom = WheelZoomTool()
box_zoom = BoxZoomTool()
box_select = BoxSelectTool()
resize = ResizeTool()
save = SaveTool()
reset = ResetTool()
- tools = [pan, wheel_zoom, box_zoom, box_select, resize, save, reset]
+ tools = [pan, zoom_in, zoom_out, wheel_zoom, box_zoom, box_select, resize, save, reset]
plot.add_tools(*tools)
vbox.children.append(plot)
objects |= set([
@@ -21,6 +21,12 @@ _known_tools = {
wheel_zoom: (plot) -> new models.WheelZoomTool(plot: plot, dimensions: ["width", "height"])
xwheel_zoom: (plot) -> new models.WheelZoomTool(plot: plot, dimensions: ["width"])
ywheel_zoom: (plot) -> new models.WheelZoomTool(plot: plot, dimensions: ["height"])
+ zoom_in: (plot) -> new models.ZoomInTool(plot: plot, dimensions: ['width', 'height'])
+ xzoom_in: (plot) -> new models.ZoomInTool(plot: plot, dimensions: ['width'])
+ yzoom_in: (plot) -> new models.ZoomInTool(plot: plot, dimensions: ['height'])
+ zoom_out: (plot) -> new models.ZoomOutTool(plot: plot, dimensions: ['width', 'height'])
+ xzoom_out: (plot) -> new models.ZoomOutTool(plot: plot, dimensions: ['width'])
+ yzoom_out: (plot) -> new models.ZoomOutTool(plot: plot, dimensions: ['height'])
resize: (plot) -> new models.ResizeTool(plot: plot)
click: (plot) -> new models.TapTool(plot: plot, behavior: "inspect")
tap: (plot) -> new models.TapTool(plot: plot)
@@ -39,6 +39,20 @@ declare namespace Bokeh {
dimensions?: Array<Dimension>;
}
+ export var ZoomInTool: { new(attributes?: IZoomInTool, options?: ModelOpts): ZoomInTool };
+ export interface ZoomInTool extends Tool, IZoomInTool {}
+ export interface IZoomInTool extends ITool {
+ factor?: Percent;
+ dimensions?: Array<Dimension>;
+ }
+
+ export var ZoomOutTool: { new(attributes?: IZoomOutTool, options?: ModelOpts): ZoomOutTool };
+ export interface ZoomOutTool extends Tool, IZoomOutTool {}
+ export interface IZoomOutTool extends ITool {
+ factor?: Percent;
+ dimensions?: Array<Dimension>;
+ }
+
export var SaveTool: { new(attributes?: ISaveTool, options?: ModelOpts): SaveTool };
export interface SaveTool extends Tool, ISaveTool {}
export interface ISaveTool extends ITool {}
@@ -136,6 +136,8 @@ module.exports = {
ButtonTool: require '../models/tools/button_tool'
ActionTool: require '../models/tools/actions/action_tool'
+ ZoomInTool: require '../models/tools/actions/zoom_in_tool'
+ ZoomOutTool: require '../models/tools/actions/zoom_out_tool'
SaveTool: require '../models/tools/actions/save_tool'
UndoTool: require '../models/tools/actions/undo_tool'
RedoTool: require '../models/tools/actions/redo_tool'
@@ -156,6 +156,10 @@ class Instance extends simple_prop("Instance", (x) -> x.properties?)
# TODO (bev) separate booleans?
class Number extends simple_prop("Number", (x) -> _.isNumber(x) or _.isBoolean(x))
+# TODO extend Number instead of copying it's predicate
+#class Percent extends Number("Percent", (x) -> 0 <= x <= 1.0)
+class Percent extends simple_prop("Number", (x) -> (_.isNumber(x) or _.isBoolean(x)) and (0 <= x <= 1.0) )
+
class String extends simple_prop("String", _.isString)
# TODO (bev) don't think this exists python side
@@ -291,6 +295,7 @@ module.exports =
LineJoin: LineJoin
Location: Location
Number: Number
+ Percent: Percent
Int: Number # TODO
Orientation: Orientation
RenderLevel: RenderLevel
@@ -0,0 +1,48 @@
+ActionTool = require "./action_tool"
+{scale_range} = require "../../../util/zoom"
+{logger} = require "../../../core/logging"
+
+p = require "../../../core/properties"
+
+class ZoomInToolView extends ActionTool.View
+
+ do: () ->
+ frame = @plot_model.frame
+ dims = @model.dimensions
+
+ # restrict to axis configured in tool's dimensions property
+ if dims.indexOf('width') == -1
+ v_axis_only = true
+ if dims.indexOf('height') == -1
+ h_axis_only = true
+
+ zoom_info = scale_range({
+ frame: frame
+ factor: @model.factor
+ v_axis_only: v_axis_only
+ h_axis_only: h_axis_only
+ })
+ @plot_view.push_state('zoom_out', {range: zoom_info})
+ @plot_view.update_range(zoom_info, false, true)
+ @plot_view.interactive_timestamp = Date.now()
+ return null
+
+class ZoomInTool extends ActionTool.Model
+ default_view: ZoomInToolView
+ type: "ZoomInTool"
+ tool_name: "Zoom In"
+ icon: "bk-tool-icon-zoom-in"
+
+ @getters {
+ tooltip: () -> @_get_dim_tooltip(@tool_name, @_check_dims(@dimensions, "zoom-in tool"))
+ }
+
+ @define {
+ factor: [ p.Percent, 0.1 ]
+ dimensions: [ p.Array, ["width", "height"] ]
+ }
+
+module.exports = {
+ Model: ZoomInTool
+ View: ZoomInToolView
+}
@@ -0,0 +1,48 @@
+ActionTool = require "./action_tool"
+{scale_range} = require "../../../util/zoom"
+{logger} = require "../../../core/logging"
+
+p = require "../../../core/properties"
+
+class ZoomOutToolView extends ActionTool.View
+
+ do: () ->
+ frame = @plot_model.frame
+ dims = @model.dimensions
+
+ # restrict to axis configured in tool's dimensions property
+ if dims.indexOf('width') == -1
+ v_axis_only = true
+ if dims.indexOf('height') == -1
+ h_axis_only = true
+
+ zoom_info = scale_range({
+ frame: frame
+ factor: -@model.factor # zooming out requires a negative factor to scale_range
+ v_axis_only: v_axis_only
+ h_axis_only: h_axis_only
+ })
+ @plot_view.push_state('zoom_out', {range: zoom_info})
+ @plot_view.update_range(zoom_info, false, true)
+ @plot_view.interactive_timestamp = Date.now()
+ return null
+
+class ZoomOutTool extends ActionTool.Model
+ default_view: ZoomOutToolView
+ type: "ZoomOutTool"
+ tool_name: "Zoom Out"
+ icon: "bk-tool-icon-zoom-out"
+
+ @getters {
+ tooltip: () -> @_get_dim_tooltip(@tool_name, @_check_dims(@dimensions, "zoom-out tool"))
+ }
+
+ @define {
+ factor: [ p.Percent, 0.1 ]
+ dimensions: [ p.Array, ["width", "height"] ]
+ }
+
+module.exports = {
+ Model: ZoomOutTool
+ View: ZoomOutToolView
+}
@@ -1,6 +1,7 @@
_ = require "underscore"
GestureTool = require "./gesture_tool"
+{scale_range} = require "../../../util/zoom"
p = require "../../../core/properties"
# Here for testing purposes
@@ -25,11 +26,20 @@ class WheelZoomToolView extends GestureTool.View
vx = @plot_view.canvas.sx_to_vx(e.bokeh.sx)
vy = @plot_view.canvas.sy_to_vy(e.bokeh.sy)
+ # if wheel-scroll events happen outside frame restrict scaling to axis in bounds
if vx < hr.start or vx > hr.end
v_axis_only = true
if vy < vr.start or vy > vr.end
h_axis_only = true
+ dims = @model.dimensions
+
+ # restrict to axis configured in tool's dimensions property
+ if dims.indexOf('width') == -1
+ v_axis_only = true
+ if dims.indexOf('height') == -1
+ h_axis_only = true
+
# we need a browser-specific multiplier to have similar experiences
if navigator.userAgent.toLowerCase().indexOf("firefox") > -1
multiplier = 20
@@ -43,52 +53,14 @@ class WheelZoomToolView extends GestureTool.View
factor = @model.speed * delta
- # clamp the magnitude of factor, if it is > 1 bad things happen
- if factor > 0.9
- factor = 0.9
- else if factor < -0.9
- factor = -0.9
-
- vx_low = hr.start
- vx_high = hr.end
-
- vy_low = vr.start
- vy_high = vr.end
-
- dims = @model.dimensions
-
- if dims.indexOf('width') > -1 and not v_axis_only
- sx0 = vx_low - (vx_low - vx)*factor
- sx1 = vx_high - (vx_high - vx)*factor
- else
- sx0 = vx_low
- sx1 = vx_high
-
- if dims.indexOf('height') > -1 and not h_axis_only
- sy0 = vy_low - (vy_low - vy)*factor
- sy1 = vy_high - (vy_high - vy)*factor
- else
- sy0 = vy_low
- sy1 = vy_high
-
- xrs = {}
- for name, mapper of frame.x_mappers
- [start, end] = mapper.v_map_from_target([sx0, sx1], true)
- xrs[name] = {start: start, end: end}
-
- yrs = {}
- for name, mapper of frame.y_mappers
- [start, end] = mapper.v_map_from_target([sy0, sy1], true)
- yrs[name] = {start: start, end: end}
-
- # OK this sucks we can't set factor independently in each direction. It is used
- # for GMap plots, and GMap plots always preserve aspect, so effective the value
- # of 'dimensions' is ignored.
- zoom_info = {
- xrs: xrs
- yrs: yrs
+ zoom_info = scale_range({
+ frame: frame
factor: factor
- }
+ center: [vx, vy]
+ v_axis_only: v_axis_only
+ h_axis_only: h_axis_only
+ })
+
@plot_view.push_state('wheel_zoom', {range: zoom_info})
@plot_view.update_range(zoom_info, false, true)
@plot_view.interactive_timestamp = Date.now()
Oops, something went wrong.

0 comments on commit c0a42f9

Please sign in to comment.