Public
Edited
Jul 14, 2023
Insert cell
Insert cell
{
var zero = 1e-6;
var normals = [
{ x: 0, y: 0, z: 1, },
{ x: 0, y: 1, z: 0, },
{ x: 0.5, y: 0, z: 0, },
]
// Parallel coordinates
// var colorScale = d3.scale.category10();
// var colorScale = d3.scale.ordinal()
// .range(["#f00", "#0f0", "#00f"])
// function color(d) {
// return colorScale(d.i);
// }

// "Globe" -> Unit sphere
var scale = map_width/ 2 / Math.PI * 2.5
var projection = d3.geo.orthographic()
.translate([map_width/2, map_height / 2])
.scale(scale)
.rotate([0,0,0])
.clipAngle(null)

var path = d3.geo.path()
.projection(projection);
var graticule = d3.geo.graticule();
var svg = d3.select(DOM.svg())
.attr('width',width)
.attr('height',map_height)
var left = svg.append("g")
// .attr("transform", "translate(100, 0)")
left.selectAll("line.normal")
.data(normals)
.enter().append("line").classed("normal", true)
.attr({
stroke: 'black',
x1: map_width/2,
y1: map_height/2,
x2: getX,
y2: getY
})
left.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path)
.attr('fill','none')
.attr('stroke','#aaa')
.attr('stroke-width',0.5)
left.selectAll("circle.normal")
.data(normals)
.enter().append("circle").classed("normal", true)
.attr({
r: 5,
// fill: color,
cx: getX,
cy: getY
})
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;
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}
}
yield svg.node();
}
Insert cell
map_width = 500
Insert cell
map_height = 500
Insert cell
scale = (map_width - 1) / 2 / Math.PI * 2.5
Insert cell
projection = d3.geo.orthographic()
.translate([map_width/2, map_height / 2])
.scale(scale)
Insert cell
function trackball(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];
}


function composedRotation(λ, ϕ, γ, δλ, δϕ) {
λ = Math.PI / 180 * λ;
ϕ = Math.PI / 180 * ϕ;
γ = Math.PI / 180 * γ;
δλ = Math.PI / 180 * δλ;
δϕ = Math.PI / 180 * δϕ;
var = Math.sin(λ), = Math.sin(ϕ), = Math.sin(γ),
sδλ = Math.sin(δλ), sδϕ = Math.sin(δϕ),
= Math.cos(λ), = Math.cos(ϕ), = 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δϕ;
var γ_ ,ϕ_ , λ_;
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]));
console.log(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
var o1 = composedRotation(o0[0], o0[1], o0[2], m1[0] - m0[0], m1[1] - m0[1])
console.log(o1)

// move to the updated rotation
dispatch.rotate(o1);

}
}

function mouseup() {
if (m0) {
mousemove();
m0 = null;
}
}
return dispatch;
}
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more