├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json ├── rollup.config.js ├── src └── lasso.js └── test └── lasso-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | build/ 3 | example/ 4 | node_modules 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/*.zip 2 | test/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016, Speros Kokenes 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the author nor the names of contributors may be used to 15 | endorse or promote products derived from this software without specific prior 16 | written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # d3-lasso 2 | 3 | d3-lasso.js is a D3 plugin that allows you to tag elements on a page by drawing a line over or around objects. Functions can be run based on the lasso action. This functionality can be useful for brushing or filtering. 4 | 5 | An example of the lasso implemented in a scatterplot can be found here: [http://bl.ocks.org/skokenes/a85800be6d89c76c1ca98493ae777572](http://bl.ocks.org/skokenes/a85800be6d89c76c1ca98493ae777572) 6 | 7 | More examples: 8 | 9 | [Reusable Bubble Chart with d3Kit](http://bl.ocks.org/timelyportfolio/02f5ee71719e3f85aeed9b33586da21e) 10 | 11 | Lassoing tags 12 | -- 13 | When the lasso is used, it tags elements with properties of possible, not possible, selected, and not selected: 14 | 15 | - possible: while drawing a lasso, if an element is part of the final selection that would be made if the lasso was completed at that instance, the element is tagged as possible 16 | - not possible: while drawing a lasso, if an element is not part of the final selection that would be made if the lasso was completed at that instance, the element is tagged as not possible 17 | - selected: when a lasso is completed, all elements that were tagged as possible get tagged as selected 18 | - not selected: when a lasso is completed, all elements that were tagged as not possible get tagged as not selected 19 | 20 | The tags can be used in combination with functions to perform actions like styling the possible or selected values while the lasso is in use. 21 | 22 | ## Installing 23 | 24 | If you use NPM, `npm install d3-lasso`. Otherwise, download the [latest release](https://github.com/skokenes/d3-lasso/releases/latest). 25 | 26 | ## API Reference 27 | 28 | **lasso**() 29 | 30 | Creates a new lasso object. This object can then have parameters set before the lasso is drawn. 31 | ``` 32 | var lasso = d3.lasso(); // creates a new lasso 33 | ``` 34 | 35 | lasso.**items**(_[selection]_) 36 | 37 | The items() parameter takes in a d3 selection. Each element in the selection will be tagged with lasso-specific properties when the lasso is used. If no input is specified, the function returns the lasso's current items. 38 | ``` 39 | lasso.items(d3.selectAll("circle")); // sets all circles on the page to be lasso-able 40 | ``` 41 | 42 | lasso.**possibleItems**(_[selection]_) 43 | 44 | The possibleItems() parameter returns items that are currently tagged as being "possible". 45 | 46 | lasso.**notPossibleItems**(_[selection]_) 47 | 48 | The notPossibleItems() parameter returns items that are currently tagged as being "not possible". 49 | 50 | lasso.**selectedItems**(_[selection]_) 51 | 52 | The selectedItems() parameter returns items that are currently tagged as being "selected". 53 | 54 | lasso.**notSelectedItems**(_[selection]_) 55 | 56 | The notSelectedItems() parameter returns items that are currently tagged as being "not selected". 57 | 58 | lasso.**hoverSelect**(_[bool]_) 59 | 60 | The hoverSelect() parameter takes in a boolean that determines whether objects can be lassoed by hovering over an element during lassoing. The default value is set to true. If no input is specified, the function returns the lasso's current hover parameter. 61 | ``` 62 | lasso.hoverSelect(true); // allows hovering of elements for selection during lassoing 63 | ``` 64 | 65 | lasso.**closePathSelect**(_[bool]_) 66 | 67 | The closePathSelect() parameter takes in a boolean that determines whether objects can be lassoed by drawing a loop around them. The default value is set to true. If no input is specified, the function returns the lasso's current parameter. 68 | ``` 69 | lasso.closePathSelect(true); // allows looping of elements for selection during lassoing 70 | ``` 71 | 72 | lasso.**closePathDistance**(_[num]_) 73 | 74 | The closePathDistance() parameter takes in a number that specifies the maximum distance in pixels from the lasso origin that a lasso needs to be drawn in order to complete the loop and select elements. This parameter only works if closePathSelect is set to true; If no input is specified, the function returns the lasso's current parameter. 75 | ``` 76 | lasso.closePathDistance(75); // the lasso loop will complete itself whenever the lasso end is within 75 pixels of the origin 77 | ``` 78 | 79 | lasso.**targetArea**(_[sel]_) 80 | 81 | The targetArea() parameter takes in a selection representing the element to be used as a target area for the lasso event. If no input is specified, the function returns the current area selection. 82 | ``` 83 | lasso.targetArea(d3.select("#myLassoRect")); // the lasso will be trigger whenever a user clicks and drags on #myLassoRect 84 | ``` 85 | 86 | lasso.**on**(_type,[func]_) 87 | 88 | The on() parameter takes in a type of event and a function for that event. There are 3 types of events that can be defined: 89 | - start: this function will be executed whenever a lasso is started 90 | - draw: this function will execute repeatedly as the lasso is drawn 91 | - end: this function will be executed whenever a lasso is completed 92 | 93 | If no function is specified, the function will return the current function defined for the type specified. 94 | ``` 95 | lasso.on("start",function() { alert("lasso started!"); }); // every time a lasso is started, an alert will trigger 96 | ``` 97 | 98 | Initiating a lasso 99 | -- 100 | Once a lasso object is defined, it can be added to a page by calling it on an element like an svg. 101 | ``` 102 | var lasso = d3.lasso() 103 | .items(d3.selectAll("circle")) // Create a lasso and provide it some target elements 104 | .targetArea(de.select("#myLassoRect")); // Sets the drag area for the lasso on the rectangle #myLassoRect 105 | d3.select("svg").call(lasso); // Initiate the lasso on an svg element 106 | ``` 107 | 108 | If a lasso is going to be used on graphical elements that have been translated via a g element acting as a container, which is a common practice for incorporating chart margins, then the lasso should be called on that g element so that it is in the same coordinate system as the graphical elements. 109 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default as lasso} from "./src/lasso"; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-lasso", 3 | "version": "0.0.5", 4 | "description": "A d3 plugin for lasso selecting elements", 5 | "keywords": [ 6 | "d3", 7 | "d3-lasso", 8 | "lasso" 9 | ], 10 | "license": "BSD-3-Clause", 11 | "main": "build/d3-lasso.js", 12 | "jsnext:main": "index", 13 | "homepage": "https://github.com/skokenes/d3-lasso", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/skokenes/d3-lasso.git" 17 | }, 18 | "scripts": { 19 | "pretest": "rm -rf build && mkdir build && rollup -c", 20 | "test": "tape 'test/**/*-test.js'", 21 | "prepublish": "npm run test && uglifyjs build/d3-lasso.js -c -m -o build/d3-lasso.min.js", 22 | "postpublish": "zip -j build/d3-lasso.zip -- LICENSE README.md build/d3-lasso.js build/d3-lasso.min.js" 23 | }, 24 | "devDependencies": { 25 | "rollup": "0.27", 26 | "rollup-plugin-commonjs": "^4.1.0", 27 | "rollup-plugin-node-resolve": "^2.0.0", 28 | "tape": "4", 29 | "uglify-js": "2" 30 | }, 31 | "dependencies": { 32 | "d3-dispatch": "^1.0.1", 33 | "d3-drag": "^1.0.1", 34 | "d3-selection": "^1.0.2", 35 | "robust-point-in-polygon": "^1.0.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from "rollup-plugin-commonjs"; 2 | import nodeResolve from "rollup-plugin-node-resolve"; 3 | 4 | export default { 5 | entry: "index.js", 6 | format: "umd", 7 | globals: { 8 | "d3-selection": "d3", 9 | "d3-drag": "d3" 10 | }, 11 | moduleName: "d3", 12 | dest: 'build/d3-lasso.js', 13 | plugins: [ 14 | nodeResolve({ 15 | jsnext: true, 16 | main: true, 17 | browser: true, 18 | extensions: [".js", ".jsx"], 19 | skip: [ "d3-selection", "d3-drag","d3-dispatch"] 20 | }), 21 | commonjs({ 22 | include: "node_modules/**", 23 | exclude: [ "node_modules/d3-selection/"] 24 | }) 25 | ] 26 | }; -------------------------------------------------------------------------------- /src/lasso.js: -------------------------------------------------------------------------------- 1 | import * as selection from "d3-selection"; 2 | import * as drag from "d3-drag"; 3 | import classifyPoint from "robust-point-in-polygon"; 4 | 5 | export default function() { 6 | 7 | var items =[], 8 | closePathDistance = 75, 9 | closePathSelect = true, 10 | isPathClosed = false, 11 | hoverSelect = true, 12 | targetArea, 13 | on = {start:function(){}, draw: function(){}, end: function(){}}; 14 | 15 | // Function to execute on call 16 | function lasso(_this) { 17 | 18 | // add a new group for the lasso 19 | var g = _this.append("g") 20 | .attr("class","lasso"); 21 | 22 | // add the drawn path for the lasso 23 | var dyn_path = g.append("path") 24 | .attr("class","drawn"); 25 | 26 | // add a closed path 27 | var close_path = g.append("path") 28 | .attr("class","loop_close"); 29 | 30 | // add an origin node 31 | var origin_node = g.append("circle") 32 | .attr("class","origin"); 33 | 34 | // The transformed lasso path for rendering 35 | var tpath; 36 | 37 | // The lasso origin for calculations 38 | var origin; 39 | 40 | // The transformed lasso origin for rendering 41 | var torigin; 42 | 43 | // Store off coordinates drawn 44 | var drawnCoords; 45 | 46 | // Apply drag behaviors 47 | var dragAction = drag.drag() 48 | .on("start",dragstart) 49 | .on("drag",dragmove) 50 | .on("end",dragend); 51 | 52 | // Call drag 53 | targetArea.call(dragAction); 54 | 55 | function dragstart() { 56 | // Init coordinates 57 | drawnCoords = []; 58 | 59 | // Initialize paths 60 | tpath = ""; 61 | dyn_path.attr("d",null); 62 | close_path.attr("d",null); 63 | 64 | // Set every item to have a false selection and reset their center point and counters 65 | items.nodes().forEach(function(e) { 66 | e.__lasso.possible = false; 67 | e.__lasso.selected = false; 68 | e.__lasso.hoverSelect = false; 69 | e.__lasso.loopSelect = false; 70 | 71 | var box = e.getBoundingClientRect(); 72 | e.__lasso.lassoPoint = [Math.round(box.left + box.width/2),Math.round(box.top + box.height/2)]; 73 | }); 74 | 75 | // if hover is on, add hover function 76 | if(hoverSelect) { 77 | items.on("mouseover.lasso",function() { 78 | // if hovered, change lasso selection attribute to true 79 | this.__lasso.hoverSelect = true; 80 | }); 81 | } 82 | 83 | // Run user defined start function 84 | on.start(); 85 | } 86 | 87 | function dragmove() { 88 | // Get mouse position within body, used for calculations 89 | var x,y; 90 | if(selection.event.sourceEvent.type === "touchmove") { 91 | x = selection.event.sourceEvent.touches[0].clientX; 92 | y = selection.event.sourceEvent.touches[0].clientY; 93 | } 94 | else { 95 | x = selection.event.sourceEvent.clientX; 96 | y = selection.event.sourceEvent.clientY; 97 | } 98 | 99 | 100 | // Get mouse position within drawing area, used for rendering 101 | var tx = selection.mouse(this)[0]; 102 | var ty = selection.mouse(this)[1]; 103 | 104 | // Initialize the path or add the latest point to it 105 | if (tpath==="") { 106 | tpath = tpath + "M " + tx + " " + ty; 107 | origin = [x,y]; 108 | torigin = [tx,ty]; 109 | // Draw origin node 110 | origin_node 111 | .attr("cx",tx) 112 | .attr("cy",ty) 113 | .attr("r",7) 114 | .attr("display",null); 115 | } 116 | else { 117 | tpath = tpath + " L " + tx + " " + ty; 118 | } 119 | 120 | drawnCoords.push([x,y]); 121 | 122 | // Calculate the current distance from the lasso origin 123 | var distance = Math.sqrt(Math.pow(x-origin[0],2)+Math.pow(y-origin[1],2)); 124 | 125 | // Set the closed path line 126 | var close_draw_path = "M " + tx + " " + ty + " L " + torigin[0] + " " + torigin[1]; 127 | 128 | // Draw the lines 129 | dyn_path.attr("d",tpath); 130 | 131 | close_path.attr("d",close_draw_path); 132 | 133 | // Check if the path is closed 134 | isPathClosed = distance<=closePathDistance ? true : false; 135 | 136 | // If within the closed path distance parameter, show the closed path. otherwise, hide it 137 | if(isPathClosed && closePathSelect) { 138 | close_path.attr("display",null); 139 | } 140 | else { 141 | close_path.attr("display","none"); 142 | } 143 | 144 | items.nodes().forEach(function(n) { 145 | n.__lasso.loopSelect = (isPathClosed && closePathSelect) ? (classifyPoint(drawnCoords,n.__lasso.lassoPoint) < 1) : false; 146 | n.__lasso.possible = n.__lasso.hoverSelect || n.__lasso.loopSelect; 147 | }); 148 | 149 | on.draw(); 150 | } 151 | 152 | function dragend() { 153 | // Remove mouseover tagging function 154 | items.on("mouseover.lasso",null); 155 | 156 | items.nodes().forEach(function(n) { 157 | n.__lasso.selected = n.__lasso.possible; 158 | n.__lasso.possible = false; 159 | }); 160 | 161 | // Clear lasso 162 | dyn_path.attr("d",null); 163 | close_path.attr("d",null); 164 | origin_node.attr("display","none"); 165 | 166 | // Run user defined end function 167 | on.end(); 168 | } 169 | } 170 | 171 | // Set or get list of items for lasso to select 172 | lasso.items = function(_) { 173 | if (!arguments.length) return items; 174 | items = _; 175 | var nodes = items.nodes(); 176 | nodes.forEach(function(n) { 177 | n.__lasso = { 178 | "possible": false, 179 | "selected": false 180 | }; 181 | }); 182 | return lasso; 183 | }; 184 | 185 | // Return possible items 186 | lasso.possibleItems = function() { 187 | return items.filter(function() { 188 | return this.__lasso.possible; 189 | }); 190 | } 191 | 192 | // Return selected items 193 | lasso.selectedItems = function() { 194 | return items.filter(function() { 195 | return this.__lasso.selected; 196 | }); 197 | } 198 | 199 | // Return not possible items 200 | lasso.notPossibleItems = function() { 201 | return items.filter(function() { 202 | return !this.__lasso.possible; 203 | }); 204 | } 205 | 206 | // Return not selected items 207 | lasso.notSelectedItems = function() { 208 | return items.filter(function() { 209 | return !this.__lasso.selected; 210 | }); 211 | } 212 | 213 | // Distance required before path auto closes loop 214 | lasso.closePathDistance = function(_) { 215 | if (!arguments.length) return closePathDistance; 216 | closePathDistance = _; 217 | return lasso; 218 | }; 219 | 220 | // Option to loop select or not 221 | lasso.closePathSelect = function(_) { 222 | if (!arguments.length) return closePathSelect; 223 | closePathSelect = _===true ? true : false; 224 | return lasso; 225 | }; 226 | 227 | // Not sure what this is for 228 | lasso.isPathClosed = function(_) { 229 | if (!arguments.length) return isPathClosed; 230 | isPathClosed = _===true ? true : false; 231 | return lasso; 232 | }; 233 | 234 | // Option to select on hover or not 235 | lasso.hoverSelect = function(_) { 236 | if (!arguments.length) return hoverSelect; 237 | hoverSelect = _===true ? true : false; 238 | return lasso; 239 | }; 240 | 241 | // Events 242 | lasso.on = function(type,_) { 243 | if(!arguments.length) return on; 244 | if(arguments.length===1) return on[type]; 245 | var types = ["start","draw","end"]; 246 | if(types.indexOf(type)>-1) { 247 | on[type] = _; 248 | } 249 | return lasso; 250 | }; 251 | 252 | // Area where lasso can be triggered from 253 | lasso.targetArea = function(_) { 254 | if(!arguments.length) return targetArea; 255 | targetArea = _; 256 | return lasso; 257 | } 258 | 259 | 260 | 261 | return lasso; 262 | }; 263 | -------------------------------------------------------------------------------- /test/lasso-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"), 2 | lasso = require("../"); 3 | 4 | tape("lasso() returns the answer to the ultimate question of life, the universe, and everything.", function(test) { 5 | //test.equal(lasso.lasso(), 42); 6 | test.end(); 7 | }); 8 | --------------------------------------------------------------------------------