Skip to content

Instantly share code, notes, and snippets.

@skokenes
Created August 31, 2015 14:24
Show Gist options
  • Save skokenes/fd6b142c0f38e71424ab to your computer and use it in GitHub Desktop.
Save skokenes/fd6b142c0f38e71424ab to your computer and use it in GitHub Desktop.
Scatterplot with Lasso on points
sepalLength sepalWidth petalLength petalWidth species
5.1 3.5 1.4 0.2 setosa
4.9 3.0 1.4 0.2 setosa
4.7 3.2 1.3 0.2 setosa
4.6 3.1 1.5 0.2 setosa
5.0 3.6 1.4 0.2 setosa
5.4 3.9 1.7 0.4 setosa
4.6 3.4 1.4 0.3 setosa
5.0 3.4 1.5 0.2 setosa
4.4 2.9 1.4 0.2 setosa
4.9 3.1 1.5 0.1 setosa
5.4 3.7 1.5 0.2 setosa
4.8 3.4 1.6 0.2 setosa
4.8 3.0 1.4 0.1 setosa
4.3 3.0 1.1 0.1 setosa
5.8 4.0 1.2 0.2 setosa
5.7 4.4 1.5 0.4 setosa
5.4 3.9 1.3 0.4 setosa
5.1 3.5 1.4 0.3 setosa
5.7 3.8 1.7 0.3 setosa
5.1 3.8 1.5 0.3 setosa
5.4 3.4 1.7 0.2 setosa
5.1 3.7 1.5 0.4 setosa
4.6 3.6 1.0 0.2 setosa
5.1 3.3 1.7 0.5 setosa
4.8 3.4 1.9 0.2 setosa
5.0 3.0 1.6 0.2 setosa
5.0 3.4 1.6 0.4 setosa
5.2 3.5 1.5 0.2 setosa
5.2 3.4 1.4 0.2 setosa
4.7 3.2 1.6 0.2 setosa
4.8 3.1 1.6 0.2 setosa
5.4 3.4 1.5 0.4 setosa
5.2 4.1 1.5 0.1 setosa
5.5 4.2 1.4 0.2 setosa
4.9 3.1 1.5 0.2 setosa
5.0 3.2 1.2 0.2 setosa
5.5 3.5 1.3 0.2 setosa
4.9 3.6 1.4 0.1 setosa
4.4 3.0 1.3 0.2 setosa
5.1 3.4 1.5 0.2 setosa
5.0 3.5 1.3 0.3 setosa
4.5 2.3 1.3 0.3 setosa
4.4 3.2 1.3 0.2 setosa
5.0 3.5 1.6 0.6 setosa
5.1 3.8 1.9 0.4 setosa
4.8 3.0 1.4 0.3 setosa
5.1 3.8 1.6 0.2 setosa
4.6 3.2 1.4 0.2 setosa
5.3 3.7 1.5 0.2 setosa
5.0 3.3 1.4 0.2 setosa
7.0 3.2 4.7 1.4 versicolor
6.4 3.2 4.5 1.5 versicolor
6.9 3.1 4.9 1.5 versicolor
5.5 2.3 4.0 1.3 versicolor
6.5 2.8 4.6 1.5 versicolor
5.7 2.8 4.5 1.3 versicolor
6.3 3.3 4.7 1.6 versicolor
4.9 2.4 3.3 1.0 versicolor
6.6 2.9 4.6 1.3 versicolor
5.2 2.7 3.9 1.4 versicolor
5.0 2.0 3.5 1.0 versicolor
5.9 3.0 4.2 1.5 versicolor
6.0 2.2 4.0 1.0 versicolor
6.1 2.9 4.7 1.4 versicolor
5.6 2.9 3.6 1.3 versicolor
6.7 3.1 4.4 1.4 versicolor
5.6 3.0 4.5 1.5 versicolor
5.8 2.7 4.1 1.0 versicolor
6.2 2.2 4.5 1.5 versicolor
5.6 2.5 3.9 1.1 versicolor
5.9 3.2 4.8 1.8 versicolor
6.1 2.8 4.0 1.3 versicolor
6.3 2.5 4.9 1.5 versicolor
6.1 2.8 4.7 1.2 versicolor
6.4 2.9 4.3 1.3 versicolor
6.6 3.0 4.4 1.4 versicolor
6.8 2.8 4.8 1.4 versicolor
6.7 3.0 5.0 1.7 versicolor
6.0 2.9 4.5 1.5 versicolor
5.7 2.6 3.5 1.0 versicolor
5.5 2.4 3.8 1.1 versicolor
5.5 2.4 3.7 1.0 versicolor
5.8 2.7 3.9 1.2 versicolor
6.0 2.7 5.1 1.6 versicolor
5.4 3.0 4.5 1.5 versicolor
6.0 3.4 4.5 1.6 versicolor
6.7 3.1 4.7 1.5 versicolor
6.3 2.3 4.4 1.3 versicolor
5.6 3.0 4.1 1.3 versicolor
5.5 2.5 4.0 1.3 versicolor
5.5 2.6 4.4 1.2 versicolor
6.1 3.0 4.6 1.4 versicolor
5.8 2.6 4.0 1.2 versicolor
5.0 2.3 3.3 1.0 versicolor
5.6 2.7 4.2 1.3 versicolor
5.7 3.0 4.2 1.2 versicolor
5.7 2.9 4.2 1.3 versicolor
6.2 2.9 4.3 1.3 versicolor
5.1 2.5 3.0 1.1 versicolor
5.7 2.8 4.1 1.3 versicolor
6.3 3.3 6.0 2.5 virginica
5.8 2.7 5.1 1.9 virginica
7.1 3.0 5.9 2.1 virginica
6.3 2.9 5.6 1.8 virginica
6.5 3.0 5.8 2.2 virginica
7.6 3.0 6.6 2.1 virginica
4.9 2.5 4.5 1.7 virginica
7.3 2.9 6.3 1.8 virginica
6.7 2.5 5.8 1.8 virginica
7.2 3.6 6.1 2.5 virginica
6.5 3.2 5.1 2.0 virginica
6.4 2.7 5.3 1.9 virginica
6.8 3.0 5.5 2.1 virginica
5.7 2.5 5.0 2.0 virginica
5.8 2.8 5.1 2.4 virginica
6.4 3.2 5.3 2.3 virginica
6.5 3.0 5.5 1.8 virginica
7.7 3.8 6.7 2.2 virginica
7.7 2.6 6.9 2.3 virginica
6.0 2.2 5.0 1.5 virginica
6.9 3.2 5.7 2.3 virginica
5.6 2.8 4.9 2.0 virginica
7.7 2.8 6.7 2.0 virginica
6.3 2.7 4.9 1.8 virginica
6.7 3.3 5.7 2.1 virginica
7.2 3.2 6.0 1.8 virginica
6.2 2.8 4.8 1.8 virginica
6.1 3.0 4.9 1.8 virginica
6.4 2.8 5.6 2.1 virginica
7.2 3.0 5.8 1.6 virginica
7.4 2.8 6.1 1.9 virginica
7.9 3.8 6.4 2.0 virginica
6.4 2.8 5.6 2.2 virginica
6.3 2.8 5.1 1.5 virginica
6.1 2.6 5.6 1.4 virginica
7.7 3.0 6.1 2.3 virginica
6.3 3.4 5.6 2.4 virginica
6.4 3.1 5.5 1.8 virginica
6.0 3.0 4.8 1.8 virginica
6.9 3.1 5.4 2.1 virginica
6.7 3.1 5.6 2.4 virginica
6.9 3.1 5.1 2.3 virginica
5.8 2.7 5.1 1.9 virginica
6.8 3.2 5.9 2.3 virginica
6.7 3.3 5.7 2.5 virginica
6.7 3.0 5.2 2.3 virginica
6.3 2.5 5.0 1.9 virginica
6.5 3.0 5.2 2.0 virginica
6.2 3.4 5.4 2.3 virginica
5.9 3.0 5.1 1.8 virginica
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.dot {
stroke: #000;
}
.lasso path {
stroke: rgb(80,80,80);
stroke-width:2px;
}
.lasso .drawn {
fill-opacity:.05 ;
}
.lasso .loop_close {
fill:none;
stroke-dasharray: 4,4;
}
.lasso .origin {
fill:#3399FF;
fill-opacity:.5;
}
.not_possible {
fill:rgb(200,200,200);
}
.possible {
fill:#EC888C;
}
svg {
margin-left: 30px;
margin-top: 30px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src = "lasso.js"></script>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear()
.range([0, width]);
var y = d3.scale.linear()
.range([height, 0]);
var color = d3.scale.category10();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Create the area where the lasso event can be triggered
svg.append("rect")
.attr('class','lassoable')
.attr("width",width)
.attr("height",height)
.style("opacity",0);
// Lasso functions to execute while lassoing
var lasso_start = function() {
lasso.items()
.attr("r",3.5) // reset size
.style("fill",null) // clear all of the fills
.classed({"not_possible":true,"selected":false}); // style as not possible
};
var lasso_draw = function() {
// Style the possible dots
lasso.items().filter(function(d) {return d.possible===true})
.classed({"not_possible":false,"possible":true});
// Style the not possible dot
lasso.items().filter(function(d) {return d.possible===false})
.classed({"not_possible":true,"possible":false});
};
var lasso_end = function() {
// Reset the color of all dots
lasso.items()
.style("fill", function(d) { return color(d.species); });
// Style the selected dots
lasso.items().filter(function(d) {return d.selected===true})
.classed({"not_possible":false,"possible":false})
.attr("r",7);
// Reset the style of the not selected dots
lasso.items().filter(function(d) {return d.selected===false})
.classed({"not_possible":false,"possible":false})
.attr("r",3.5);
};
// Define the lasso
var lasso = d3.lasso()
.closePathDistance(75) // max distance for the lasso loop to be closed
.closePathSelect(true) // can items be selected by closing the path?
.hoverSelect(true) // can items by selected by hovering over them?
.on("start",lasso_start) // lasso start function
.on("draw",lasso_draw) // lasso draw function
.on("end",lasso_end); // lasso end function
d3.tsv("data.tsv", function(error, data) {
data.forEach(function(d) {
d.sepalLength = +d.sepalLength;
d.sepalWidth = +d.sepalWidth;
});
x.domain(d3.extent(data, function(d) { return d.sepalWidth; })).nice();
y.domain(d3.extent(data, function(d) { return d.sepalLength; })).nice();
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Sepal Width (cm)");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Sepal Length (cm)")
var dot_g = svg.append('g')
.attr('class','lassoable');
dot_g.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("id",function(d,i) {return "dot_" + i;}) // added
.attr("class", "dot")
.attr("r", 3.5)
.attr("cx", function(d) { return x(d.sepalWidth); })
.attr("cy", function(d) { return y(d.sepalLength); })
.style("fill", function(d) { return color(d.species); });
lasso.items(d3.selectAll(".dot"));
//lasso.items(d3.selectAll("#dot_118"));
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
lasso.area(svg.selectAll('.lassoable'));
// Init the lasso on the svg:g that contains the dots
svg.call(lasso);
});
</script>
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 body = d3.select('body');
var svg = d3.select('svg');
var g = _this.append("g")
.attr("class","lasso");
var dyn_path = g.append("path")
.attr("class","drawn");
var calc_path = g.append("path")
.attr("display","none");
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 tpath;
var origin;
var torigin;
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="";
tpath = "";
dyn_path.attr("d",null);
close_path.attr("d",null);
// Set path length start
path_length_start = 0;
var offset_box = _this[0][0].getBoundingClientRect();
// 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();
var new_box = d.getBoundingClientRect(); // relative to body! use this instead of the other formulas that calculate offsets and what not
d.lassoPoint = {
//cx: Math.round(new_box.left-offset_box.left + new_box.width/2),
cx: Math.round(new_box.left + new_box.width/2),
//cy: Math.round(new_box.top-offset_box.top + new_box.height/2),
cy: Math.round(new_box.top + new_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() {
// GET MOUSE POSITION WITHIN BODY / WINDOW
//console.log(d3.event);
var x = d3.event.sourceEvent.clientX;//d3.mouse(this)[0];
var y = d3.event.sourceEvent.clientY;//d3.mouse(this)[1];
var tx = d3.mouse(this)[0];
var ty = d3.mouse(this)[1];
// Initialize the path or add the latest point to it
if (path=="") {
path = path + "M " + x + " " + y;
tpath = tpath + "M " + tx + " " + ty;
origin = [x,y];
torigin = [tx,ty];
// Draw origin node
origin_node
.attr("cx",tx)
.attr("cy",ty)
.attr("r",7)
.attr("display",null);
}
else {
path = path + " L " + x + " " + y;
tpath = tpath + " L " + tx + " " + ty;
}
// 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 " + tx + " " + ty + " L " + torigin[0] + " " + torigin[1];
// Draw the lines
dyn_path.attr("d",tpath);
// path for calcs
calc_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 = path + "Z";
complete_path.attr("d",complete_path_d);
// get path length
var path_node = calc_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}))
.each(function(d) { d.possible = true;});
//.attr("d",function(d) {return d.possible = true;});
d3.selectAll(items[0].filter(function(d) {return !((d.loopSelected && isPathClosed) || d.hoverSelected)}))
.each(function(d) {d.possible = false;});
//.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})
.each(function(d) {d.selected = true;});
//.attr("d",function(d) {return d.selected = true;});
items.filter(function(d) {return d.possible === false})
.each(function(d) {d.selected = false;});
//.attr("d",function(d) {return d.selected = false;});
// Reset possible items
items
.each(function(d) {d.possible = false;});
//.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;});
var e = item.datum();
e.possible = false;
e.selected = false;
item.datum(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;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment