Skip to content

Instantly share code, notes, and snippets.

@patricksurry
Created March 22, 2019 18:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save patricksurry/9631b2e6f6bd0383aec0bb819f9c6e65 to your computer and use it in GitHub Desktop.
Save patricksurry/9631b2e6f6bd0383aec0bb819f9c6e65 to your computer and use it in GitHub Desktop.
Logistic fisheye distortion
<!DOCTYPE html>
<html class="ocks-org do-not-copy">
<meta charset="utf-8">
<title>Fisheye Distortion</title>
<style>
@import url(../style.css?aea6f0a);
#chart1 {
width: 960px;
height: 500px;
border: solid 1px #ccc;
}
text {
font: 10px sans-serif;
}
.background {
fill: none;
pointer-events: all;
}
#chart1 .node {
stroke: #fff;
stroke-width: 1.5px;
}
#chart1 .link {
stroke: #999;
stroke-opacity: .6;
stroke-width: 1.5px;
}
#chart2, #chart3 {
width: 960px;
height: 180px;
border: solid 1px #ccc;
}
#chart2 path, #chart3 line {
fill: none;
stroke: #333;
}
#chart3 line {
shape-rendering: crispEdges;
}
#chart4 {
margin-left: -40px;
height: 506px;
}
#chart4 .background {
fill: #ddd;
}
#chart4 .dot {
stroke: #000;
}
.axis path, .axis line {
fill: none;
stroke: #fff;
shape-rendering: crispEdges;
}
</style>
<header>
<aside>June 21, 2012</aside>
<a href="../" rel="author">Mike Bostock</a>
</header>
<h1>Fisheye Distortion</h1>
<p id="chart1">
<p><aside>Mouseover to distort the nodes.</aside>
<p>It can be difficult to observe micro and macro features simultaneously with complex graphs. If you zoom in for detail, the graph is too big to view in its entirety. If you zoom out to see the overall structure, small details are lost. <a href="http://www.infovis-wiki.net/index.php/Focus-plus-Context">Focus + context</a> techniques allow interactive exploration of an area of interest (the <i>focus</i>) in greater detail, while preserving the surrounding environment (the <i>context</i>).
<p>In the graph above, <b>fisheye distortion</b> magnifies the local region around the mouse, while leaving the larger graph unaffected for context. The localized, circular nature of the distortion can be seen clearly by applying it to a uniform grid:
<p id="chart2">
<p>This type of distortion is particularly useful for disambiguating edge-crossings in static network layouts: edges between distant nodes are distorted more strongly than local ones. If you dislike the chaotic appearance of <a href="http://mbostock.github.com/d3/ex/force.html">dynamic force layout</a>, consider using distortion instead.
<aside>For more network diagrams, see my posts on <a href="../miserables/">matrix diagrams</a>, <a href="../hive/">hive plots</a> and <a href="../uberdata/">chord diagrams</a>.</aside>
<h2><a name="cartesian" href="#cartesian">#</a>Cartesian Distortion</h2>
<p>Circular fisheye is only one of many possible distortion functions. Two disadvantages of circular distortion are that it compresses (rather than magnifies) the area near the circumference of the circle, and that it requires curved grid lines to show the distortion accurately. The latter makes it unsuitable for visualizations that have quantitative position encodings, such as scatterplots.
<p><a href="http://dl.acm.org/citation.cfm?id=142763">Sarkar and Brown</a> therefore recommend a different function that magnifies continuously so as to avoid local minification. Furthermore, they demonstrate applying the distortion to each dimension separately, resulting in <i>Cartesian distortion</i>:
<p id="chart3">
<p>With this technique, straight lines parallel to the <i>x</i> or <i>y</i> axis remain straight even after distortion. This means you can use standard axes in conjunction with fisheye distortion in scatterplots:
<p id="chart4">
<p>Fisheye distortion allows you to zoom into small areas of the chart without losing sense of the overall distribution. For example, the Western European countries (purple) are densely clustered in the <a href="../nations/">original chart</a>, making them difficult to compare; with distortion, you can easily differentiate individual countries while retaining global context and comparing regions.
<h2><a name="implementation" href="#implementation">#</a>Implementation</h2>
<p>These examples use D3’s <a href="https://github.com/d3/d3-plugins/tree/master/fisheye">fisheye plugin</a>, which supports both circular and Cartesian distortion. The latter is implemented on top of D3’s <a href="https://github.com/mbostock/d3/wiki/Quantitative-Scales">quantitative scales</a>, allowing distortion of linear, logarithmic, and exponential scales, as well as compatibility with D3’s <a href="https://github.com/mbostock/d3/wiki/SVG-Axes">axis component</a>. For additional implementations, see <a href="http://flare.prefuse.org">Flare</a> and <a href="http://sigmajs.org">Sigma.js</a>.
<footer>
<aside>June 21, 2012</aside>
<a href="../" rel="author">Mike Bostock</a>
</footer>
<script src="//d3js.org/d3.v2.min.js" charset="utf-8"></script>
<script src="fisheye.js"></script>
<script>
(function chart1() {
var width = 960,
height = 500;
var color = d3.scale.category20();
var fisheye = d3.fisheye.circular()
.radius(120);
var force = d3.layout.force()
.charge(-240)
.linkDistance(40)
.size([width, height]);
var svg = d3.select("#chart1").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
d3.json("miserables.json", function(data) {
var n = data.nodes.length;
force.nodes(data.nodes).links(data.links);
// Initialize the positions deterministically, for better results.
data.nodes.forEach(function(d, i) { d.x = d.y = width / n * i; });
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
// Of course, don't run too long—you'll hang the page!
force.start();
for (var i = n; i > 0; --i) force.tick();
force.stop();
// Center the nodes in the middle.
var ox = 0, oy = 0;
data.nodes.forEach(function(d) { ox += d.x, oy += d.y; });
ox = ox / n - width / 2, oy = oy / n - height / 2;
data.nodes.forEach(function(d) { d.x -= ox, d.y -= oy; });
var link = svg.selectAll(".link")
.data(data.links)
.enter().append("line")
.attr("class", "link")
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; })
.style("stroke-width", function(d) { return Math.sqrt(d.value); });
var node = svg.selectAll(".node")
.data(data.nodes)
.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5)
.style("fill", function(d) { return color(d.group); })
.call(force.drag);
svg.on("mousemove", function() {
fisheye.focus(d3.mouse(this));
node.each(function(d) { d.fisheye = fisheye(d); })
.attr("cx", function(d) { return d.fisheye.x; })
.attr("cy", function(d) { return d.fisheye.y; })
.attr("r", function(d) { return d.fisheye.z * 4.5; });
link.attr("x1", function(d) { return d.source.fisheye.x; })
.attr("y1", function(d) { return d.source.fisheye.y; })
.attr("x2", function(d) { return d.target.fisheye.x; })
.attr("y2", function(d) { return d.target.fisheye.y; });
});
});
})();
(function chart2() {
var width = 960,
height = 180,
xStepsBig = d3.range(10, width, 16),
yStepsBig = d3.range(10, height, 16),
xStepsSmall = d3.range(0, width + 6, 6),
yStepsSmall = d3.range(0, height + 6, 6);
var fisheye = d3.fisheye.circular()
.focus([360, 90])
.radius(100);
var line = d3.svg.line();
var svg = d3.select("#chart2").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", fishline);
svg.on("mousemove", function() {
fisheye.focus(d3.mouse(this));
path.attr("d", fishline);
});
function fishline(d) {
return line(d.map(function(d) {
d = fisheye({x: d[0], y: d[1]});
return [d.x, d.y];
}));
}
})();
(function chart3() {
var width = 960,
height = 180,
xSteps = d3.range(10, width, 16),
ySteps = d3.range(10, height, 16);
var xFisheye = d3.fisheye.scale(d3.scale.identity).domain([0, width]).focus(360),
yFisheye = d3.fisheye.scale(d3.scale.identity).domain([0, height]).focus(90);
var svg = d3.select("#chart3").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);
var xLine = svg.selectAll(".x")
.data(xSteps)
.enter().append("line")
.attr("class", "x")
.attr("y2", height);
var yLine = svg.selectAll(".y")
.data(ySteps)
.enter().append("line")
.attr("class", "y")
.attr("x2", width);
redraw();
svg.on("mousemove", function() {
var mouse = d3.mouse(this);
xFisheye.focus(mouse[0]);
yFisheye.focus(mouse[1]);
redraw();
});
function redraw() {
xLine.attr("x1", xFisheye).attr("x2", xFisheye);
yLine.attr("y1", yFisheye).attr("y2", yFisheye);
}
})();
(function chart4() {
// Various accessors that specify the four dimensions of data to visualize.
function x(d) { return d.income; }
function y(d) { return d.lifeExpectancy; }
function radius(d) { return d.population; }
function color(d) { return d.region; }
// Chart dimensions.
var margin = {top: 5.5, right: 19.5, bottom: 12.5, left: 39.5},
width = 960,
height = 500 - margin.top - margin.bottom;
// Various scales and distortions.
var xScale = d3.fisheye.scale(d3.scale.log).domain([300, 1e5]).range([0, width]),
yScale = d3.fisheye.scale(d3.scale.linear).domain([20, 90]).range([height, 0]),
radiusScale = d3.scale.sqrt().domain([0, 5e8]).range([0, 40]),
colorScale = d3.scale.category10().domain(["Sub-Saharan Africa", "South Asia", "Middle East & North Africa", "America", "Europe & Central Asia", "East Asia & Pacific"]);
// The x & y axes.
var xAxis = d3.svg.axis().orient("bottom").scale(xScale).tickFormat(d3.format(",d")).tickSize(-height),
yAxis = d3.svg.axis().scale(yScale).orient("left").tickSize(-width);
// Create the SVG container and set the origin.
var svg = d3.select("#chart4").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 + ")");
// Add a background rect for mousemove.
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height);
// Add the x-axis.
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the y-axis.
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Add an x-axis label.
svg.append("text")
.attr("class", "x label")
.attr("text-anchor", "end")
.attr("x", width - 6)
.attr("y", height - 6)
.text("income per capita, inflation-adjusted (dollars)");
// Add a y-axis label.
svg.append("text")
.attr("class", "y label")
.attr("text-anchor", "end")
.attr("x", -6)
.attr("y", 6)
.attr("dy", ".75em")
.attr("transform", "rotate(-90)")
.text("life expectancy (years)");
// Load the data.
d3.json("nations.json", function(nations) {
// Add a dot per nation. Initialize the data at 1800, and set the colors.
var dot = svg.append("g")
.attr("class", "dots")
.selectAll(".dot")
.data(nations)
.enter().append("circle")
.attr("class", "dot")
.style("fill", function(d) { return colorScale(color(d)); })
.call(position)
.sort(function(a, b) { return radius(b) - radius(a); });
// Add a title.
dot.append("title")
.text(function(d) { return d.name; });
// Positions the dots based on data.
function position(dot) {
dot .attr("cx", function(d) { return xScale(x(d)); })
.attr("cy", function(d) { return yScale(y(d)); })
.attr("r", function(d) { return radiusScale(radius(d)); });
}
svg.on("mousemove", function() {
var mouse = d3.mouse(this);
xScale.distortion(2.5).focus(mouse[0]);
yScale.distortion(2.5).focus(mouse[1]);
dot.call(position);
svg.select(".x.axis").call(xAxis);
svg.select(".y.axis").call(yAxis);
});
});
})();
</script>
<script>
GoogleAnalyticsObject = "ga", ga = function() { ga.q.push(arguments); }, ga.q = [], ga.l = +new Date;
ga("create", "UA-48272912-3", "ocks.org");
ga("send", "pageview");
</script>
<script async src="//www.google-analytics.com/analytics.js"></script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment