This repository explains how to make a map of the solar system using open-source code and data from NASA. Software used includes
Illustrator CC 2019 and
Photoshop CC 2019. If you have comments or suggestions for this tutorial, please let me know on my blog! You can buy the finished map here.
urllib. Dependencies can be installed with
pip install -r requirements.txt.
Software Carpentry has great tutorials for installing Python (scroll down and follow the directions in the Bash Shell and Python sections), getting starting with Jupyter Notebooks, and beginner-friendly Python programming. After you've installed Python using these tutorials, you can use Git Desktop and the instructions in this tutorial to download the code and data in this tutorial.
You'll need software for editing raster and vector images (this article explains the difference). I use Adobe Photoshop and Illustrator, but you can also use the free open-source programs Gimp and Inkscape. There is no perfect software that fits everyone's needs, so you'll want to understand the pros and cons for the different raster and vector programs before choosing.
This map combines five different datasets from NASA PDS and NASA JPL. The primary dataset at the core of the project is the NASA JPL Small-Body Database Search Engine, which I used to make a list of all known asteroids and comets in the solar system. To use this search engine, go to the linked website, fill in the data fields you want to request, and then download the data as a CSV. For this project, these were the data fields I requested:
object internal database ID,
object primary SPK-ID,
object full name/designation,
object primary designation: These were all of the available ID formats for asteroids and comets. I downloaded all of them in case I needed to join or reference different datasets later on in the project.
object IAU name: This is the common name of the object. I used this data to annotate the map with labels.
comet designation prefix,
Near-Earth Object (NEO) flag (Y/N),
Potentially Hazardous Asteroid (PHA) flag (Y/N),
orbit classification: At first I wasn't sure what kind of color scheme or symbols I'd use for the map, so I downloaded all interesting-sounding parameters so I could look at them later. In the end I used orbit classifications to determine the color. The rest I didn't end up using, since the information was more or less similar to the orbit classifications.
object diameter (from equivalent sphere) (km): This is the diameter of each object (not all objects have a known diamater). In this map, each asteroid or comet is plotted proportional to its actual size on a log scale.
[q] perihelion distance (au): The perihelion distance is the closest distance from the object to the sun. I didn't use this data directly in the map, but I used this information to get an initial idea of the distribution of objects in the solar system.
sidereal orbital period (d): I use this data to customize the HORIZONS query time range for each object. For most objects I wanted to get the orbit path for a time period of 10 years. But for asteroids that move very quickly, I asked for a maximum of 1/4 the length of a full orbit.
I first downloaded this data into
all_comets.csv. Next, I added in some missing data from the TNO Diameter Dataset (described below) and saved the result in
all_comets_wrangled.csv. Finally, I split the dataset into separate files for large asteroids >20km
large_asteroids.csv, small asteroids 10-20km
small_asteroids.csv, and asteroids of unknown size
any_outer_asteroids.csv. Since there were only 12 comets >10km, I combined all of them into one data file
The JPL Small-Body Database doesn't seem to include diameter sizes for Trans-Neptunian Objects such as Pluto. I found this information in the TNO and Centaur Diameters, Albedos, and Densities dataset from the NASA Planetary Science Institute, and merged it into the records downloaded from the Small-Body Database. Overall I found this dataset difficult to clean, so I might have made mistakes - please use the altered data with caution. These are the cleaning steps I used:
-999.9, or some other such variant. Some diameters are also marked with a negative value (but are actual values, not
999). I wasn't quite sure what these values meant, but regardless I removed any diameter values below 0.
.tabfile, but I couldn't find any tabs in either Python or text editing software. So I split the rows as best as I could by whitespace, but this caused several other issues:
-to mark missing data.
Planets and moons are not considered "small bodies", so they aren't included in the Small-Body Database. For planets and moons, I compiled two separate
CSV files containing data for planets and moons using data published by the JPL Solar System Dynamics group and the NASA Space Science Data Coordinated Archive:
You can calculate the position of asteroids based on Keplerian orbital elements in the Small-Body Database, but most of the tutorials I found seemed to recommend using NASA HORIZONS instead. Additionally, the Keplerian element estimate doesn't take into account the gravitational fields of nearby objects, so I wanted to use the more accurate position data generated by HORIZONS.
The JPL HORIZONS system generates ephemerides for solar system objects (ephemerides describe movement trajectories over time). It can be accessed in several different ways, and the Web-Interface is probably the most user-friendly. To use the Web-Interface, fill in the
Observer Location, and
Time Span Settings and click
Generate Ephemeris. For example, to find the path of Venus as seen from Earth, set the
Observer Location to
Geocentric and the
Target Body to
The Web-Interface is great for single searches, but I needed the orbital trajectories of several thousand asteroids. So for this project I used the HORIZONS Batch-Interface tutorial to structure URL submissions for every asteroid, and then wrote a web scraper to request data for each object:
I'm open-sourcing this code because HORIZONS specifically allows scraping, and even provides tutorials and sample code. But if you're using a web scraper for the first time, it's important to know that scrapers can be unethical, either by accident or on purpose. Even in the best case scenario, you're using the bandwidth of the server owner, which costs money and resources. At worst, you might be publicizing data that has real consequences for someone's safety and privacy. Here are some questions I try to consider before running an automated scraper: - Is there a public API or dataset I can use instead? - Does the person who created this data know it's being collected by a scraper? - Can the data identify or be traced back to real people? - Does my scraping project help the data owners, or give back to their community in some way? - Does my scraper identify itself accurately and provide contact information in case of problems?
In this project I've included all output files used in this map in the
data folder, so if you'd like to recreate the map please use the provided data instead of running the spider again.
Next, I made about ten Python plots with different subsections of data. For example, I saved the orbit paths and scatterpoints as separate files, and I also saved the annotated text separately. I often split up data for plotting so I can easily apply section-specific effects in Photoshop or Illustrator.
Time: At first I wanted to map each asteroid with an orbit tail reaching back 10 years. But many asteroids didn’t have enough data, and the inner asteroids moved so fast that the map was illegible from overlapping lines. So in the end the asteroid tails reach back to the last possible data point, or 10 years, or a quarter of the object’s orbit - whichever is smallest.. I plotted the full orbit length for all planets (except Neptune, which did not have data available before 1950).
I also spent a long time experimenting with different ways to plot orbit trajectories. Although I didn’t end up using the code, I still really like the orbit ribbons on the far left (the ribbon thickness shows the orbit movement in the z axis):
Distance from the sun: I used a radial logarithmic plot to map orbit trajectories, with the minimum distance at the center of the plot set to 27,000,000km. In our solar system there are disproportionately more objects closer to the sun, so it's very hard to differentiate important objects like planets using a linear scale:
Diameter: The size of each object is also scaled to a separate logarithmic scale. If the size is unknown (which is the majority of objects in the outer solar system) I marked the object using a dashed scatterpoint angled to the direction of motion.
Name: In the outer solar system there are only 78 named asteroids, so I annotated all of these. For the inner solar system, I tried to annotate all of the largest objects of each asteroid class (I removed some when multiple markers were overplotted on top of each other). I also included many of the named asteroids of relatively rare orbit classes. Finally, I annotated about 50 additional asteroids with names I liked, like Moomintroll, O’Keefe, and Sauron.
Orbit direction: In annotated objects, the direction of text follows the direction of motion (Ka'epaoka'awaela is the only named object in this map moving clockwise). In non-annotated objects the orbit tail shows the direction of motion.
Orbit classification: I also used colors to encode the orbit classifications of each asteroid. For maps with a lot of different elements, I like to save my design settings as a
CSV file so I can easily try out different color schemes without rewriting any code. In this project the
./data/plotting_functions/colors.csv file maps each type of object (comets, main belt asteroids, etc) to a specific color.
I usually save figures as a PDF so I can edit the text and shapes in Illustrator. There are a couple standard commands I use to export Matplotlib figures so they're easy to edit:
import matplotlib import matplotlib.pyplot as plt import matplotlib.backends.backend_pdf as pdf # Export text as editable text instead of shapes: matplotlib.rcParams['pdf.fonttype'] = 42 # Preserve the vertical order of embedded images: matplotlib.rcParams['image.composite_image'] = False # Remove borders and ticks from subplots: ax.axis('off') # Remove padding and margins from the figure and all its subplots plt.margins(0,0) plt.subplots_adjust(top=1, bottom=0, right=1, left=0, hspace=0, wspace=0) plt.gca().xaxis.set_major_locator(plt.NullLocator()) plt.gca().yaxis.set_major_locator(plt.NullLocator()) # Save the Matplotlib figure as a PDF file: pp = pdf.PdfPages('./savename.pdf', keep_empty=False) pp.savefig(fig) pp.close() # If I don't need to edit vector paths I save the file as a # PNG so I can import it directly into Photoshop: plt.savefig('./savename.png', format='png', dpi=600, pad_inches=0, transparent=True)
After saving the figure, the
Clipping Mask -->
Release. At this point you can also delete the background and axis border objects, if you included them in the output file.
I export Python figures to Illustrator and Photoshop because several great design features are impossible or very time-consuming in Matplotlib. I'm linking tutorials here for the features I use most often - font alternates and ligatures, custom brushes, layering effects, blur effects, gradients along a path, variable width paths, type on a path, and selecting objects by characteristic.
I've included a small section of the map in the
figures folder as the Photoshop file
asteroid_sample.psd. The file is small enough to upload online, but since it still has the original layers you should be able to use it as a reference for layering effects.
One of the most important effects in this map is the gradient color in the orbit tails. You can simulate this in Python, but these methods are difficult to implement for a map with 18,000 paths, each containing ~4000 data points. Instead, you can apply gradient colors in Illustrator:
Stroke along pathoption.
To emphasize the radial axis on this map, I decided to label the asteroid names radially as well. First I used Python to plot all of the asteroids I wanted to label in one
Type on a Path tool to copy and paste the text for each object onto its orbit path vector.
Left indent to offset the label from the object marker.
Set the baseline shift to center the text vertically along the orbit.
The asteroid belt objects were too close together to plot this way, so I shifted the names radially to a nearby spot with a little more room. To do this I needed a lot of concentric circle vectors (to type the names onto), as well as label lines pointing from the center of the object to the shifted name label. The python output already includes these label lines in the correct place and angle, but the length needs to be adjusted in Illustrator:
1. Use the
Direct Selection Tool to move a vertex from this generated line to an annotation point near the text. While moving the point, the pink helper text
Line Extension should be visible the entire time.
2. Use the
Direct Selection Tool to remove the unused annotation vertex on the opposite side of the scatterpoint.
Making concentric circles for the labels was a little more involved:
1. Create one circle the size of the entire map, and one small circle centered in the middle.
2. Go to
Blend Options and set the Blend Options to
Specified Steps : 200
3. Select both objects, then go to
4. Select the new blended object, then go to
5. Use the
Type on a Path tool to copy and paste the text for each object onto one of the concentric circles an appropriate distance away.
6. Adjust the label line if necessary to point to the text.
To create a glow effect around an object, duplicate the object layer and go to
Blur Gallery -->
Field Blur. For glowing text I usually create two blur layers set to 20% opacity - one with a
Blur of 4px and the other 10px. In this map I added a glow effect to the text labels and all of the scatterpoints. You can also make a shadow effect in almost exactly the same way: Before applying the
Field Blur, change the color of all objects in the duplicated layer to the color you'd like to use for the shadow. I think it's easiest to change the colors in the original Illustrator image instead of in Photoshop (especially for text and complex object shapes).
I wanted the maps in this series to look cohesive, so I made a palette of ~70 different colors and picked from these choices in every map. I also used the same two fonts (Redflowers and Moon) in all maps. You're welcome to use the color palette and font styling if you'd like.
Code: All of the code in this repository is shared under the GPL-3.0 license.
Data: The data in this repository belongs to the original authors of the data. Please use the references section to look up the original version. In cases where I edited or revised the data, I impose no additional restrictions to the original license. Any data files I created myself are shared under the ODC Open Database License.
Artwork: The artwork included in this repository are shared under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.