Based on the d3 fisheye distortion demo, but modified to work well with circos plots:
- Zoom foci constrained to lie in a circle.
- Click to add a new foci.
- Shift-click to remove foci.
license: gpl-3.0 |
.idea/ |
Based on the d3 fisheye distortion demo, but modified to work well with circos plots:
(function() { | |
d3.fisheye = function() { | |
var radius = 200, | |
power = 2, | |
k0, | |
k1, | |
foci = [[0, 0]]; | |
function fisheye(d) { | |
var x = d[0]; | |
var y = d[1]; | |
for (focus of foci) { | |
var dx = x - focus[0], | |
dy = y - focus[1], | |
dd = Math.sqrt(dx * dx + dy * dy); | |
if (dd < radius) { | |
var k = k0 * (1 - Math.exp(-dd * k1)) / dd * .75 + .25; | |
x = focus[0] + dx * k; | |
y = focus[1] + dy * k; | |
} | |
} | |
return [x, y]; | |
} | |
function rescale() { | |
k0 = Math.exp(power); | |
k0 = k0 / (k0 - 1) * radius; | |
k1 = power / radius; | |
return fisheye; | |
} | |
fisheye.radius = function(_) { | |
if (!arguments.length) return radius; | |
radius = +_; | |
return rescale(); | |
}; | |
fisheye.power = function(_) { | |
if (!arguments.length) return power; | |
power = +_; | |
return rescale(); | |
}; | |
fisheye.foci = function(_) { | |
if (!arguments.length) return foci; | |
foci = _; | |
return fisheye; | |
}; | |
return rescale(); | |
}; | |
})(); |
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
.background { | |
fill: none; | |
pointer-events: all; | |
} | |
path { | |
fill: none; | |
stroke: #333; | |
} | |
</style> | |
<body> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<script src="fisheye.js"></script> | |
<script> | |
(function(){ | |
var width = 960, | |
height = 500, | |
xStepsBig = d3.range(10, width, 20), | |
yStepsBig = d3.range(10, height, 20), | |
xStepsSmall = d3.range(0, width + 6, 6), | |
yStepsSmall = d3.range(0, height + 6, 6); | |
var fisheye = d3.fisheye(); | |
var line = d3.svg.line(); | |
var svg = d3.select("body").append("svg") | |
.attr("width", width) | |
.attr("height", height) | |
.append("g") | |
.attr("transform", "translate(-.5,-.5)"); | |
svg.append("rect") | |
.attr("class", "background") | |
.attr("width", width) | |
.attr("height", height); | |
svg.selectAll(".x") | |
.data(xStepsBig) | |
.enter().append("path") | |
.attr("class", "x") | |
.datum(function(x) { return yStepsSmall.map(function(y) { return [x, y]; }); }); | |
svg.selectAll(".y") | |
.data(yStepsBig) | |
.enter().append("path") | |
.attr("class", "y") | |
.datum(function(y) { return xStepsSmall.map(function(x) { return [x, y]; }); }); | |
var path = svg.selectAll("path") | |
.attr("d", line); | |
function find_target(mouse) { | |
var center = [width / 2, height / 2]; | |
var r = Math.sqrt(Math.pow(mouse[0]-center[0], 2) + Math.pow(mouse[1]-center[1], 2)); | |
var scale = Math.min(width, height)/2; | |
return [ | |
scale*(mouse[0]-center[0])/r + center[0], | |
scale*(mouse[1]-center[1])/r + center[1] | |
] | |
} | |
function zoomer(do_pop) { | |
return function() { | |
var target = find_target(d3.mouse(this)); | |
var foci = fisheye.foci(); | |
if (do_pop) { | |
foci.pop(); | |
} else { | |
if (d3.event.shiftKey) { | |
foci.pop(); | |
remove_closest(foci, target); | |
} | |
} | |
foci.push(target); | |
fisheye.foci(foci); | |
path.attr("d", function(d) { return line(d.map(fisheye)); }); | |
} | |
} | |
function remove_closest(foci, target) { | |
// Modifies foci in-place, removing the element | |
// which is nearest to target. | |
var dists = foci.map(function(p, i) { | |
return { | |
dist: Math.sqrt(Math.pow(p[0]-target[0], 2) + Math.pow(p[1]-target[1], 2)), | |
index: i } | |
}); | |
var min_dist = Infinity; | |
var i = false; | |
for (dist of dists) { | |
if (dist.dist < min_dist) { | |
i = dist.index; | |
} | |
} | |
foci.splice(i, 1); | |
} | |
svg.on("mousemove", zoomer(true)); | |
svg.on("click", zoomer(false)); | |
})() | |
</script> |