Skip to content

Instantly share code, notes, and snippets.

@enjalot
Last active November 1, 2015 02:57
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 enjalot/d0c8b275a5a4fc4d3751 to your computer and use it in GitHub Desktop.
Save enjalot/d0c8b275a5a4fc4d3751 to your computer and use it in GitHub Desktop.
spherical coordinates
.parcoords > svg, .parcoords > canvas {
font: 14px sans-serif;
position: absolute;
}
.parcoords > canvas {
pointer-events: none;
}
.parcoords rect.background {
fill: transparent;
}
.parcoords rect.background:hover {
fill: rgba(120,120,120,0.2);
}
.parcoords .resize rect {
fill: rgba(0,0,0,0.1);
}
.parcoords rect.extent {
fill: rgba(255,255,255,0.25);
stroke: rgba(0,0,0,0.6);
}
.parcoords .axis line, .parcoords .axis path {
fill: none;
stroke: #222;
shape-rendering: crispEdges;
}
.parcoords canvas {
opacity: 1;
-moz-transition: opacity 0.3s;
-webkit-transition: opacity 0.3s;
-o-transition: opacity 0.3s;
}
.parcoords canvas.faded {
opacity: 0.25;
}
d3.parcoords = function(config) {
var __ = {
data: [],
dimensions: [],
types: {},
brushed: false,
mode: "default",
rate: 10,
width: 600,
height: 300,
margin: { top: 24, right: 0, bottom: 12, left: 0 },
color: "#069",
composite: "source-over",
alpha: "0.7"
};
extend(__, config);
var pc = function(selection) {
selection = pc.selection = d3.select(selection);
__.width = selection[0][0].clientWidth;
__.height = selection[0][0].clientHeight;
// canvas data layers
["extents", "shadows", "marks", "foreground", "highlight"].forEach(function(layer) {
canvas[layer] = selection
.append("canvas")
.attr("class", layer)[0][0];
ctx[layer] = canvas[layer].getContext("2d");
});
// svg tick and brush layers
pc.svg = selection
.append("svg")
.attr("width", __.width)
.attr("height", __.height)
.append("svg:g")
.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
return pc;
};
var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush"].concat(d3.keys(__))),
w = function() { return __.width - __.margin.right - __.margin.left; },
h = function() { return __.height - __.margin.top - __.margin.bottom },
flags = {
brushable: false,
reorderable: false,
axes: false,
interactive: false,
shadows: false,
debug: false
},
xscale = d3.scale.ordinal(),
yscale = {},
dragging = {},
line = d3.svg.line(),
axis = d3.svg.axis().orient("left").ticks(5),
g, // groups for axes, brushes
ctx = {},
canvas = {};
// side effects for setters
var side_effects = d3.dispatch.apply(this,d3.keys(__))
.on("composite", function(d) { ctx.foreground.globalCompositeOperation = d.value; })
.on("alpha", function(d) { ctx.foreground.globalAlpha = d.value; })
.on("width", function(d) { pc.resize(); })
.on("height", function(d) { pc.resize(); })
.on("margin", function(d) { pc.resize(); })
.on("rate", function(d) { rqueue.rate(d.value); })
.on("data", function(d) {
if (flags.shadows) paths(__.data, ctx.shadows);
})
.on("dimensions", function(d) {
xscale.domain(__.dimensions);
if (flags.interactive) pc.render().updateAxes();
});
pc.toString = function() { return "Parallel Coordinates: " + __.dimensions.length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };
// expose the state of the chart
pc.state = __;
pc.flags = flags;
// create getter/setters
getset(pc, __, events);
// expose events
d3.rebind(pc, events, "on");
// tick formatting
d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");
pc.autoscale = function() {
// xscale
xscale.rangePoints([0, w()], 1);
// yscale
__.dimensions.forEach(function(k) {
yscale[k] = d3.scale.linear()
.domain(d3.extent(__.data, function(d) { return +d[k]; }))
.range([h()+1, 1])
});
// canvas sizes
pc.selection.selectAll("canvas")
.style("margin-top", __.margin.top + "px")
.style("margin-left", __.margin.left + "px")
.attr("width", w()+2)
.attr("height", h()+2)
// default styles, needs to be set when canvas width changes
ctx.foreground.strokeStyle = __.color;
ctx.foreground.lineWidth = 1.4;
ctx.foreground.globalCompositeOperation = __.composite;
ctx.foreground.globalAlpha = __.alpha;
ctx.highlight.lineWidth = 3;
ctx.shadows.strokeStyle = "#dadada";
ctx.extents.strokeStyle = "rgba(140,140,140,0.25)";
ctx.extents.fillStyle = "rgba(255,255,255,0.4)";
return this;
};
pc.detectDimensions = function() {
pc.types(d3.parcoords.detectDimensionTypes(__.data));
pc.dimensions(d3.parcoords.quantitative(__.data));
return this;
};
var rqueue = d3.renderQueue(path_foreground)
.rate(50)
.clear(function() { pc.clear('foreground'); });
pc.render = function() {
// try to autodetect dimensions and create scales
if (!__.dimensions.length) pc.detectDimensions();
if (!(__.dimensions[0] in yscale)) pc.autoscale();
pc.render[__.mode]();
events.render.call(this);
return this;
};
pc.render.default = function() {
pc.clear('foreground');
if (__.brushed) {
__.brushed.forEach(path_foreground);
} else {
__.data.forEach(path_foreground);
}
};
pc.render.queue = function() {
if (__.brushed) {
rqueue(__.brushed);
} else {
rqueue(__.data);
}
};
pc.shadows = function() {
flags.shadows = true;
if (__.data.length > 0) paths(__.data, ctx.shadows);
return this;
};
pc.axisDots = function() {
var ctx = pc.ctx.marks;
ctx.globalAlpha = d3.min([1/Math.pow(data.length, 1/2), 1]);
__.data.forEach(function(d) {
__.dimensions.map(function(p,i) {
ctx.fillRect(position(p)-0.75,yscale[p](d[p])-0.75,1.5,1.5);
});
});
return this;
};
pc.clear = function(layer) {
ctx[layer].clearRect(0,0,w()+2,h()+2);
return this;
};
pc.createAxes = function() {
if (g) pc.removeAxes();
// Add a group element for each dimension.
g = pc.svg.selectAll(".dimension")
.data(__.dimensions, function(d) { return d; })
.enter().append("svg:g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(" + xscale(d) + ")"; })
// Add an axis and title.
g.append("svg:g")
.attr("class", "axis")
.attr("transform", "translate(0,0)")
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
.append("svg:text")
.attr({
"text-anchor": "middle",
"y": 0,
"transform": "translate(0,-12)",
"x": 0,
"class": "label"
})
.text(String)
flags.axes= true;
return this;
};
pc.removeAxes = function() {
g.remove();
return this;
};
pc.updateAxes = function() {
var g_data = pc.svg.selectAll(".dimension")
.data(__.dimensions, function(d) { return d; })
g_data.enter().append("svg:g")
.attr("class", "dimension")
.attr("transform", function(p) { return "translate(" + position(p) + ")"; })
.style("opacity", 0)
.append("svg:g")
.attr("class", "axis")
.attr("transform", "translate(0,0)")
.each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
.append("svg:text")
.attr({
"text-anchor": "middle",
"y": 0,
"transform": "translate(0,-12)",
"x": 0,
"class": "label"
})
.text(String);
g_data.exit().remove();
g = pc.svg.selectAll(".dimension");
g.transition().duration(1100)
.attr("transform", function(p) { return "translate(" + position(p) + ")"; })
.style("opacity", 1)
if (flags.shadows) paths(__.data, ctx.shadows);
return this;
};
pc.brushable = function() {
if (!g) pc.createAxes();
// Add and store a brush for each axis.
g.append("svg:g")
.attr("class", "brush")
.each(function(d) {
d3.select(this).call(
yscale[d].brush = d3.svg.brush()
.y(yscale[d])
.on("brush", pc.brush)
);
})
.selectAll("rect")
.style("visibility", null)
.attr("x", -15)
.attr("width", 30)
flags.brushable = true;
return this;
};
// Jason Davies, http://bl.ocks.org/1341281
pc.reorderable = function() {
if (!g) pc.createAxes();
g.style("cursor", "move")
.call(d3.behavior.drag()
.on("dragstart", function(d) {
dragging[d] = this.__origin__ = xscale(d);
})
.on("drag", function(d) {
dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
__.dimensions.sort(function(a, b) { return position(a) - position(b); });
xscale.domain(__.dimensions);
pc.render();
g.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
})
.on("dragend", function(d) {
delete this.__origin__;
delete dragging[d];
d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
pc.render();
}));
flags.reorderable = true;
return this;
};
pc.interactive = function() {
flags.interactive = true;
return this;
};
// Get data within brushes
pc.brush = function() {
__.brushed = selected();
pc.render();
//extent_area();
events.brush.call(pc,__.brushed);
};
// expose a few objects
pc.xscale = xscale;
pc.yscale = yscale;
pc.ctx = ctx;
pc.canvas = canvas;
pc.g = function() { return g; };
// TODO
pc.brushReset = function(dimension) {
yscale[dimension].brush.clear()(
pc.g()
.filter(function(p) {
return dimension == p;
})
)
return this;
};
// rescale for height, width and margins
pc.resize = function() {
// selection size
pc.selection.select("svg")
.attr("width", __.width)
.attr("height", __.height)
pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
// scales
pc.autoscale();
// axes
if (g) {
g.attr("transform", function(d) { return "translate(" + xscale(d) + ")"; })
g.selectAll("g.axis").each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
};
pc.render();
events.resize.call(this, {width: __.width, height: __.height, margin: __.margin});
return this;
};
// highlight an array of data
pc.highlight = function(data) {
pc.clear("highlight");
d3.select(canvas.foreground).classed("faded", true);
data.forEach(path_highlight);
events.highlight.call(this,data);
return this;
};
// clear highlighting
pc.unhighlight = function(data) {
pc.clear("highlight");
d3.select(canvas.foreground).classed("faded", false);
return this;
};
// draw single polyline
function color_path(d, ctx) {
ctx.strokeStyle = d3.functor(__.color)(d);
ctx.beginPath();
__.dimensions.map(function(p,i) {
if (i == 0) {
ctx.moveTo(position(p),yscale[p](d[p]));
} else {
ctx.lineTo(position(p),yscale[p](d[p]));
}
});
ctx.stroke();
};
// draw many polylines of the same color
function paths(data, ctx) {
ctx.clearRect(-1,-1,w()+2,h()+2);
ctx.beginPath();
data.forEach(function(d) {
__.dimensions.map(function(p,i) {
if (i == 0) {
ctx.moveTo(position(p),yscale[p](d[p]));
} else {
ctx.lineTo(position(p),yscale[p](d[p]));
}
});
});
ctx.stroke();
};
function extent_area() {
pc.clear('extents');
// no active brushes
var actives = __.dimensions.filter(is_brushed);
if (actives.length == 0) return;
// create envelope
var ctx = pc.ctx.extents;
ctx.beginPath();
__.dimensions.map(function(p,i) {
if (i == 0) {
ctx.moveTo(xscale(p), brush_max(p));
} else {
ctx.lineTo(xscale(p), brush_max(p));
}
});
__.dimensions.reverse().map(function(p,i) {
ctx.lineTo(xscale(p), brush_min(p));
});
ctx.fill();
ctx.stroke();
};
function is_brushed(p) {
return !yscale[p].brush.empty();
};
function brush_max(p) {
return is_brushed(p) ? yscale[p](yscale[p].brush.extent()[1]) : 0;
};
function brush_min(p) {
return is_brushed(p) ? yscale[p](yscale[p].brush.extent()[0]) : h();
};
function position(d) {
var v = dragging[d];
return v == null ? xscale(d) : v;
}
// data within extents
function selected() {
var actives = __.dimensions.filter(is_brushed),
extents = actives.map(function(p) { return yscale[p].brush.extent(); });
return __.data
.filter(function(d) {
return actives.every(function(p, dimension) {
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1];
});
});
};
function path_foreground(d) {
return color_path(d, ctx.foreground);
};
function path_highlight(d) {
return color_path(d, ctx.highlight);
};
// getter/setter with event firing
function getset(obj,state,events) {
d3.keys(state).forEach(function(key) {
obj[key] = function(x) {
if (!arguments.length) return state[key];
var old = state[key];
state[key] = x;
side_effects[key].call(pc,{"value": x, "previous": old});
events[key].call(pc,{"value": x, "previous": old});
return obj;
};
});
};
function extend(target, source) {
for (key in source) {
target[key] = source[key];
}
return target;
};
return pc;
};
d3.parcoords.version = "0.1.6";
// quantitative dimensions based on numerical or null values in the first row
d3.parcoords.quantitative = function(data) {
return d3.keys(data[0])
.filter(function(col) {
var v = data[0][col];
return (parseFloat(v) == v) && (v != null);
});
};
// a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
d3.parcoords.toType = function(v) {
return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
};
// try to coerce to number before returning type
d3.parcoords.toTypeCoerceNumbers = function(v) {
if ((parseFloat(v) == v) && (v != null)) return "number";
return d3.parcoords.toType(v);
};
// attempt to determine types of each dimension based on first row of data
d3.parcoords.detectDimensionTypes = function(data) {
var types = {}
d3.keys(data[0])
.forEach(function(col) {
types[col] = d3.parcoords.toTypeCoerceNumbers(data[0][col]);
});
return types;
};
// pairs of adjacent dimensions
d3.parcoords.adjacent_pairs = function(arr) {
var ret = [];
for (var i = 0; i < arr.length-1; i++) {
ret.push([arr[i],arr[i+1]]);
};
return ret;
};
// calculate 2d intersection of line a->b with line c->d
// points are objects with x and y properties
d3.parcoords.intersection = function(a, b, c, d) {
return {
x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x))
};
};
d3.renderQueue = (function(func) {
var _queue = [], // data to be rendered
_rate = 10, // number of calls per frame
_invalidate = function() {}, // invalidate last render queue
_clear = function() {}; // clearing function
var rq = function(data) {
if (data) rq.data(data);
_invalidate();
_clear();
rq.render();
};
rq.render = function() {
var valid = true;
_invalidate = rq.invalidate = function() {
valid = false;
};
function doFrame() {
if (!valid) return true;
if (!_queue.length) return true;
var chunk = _queue.splice(0,_rate);
chunk.map(func);
timer_frame(doFrame);
}
doFrame();
};
rq.data = function(data) {
_invalidate();
_queue = data.slice(0);
return rq;
};
rq.add = function(data) {
_queue = _queue.concat(data);
};
rq.rate = function(value) {
if (!arguments.length) return _rate;
_rate = value;
return rq;
};
rq.remaining = function() {
return _queue.length;
};
// clear the canvas
rq.clear = function(func) {
if (!arguments.length) {
_clear();
return rq;
}
_clear = func;
return rq;
};
rq.invalidate = _invalidate;
var timer_frame = window.requestAnimationFrame
|| window.webkitRequestAnimationFrame
|| window.mozRequestAnimationFrame
|| window.oRequestAnimationFrame
|| window.msRequestAnimationFrame
|| function(callback) { setTimeout(callback, 17); };
return rq;
});
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="trackball.js"></script>
<script src="d3.parcoords.js"></script>
<link rel="stylesheet" type="text/css" href="d3.parcoords.css"></link>
<style>
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; }
svg {
width: 100%; height: 300px;
float: left;
}
.parcoords {
width: 100%; height: 200px;
float:left; clear: left;
}
path.foreground {
fill: none;
stroke: #333;
stroke-width: 1.5px;
}
path.graticule {
fill: none;
stroke: #aaa;
stroke-width: .5px;
}
</style>
</head>
<body>
<svg></svg>
<div class="parcoords"></div>
<script>
var zero = 1e-6;
var normals = [
{ x: 0, y: 0, z: 1, i: 0 },
{ x: 0, y: 1, z: 0, i: 1 },
{ x: 1, y: 0, z: 0, i: 2 },
]
// Parallel coordinates
var colorScale = d3.scale.category10();
var colorScale = d3.scale.ordinal()
.range(["#f00", "#0f0", "#00f"])
function color(d) {
return colorScale(d.i);
}
var pc = d3.parcoords()(".parcoords")
.dimensions(['x', 'y', 'z'])
.data([
{x: -1, y: -1, z: -1},
{x: 1, y: 1, z: 1}
])
.autoscale()
.color(color)
.alpha(0.8)
.createAxes()
.data(normals)
.render();
// "Globe" -> Unit sphere
var map_width = 300;
var map_height = 300;
var scale = (map_width - 1) / 2 / Math.PI * 2.5
var projection = d3.geo.orthographic()
.translate([map_width/2, map_height / 2])
.scale(scale)
.rotate([0,0,0])
.clipAngle(90)
var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select("svg")
var left = svg.append("g")
.attr("transform", "translate(100, 0)")
left.selectAll("line.normal")
.data(normals)
.enter().append("line").classed("normal", true)
.attr({
stroke: color,
x1: map_width/2,
y1: map_height/2,
x2: getX,
y2: getY
})
left.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
left.selectAll("circle.normal")
.data(normals)
.enter().append("circle").classed("normal", true)
.attr({
r: 5,
fill: color,
cx: getX,
cy: getY
})
d3.behavior.trackball(svg).on("rotate", function(rot) {
//update the rotation in our projection
projection.rotate(rot);
//redraw our visualization with the updated projection
left.selectAll("path.graticule")
.attr("d", path)
left.selectAll("circle.normal")
.attr({
cx: getX,
cy: getY
})
left.selectAll("line.normal")
.attr({
x2: getX,
y2: getY
})
//update the parallel coordinates
var rotated = normals.map(function(n) {
var ll = unit2latlon(n);
ll[0] += rot[0];
ll[1] += rot[1];
var r = latlon2unit(ll)
r.i = n.i;
return r;
})
pc.data(rotated)
.render();
})
function getX(d) {
var ll = unit2latlon(d);
return projection(ll)[0]
}
function getY(d) {
var ll = unit2latlon(d);
return projection(ll)[1]
}
// convert our unit vectors into lat/lon
function unit2latlon(v) {
//http://stackoverflow.com/questions/5674149/3d-coordinates-on-a-sphere-to-latitude-and-longitude
var r = 1; //Math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
//var theta = Math.acos(v.z/r);
//var phi = Math.atan(v.x/(v.y ? v.y : zero));
// we switch the given formulation with z -> x and y <-> z
// so that our axes match the traditional x is right, y is up and z is out
var theta = Math.acos(v.x/r);
var phi = Math.atan(v.y/(v.z ? v.z : zero));
// lat, lon
return [90 - rad2deg(theta), rad2deg(phi)]
}
function rad2deg(r) {
return r * 180/Math.PI;
}
function deg2rad(d) {
return d * Math.PI/180;
}
function latlon2unit(latlon) {
var lat = latlon[0];
var lon = latlon[1];
if(!lat) lat = zero;
if(!lon) lon = zero;
lat = deg2rad(lat);
lon = deg2rad(lon);
// we switch the given formulation with z -> x and y <-> z
var z = Math.cos(lat) * Math.cos(lon);
var y = Math.cos(lat) * Math.sin(lon);
var x = Math.sin(lat)
return {x: x, y:y, z: z}
}
// on rotate
</script>
</body>
// this isn't structured like a proper behavior. something
// that would probably be useful, for now I just want to encapsulate it
// all the code below comes from http://bl.ocks.org/patricksurry/5721459
d3.behavior.trackball = function(svg) {
svg
.on("mousedown", mousedown)
.on("mousemove", mousemove)
.on("mouseup", mouseup);
function trackballAngles(pt) {
// based on http://www.opengl.org/wiki/Trackball
// given a click at (x,y) in canvas coords on the globe (trackball),
// calculate the spherical coordianates for the point as a rotation around
// the vertical and horizontal axes
var r = projection.scale();
var c = projection.translate();
var x = pt[0] - c[0], y = - (pt[1] - c[1]), ss = x*x + y*y;
var z = r*r > 2 * ss ? Math.sqrt(r*r - ss) : r*r / 2 / Math.sqrt(ss);
var lambda = Math.atan2(x, z) * 180 / Math.PI;
var phi = Math.atan2(y, z) * 180 / Math.PI
return [lambda, phi];
}
/*
This is the cartesian equivalent of the rotation matrix,
which is the product of the following rotations (in numbered order):
1. longitude: λ around the y axis (which points up in the canvas)
2. latitude: -ϕ around the x axis (which points right in the canvas)
3. yaw: γ around the z axis (which points out of the screen)
NB. If you measure rotations in a positive direction according to the right-hand rule
(point your right thumb in the positive direction of the rotation axis, and rotate in the
direction of your curled fingers), then the latitude rotation is negative.
R(λ, ϕ, γ) =
[[ sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ), −sin(γ)cos(ϕ), −sin(γ)sin(ϕ)cos(λ)+sin(λ)cos(γ)],
[ −sin(λ)sin(ϕ)cos(γ)+sin(γ)cos(λ), cos(γ)cos(ϕ), sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ)],
[ −sin(λ)cos(ϕ), −sin(ϕ), cos(λ)cos(ϕ)]]
If you then apply a "trackball rotation" of δλ around the y axis, and -δϕ around the
x axis, you get this horrible composite matrix:
R2(λ, ϕ, γ, δλ, δϕ) =
[[−sin(δλ)sin(λ)cos(ϕ)+(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))cos(δλ),
−sin(γ)cos(δλ)cos(ϕ)−sin(δλ)sin(ϕ),
sin(δλ)cos(λ)cos(ϕ)−(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))cos(δλ)],
[−sin(δϕ)sin(λ)cos(δλ)cos(ϕ)−(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))sin(δλ)sin(δϕ)−(sin(λ)sin(ϕ)cos(γ)−sin(γ)cos(λ))cos(δϕ),
sin(δλ)sin(δϕ)sin(γ)cos(ϕ)−sin(δϕ)sin(ϕ)cos(δλ)+cos(δϕ)cos(γ)cos(ϕ),
sin(δϕ)cos(δλ)cos(λ)cos(ϕ)+(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))sin(δλ)sin(δϕ)+(sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ))cos(δϕ)],
[−sin(λ)cos(δλ)cos(δϕ)cos(ϕ)−(sin(γ)sin(λ)sin(ϕ)+cos(γ)cos(λ))sin(δλ)cos(δϕ)+(sin(λ)sin(ϕ)cos(γ)−sin(γ)cos(λ))sin(δϕ),
sin(δλ)sin(γ)cos(δϕ)cos(ϕ)−sin(δϕ)cos(γ)cos(ϕ)−sin(ϕ)cos(δλ)cos(δϕ),
cos(δλ)cos(δϕ)cos(λ)cos(ϕ)+(sin(γ)sin(ϕ)cos(λ)−sin(λ)cos(γ))sin(δλ)cos(δϕ)−(sin(ϕ)cos(γ)cos(λ)+sin(γ)sin(λ))sin(δϕ)]]
by equating components of the matrics
(label them [[a00, a01, a02], [a10, a11, a12], [a20, a21, a22]])
we can find an equivalent rotation R(λ', ϕ', γ') == RC(λ, ϕ, γ, δλ, δϕ) :
if cos(ϕ') != 0:
γ' = atan2(-RC01, RC11)
ϕ' = atan2(-RC21, γ' == 0 ? RC11 / cos(γ') : - RC01 / sin(γ'))
λ' = atan2(-RC20, RC22)
else:
// when cos(ϕ') == 0, RC21 == - sin(ϕ') == +/- 1
// the solution is degenerate, requiring just that
// γ' - λ' = atan2(RC00, RC10) if RC21 == -1 (ϕ' = π/2)
// or γ' + λ' = atan2(RC00, RC10) if RC21 == 1 (ϕ' = -π/2)
// so choose:
γ' = atan2(RC10, RC00) - RC21 * λ
ϕ' = - RC21 * π/2
λ' = λ
*/
function composedRotation(λ, ϕ, γ, δλ, δϕ) {
λ = Math.PI / 180 * λ;
ϕ = Math.PI / 180 * ϕ;
γ = Math.PI / 180 * γ;
δλ = Math.PI / 180 * δλ;
δϕ = Math.PI / 180 * δϕ;
var sλ = Math.sin(λ), sϕ = Math.sin(ϕ), sγ = Math.sin(γ),
sδλ = Math.sin(δλ), sδϕ = Math.sin(δϕ),
cλ = Math.cos(λ), cϕ = Math.cos(ϕ), cγ = Math.cos(γ),
cδλ = Math.cos(δλ), cδϕ = Math.cos(δϕ);
var m00 = -sδλ * sλ * cϕ + (sγ * sλ * sϕ + cγ * cλ) * cδλ,
m01 = -sγ * cδλ * cϕ - sδλ * sϕ,
m02 = sδλ * cλ * cϕ - (sγ * sϕ * cλ - sλ * cγ) * cδλ,
m10 = - sδϕ * sλ * cδλ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * sδϕ - (sλ * sϕ * cγ - sγ * cλ) * cδϕ,
m11 = sδλ * sδϕ * sγ * cϕ - sδϕ * sϕ * cδλ + cδϕ * cγ * cϕ,
m12 = sδϕ * cδλ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * sδϕ + (sϕ * cγ * cλ + sγ * sλ) * cδϕ,
m20 = - sλ * cδλ * cδϕ * cϕ - (sγ * sλ * sϕ + cγ * cλ) * sδλ * cδϕ + (sλ * sϕ * cγ - sγ * cλ) * sδϕ,
m21 = sδλ * sγ * cδϕ * cϕ - sδϕ * cγ * cϕ - sϕ * cδλ * cδϕ,
m22 = cδλ * cδϕ * cλ * cϕ + (sγ * sϕ * cλ - sλ * cγ) * sδλ * cδϕ - (sϕ * cγ * cλ + sγ * sλ) * sδϕ;
if (m01 != 0 || m11 != 0) {
γ_ = Math.atan2(-m01, m11);
ϕ_ = Math.atan2(-m21, Math.sin(γ_) == 0 ? m11 / Math.cos(γ_) : - m01 / Math.sin(γ_));
λ_ = Math.atan2(-m20, m22);
} else {
γ_ = Math.atan2(m10, m00) - m21 * λ;
ϕ_ = - m21 * Math.PI / 2;
λ_ = λ;
}
return([λ_ * 180 / Math.PI, ϕ_ * 180 / Math.PI, γ_ * 180 / Math.PI]);
}
var m0 = null,
o0;
var dispatch = d3.dispatch("rotate")
function mousedown() { // remember where the mouse was pressed, in canvas coords
m0 = trackballAngles(d3.mouse(svg[0][0]));
o0 = projection.rotate();
d3.event.preventDefault();
}
function mousemove() {
if (m0) { // if mousedown
var m1 = trackballAngles(d3.mouse(svg[0][0]));
// we want to find rotate the current projection so that the point at m0 rotates to m1
// along the great circle arc between them.
// when the current projection is at rotation(0,0), with the north pole aligned
// to the vertical canvas axis, and the equator aligned to the horizontal canvas
// axis, this is easy to do, since D3's longitude rotation corresponds to trackball
// rotation around the vertical axis, and then the subsequent latitude rotation
// corresponds to the trackball rotation around the horizontal axis.
// But if the current projection is already rotated, it's harder.
// We need to find a new rotation equivalent to the composition of both
// Choose one of these three update schemes:
// Best behavior
o1 = composedRotation(o0[0], o0[1], o0[2], m1[0] - m0[0], m1[1] - m0[1])
// Improved behavior over original example
//o1 = [o0[0] + (m1[0] - m0[0]), o0[1] + (m1[1] - m0[1])];
// Original example from http://mbostock.github.io/d3/talk/20111018/azimuthal.html
// o1 = [o0[0] - (m0[0] - m1[0]) / 8, o0[1] - (m1[1] - m0[1]) / 8];
// move to the updated rotation
dispatch.rotate(o1);
//projection.rotate(o1);
// We can optionally update the "origin state" at each step. This has the
// advantage that each 'trackball movement' is small, but the disadvantage of
// potentially accumulating many small drifts (you often see a twist creeping in
// if you keep rolling the globe around with the mouse button down)
// o0 = o1;
// m0 = m1;
//svg.selectAll("path").attr("d", path);
}
}
function mouseup() {
if (m0) {
mousemove();
m0 = null;
}
}
return dispatch;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment