Skip to content

Instantly share code, notes, and snippets.

@monfera
Last active August 29, 2015 14:00
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 monfera/11085004 to your computer and use it in GitHub Desktop.
Save monfera/11085004 to your computer and use it in GitHub Desktop.
circinus b

See http://bl.ocks.org/monfera/10527804 for the motivation. This example uses transitions with, or among projections.

  1. It is preferred that a transformation is carried out by changing the parameters of a projection (translate, rotation, parallels etc.). For example, the small multiple line chart, the spiral chart and the transition between them is using the same conic projection, with edge cases of secant parallels and their tweening. Note: cartographic projections are less than ideal for regular charts, but d3.js has a well developed set of geo functions, enabling rapid experiments in line with the motivation linked above - moreover, cartographic projections are themselves necessary for visualizing the often present geographic dimensions.

  2. If it is not possible to stay within one projection, then we need to interpolate between two projections that have an identical edge case, to preserve a continuous visual flow.

It is therefore possible to have a smooth transitioning among views that are traditionally in very different 'chart' pigeonholes, such as maps, map overlays, bubble charts, scatterplots, line charts, radial charts.

Object constancy can be preserved, which can greatly assist users who are not typical consumers of analytics, but who can engage with intuitive data presentation.

The real potential may be beyond the 'gamification' aspect and the reduction of the cognitive load inherent in switching views - we may come up with new ways of presenting information, outside the boxes of traditional chart types and their transitions with object constancy.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.graticule {
fill: none;
stroke: #777;
stroke-width: 1px;
}
.graticule:nth-child(1) {
fill: none;
stroke-opacity: 0.6;
stroke: #000;
stroke-width: 6px;
}
.graticule:nth-child(2) {
fill: none;
stroke-opacity: 0.6;
stroke: #00f;
stroke-width: 6px;
}
.graticule:nth-child(3) {
fill: none;
stroke-opacity: 0.6;
stroke: #f00;
stroke-width: 6px;
}
.graticule:nth-child(4) {
fill: none;
stroke-opacity: 0.6;
stroke: #f70;
stroke-width: 6px;
}
.spiral {
fill: lightblue;
stroke: #000;
stroke-width: 0.5px;
}
</style>
<body></body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.projection.v0.min.js"></script>
<script>
var width = 960,
height = 500;
// Graticule to indicate time of period
var graticule = d3.geo.graticule()
.majorStep([90,90]) // Major step indicates 6-hour intervals
.minorStep([15,0]) // Minor step indicates 1-hour intervals
.majorExtent([[-180,-70], [180,65]])
.minorExtent([[-180,-60], [180,55]])
// Spiral function derived from https://www.jasondavies.com/maps/spiral/
var n = 1e4, dy = 3, rot=50;
var deadArea = 0.2
var spiral = d3.range(0+deadArea, 1 + 1 / n - deadArea, 1 / n).map(function(t) {
return [(360 * rot * t) % 360 - 180, -90 + dy
- Math.random()
+ (Math.cos(100 * Math.PI * t) - 1) / 2
+ (Math.cos(3 * Math.PI * t) - 1) / 2
+ (90 - dy) * 2 * t];
}).concat(d3.range(1-deadArea, 0+ deadArea, -1 / n).map(function(t) {
return [(360 * rot * t) % 360 - 180, -90 + (90 - dy ) * 2 * t];
}));
var spiralPoly = {type: "Polygon", coordinates: [spiral]};
// Interpolating projection from Michael Bostock's http://bl.ocks.org/mbostock/5731632
function interpolatedProjection(a, b) {
var projection = d3.geo.projection(raw).scale(1),
translate = projection.translate,
α;
function raw(λ, φ) {
var pa = a([λ *= 180 / Math.PI, φ *= 180 / Math.PI]), pb = b([λ, φ]);
return [(1 - α) * pa[0] + α * pb[0], (α - 1) * pa[1] - α * pb[1]];
}
projection.alpha = function(_) {
if (!arguments.length) return α;
α = +_;
var ta = a.translate(), tb = b.translate();
translate([(1 - α) * ta[0] + α * tb[0], (1 - α) * ta[1] + α * tb[1]]);
return projection;
};
delete projection.scale;
delete projection.translate;
return projection.alpha(0);
}
// Projection transforms
function pause(proj, directionForward) {
return function(tweenArg) {
};
}
function conicCartesianToSpiral(proj, directionForward, features, path) {
return function(tweenArg) {
var _ = directionForward?tweenArg:(1-tweenArg);
proj.parallels([_*89.99, _*89.99]);
proj.scale((1-_)*(1-_)*110+40);
proj.translate([width / 2 - .5, height / 2 + Math.sqrt(_)*87])
features.attr("d", path);
};
}
function interpolationSpiralToSphere(proj, directionForward, features, path) {
return function(tweenArg) {
var _ = directionForward?tweenArg:(1-tweenArg);
proj.alpha(_);
features.attr("d", path);
};
}
function ortographicSpin(proj, directionForward, features, path) {
var originalRotation = proj.rotate();
return function(tweenArg) {
var _ = directionForward?tweenArg:(1-tweenArg);
proj.clipAngle(95);
proj.rotate([_*360, _*-90, _*-90]);
features.attr("d", path);
};
}
// Showreel with projection/transform pairs
// Items are out of order so one projection can refer to another (e.g. next one)
showReel = []
showReel[0] = {
projection: d3.geo.conicConformal().parallels([0,0]).scale(150).translate([width / 2, height / 2 ]),
transform: pause
};
showReel[1] = {
projection: showReel[0].projection,
transform: conicCartesianToSpiral
};
showReel[3] = {
projection: d3.geo.orthographic().rotate([0,0,0]).scale(250).translate([width / 2 , height / 2 ]),
transform: ortographicSpin
};
showReel[2] = {
projection: interpolatedProjection(showReel[1].projection, showReel[3].projection),
transform: interpolationSpiralToSphere
};
// Animation and initial output
function animate(index, directionForward) {
var delta = directionForward?1:-1,
nextIndex = Math.min(Math.max(index+delta,0), showReel.length-1),
nextDirectionForward = nextIndex===index+delta?directionForward:!directionForward,
keyframe = showReel[index],
path = d3.geo.path().projection(keyframe.projection),
features = render(path);
svg.transition()
.duration(2000)
.tween("projection", function() {
return keyframe.transform(keyframe.projection, directionForward, features, path);
})
.transition()
.duration(0)
.each('end', animate.bind(this, nextIndex, nextDirectionForward));
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
function render(path) {
svg.selectAll(".graticule")
.data(graticule.lines)
.enter().append("path")
.attr("class", "graticule")
.attr("d", path);
svg.selectAll(".spiral")
.data([spiralPoly])
.enter().append("path")
.attr("class", "spiral")
.attr("d", path);
return svg.selectAll("path");
}
animate(0, true);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment