In this section, we will use the Stereographic projection and a star catalog to create an interactive celestial map. The Stereographic projection displays the sphere as seen from inside. A star map created with the Stereographic projection is shown in the following screenshot:
Celestial coordinate systems describe positions of celestial objects as seen from Earth. As the Earth rotates on its axis and around the Sun, the position of the stars relative to points on the surface of the Earth changes. Besides rotation and translation, a third movement called precession slowly rotates the Earth's axis by one degree every 72 years. The Equatorial coordinate system describes the position of stars by two coordinates, the declination and the angle from the projection of the Earth's equator to the poles. This angle is equivalent to the Earth's longitude. The right ascension is the angle measured from the intersection of the celestial equator to the ecliptic, measured eastward. The ecliptic is the projection of the Earth's orbit in the celestial sphere. The right ascension is measured in hours instead of degrees, but it is the equivalent of longitude.
To create our star map, we will use the HYG database, which is a celestial catalog that combines information from the Hipparcos Catalog, the Yale Bright Star Catalog, and the Gliese Catalog of Nearby Stars. This contains about 120,000 stars, most of which are not visible to the naked eye. The most recent version of the HYG database is available at https://github.com/astronexus/HYG-Database.
As we did in the previous sections, we will add targets to Makefile
in order to download and parse the data files that we need. In order to filter and process the stars of the catalog, we wrote a small Python script that filters out the less bright stars and writes a GeoJSON file that translates the declination and right ascension coordinates to equivalent latitudes and longitudes. Note that due to the rotation of the Earth, the equivalent longitude is not related to the Earth's longitude, but it would be useful to create our visualization. We can compute a coordinate equivalent to the right ascension using the longitude = 360 * RA / 24 - 180
expression. The generated GeoJSON file will have the following structure:
{ "type": "FeatureCollection", "features": [ { "geometry": { "type": "Point", "coordinates": [-179.6006208, -77.06529438] }, "type": "Feature", "properties": { "color": 1.254, "name": "", "mag": 4.78 } }, ... ] }
In this case, every feature in the GeoJSON file is a point. We will begin by creating a chart using the Equirectangular projection to have a complete view of the sky while we are implementing the map and change the projection to stereographic later. We begin by loading the GeoJSON data and creating the svg container for the map:
d3.json('/chapter11/data/hyg.json', function(error, data) { // Handle errors getting and parsing the data if (error) { console.log(error); } // Container width and height var width = 600, height = 300; // Select the container div and creates the svg container var div = d3.select('#equirectangular'), svg = div.append('svg') .attr('width', width) .attr('height', height); // Creates an instance of the Equirectangular projection... });
We create and configure an instance of the Equirectangular projection, setting the scale to display the entire sky in the svg container:
// Creates an instance of the Equirectangular projection var projection = d3.geo.equirectangular() .scale(width / (2 * Math.PI)) .translate([width / 2, height / 2]);
We will represent the stars with small circles, with bigger circles for brighter stars. For this, we need to create a scale for the radius, which will map the apparent magnitude of each star to a radius. Lower magnitude values correspond to brighter stars:
// Magnitude extent var magExtent = d3.extent(data.features, function(d) { return d.properties.mag; }); // Compute the radius for the point features var rScale = d3.scale.linear() .domain(magExtent) .range([3, 1]);
By default, the path generator will create circles of constant radius for features of the Point
type. We can configure the radius by setting the path's pathRadius
attribute. As we might use the same path generator to draw point features other than stars, we will return a default value if the feature doesn't have the properties
attribute:
// Create and configure the geographic path generator var path = d3.geo.path() .projection(projection) .pointRadius(function(d) { return d.properties ? rScale(d.properties.mag) : 1; });
With our path generator configured, we can append the graticule lines and the features for the stars to the svg container:
// Add graticule lines var graticule = d3.geo.graticule(); svg.selectAll('path.graticule-black').data([graticule()]) .enter().append('path') .attr('class', 'graticule-black') .attr('d', path); // Draw the stars in the chart svg.selectAll('path.star-black').data(data.features) .enter().append('path') .attr('class', 'star-black') .attr('d', path);
We obtain the following celestial map, which shows us the graticule and the stars as small black circles:
We will replace the Equirectangular projection with the Stereographic projection and add styles to the elements of the map to make it more attractive. We will use the drag behavior to allow the user to rotate the chart. As we did with the rotating globe, we will create a variable to store the current rotation of the projection:
// Store the current rotation of the projection var rotate = {x: 0, y: 45};
We can create and configure an instance of the Stereographic projection. We will choose a suitable scale, translate the projection center to the center of the svg container, and clip the projection to show only a small part of the sphere at a time. We use the rotation variable to set the initial rotation of the projection:
// Create an instance of the Stereographic projection var projection = d3.geo.stereographic() .scale(1.5 * height / Math.PI) .translate([width / 2, height / 2]) .clipAngle(120) .rotate([rotate.x, -rotate.y]);
We won't duplicate the code to create the svg container, graticule, and features because it's very similar to the rotating globe from earlier. The complete code for this example is available in the chapter11/03-celestial-sphere
file. In this example, we also have an invisible overlay. We create and configure a drag behavior instance and add event listeners for the drag gesture to the invisible overlay:
// Create and configure the drag behavior var dragBehavior = d3.behavior.drag() .on('drag', drag); // Add event listeners for drag gestures to the overlay overlay.call(dragBehavior);
The drag
function will be invoked when the user drags the map. For drag
events, the d3.event
object stores the gesture's x and y coordinates. We will transform the coordinates to horizontal and vertical rotations of the projection with the same method as the one used in the previous section:
// Callback for drag gestures function drag(d) { // Compute the projection rotation angles d.x = 180 * d3.event.x / width; d.y = -180 * d3.event.y / height; // Updates the projection rotation... }
We update the projection rotation and the paths of the stars and graticule lines. As we have clipping in this example, the path will be undefined for stars outside the clipping angle. In this case, we return a dummy svg command that just moves the drawing cursor to avoid getting errors:
// Updates the projection rotation projection.rotate([d.x, d.y]); // Update the paths for the stars and graticule lines stars.attr('d', function(u) { return path(u) ? path(u) : 'M 10 10'; }); lines.attr('d', path);
In the style sheet file, chapter11/maps.css
, we have included styles for this map to display a dark blue background, light graticule lines, and white stars. The result is a rotating star map that displays a coarse approximation of how the stars look from Earth.
We will create a fullscreen version of the star map. The source code for this version of the map is available in the chapter11/04-fullscreen
file in the code bundle. For this example, we need to set the body element and the container div to cover the complete viewport. We set the width and height of the body, HTML, and the container div to 100 percent and set the padding and margins to zero. To create the svg element with the correct size, we need to retrieve the width and height in pixels, which are computed by the browser when it renders the page:
// Computes the width and height of the container div var width = parseInt(div.style('width'), 10), height = parseInt(div.style('height'), 10);
We create the projection and the path generator as done earlier. In this version, we will add colors to the stars. Each star feature contains the attribute color, which indicates the color index of the star. The color index is a number that characterizes the color of the star. We can't compute a precise scale for the color index, but we will use a color scale that approximates the colors:
// Approximation of the colors of the stars var cScale = d3.scale.linear() .domain([-0.3, 0, 0.6, 0.8, 1.42]) .range(['#6495ed', '#fff', '#fcff6c', '#ffb439', '#ff4039']);
We will set the color to the features using the fill
attribute of the paths that correspond to the stars:
// Add the star features to the svg container var stars = svg.selectAll('path.star-color') .data(data.features) .enter().append('path') .attr('class', 'star-color') .attr('d', path) .attr('fill', function(d) { return cScale(d.properties.color); });
We will also add labels for each star. Here, we use the projection directly to compute the position where the labels should be, and we also compute a small offset:
// Add labels for the stars var name = svg.selectAll('text').data(data.features) .enter().append('text') .attr('class', 'star-label') .attr('x', function(d) { return projection(d.geometry.coordinates)[0] + 8; }) .attr('y', function(d) { return projection(d.geometry.coordinates)[1] + 8; }) .text(function(d) { return d.properties.name; }) .attr('fill', 'white'),
The star map visualization in fullscreen is shown in the following screenshot:
We create the overlay and the drag behavior and configure the callback of the zoom
event as earlier, updating the position of the labels in the zoom
function. Now we have a fullscreen rotating star map.