block by pnavarrc 9730300

Celestial Sphere

Full Screen

Celestial Sphere

This gist demonstrate how to create a celestial sphere using the orthographic projection.

Data

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

index.html

<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>

Makefile

hygfull.csv:
	curl -LO 'https://github.com/astronexus/HYG-Database/raw/master/hygfull.csv'

hyg.json: hygfull.csv
	python parse-catalog.py

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()


styles.css

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;
}