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