This gist demonstrate how to create a celestial sphere using the orthographic projection.
This gist uses a subset of the stars visible to the naked eye from the combined catalog HYG Databse. To generate the subset, run the command:
make hyg.json
<html>
<head>
<title>Celestial Sphere</title>
<link rel="stylesheet" type="text/css" href="styles.css">
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div id="map"></div>
<script type="text/javascript">
// Set the width and height of the SVG container
var width = 400,
height = 400;
// Select the container div and append the SVG element
var div = d3.select('#map'),
svg = div.append('svg').attr('width', width).attr('height', height),
grp = svg.append('g').attr('class', 'gmap');
// Add a lighting effect to give the circle a spherical aspect
var filter = svg.append('filter').attr('id', 'lightMe');
filter.append('feDiffuseLighting')
.attr('in', 'SourceGraphic')
.attr('result', 'light')
.attr('lighting-color', 'white')
.append('fePointLight')
.attr('x', 0.85 * width)
.attr('y', 0.85 * height)
.attr('z', 50);
filter.append('feComposite')
.attr('in', 'SourceGraphic')
.attr('in2', 'light')
.attr('operator', 'arithmetic')
.attr('k1', '1')
.attr('k2', '0')
.attr('k3', '0')
.attr('k4', '0');
// Projectioon and Path Generator
// ------------------------------
// Store the current rotation
var rotate = {x: 0, y: 90};
// Create and configure an instance of the orthographic projection
var projection = d3.geo.orthographic()
.scale(width / 2)
.translate([width / 2, height / 2])
.clipAngle(90)
.rotate([rotate.x / 2, -rotate.y / 2]);
// Create and configure the geographic path generator
var path = d3.geo.path().projection(projection);
// Overlay
// -------
var overlay = svg.selectAll('circle').data([rotate])
.enter().append('circle')
.attr('transform', 'translate(' + [width / 2, height / 2] + ')')
.attr('r', width / 2)
.attr('filter', 'url(#lightMe)')
.attr('class', 'overlay');
// Globe Outline
// -------------
var globe = grp.selectAll('path.globe').data([{type: 'Sphere'}])
.enter().append('path')
.attr('class', 'globe')
.attr('d', path);
// Graticule
// ---------
var graticule = d3.geo.graticule();
// Draw graticule lines
grp.selectAll('path.graticule').data([graticule()])
.enter().append('path')
.attr('class', 'graticule')
.attr('d', path);
// Load the stellar catalog
d3.json('hyg.json', function(error, data) {
// Handle errors getting and parsing the data
if (error) { return error; }
// Compute the radius scale. The radius will be proportional to
// the aparent magnitude
var rScale = d3.scale.linear()
.domain(d3.extent(data.features, function(d) { return d.properties.mag; }))
.range([3, 1]);
// Compute the radius for the point features
path.pointRadius(function(d) {
return d.properties ? rScale(d.properties.mag) : 1;
});
// Stars
// -----
grp.selectAll('path.star').data(data.features)
.enter().append('path')
.attr('class', 'star')
.attr('d', path);
// Drag Behavior
// -------------
var dragBehavior = d3.behavior.drag()
.origin(Object)
.on('drag', function(d) {
projection.rotate([(d.x = d3.event.x) / 2, -(d.y = d3.event.y) / 2]);
svg.selectAll('path').attr('d', function(u) {
// The circles are not properly generated when the
// projection has the clipAngle option set.
return path(u) ? path(u) : 'M 10 10';
});
});
// Add the drag behavior to the overlay
overlay.call(dragBehavior);
});
</script>
</body>
</html>
hygfull.csv:
curl -LO 'https://github.com/astronexus/HYG-Database/raw/master/hygfull.csv'
hyg.json: hygfull.csv
python parse-catalog.py
import csv
import json
import os
class CSVReader:
"""Iterate through the rows of the CSV file.
The iterator returns a dictionary with the column names as keys. The
values of the dictionary are strings, they may need to be casted.
"""
def __init__(self, csvpath):
self.csvfile = open(csvpath, 'r')
self.reader = csv.reader(self.csvfile, delimiter=',')
self.header = self.reader.next()
def __iter__(self):
return self
def next(self):
"""Returns a dictionary with column names as keys and the cell contents
as values. The values are strings.
"""
# Retrieve the next row from the file.
try:
row = self.reader.next()
except StopIteration:
self.csvfile.close()
raise
item = dict()
for ncol in range(len(row)):
item[self.header[ncol]] = row[ncol]
return item
if __name__ == '__main__':
# Create the CSV reader
reader = CSVReader('hygfull.csv')
# The data will be stored as a feature collection
feature = {'type': 'FeatureCollection', 'features': []}
for row in reader:
# Compute the magnitude and equivalent (ish) longitude and latitude
mag = float(row['Mag'])
lon = 360 * float(row['RA']) / 24 - 180
lat = float(row['Dec'])
# Store only the stars visible to the naked eye
if (-1 < mag) and (mag < 5):
feature['features'].append({
'type': 'Feature',
'properties': {'mag': mag},
'geometry': {'type': 'Point', 'coordinates': [lon, lat]}
})
# Store the stars as a GeoJSON file
jsonfile = open('hyg.json', 'w')
json.dump(feature, jsonfile)
jsonfile.close()
body {
background-color: #eee;
}
#map {
width: 400px;
height: 400px;
display: block;
margin-left: auto;
margin-right: auto;
margin-top: 40px;
margin-bottom: 10px;
}
.graticule {
fill: none;
stroke: #0053ad;
stroke-width: 1px;
}
.globe {
fill: #060061;
}
.star {
fill: #fff;
fill-opacity: 0.9;
}
.overlay {
fill: #005fc7;
fill-opacity: 0.4;
}