Skip to content
This repository has been archived by the owner on Nov 7, 2018. It is now read-only.

Commit

Permalink
Added lasso
Browse files Browse the repository at this point in the history
Initial commit
  • Loading branch information
skokenes authored and biovisualize committed Jan 7, 2015
1 parent 44c77c5 commit a4f35c7
Show file tree
Hide file tree
Showing 2 changed files with 388 additions and 0 deletions.
88 changes: 88 additions & 0 deletions lasso/README.md
@@ -0,0 +1,88 @@
lasso
=========

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.

An example of the lasso implemented in a scatterplot can be found here: [http://bl.ocks.org/skokenes/511c5b658c405ad68941](http://bl.ocks.org/skokenes/511c5b658c405ad68941)

This example is based off of Mike Bostock's scatterplot example here: [http://bl.ocks.org/mbostock/3887118](http://bl.ocks.org/mbostock/3887118)

Lassoing tags
--
When the lasso is used, it tags elements by adding properties to their data. The properties are:

- 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, this value is true. Otherwise, it is false.
- selected: when a lasso is completed, all elements that were tagged as possible are given a selected value of true. Otherwise, the value is false.

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.

Note that the lasso only works with elements whose data is defined as an object.


Function Overview
--
**d3.lasso**()

Creates a new lasso object. This object can then have parameters set before the lasso is drawn.
```
var lasso = d3.lasso(); // creates a new lasso
```

lasso.**items**(_[selection]_)

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.
```
lasso.items(d3.selectAll("circle")); // sets all circles on the page to be lasso-able
```

lasso.**hoverSelect**(_[bool]_)

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.
```
lasso.hoverSelect(true); // allows hovering of elements for selection during lassoing
```

lasso.**closePathSelect**(_[bool]_)

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.
```
lasso.closePathSelect(true); // allows looping of elements for selection during lassoing
```

lasso.**closePathDistance**(_[num]_)

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.
```
lasso.closePathDistance(75); // the lasso loop will complete itself whenever the lasso end is within 75 pixels of the origin
```

lasso.**area**(_[sel]_)

The area() 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.
```
lasso.area(d3.select("#myLassoRect")); // the lasso will be trigger whenever a user clicks and drags on #myLassoRect
```

lasso.**on**(_type,[func]_)

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:
- start: this function will be executed whenever a lasso is started
- draw: this function will execute repeatedly as the lasso is drawn
- end: this function will be executed whenever a lasso is completed

If no function is specified, the function will return the current function defined for the type specified.
```
lasso.on("start",function() { alert("lasso started!"); }); // every time a lasso is started, an alert will trigger
```

Initiating a lasso
--
Once a lasso object is defined, it can be added to a page by calling it on an element like an svg.
```
var lasso = d3.lasso()
.items(d3.selectAll("circle")) // Create a lasso and provide it some target elements
.area(de.select("#myLassoRect")); // Sets the drag area for the lasso on the rectangle #myLassoRect
d3.select("svg").call(lasso); // Initiate the lasso on an svg element
```

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.
300 changes: 300 additions & 0 deletions lasso/lasso.js
@@ -0,0 +1,300 @@
d3.lasso = function() {

var items = null,
closePathDistance = 75,
closePathSelect = true,
isPathClosed = false,
hoverSelect = true,
points = [],
area = null,
on = {start:function(){}, draw: function(){}, end: function(){}};

function lasso() {
var _this = d3.select(this[0][0]);
var g = _this.append("g")
.attr("class","lasso");
var dyn_path = g.append("path")
.attr("class","drawn");
var close_path = g.append("path")
.attr("class","loop_close");
var complete_path = g.append("path")
.attr("display","none");
var origin_node = g.append("circle")
.attr("class","origin");
var path;
var origin;
var last_known_point;
var path_length_start;
var drag = d3.behavior.drag()
.on("dragstart",dragstart)
.on("drag",dragmove)
.on("dragend",dragend);
area.call(drag);

function dragstart() {
// Reset blank lasso path
path="";
dyn_path.attr("d",null);
close_path.attr("d",null);
// Set path length start
path_length_start = 0;
// Set every item to have a false selection and reset their center point and counters
items[0].forEach(function(d) {
d.hoverSelected = false;
d.loopSelected = false;
var cur_box = d.getBBox();
d.lassoPoint = {
cx: Math.round(cur_box.x + cur_box.width/2),
cy: Math.round(cur_box.y + cur_box.height/2),
edges: {top:0,right:0,bottom:0,left:0},
close_edges: {left: 0, right: 0}
};
})

// if hover is on, add hover function
if(hoverSelect==true) {
items.on("mouseover.lasso",function() {
// if hovered, change lasso selection attribute to true
d3.select(this)[0][0].hoverSelected = true;
})
}

// Run user defined start function
on.start();
}

function dragmove() {
var x = d3.mouse(this)[0];
var y = d3.mouse(this)[1];
// Initialize the path or add the latest point to it
if (path=="") {
path = path + "M " + x + " " + y;
origin = [x,y];
// Draw origin node
origin_node
.attr("cx",x)
.attr("cy",y)
.attr("r",7)
.attr("display",null);
}
else {
path = path + " L " + x + " " + y;
}

// Reset closed edges counter
items[0].forEach(function(d) {
d.lassoPoint.close_edges = {left:0,right:0};
});

// Calculate the current distance from the lasso origin
var distance = Math.sqrt(Math.pow(x-origin[0],2)+Math.pow(y-origin[1],2));

// Set the closed path line
var close_draw_path = "M " + x + " " + y + " L " + origin[0] + " " + origin[1];

// Draw the lines
dyn_path.attr("d",path);


// If within the closed path distance parameter, show the closed path. otherwise, hide it
if(distance<=closePathDistance) {
close_path.attr("display",null);
}
else {
close_path.attr("display","none");
}

isPathClosed = distance<=closePathDistance ? true : false;

// create complete path
var complete_path_d = d3.select("path")[0][0].attributes.d.value + "Z";
complete_path.attr("d",complete_path_d);

// get path length
var path_node = dyn_path.node();
var path_length_end = path_node.getTotalLength();
var last_pos = path_node.getPointAtLength(path_length_start-1);

for (var i = path_length_start; i<=path_length_end; i++) {
var cur_pos = path_node.getPointAtLength(i);
var cur_pos_obj = {
x:Math.round(cur_pos.x*100)/100,
y:Math.round(cur_pos.y*100)/100,
};
var prior_pos = path_node.getPointAtLength(i-1);
var prior_pos_obj = {
x:Math.round(prior_pos.x*100)/100,
y:Math.round(prior_pos.y*100)/100,
};

items[0].filter(function(d) {
var a;
if(d.lassoPoint.cy === cur_pos_obj.y && d.lassoPoint.cy != prior_pos_obj.y) {
last_known_point = {
x: prior_pos_obj.x,
y: prior_pos_obj.y
};
a=false;
}
else if (d.lassoPoint.cy === cur_pos_obj.y && d.lassoPoint.cy === prior_pos_obj.y) {
a = false;
}
else if (d.lassoPoint.cy === prior_pos_obj.y && d.lassoPoint.cy != cur_pos_obj.y) {
a = sign(d.lassoPoint.cy-cur_pos_obj.y)!=sign(d.lassoPoint.cy-last_known_point.y);
}
else {
last_known_point = {
x: prior_pos_obj.x,
y: prior_pos_obj.y
};
a = sign(d.lassoPoint.cy-cur_pos_obj.y)!=sign(d.lassoPoint.cy-prior_pos_obj.y);
}
return a;
}).forEach(function(d) {
if(cur_pos_obj.x>d.lassoPoint.cx) {
d.lassoPoint.edges.right = d.lassoPoint.edges.right+1;
}
if(cur_pos_obj.x<d.lassoPoint.cx) {
d.lassoPoint.edges.left = d.lassoPoint.edges.left+1;
}
});
}


if(isPathClosed == true && closePathSelect == true) {
close_path.attr("d",close_draw_path);
close_path_node =close_path.node();
var close_path_length = close_path_node.getTotalLength();
var close_path_edges = {left:0,right:0};
for (var i = 0; i<=close_path_length; i++) {
var cur_pos = close_path_node.getPointAtLength(i);
var prior_pos = close_path_node.getPointAtLength(i-1);

items[0].filter(function(d) {return d.lassoPoint.cy==Math.round(cur_pos.y)}).forEach(function(d) {
if(Math.round(cur_pos.y)!=Math.round(prior_pos.y) && Math.round(cur_pos.x)>d.lassoPoint.cx) {
d.lassoPoint.close_edges.right = 1;
}
if(Math.round(cur_pos.y)!=Math.round(prior_pos.y) && Math.round(cur_pos.x)<d.lassoPoint.cx) {
d.lassoPoint.close_edges.left = 1;
}
});

}

items[0].forEach(function(a) {
if((a.lassoPoint.edges.left+a.lassoPoint.close_edges.left)>0 && (a.lassoPoint.edges.right + a.lassoPoint.close_edges.right)%2 ==1) {
a.loopSelected = true;
}
else {
a.loopSelected = false;
}
});
}
else {
items[0].forEach(function(d) {
d.loopSelected = false;
})
}

// Tag possible items
d3.selectAll(items[0].filter(function(d) {return (d.loopSelected && isPathClosed) || d.hoverSelected}))
.attr("d",function(d) {return d.possible = true;});

d3.selectAll(items[0].filter(function(d) {return !((d.loopSelected && isPathClosed) || d.hoverSelected)}))
.attr("d",function(d) {return d.possible = false;});

on.draw();

// Continue drawing path from where it left off
path_length_start = path_length_end+1;
}

function dragend() {
// Remove mouseover tagging function
items.on("mouseover.lasso",null);

// Tag selected items
items.filter(function(d) {return d.possible === true})
.attr("d",function(d) {return d.selected = true;});

items.filter(function(d) {return d.possible === false})
.attr("d",function(d) {return d.selected = false;});

// Reset possible items
items.attr("d",function(d) {return d.possible = false});

// Clear lasso
dyn_path.attr("d",null);
close_path.attr("d",null);
origin_node.attr("display","none");

// Run user defined end function
on.end();

}
}

lasso.items = function(_) {

if (!arguments.length) return items;
items = _;
items[0].forEach(function(d) {
var item = d3.select(d);
if(typeof item.datum() === 'undefined') {
item.datum({possible:false,selected:false});
}
else {
item.attr("d",function(e) {e.possible = false; e.selected = false; return e;});
}
})
return lasso;
};

lasso.closePathDistance = function(_) {
if (!arguments.length) return closePathDistance;
closePathDistance = _;
return lasso;
};

lasso.closePathSelect = function(_) {
if (!arguments.length) return closePathSelect;
closePathSelect = _==true ? true : false;
return lasso;
};

lasso.isPathClosed = function(_) {
if (!arguments.length) return isPathClosed;
isPathClosed = _==true ? true : false;
return lasso;
};

lasso.hoverSelect = function(_) {
if (!arguments.length) return hoverSelect;
hoverSelect = _==true ? true : false;
return lasso;
};

lasso.on = function(type,_) {
if(!arguments.length) return on;
if(arguments.length===1) return on[type];
var types = ["start","draw","end"];
if(types.indexOf(type)>-1) {
on[type] = _;
};
return lasso;
}

lasso.area = function(_) {
if(!arguments.length) return area;
area=_;
return lasso;
}

function sign(x) {
return x?x<0?-1:1:0;
}


return lasso;

};

0 comments on commit a4f35c7

Please sign in to comment.