Move the mouse to change the focal point of the fisheye. The left side of the grid shows fisheye with smoothing which is implemented in this plugin https://github.com/duaneatat/d3-fisheye and described here https://observablehq.com/@duaneatat/smoothed-fisheye - The right side is the sarkar-brown fisheye effect which has the abrupt discontinuity at the edge of the fisheye effect.
-
-
Save duaneatat/315b00c4747e747054ba0287035794d6 to your computer and use it in GitHub Desktop.
Smoothed Fisheye (d3-fisheye)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var d3Fisheye = {}; | |
d3Fisheye.radial = function() { | |
var radius = 200, | |
smoothingRatio = 0, | |
distortion = 3, | |
center = [0, 0], | |
A1, | |
A2, | |
dw; | |
function fisheye(point) { | |
if (smoothingRatio === 1 || distortion === 0) return point; | |
var x = point[0]; | |
var y = point[1]; | |
var fx = center[0]; | |
var fy = center[1]; | |
var dx = x - fx; | |
var dy = y - fy; | |
if (Math.abs(dx) > radius || Math.abs(dy) > radius) { | |
return point; | |
} | |
var dr = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)); | |
if (Math.abs(dr) > radius || dr == 0) { | |
return point; | |
} | |
var theta = Math.atan2(dy, dx); | |
var cos = Math.cos(theta); | |
var sin = Math.sin(theta); | |
var rescaled = dr / radius; | |
var newPoint = [0, 0]; | |
var newR = fisheyeContinuous(rescaled); | |
newPoint[0] = fx + cos * radius * newR; | |
newPoint[1] = fy + sin * radius * newR; | |
return newPoint; | |
} | |
function fisheyeContinuous(x) { | |
if (x <= 1 - smoothingRatio) { | |
return (A2 * x * (dw + 1)) / (dw * x + 1); | |
} else { | |
return (A1 * Math.pow(x - 1, 2)) / 2 + x; | |
} | |
} | |
function recalculate() { | |
var constants = solveForConstants(); | |
A1 = constants[0]; | |
A2 = constants[1]; | |
dw = constants[2]; | |
return fisheye; | |
} | |
function solveForConstants() { | |
if (smoothingRatio === 0 || smoothingRatio === 1) { | |
return [0, 1, distortion]; | |
} | |
var xw = 1 - smoothingRatio; | |
var d = distortion; | |
var A1 = | |
(2 * | |
xw * | |
(d * xw - | |
Math.sqrt( | |
Math.pow(d, 2) * Math.pow(xw, 2) + d * Math.pow(xw, 2) + d + 1 | |
) + | |
1)) / | |
(Math.pow(xw, 3) - 3 * Math.pow(xw, 2) + 3 * xw - 1); | |
var A2 = | |
(xw * | |
(Math.pow(d, 2) * xw + | |
d * Math.pow(xw, 2) + | |
2 * d * xw - | |
d * | |
Math.sqrt( | |
Math.pow(d, 2) * Math.pow(xw, 2) + d * Math.pow(xw, 2) + d + 1 | |
) - | |
d + | |
Math.pow(xw, 2) + | |
xw - | |
Math.sqrt( | |
Math.pow(d, 2) * Math.pow(xw, 2) + d * Math.pow(xw, 2) + d + 1 | |
) - | |
1)) / | |
(2 * d * Math.pow(xw, 2) - | |
d * xw - | |
d + | |
Math.pow(xw, 3) + | |
Math.pow(xw, 2) - | |
2 * xw); | |
var dw = | |
(d * xw + Math.sqrt((d + 1) * (d * Math.pow(xw, 2) + 1)) - 1) / | |
(xw * (xw + 1)); | |
return [A1, A2, dw]; | |
} | |
fisheye.radius = function(_) { | |
if (!arguments.length) return radius; | |
radius = +_; | |
return recalculate(); | |
}; | |
fisheye.smoothingRatio = function(_) { | |
if (!arguments.length) return smoothingRatio; | |
smoothingRatio = +_; | |
return recalculate(); | |
}; | |
fisheye.distortion = function(_) { | |
if (!arguments.length) return distortion; | |
distortion = +_; | |
return recalculate(); | |
}; | |
fisheye.center = function(_) { | |
if (!arguments.length) return center; | |
center = _; | |
return fisheye; | |
}; | |
fisheye.focus = fisheye.center; | |
fisheye.fisheyeRadial = fisheye; | |
fisheye.fisheyeFunction = function(x) { | |
if (x <= 0 || x >= 1) return x; | |
return fisheyeContinuous(x); | |
}; | |
return recalculate(); | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
d3fisheyeDeprecated = { | |
scale: function(scaleType) { | |
return d3_fisheye_scale(scaleType(), 3, 0); | |
}, | |
circular: function() { | |
var radius = 200, | |
distortion = 2, | |
k0, | |
k1, | |
focus = [0, 0]; | |
function fisheye(d) { | |
var dx = d.x - focus[0], | |
dy = d.y - focus[1], | |
dd = Math.sqrt(dx * dx + dy * dy); | |
if (!dd || dd >= radius) | |
return { x: d.x, y: d.y, z: dd >= radius ? 1 : 10 }; | |
var k = ((k0 * (1 - Math.exp(-dd * k1))) / dd) * 0.75 + 0.25; | |
return { x: focus[0] + dx * k, y: focus[1] + dy * k, z: Math.min(k, 10) }; | |
} | |
function rescale() { | |
k0 = Math.exp(distortion); | |
k0 = (k0 / (k0 - 1)) * radius; | |
k1 = distortion / radius; | |
return fisheye; | |
} | |
fisheye.radius = function(_) { | |
if (!arguments.length) return radius; | |
radius = +_; | |
return rescale(); | |
}; | |
fisheye.distortion = function(_) { | |
if (!arguments.length) return distortion; | |
distortion = +_; | |
return rescale(); | |
}; | |
fisheye.focus = function(_) { | |
if (!arguments.length) return focus; | |
focus = _; | |
return fisheye; | |
}; | |
return rescale(); | |
} | |
}; | |
function d3_fisheye_scale(scale, d, a) { | |
function fisheye(_) { | |
var x = scale(_), | |
left = x < a, | |
range = d3.extent(scale.range()), | |
min = range[0], | |
max = range[1], | |
m = left ? a - min : max - a; | |
if (m == 0) m = max - min; | |
return ((left ? -1 : 1) * m * (d + 1)) / (d + m / Math.abs(x - a)) + a; | |
} | |
fisheye.distortion = function(_) { | |
if (!arguments.length) return d; | |
d = +_; | |
return fisheye; | |
}; | |
fisheye.focus = function(_) { | |
if (!arguments.length) return a; | |
a = +_; | |
return fisheye; | |
}; | |
fisheye.copy = function() { | |
return d3_fisheye_scale(scale.copy(), d, a); | |
}; | |
fisheye.nice = scale.nice; | |
fisheye.ticks = scale.ticks; | |
fisheye.tickFormat = scale.tickFormat; | |
return d3.rebind(fisheye, scale, 'domain', 'range'); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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-deprecated.js"></script> | |
<script src="d3-fisheye.js"></script> | |
<script> | |
var width = 960, | |
height = 500, | |
xStepsBig = d3.range(10, width, 20), | |
yStepsBig = d3.range(10, height, 20), | |
xStepsSmall = d3.range(0, width + 4, 4), | |
yStepsSmall = d3.range(0, height + 4, 4); | |
const points = []; | |
for (var i = 0; i < 100; i++) { | |
points.push({ x: Math.random() * width, y: Math.random() * height }); | |
} | |
var fisheye = d3Fisheye | |
.radial() | |
.radius(120) | |
.distortion(5) | |
.smoothingRatio(0.5); | |
var fisheyeDeprecated = d3fisheyeDeprecated | |
.circular() | |
.radius(100) | |
.distortion(5); | |
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); | |
const pointsSelection = svg | |
.selectAll('dot') | |
.data(points) | |
.enter() | |
.append('circle') | |
.attr('r', 5) | |
.attr('cx', function(d) { | |
return d.x; | |
}) | |
.attr('cy', function(d) { | |
return d.y; | |
}); | |
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); | |
svg.on('mousemove', function() { | |
const mouse = d3.mouse(this); | |
fisheye.focus(mouse); | |
fisheyeDeprecated.focus(mouse); | |
pointsSelection | |
.each(d => { | |
if (mouse[0] > 500) { | |
d.fisheye = fisheyeDeprecated(d); | |
} else { | |
d.fisheye = fisheye([d.x, d.y]); | |
} | |
}) | |
.attr('cx', function(d) { | |
if (mouse[0] > 500) { | |
return d.fisheye.x; | |
} else { | |
return d.fisheye[0]; | |
} | |
}) | |
.attr('cy', function(d) { | |
if (mouse[0] > 500) { | |
return d.fisheye.y; | |
} else { | |
return d.fisheye[1]; | |
} | |
}); | |
path.attr('d', function(d) { | |
return line( | |
d.map(tuple => { | |
if (mouse[0] > 500) { | |
const point = fisheyeDeprecated({ x: tuple[0], y: tuple[1] }); | |
return [point.x, point.y]; | |
} else { | |
return fisheye(tuple); | |
} | |
}) | |
); | |
}); | |
}); | |
</script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment