Skip to content

Instantly share code, notes, and snippets.

@fabiovalse
Last active December 20, 2016 12:13
Show Gist options
  • Save fabiovalse/16f2dda956d7632e0d53f79b93c060ee to your computer and use it in GitHub Desktop.
Save fabiovalse/16f2dda956d7632e0d53f79b93c060ee to your computer and use it in GitHub Desktop.
Main object recognizer

This examples extends the previous one by including two viewports:

  • the first one, depicted in blue, is used for filtering objects when outside the blue viewport. By zooming in it is possible to see that the DOM is continuosly updated by adding and removing the object inside and outside the viewport;
  • the second one, depicted in red, is used for recognizing which is the object the user is interested in. Once a certain level of zoom is reached the interested object is colored in red.

The algorithm for calculating the intersect area between two rectangles has been taken from this Math Stackexchange discussion.

svg = d3.select 'svg'
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height
zoomable_layer = svg.append 'g'
zoom = d3.zoom()
.scaleExtent([1, 1000])
.on 'zoom', () ->
lod d3.event.transform
zoomable_layer
.attrs
transform: d3.event.transform
svg.call zoom
### Data
###
side = 5
n_columns = 5
n_rows = 2
fx = side*2 + 30 # the margin between circles on the columns
fy = side*2 + 30 # the margin between circles on the rows
data = [0..9].map (d,i) ->
return {
i: i
s1: side+Math.floor(Math.random()*20)
s2: side+Math.floor(Math.random()*20)
x: fx * (i%n_columns)
y: fy * (i%n_rows)
}
### Visualization
###
redraw = (x1, x2, y1, y2) ->
in_viewport = (d) ->
return d.x < x2 and d.x+d.s1 > x1 and d.y < y2 and d.y+d.s2 > y1
items = zoomable_layer.selectAll '.item'
.data data.filter (d) -> in_viewport d
en_items = items.enter().append 'rect'
.attrs
class: 'item'
all_items = en_items.merge(items)
all_items
.attrs
width: (d) -> d.s1
height: (d) -> d.s2
x: (d) -> d.x
y: (d) -> d.y
.styles
fill: '#f2f2f2'
.on 'click', (d) ->
center = {
x: d.x + d.s1/2
y: d.y + d.s2/2
}
transform = to_bounding_box(width, height, center, d.s1, d.s2, height/10)
svg.transition().duration(2000).call(zoom.transform, transform)
items.exit().remove()
###
###
lod = (transform) ->
v_margin = 200/transform.k
h_margin = 300/transform.k
dx = width/transform.k
dy = height/transform.k
x = -transform.x/transform.k
y = -transform.y/transform.k
redraw x, x+dx, y, y+dy
if transform.k > 22
get_main_object x, x+dx, y, y+dy, h_margin, v_margin
d3.select '.viewport'
.attrs
x: x
y: y
width: dx
height: dy
d3.select '.m_viewport'
.attrs
x: x+h_margin
y: y+v_margin
width: dx-h_margin*2
height: dy-v_margin*2
get_main_object = (x1, x2, y1, y2, h_margin, v_margin) ->
x1 += h_margin
x2 -= h_margin
y1 += v_margin
y2 -= v_margin
max = 0
index = null
items = zoomable_layer.selectAll '.item'
items.each (d,i) ->
x_overlap = Math.max(0, Math.min(x2, d.x+d.s1) - Math.max(x1, d.x))
y_overlap = Math.max(0, Math.min(y2, d.y+d.s2) - Math.max(y1, d.y))
d.overlap = x_overlap*y_overlap
if d.overlap > max
max = d.overlap
index = i
items.filter((d,i) -> i is index)
.styles
fill: '#fb8072'
### Returns a transform for center a bounding box in the browser viewport
- W and H are the witdh and height of the window
- w and h are the witdh and height of the bounding box
- center cointains the coordinates of the bounding box center
- margin defines the margin of the bounding box once zoomed
###
to_bounding_box = (W, H, center, w, h, margin) ->
kw = (W - margin) / w
kh = (H - margin) / h
k = d3.min [kw, kh]
x = W/2 - center.x*k
y = H/2 - center.y*k
return d3.zoomIdentity
.translate x, y
.scale k
### Init
###
redraw()
# viewport used for computing item intersections
zoomable_layer.append 'rect'
.attrs
class: 'viewport'
.styles
fill: 'transparent'
stroke: '#80b1d3'
'stroke-width': 8
'vector-effect': 'non-scaling-stroke'
zoomable_layer.append 'rect'
.attrs
class: 'm_viewport'
.styles
fill: 'transparent'
stroke: '#fb8072'
'stroke-width': 5
'vector-effect': 'non-scaling-stroke'
# Center vis on load
transform = to_bounding_box(width, height, {x: 170/2, y: 70/2}, 170, 70, 150)
svg.call(zoom.transform, transform)
body, html {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
svg {
width: 100%;
height: 100%;
background: #F8F8F8;
}
.item {
stroke: #606060;
stroke-width: 1;
}
.item:hover {
stroke: #404040;
cursor: pointer;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
<link rel="stylesheet" href="index.css">
<title>Main object recognizer</title>
</head>
<body>
<svg></svg>
<script src="index.js"></script>
</body>
</html>
// Generated by CoffeeScript 1.10.0
(function() {
var data, fx, fy, get_main_object, height, lod, n_columns, n_rows, redraw, side, svg, to_bounding_box, transform, width, zoom, zoomable_layer;
svg = d3.select('svg');
width = svg.node().getBoundingClientRect().width;
height = svg.node().getBoundingClientRect().height;
zoomable_layer = svg.append('g');
zoom = d3.zoom().scaleExtent([1, 1000]).on('zoom', function() {
lod(d3.event.transform);
return zoomable_layer.attrs({
transform: d3.event.transform
});
});
svg.call(zoom);
/* Data
*/
side = 5;
n_columns = 5;
n_rows = 2;
fx = side * 2 + 30;
fy = side * 2 + 30;
data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(d, i) {
return {
i: i,
s1: side + Math.floor(Math.random() * 20),
s2: side + Math.floor(Math.random() * 20),
x: fx * (i % n_columns),
y: fy * (i % n_rows)
};
});
/* Visualization
*/
redraw = function(x1, x2, y1, y2) {
var all_items, en_items, in_viewport, items;
in_viewport = function(d) {
return d.x < x2 && d.x + d.s1 > x1 && d.y < y2 && d.y + d.s2 > y1;
};
items = zoomable_layer.selectAll('.item').data(data.filter(function(d) {
return in_viewport(d);
}));
en_items = items.enter().append('rect').attrs({
"class": 'item'
});
all_items = en_items.merge(items);
all_items.attrs({
width: function(d) {
return d.s1;
},
height: function(d) {
return d.s2;
},
x: function(d) {
return d.x;
},
y: function(d) {
return d.y;
}
}).styles({
fill: '#f2f2f2'
}).on('click', function(d) {
var center, transform;
center = {
x: d.x + d.s1 / 2,
y: d.y + d.s2 / 2
};
transform = to_bounding_box(width, height, center, d.s1, d.s2, height / 10);
return svg.transition().duration(2000).call(zoom.transform, transform);
});
return items.exit().remove();
};
/*
*/
lod = function(transform) {
var dx, dy, h_margin, v_margin, x, y;
v_margin = 200 / transform.k;
h_margin = 300 / transform.k;
dx = width / transform.k;
dy = height / transform.k;
x = -transform.x / transform.k;
y = -transform.y / transform.k;
redraw(x, x + dx, y, y + dy);
if (transform.k > 22) {
get_main_object(x, x + dx, y, y + dy, h_margin, v_margin);
}
d3.select('.viewport').attrs({
x: x,
y: y,
width: dx,
height: dy
});
return d3.select('.m_viewport').attrs({
x: x + h_margin,
y: y + v_margin,
width: dx - h_margin * 2,
height: dy - v_margin * 2
});
};
get_main_object = function(x1, x2, y1, y2, h_margin, v_margin) {
var index, items, max;
x1 += h_margin;
x2 -= h_margin;
y1 += v_margin;
y2 -= v_margin;
max = 0;
index = null;
items = zoomable_layer.selectAll('.item');
items.each(function(d, i) {
var x_overlap, y_overlap;
x_overlap = Math.max(0, Math.min(x2, d.x + d.s1) - Math.max(x1, d.x));
y_overlap = Math.max(0, Math.min(y2, d.y + d.s2) - Math.max(y1, d.y));
d.overlap = x_overlap * y_overlap;
if (d.overlap > max) {
max = d.overlap;
return index = i;
}
});
return items.filter(function(d, i) {
return i === index;
}).styles({
fill: '#fb8072'
});
};
/* Returns a transform for center a bounding box in the browser viewport
- W and H are the witdh and height of the window
- w and h are the witdh and height of the bounding box
- center cointains the coordinates of the bounding box center
- margin defines the margin of the bounding box once zoomed
*/
to_bounding_box = function(W, H, center, w, h, margin) {
var k, kh, kw, x, y;
kw = (W - margin) / w;
kh = (H - margin) / h;
k = d3.min([kw, kh]);
x = W / 2 - center.x * k;
y = H / 2 - center.y * k;
return d3.zoomIdentity.translate(x, y).scale(k);
};
/* Init
*/
redraw();
zoomable_layer.append('rect').attrs({
"class": 'viewport'
}).styles({
fill: 'transparent',
stroke: '#80b1d3',
'stroke-width': 8,
'vector-effect': 'non-scaling-stroke'
});
zoomable_layer.append('rect').attrs({
"class": 'm_viewport'
}).styles({
fill: 'transparent',
stroke: '#fb8072',
'stroke-width': 5,
'vector-effect': 'non-scaling-stroke'
});
transform = to_bounding_box(width, height, {
x: 170 / 2,
y: 70 / 2
}, 170, 70, 150);
svg.call(zoom.transform, transform);
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment