block by mbostock c7e85d2b47d11982db38

Star Map

Full Screen

A star map of the Northern Hemisphere using a flipped stereographic projection, showing declination and right ascension. I believe the data is from the Yale Bright Star Catalog, but I got it from Matteo Abrate’s sky map.

Updated Example →

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  background: radial-gradient(720px at 490px, #081f2b 0%, #061616 100%);
  height: 960px;
}

path {
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
}

text {
  font: 10px sans-serif;
}

.horizon {
  stroke: #fff;
  stroke-width: 1.5px;
}

.graticule {
  stroke: #fff;
  stroke-opacity: .15;
}

.ticks line {
  stroke: #fff;
}

.ticks text {
  fill: #fff;
  text-anchor: middle;
}

.ticks--right-ascension text:nth-of-type(6n + 1) {
  font-weight: bold;
  font-size: 14px;
}

.stars {
  fill: #fff;
  stroke: #000;
}

.star--active {
  stroke: yellow;
}

.voronoi {
  fill: none;
  pointer-events: all;
}

.cross {
  fill: none;
  stroke: yellow;
  stroke-width: 0.5px;
}

</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>

var width = 960,
    height = 960,
    scale = width * 0.45;

var radius = d3.scale.linear()
    .domain([-1, 6])
    .range([8, 0]);

var projection = d3.geo.projection(flippedStereographic)
    .scale(scale)
    .clipAngle(130)
    .rotate([0, -90])
    .translate([width / 2, height / 2])
    .precision(0.1);

var path = d3.geo.path()
    .projection(projection);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append("g")
    .attr("transform", "translate(0.5,0.5)");

svg.append("path")
    .datum(d3.geo.circle().origin([0, 90]).angle(90))
    .attr("class", "horizon")
    .attr("d", path);

svg.append("path")
    .datum(d3.geo.graticule().minorStep([15, 10]))
    .attr("class", "graticule")
    .attr("d", path);

var crossDeclination = svg.append("circle")
    .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
    .attr("class", "cross cross--declination");

var crossRightAscension = svg.append("line")
    .attr("x1", width / 2)
    .attr("y1", height / 2)
    .attr("x2", width / 2)
    .attr("y2", height / 2)
    .attr("class", "cross cross--right-ascension");

var ticksRightAscension = svg.append("g")
    .attr("class", "ticks ticks--right-ascension");

ticksRightAscension.selectAll("line")
    .data(d3.range(0, 1440, 5)) // every 5 minutes
  .enter().append("line")
    .each(function(d) {
      var p0 = projection([d / 4, 0]),
          p1 = projection([d / 4, d % 60 ? -1 : -2]);

      d3.select(this)
          .attr("x1", p0[0])
          .attr("y1", p0[1])
          .attr("x2", p1[0])
          .attr("y2", p1[1]);
    });

ticksRightAscension.selectAll("text")
    .data(d3.range(24)) // every hour
  .enter().append("text")
    .each(function(d) {
      var p = projection([d * 15, -4]);

      d3.select(this)
          .attr("x", p[0])
          .attr("y", p[1]);
    })
    .attr("dy", ".35em")
    .text(function(d) { return d + "h"; });

svg.append("g")
    .attr("class", "ticks ticks--declination")
  .selectAll("text")
    .data(d3.range(10, 91, 10))
  .enter().append("text")
    .each(function(d) {
      var p = projection([0, d]);

      d3.select(this)
          .attr("x", p[0])
          .attr("y", p[1]);
    })
    .attr("dy", ".35em")
    .text(function(d) { return d + "°"; });

d3.csv("stars.csv", type, function(error, stars) {
  if (error) throw error;

  svg.insert("g", ".ticks")
      .attr("class", "stars")
    .selectAll("circle")
      .data(stars)
    .enter().append("circle")
      .attr("id", function(d, i) { return "star-" + i; })
      .attr("r", function(d) { return radius(d.magnitude); })
      .attr("transform", function(d) { return "translate(" + d[0] + "," + d[1] + ")"; });

  svg.append("g")
      .attr("class", "voronoi")
    .selectAll("path")
      .data(d3.geom.voronoi()(stars))
    .enter().append("path")
      .on("mouseover", mouseovered)
      .on("mouseout", mouseouted)
    .filter(function(d) { return d; })
      .attr("d", function(d) { return "M" + d.join("L"); })
      .datum(function(d) { return d.point; })
    .append("title")
      .text(function(d) { return "HR" + d.ID + (d.greek_letter || d.constellation ? "\n" + d.constellation + " " + d.greek_letter : ""); });
});

function mouseovered(d, i) {
  var dx = d[0] - width / 2,
      dy = d[1] - height / 2,
      a = Math.atan2(dy, dx);
  crossDeclination.attr("r", Math.sqrt(dx * dx + dy * dy));
  crossRightAscension.attr("x2", width / 2 + width * Math.cos(a)).attr("y2", height / 2 + height * Math.sin(a));
  console.log(d3.select("#star-" + i).node());
  d3.select("#star-" + i).classed("star--active", true);
}

function mouseouted(d, i) {
  d3.select("#star-" + i).classed("star--active", false);
}

function type(d) {
  var p = projection([(+d.RA_hour + d.RA_min / 60 + d.RA_sec / 3600) * 15, +d.dec_deg + d.dec_min / 60 + d.dec_sec / 3600]);
  d[0] = p[0], d[1] = p[1];
  d.magnitude = +d.magnitude;
  return d;
}

function flippedStereographic(lambda, phi)  {
  var coslambda = Math.cos(lambda),
      cosphi = Math.cos(phi),
      k = 1 / (1 + coslambda * cosphi);
  return [
    k * cosphi * Math.sin(lambda),
    -k * Math.sin(phi)
  ];
}

</script>