Olaf Frohn

Celestial map with D3.js

Interactive, adaptable celestial map done with the D3.js visualization library. So, GeoJSON for sky stuff. Which surprisingly nobody has done yet, it seems.

Features display of stars and deep sky objects (DSOs) with a selectable magnitude limit up to 6, or choose different GeoJSON data source for higher magnitudes. Also shows constellations with names, lines and/or boundaries, the Milky Way band and grid lines. Alternate coordinate spaces e.g. ecliptc, galactic or supergalactic are also possible. Full support for zoom and rotation with mouse or gestures.

Since it uses D3.js and HTML5 canvas, it needs a modern browser with canvas support, so any recent flavor of Chrome/Firefox/Safari/Opera or IE 9 and above should suffice. Check out the demo at armchairastronautics.blogspot.com or clone/download it for local usage, which works with Chrome if it is started with command line parameter --allow-file-access-from-files to load local json files. Or use a local web server environment, quite easy to do with node.js.

Demos:
Simple map with editable configuration
Interactive form map viewer with all config options
Wall map for printing
Setting time/location and see the current sky
Animated planets moving about the ecliptic
Starry sky just the stars
Alternative Stars different way to display stars
Summer triangle adding data
Supernova remnants adding point data
Traditional Chinese constellation a different culture altogether
(Source files on github)

Some more examples:
Embedded interactive form
Spinning sky globe
The Milky Way halo, globular clusters & satellite galaxies
The Local Group of galaxies
Asterisms with locations & time selection
Asterisms with zoom & pan
Zoom & pan animations
A different kind of Messier marathon
Show coordinates, DSO colors, Download button
Geolocator gadget part I: Geolocator globe - Part II: Daylight sky - Part III: Geomarker - Part IV: Night sky

Usage

var config = { 
  width: 0,           // Default width, 0 = full parent element width; 
                      // height is determined by projection
  projection: "aitoff",    // Map projection used: see below
  projectionRatio: null,   // Optional override for default projection ratio
  transform: "equatorial", // Coordinate transformation: equatorial (default),
                           // ecliptic, galactic, supergalactic
  center: null,       // Initial center coordinates in set transform
                      // [longitude, latitude, orientation] all in degrees 
                      // null = default center [0,0,0]
  orientationfixed: true,  // Keep orientation angle the same as center[2]
  geopos: null,       // optional initial geographic position [lat,lon] in degrees, 
                      // overrides center
  follow: "zenith",   // on which coordinates to center the map, default: zenith, if location enabled, 
                      // otherwise center
  zoomlevel: null,    // initial zoom level 0...zoomextend; 0|null = default, 1 = 100%, 0 < x <= zoomextend
  zoomextend: 10,     // maximum zoom level
  adaptable: true,    // Sizes are increased with higher zoom-levels
  interactive: true,  // Enable zooming and rotation with mousewheel and dragging
  form: true,         // Display form for interactive settings. Needs a div with
                      // id="celestial-form", created automatically if not present
  location: false,    // Display location settings. Deprecated, use formFields below
  formFields: {"location": true,  // Set visiblity for each group of fields with the respective id
               "general": true,  
               "stars": true,  
               "dsos": true,  
               "constellations": true,  
               "lines": true,  
               "other": true,  
               "download": false},  
  advanced: true,     // Display fewer form fields if false 
  daterange: [],      // Calender date range; null: displaydate-+10; [n<100]: displaydate-+n; [yr]: yr-+10; 
                      // [yr, n<100]: [yr-n, yr+n]; [yr0, yr1]  
  controls: true,     // Display zoom controls
  lang: "",           // Global language override for names, any name setting that has the chosen language available
                      // Default: desig or empty string for designations, other languages as used anywhere else
  culture: "",        // Source of constellations and star names, default "iau", other: "cn" Traditional Chinese
  container: "map",   // ID of parent element, e.g. div, null = html-body
  datapath: "data/",  // Path/URL to data files, empty = subfolder 'data'
  stars: {
    show: true,    // Show stars
    limit: 6,      // Show only stars brighter than limit magnitude
    colors: true,  // Show stars in spectral colors, if not use default color
    style: { fill: "#ffffff", opacity: 1 }, // Default style for stars
    designation: true, // Show star names (Bayer, Flamsteed, Variable star, Gliese or designation, 
                       // i.e. whichever of the previous applies first); may vary with culture setting
    designationType: "desig",  // Which kind of name is displayed as designation (fieldname in starnames.json)
    designationStyle: { fill: "#ddddbb", font: "11px 'Palatino Linotype', Georgia, Times, 'Times Roman', serif", align: "left", baseline: "top" },
    designationLimit: 2.5,  // Show only names for stars brighter than nameLimit
    propername: false,   // Show proper name (if present)
    propernameType: "name", // Languge for proper name, default IAU name; may vary with culture setting 
                            // (see list below of languages codes available for stars)
    propernameStyle: { fill: "#ddddbb", font: "13px 'Palatino Linotype', Georgia, Times, 'Times Roman', serif", align: "right", baseline: "bottom" },
    propernameLimit: 1.5,  // Show proper names for stars brighter than propernameLimit
    size: 7,       // Maximum size (radius) of star circle in pixels
    exponent: -0.28, // Scale exponent for star size, larger = more linear
    data: 'stars.6.json' // Data source for stellar data, 
                         // number indicates limit magnitude
  },
  dsos: {
    show: true,    // Show Deep Space Objects 
    limit: 6,      // Show only DSOs brighter than limit magnitude
    colors: true,  // // Show DSOs in symbol colors if true, use style setting below if false
    style: { fill: "#cccccc", stroke: "#cccccc", width: 2, opacity: 1 }, // Default style for dsos
    names: true,   // Show DSO names
    namesType: "name",  // Type of DSO ('desig' or language) name shown
                        // (see list below for languages codes available for dsos)
    nameStyle: { fill: "#cccccc", font: "11px Helvetica, Arial, serif", 
                 align: "left", baseline: "top" }, // Style for DSO names
    nameLimit: 6,  // Show only names for DSOs brighter than namelimit
    size: null,    // Optional seperate scale size for DSOs, null = stars.size
    exponent: 1.4, // Scale exponent for DSO size, larger = more non-linear
    data: 'dsos.bright.json', // Data source for DSOs, 
                              // opt. number indicates limit magnitude
    symbols: {  //DSO symbol styles, 'stroke'-parameter present = outline
      gg: {shape: "circle", fill: "#ff0000"},          // Galaxy cluster
      g:  {shape: "ellipse", fill: "#ff0000"},         // Generic galaxy
      s:  {shape: "ellipse", fill: "#ff0000"},         // Spiral galaxy
      s0: {shape: "ellipse", fill: "#ff0000"},         // Lenticular galaxy
      sd: {shape: "ellipse", fill: "#ff0000"},         // Dwarf galaxy
      e:  {shape: "ellipse", fill: "#ff0000"},         // Elliptical galaxy
      i:  {shape: "ellipse", fill: "#ff0000"},         // Irregular galaxy
      oc: {shape: "circle", fill: "#ffcc00", 
           stroke: "#ffcc00", width: 1.5},             // Open cluster
      gc: {shape: "circle", fill: "#ff9900"},          // Globular cluster
      en: {shape: "square", fill: "#ff00cc"},          // Emission nebula
      bn: {shape: "square", fill: "#ff00cc", 
           stroke: "#ff00cc", width: 2},               // Generic bright nebula
      sfr:{shape: "square", fill: "#cc00ff", 
           stroke: "#cc00ff", width: 2},               // Star forming region
      rn: {shape: "square", fill: "#00ooff"},          // Reflection nebula
      pn: {shape: "diamond", fill: "#00cccc"},         // Planetary nebula 
      snr:{shape: "diamond", fill: "#ff00cc"},         // Supernova remnant
      dn: {shape: "square", fill: "#999999", 
           stroke: "#999999", width: 2},               // Dark nebula grey
      pos:{shape: "marker", fill: "#cccccc", 
           stroke: "#cccccc", width: 1.5}              // Generic marker
    }
  },
  planets: {  //Show planet locations, if date-time is set
    show: false,
    // List of all objects to show
    which: ["sol", "mer", "ven", "ter", "lun", "mar", "jup", "sat", "ura", "nep"],
    // Font styles for planetary symbols
    symbols: {  // Character and color for each symbol in 'which' above (simple circle: \u25cf), optional size override for Sun & Moon
      "sol": {symbol: "\u2609", letter:"Su", fill: "#ffff00", size:""},
      "mer": {symbol: "\u263f", letter:"Me", fill: "#cccccc"},
      "ven": {symbol: "\u2640", letter:"V", fill: "#eeeecc"},
      "ter": {symbol: "\u2295", letter:"T", fill: "#00ccff"},
      "lun": {symbol: "\u25cf", letter:"L", fill: "#ffffff", size:""}, // overridden by generated crecent, except letter & size
      "mar": {symbol: "\u2642", letter:"Ma", fill: "#ff6600"},
      "cer": {symbol: "\u26b3", letter:"C", fill: "#cccccc"},
      "ves": {symbol: "\u26b6", letter:"Ma", fill: "#cccccc"},
      "jup": {symbol: "\u2643", letter:"J", fill: "#ffaa33"},
      "sat": {symbol: "\u2644", letter:"Sa", fill: "#ffdd66"},
      "ura": {symbol: "\u2645", letter:"U", fill: "#66ccff"},
      "nep": {symbol: "\u2646", letter:"N", fill: "#6666ff"},
      "plu": {symbol: "\u2647", letter:"P", fill: "#aaaaaa"},
      "eri": {symbol: "\u26aa", letter:"E", fill: "#eeeeee"}
    },
    symbolStyle: { fill: "#00ccff", font: "bold 17px 'Lucida Sans Unicode', Consolas, sans-serif", 
             align: "center", baseline: "middle" },
    symbolType: "symbol",  // Type of planet symbol: 'symbol' graphic planet sign, 'disk' filled circle scaled by magnitude
                           // 'letter': 1 or 2 letters S Me V L Ma J S U N     
    names: false,          // Show name in nameType language next to symbol
    nameStyle: { fill: "#00ccff", font: "14px 'Lucida Sans Unicode', Consolas, sans-serif", align: "right", baseline: "top" },
    namesType: "desig"     // Language of planet name (see list below of language codes available for planets), 
                           // or desig = 3-letter designation
  },
  constellations: {
    names: true,      // Show constellation names 
    namesType: "iau", // Type of name Latin (iau, default), 3 letter designation (desig) or other language (see list below)
    nameStyle: { fill:"#cccc99", align: "center", baseline: "middle", 
                 font: ["14px Helvetica, Arial, sans-serif",  // Style for constellations
                        "12px Helvetica, Arial, sans-serif",  // Different fonts for diff.
                        "11px Helvetica, Arial, sans-serif"]},// ranked constellations
    lines: true,   // Show constellation lines, style below
    lineStyle: { stroke: "#cccccc", width: 1, opacity: 0.6 }, 
    bounds: false, // Show constellation boundaries, style below
    boundStyle: { stroke: "#cccc00", width: 0.5, opacity: 0.8, dash: [2, 4] }
  },  
  mw: {
    show: true,     // Show Milky Way as filled multi-polygon outlines 
    style: { fill: "#ffffff", opacity: 0.15 }  // Style for MW layers
  },
  lines: {  // Display & styles for graticule & some planes
    graticule: { show: true, stroke: "#cccccc", width: 0.6, opacity: 0.8,   
      // grid values: "outline", "center", or [lat,...] specific position
      lon: {pos: [""], fill: "#eee", font: "10px Helvetica, Arial, sans-serif"}, 
      // grid values: "outline", "center", or [lon,...] specific position
      lat: {pos: [""], fill: "#eee", font: "10px Helvetica, Arial, sans-serif"}},    
    equatorial: { show: true, stroke: "#aaaaaa", width: 1.3, opacity: 0.7 },  
    ecliptic: { show: true, stroke: "#66cc66", width: 1.3, opacity: 0.7 },     
    galactic: { show: false, stroke: "#cc6666", width: 1.3, opacity: 0.7 },    
    supergalactic: { show: false, stroke: "#cc66cc", width: 1.3, opacity: 0.7 }
  },
  background: {        // Background style
    fill: "#000000",   // Area fill
    opacity: 1, 
    stroke: "#000000", // Outline
    width: 1.5
  }, 
  horizon: {  //Show horizon marker, if location is set and map projection is all-sky
    show: false, 
    stroke: "#cccccc", // Line
    width: 1.0, 
    fill: "#000000",   // Area below horizon
    opacity: 0.5
  },  
  daylight: {  //Show day sky as a gradient, if location is set and map projection is hemispheric
    show: false
  }
};

// Display map with the configuration above or any subset thereof
Celestial.display(config);

Supported projections: Airy, Aitoff, Armadillo, August, Azimuthal Equal Area, Azimuthal Equidistant, Baker, Berghaus, Boggs, Bonne, Bromley, Cassini, Collignon, Craig, Craster, Cylindrical Equal Area, Cylindrical Stereographic, Eckert 1, Eckert 2, Eckert 3, Eckert 4, Eckert 5, Eckert 6, Eisenlohr, Equirectangular, Fahey, Foucaut, Ginzburg 4, Ginzburg 5, Ginzburg 6, Ginzburg 8, Ginzburg 9, Hammer, Hatano, HEALPix, Hill, Homolosine, Kavrayskiy 7, Lagrange, l’Arrivee, Laskowski, Loximuthal, Mercator, Miller, Mollweide, Flat Polar Parabolic, Flat Polar Quartic, Flat Polar Sinusoidal, Natural Earth, Nell Hammer, Orthographic, Patterson, Polyconic, Rectangular Polyconic, Robinson, Sinusoidal, Stereographic, Times, 2 Point Equidistant, van der Grinten, van der Grinten 2, van der Grinten 3, van der Grinten 4, Wagner 4, Wagner 6, Wagner 7, Wiechel and Winkel Tripel. Most of these need the extension d3.geo.projections

Supported languages for constellation, star and planet name display: (name) Official IAU name, (desig) 3-Letter-Designation, (la) Latin, (en) English, (ar) Arabic, (zh) Chinese, (cz) Czech, (ee) Estonian, (fi) Finnish, (fr) French, (de) German, (el) Greek, (he) Hebrew, (it) Italian, (ja) Japanese, (ko) Korean, (hi) Hindi, (fa) Persian, (ru) Russian, (es) Spanish, (tr) Turkish

Style settings
fill: fill color (css color value)
opacity: opacity (number 0..1)
Line styles
stroke: outline color (css color value)
width: line width in pixels (number 0..)
dash: line dash ([line length, gap length])
Text styles
font: well, the font (css font property)
align: horizontal align (left|center|right|start|end)
baseline: vertical align (alphabetic|top|hanging|middle|ideographic|bottom)
Symbol style
shape: symbol shape (circle|square|diamond|ellipse|marker or whatever else is defined in canvas.js)
symbol: unicode charcter to represent solar system object.

Getting Info

Exposed functions & objects

Adding Data

Exposed functions & objects

Adding Behaviour

Manipulating the Map

Exposed functions

Animations

Exposed functions

Animation sequence format:
[
{param: Animated parameter projection|center|zoom
value: Adequate value for each parameter
duration: in milliseconds, 0 = exact length of transition
callback: optional callback function called at the end of the transition
}, …]

HowTo

1. Add your own data

First of all, whatever you add needs to be valid geoJSON. The various types of objects are described in the readme of the data folder. This can be a separate file or a JSON object filled at runtime or defined inline. Like so:

var jsonLine = {
  "type":"FeatureCollection",
  // this is an array, add as many objects as you want
  "features":[
    {"type":"Feature",
     "id":"SummerTriangle",
     "properties": {
       // Name
       "n":"Summer Triangle",
       // Location of name text on the map
       "loc": [-67.5, 52]
     }, "geometry":{
       // the line object as an array of point coordinates, 
       // always as [ra -180..180 degrees, dec -90..90 degrees]
       "type":"MultiLineString",
       "coordinates":[[
         [-80.7653, 38.7837],
         [-62.3042, 8.8683],
         [-49.642, 45.2803],
         [-80.7653, 38.7837]
       ]]
     }
    }  
  ]
}; 

As you can see, this defines the Summer Triangle asterism, consisting of the bright stars Vega (Alpha Lyr), Deneb (Alpha Cyg) and Altair (Alpha Aql).
Note: Since astronomical data is usually given in right ascension from 0 to 24 h and the geoJSON-format used in D3 expects positions in degrees from -180 to 180 deg, you may need this function to convert your data first:

function hour2degree(ra) { 
  return ra > 12 ? (ra - 24) * 15 : ra * 15;
}

You also need to define how the triangle is going to look like with some styles (see definitions above). The parameters and values usually have the same formats as SVG- or CSS-data:

var lineStyle = { 
      stroke: "#f00", 
      fill: "rgba(255, 204, 204, 0.4)",
      width: 3 
    };
var textStyle = { 
      fill: "#f00", 
      font: "bold 15px Helvetica, Arial, sans-serif", 
      align: "center", 
      baseline: "bottom" 
    };

Now we can get to work, with the function

Celestial.add({file:string, type:json|raw, callback:function, redraw:function)

The file argument is optional for providing an external geoJSON file; since we already defined our data, we don’t need it. Type is ‘json’ for JSON-Formatted data. That leaves two function definitions, the first one gets called on loading, this is where we add our data to the d3-celestial data container, and redraw is called on every redraw event for the map, this is where you define how to display the added object(s).

callback: function(error, json) {
  if (error) return console.warn(error);
  // Load the geoJSON file and transform to correct coordinate system, if necessary
  var asterism = Celestial.getData(jsonLine, config.transform);

  // Add to celestial objects container in d3
  Celestial.container.selectAll(".asterisms")
    .data(asterism.features)
    .enter().append("path")
    .attr("class", "ast"); 
  // Trigger redraw to display changes
  Celestial.redraw();
}

The callback funtion is pretty straight forward: Load the data with Celestial.getData, add to Celestial.container in the usual d3 manner, and redraw. It also provides a json parameter that contains the parsed JSON if a file property is given, but we already have defined jsonLine above, so we use that.

redraw: function() {   
  // Select the added objects by class name as given previously
  Celestial.container.selectAll(".ast").each(function(d) {
    // Set line styles 
    Celestial.setStyle(lineStyle);
    // Project objects on map
    Celestial.map(d);
    // draw on canvas
    Celestial.context.fill();
    Celestial.context.stroke();
    
    // If point is visible (this doesn't work automatically for points)
    if (Celestial.clip(d.properties.loc)) {
      // get point coordinates
      pt = Celestial.mapProjection(d.properties.loc);
      // Set text styles       
      Celestial.setTextStyle(textStyle);
      // and draw text on canvas
      Celestial.context.fillText(d.properties.n, pt[0], pt[1]);
    }      
  })
}

And the redraw function with the actual display of the elements, contained in a d3.selectAll call on the previously set class property of the added objects. Celestial.setStyle applies the predefined canvas styles, Celestial.map projects each line on the map. However, that doesn’t work for points, so that is done manually with Celestial.clip (true if point is currently visible) and Celestial.mapProjection. and the rest are standard canvas fill and stroke operations. The beginPath and closePath commands are done automatically.

Celestial.display();

Finally, the whole map is displayed. The complete sample code is in the file triangle.html in the demo folder

2. Add point sources

First we have to define the objects as valid geoJSON data again, as described in the readme of the data folder. Since we’re dealing with point sources, the definition is quite simple, the geometry only needs single points. If distinct point sizes are desired, a size criterion in the properties section is required, like the magnitude or extension of each object, and also a name if you want to label the objects on the map. This example uses supernova remnants filtered from the main deep space objects data file that comes with d3-celestial, but you can define your own data as below:

var jsonSN = {
  "type":"FeatureCollection",
  // this is an array, add as many objects as you want
  "features":[
    {"type":"Feature",
     "id":"SomeDesignator",
     "properties": {
       // Name
       "name":"Some Name",
       // magnitude, dimension in arcseconds or any other size criterion
       "mag": 10,
       "dim": 30
     }, "geometry":{
       // the location of the object as a [ra, dec] array in degrees [-180..180, -90..90]
       "type":"Point",
       "coordinates": [-80.7653, 38.7837]
     }
    }  
  ]};

Next we define the appearance of the objects and labels as they will appear on the map. The values are equivalent to CSS-formats. Fill and stroke colors are only necessary if the objects should appear solid (fill) or as an outline (stroke), or an outline with a semitransparent filling as below. Width gives the line width for outlines.

var pointStyle = { 
      stroke: "#f0f", 
      width: 3,
      fill: "rgba(255, 204, 255, 0.4)"
    };
var textStyle = { 
      fill:"#f0f", 
      font: "bold 15px Helvetica, Arial, sans-serif", 
      align: "left", 
      baseline: "bottom" 
    };

Now we are ready to add the functions that do the real work of putting the data on the map.

Celestial.add({file:string, type:json|raw, callback:function, redraw:function)

The file argument is optional for providing an external geoJSON file; since we already defined our data, we don’t need it. Type is ‘line’, that leaves two function definitions: the first one is called at loading, this is where we add our data to the d3-celestial data container, while the second function ‘redraw’ is called at every redraw event for the map. So here you need to define how to display the added object(s). Here are two different possibilities to add data to the D3 data container. Either add the data defined as a JSON-Object in-page, as below with the jsonSN object we defined before.

callback: function(error, json) {
  if (error) return console.warn(error);
  // Load the geoJSON file and transform to correct coordinate system, if necessary
  var dsn = Celestial.getData(jsonSN, config.transform);

  // Add to celestial objects container in d3
  Celestial.container.selectAll(".snrs")
    .data(asterism.features)
    .enter().append("path")
    .attr("class", "snr"); 
  // Trigger redraw to display changes
  Celestial.redraw();
}

Or add data from an external file with optional filtering, as shown below. In this case the file parameter of the Celsestial.add() function needs to give a valid url to the data file, while the filter function returns true for every object that meets the intended criteria.

callback: function(error, json) {
  if (error) return console.warn(error);
  // Load the geoJSON file and transform to correct coordinate system, if necessary
  var dsos = Celestial.getData(json, config.transform);

  // Filter SNRs and add to celestial objects container in d3
  Celestial.container.selectAll(".snrs")
    .data(dsos.features.filter(function(d) {
      return d.properties.type === 'snr'
    }))
    .enter().append("path")
    .attr("class", "snr"); 
  // Trigger redraw to display changes
  Celestial.redraw();
}

However you add the data, as long as they receive the same class name - ‘snr’ in the examples above - the display method is the same, as shown below. With point data we can’t rely on the map function to do all the work, we need to paint on the canvas step by step. First, check if the point is currently displayed (clip), then get the location (mapProjection), size (whatever scaling formula you like) and styling.

Now we are ready to throw pixels at the canvas: set the styles (fill color, stroke color & width), followed by whatever canvas commands are required to draw the object shape, here a filled circle outline. And then the same for the adjacent object name offset by the previously calculated radius.

redraw: function() {
  // Select the added objects by class name as given previously
  Celestial.container.selectAll(".snr").each(function(d) {
    // If point is visible (this doesn't work automatically for points)
    if (Celestial.clip(d.geometry.coordinates)) {
      // get point coordinates
      var pt = Celestial.mapProjection(d.geometry.coordinates);
      // object radius in pixel, could be varable depending on e.g. dimension or magnitude 
      var r = Math.pow(20 - prop.mag, 0.7); // replace 20 with dimmest magnitude in the data
   
      // draw on canvas
      //  Set object styles fill color, line color & width etc.
      Celestial.setStyle(pointStyle);
      // Start the drawing path
      Celestial.context.beginPath();
      // Thats a circle in html5 canvas
      Celestial.context.arc(pt[0], pt[1], r, 0, 2 * Math.PI);
      // Finish the drawing path
      Celestial.context.closePath();
      // Draw a line along the path with the prevoiusly set stroke color and line width      
      Celestial.context.stroke();
      // Fill the object path with the prevoiusly set fill color
      Celestial.context.fill();     

      // Set text styles       
      Celestial.setTextStyle(textStyle);
      // and draw text on canvas
      Celestial.context.fillText(d.properties.name, pt[0] + r - 1, pt[1] - r + 1);
    }      
  }); 
}});

Finally, the whole map can be displayed.

Celestial.display();

Bonus: Avoid overlapping labels

You will note that there is a lot of overlap between distinct labels. Fortunately, d3 already has a solution for this: d3.geom.quadtree, which builds a hiearchical data structure ordered by proximity. First we set the closest allowed distance between two labels in pixels, get the map dimensions from Celestial.metrics, and create a quadtree object with the extent of those dimensions.

var PROXIMITY_LIMIT = 20,
    m = Celestial.metrics(), 
    quadtree = d3.geom.quadtree().extent([[-1, -1], [m.width + 1, m. height + 1]])([]);

After proceeding as above - get the projected map position in pixelspace (pt) and draw the snr symbol - we use the quadtree.find() function to find the nearest neighbor relative to this position, and if it is more distant than our limit above, add it to quadtree and draw the label, otherwise don’t.

var nearest = quadtree.find(pt);

if (!nearest || distance(nearest, pt) > PROXIMITY_LIMIT) { 
  quadtree.add(pt)
  // draw the label as above
}

This will only draw non-overlapping labels and scales with zoom-level, since it checks in pixel-space and not in coordinate-space.

Now we need just one more thing, the distance function used above, which is the standard Pythagorean square root of the sum of the differences squared function.

// Simple point distance function
function distance(p1, p2) {
  var d1 = p2[0] - p1[0],
      d2 = p2[1] - p1[1];
  return Math.sqrt(d1 * d1 + d2 * d2);
}

The complete sample code is in the file snr.html in the demo folder.

Files

GeoJSON data files
(See format specification in the readme for the data folder)

Sources

All data converted to GeoJSON at J2000 epoch, positions converted from 0…24h right ascension to -180…180 degrees longitude as per GeoJSON requirements, so 0…12h becomes 0…180 degrees, and 12…24h becomes -180…0 degrees.

Other files

Thanks to Mike Bostock and Jason Davies for D3.js and d3.geo.projections. And also thanks to Jason Davies for d3.geo.zoom, which saved me some major headaches in figuring out how to rotate/zoom the map.

Released under BSD License