Skip to content

Instantly share code, notes, and snippets.

@duaneatat
Forked from mbostock/.block
Last active February 5, 2024 17:19
Show Gist options
  • Save duaneatat/315b00c4747e747054ba0287035794d6 to your computer and use it in GitHub Desktop.
Save duaneatat/315b00c4747e747054ba0287035794d6 to your computer and use it in GitHub Desktop.
Smoothed Fisheye (d3-fisheye)
license: gpl-3.0
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();
};
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');
}
<!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