1 /**
  2  * Returns a new point behavior to be registered on mousemove events.
  3  *
  4  * @class Implements interactive fuzzy pointing, identifying marks that are in
  5  * close proximity to the mouse cursor. This behavior is an alternative to the
  6  * native mouseover and mouseout events, improving usability. Rather than
  7  * requiring the user to mouseover a mark exactly, the mouse simply needs to
  8  * move near the given mark and a "point" event is triggered. In addition, if
  9  * multiple marks overlap, the point behavior can be used to identify the mark
 10  * instance closest to the cursor, as opposed to the one that is rendered on
 11  * top.
 12  *
 13  * <p>The point behavior can also identify the closest mark instance for marks
 14  * that produce a continuous graphic primitive. The point behavior can thus be
 15  * used to provide details-on-demand for both discrete marks (such as dots and
 16  * bars), as well as continuous marks (such as lines and areas).
 17  *
 18  * <p>This behavior is implemented by finding the closest mark instance to the
 19  * mouse cursor on every mousemove event. If this closest mark is within the
 20  * given radius threshold, which defaults to 30 pixels, a "point" psuedo-event
 21  * is dispatched to the given mark instance. If any mark were previously
 22  * pointed, it would receive a corresponding "unpoint" event. These two
 23  * psuedo-event types correspond to the native "mouseover" and "mouseout"
 24  * events, respectively. To increase the radius at which the point behavior can
 25  * be applied, specify an appropriate threshold to the constructor, up to
 26  * <tt>Infinity</tt>.
 27  *
 28  * <p>By default, the standard Cartesian distance is computed. However, with
 29  * some visualizations it is desirable to consider only a single dimension, such
 30  * as the <i>x</i>-dimension for an independent variable. In this case, the
 31  * collapse parameter can be set to collapse the <i>y</i> dimension:
 32  *
 33  * <pre>    .event("mousemove", pv.Behavior.point(Infinity).collapse("y"))</pre>
 34  *
 35  * <p>This behavior only listens to mousemove events on the assigned panel,
 36  * which is typically the root panel. The behavior will search recursively for
 37  * descendant marks to point. If the mouse leaves the assigned panel, the
 38  * behavior no longer receives mousemove events; an unpoint psuedo-event is
 39  * automatically dispatched to unpoint any pointed mark. Marks may be re-pointed
 40  * when the mouse reenters the panel.
 41  *
 42  * <p>Panels have transparent fill styles by default; this means that panels may
 43  * not receive the initial mousemove event to start pointing. To fix this
 44  * problem, either given the panel a visible fill style (such as "white"), or
 45  * set the <tt>events</tt> property to "all" such that the panel receives events
 46  * despite its transparent fill.
 47  *
 48  * <p>Note: this behavior does not currently wedge marks.
 49  *
 50  * @extends pv.Behavior
 51  *
 52  * @param {number} [r] the fuzzy radius threshold in pixels
 53  * @see <a href="http://www.tovigrossman.com/papers/chi2005bubblecursor.pdf"
 54  * >"The Bubble Cursor: Enhancing Target Acquisition by Dynamic Resizing of the
 55  * Cursor's Activation Area"</a> by T. Grossman & R. Balakrishnan, CHI 2005.
 56  */
 57 pv.Behavior.point = function(r) {
 58   var unpoint, // the current pointer target
 59       collapse = null, // dimensions to collapse
 60       kx = 1, // x-dimension cost scale
 61       ky = 1, // y-dimension cost scale
 62       r2 = arguments.length ? r * r : 900; // fuzzy radius
 63 
 64   /** @private Search for the mark closest to the mouse. */
 65   function search(scene, index) {
 66     var s = scene[index],
 67         point = {cost: Infinity};
 68     for (var i = 0, n = s.visible && s.children.length; i < n; i++) {
 69       var child = s.children[i], mark = child.mark, p;
 70       if (mark.type == "panel") {
 71         mark.scene = child;
 72         for (var j = 0, m = child.length; j < m; j++) {
 73           mark.index = j;
 74           p = search(child, j);
 75           if (p.cost < point.cost) point = p;
 76         }
 77         delete mark.scene;
 78         delete mark.index;
 79       } else if (mark.$handlers.point) {
 80         var v = mark.mouse();
 81         for (var j = 0, m = child.length; j < m; j++) {
 82           var c = child[j],
 83               dx = v.x - c.left - (c.width || 0) / 2,
 84               dy = v.y - c.top - (c.height || 0) / 2,
 85               dd = kx * dx * dx + ky * dy * dy;
 86           if (dd < point.cost) {
 87             point.distance = dx * dx + dy * dy;
 88             point.cost = dd;
 89             point.scene = child;
 90             point.index = j;
 91           }
 92         }
 93       }
 94     }
 95     return point;
 96   }
 97 
 98   /** @private */
 99   function mousemove() {
100     /* If the closest mark is far away, clear the current target. */
101     var point = search(this.scene, this.index);
102     if ((point.cost == Infinity) || (point.distance > r2)) point = null;
103 
104     /* Unpoint the old target, if it's not the new target. */
105     if (unpoint) {
106       if (point
107           && (unpoint.scene == point.scene)
108           && (unpoint.index == point.index)) return;
109       pv.Mark.dispatch("unpoint", unpoint.scene, unpoint.index);
110     }
111 
112     /* Point the new target, if there is one. */
113     if (unpoint = point) {
114       pv.Mark.dispatch("point", point.scene, point.index);
115 
116       /* Unpoint when the mouse leaves the root panel. */
117       pv.listen(this.root.canvas(), "mouseout", mouseout);
118     }
119   }
120 
121   /** @private */
122   function mouseout(e) {
123     if (unpoint && !pv.ancestor(this, e.relatedTarget)) {
124       pv.Mark.dispatch("unpoint", unpoint.scene, unpoint.index);
125       unpoint = null;
126     }
127   }
128 
129   /**
130    * Sets or gets the collapse parameter. By default, the standard Cartesian
131    * distance is computed. However, with some visualizations it is desirable to
132    * consider only a single dimension, such as the <i>x</i>-dimension for an
133    * independent variable. In this case, the collapse parameter can be set to
134    * collapse the <i>y</i> dimension:
135    *
136    * <pre>    .event("mousemove", pv.Behavior.point(Infinity).collapse("y"))</pre>
137    *
138    * @function
139    * @returns {pv.Behavior.point} this, or the current collapse parameter.
140    * @name pv.Behavior.point.prototype.collapse
141    * @param {string} [x] the new collapse parameter
142    */
143   mousemove.collapse = function(x) {
144     if (arguments.length) {
145       collapse = String(x);
146       switch (collapse) {
147         case "y": kx = 1; ky = 0; break;
148         case "x": kx = 0; ky = 1; break;
149         default: kx = 1; ky = 1; break;
150       }
151       return mousemove;
152     }
153     return collapse;
154   };
155 
156   return mousemove;
157 };
158