How to Make a Live Map of the ISS’s Location with Python and Plotly Dash

Proto Bioengineering
10 min readApr 8, 2023

--

See the latitude/longitude of the International Space Station every second.

The International Space Station is a manned space satellite that was launched nearly 25 years ago. As of April 8th, 2023, there are 7 people aboard, orbiting the Earth at over 17,000 mph.

Where are these people? Let’s find out and make a live dashboard out of it with Python 3, Plotly Dash, and the Open Notify API.

Photo by Kai Dahms on Unsplash

This is a continuation of our first article on How to Find the ISS’s Location with Python, R, or Bash. We’ll use the code from that tutorial, which grabs latitude and longitude from the Open Notify API, then plug that lat/long data into a Plotly Dash app.

Requirements

  • Python 3
  • Plotly Dash — makes the dashboard
  • Requests — gets the ISS location data from Open Notify
  • Pandas — helps Plotly do its job

You can install Plotly Dash, Requests, and Pandas with:

pip install dash
pip install requests
pip install pandas

Run the above pip commands from the command line. Read more about Pip here, if you’re not familiar.

Steps

  1. Get the code from the original ISS API tutorial
  2. Create a blank Plotly Dash app
  3. Add an empty map to the dashboard
  4. Plug the ISS data into the map

The full script is available at the end of this article.

Step 1: Get the Code from the Original Tutorial

The code from the original tutorial was only 4 lines:

# basic_iss.py

import requests

open_notify_api = "http://api.open-notify.org/iss-now.json"
iss_location = requests.get(open_notify_api)
print(iss_location.json())

This grabs data from the Open Notify API, then extracts the JSON from it and prints it. You can preview the JSON here.

Running the above in its own file get us the latitude and longitude of where the ISS is at right this second:

We’ll be making this request once per second in the code below, so that we get a constant, live update of ISS’s location.

Step 2: Create a Blank Plotly Dash App

We’ll start with a blank Plotly Dash app, since Dash has a lot of moving parts. Create a new file, called app.py, and put the code below into it.

# app.py

from dash import Dash, html

app = Dash(__name__)

app.layout = html.Div([
html.H1('Hello World')
])

if __name__ == '__main__':
app.run_server(debug=True)

We’ll leave out the ISS location-getting code from the last step for now, but we’ll reincorporate it later.

Above, we create an app with app = Dash(), then add a simple h1-sized HTML header that says Hello World.

If we run this in a new file, called app.py, our code will start up a local “website” that we can visit in our browser. Below, the output tells us that our Dash app is running at http://127.0.0.1:8050/.

If we go to http://127.0.0.1:8050/ in our browser, we see a page with a big header that says “Hello World.”

Now let’s add some more text and an empty map to put our ISS data into.

Step 3: Add a Blank Map to the Dashboard

Dash has a bunch of ways to add graphs, maps, and other HTML components to the dashboard.

We’ll add a few HTML elements to app.layout() (the same spot we added our ‘Hello World’ header), and then in Step 4, we’ll plug the ISS location data into them.

We’ll make these changes:

  1. Change the header title to ISS Location Tracker
  2. Add a text area to write the latitude and longitude to
  3. Add a map to visualize the lat/long data

Our blank dashboard will look like:

Change the Header

Rename html.H1 to ISS Location Tracker or something similar.

# app.py

from dash import Dash, html

app = Dash(__name__)

app.layout = html.Div([
html.H1('ISS Location Tracker')
])

if __name__ == '__main__':
app.run_server(debug=True)

Add a Text Div For the Latitude and Longitude

We’ll display the numbers for latitude and longitude alongside the graphs and map. (What is a “div”?)

# app.py

from dash import Dash, html

app = Dash(__name__)

app.layout = html.Div([
html.H1('ISS Location Tracker'),
html.Div(id='lat-long-text')
])

if __name__ == '__main__':
app.run_server(debug=True)

We added an HMTL div (html.Div()), which is where we’re going to put the raw numbers for latitude and longitude. If you run this now, this div will be empty, because we haven’t added any functions for getting ISS data yet.

Add a Blank Map

To do this we’ll use Dash’s Core Components class, dcc, and Plotly’s express, which both help with make maps and graphs.

Import them like so:

from dash import dcc
import plotly.express as px

Then we do 2 things:

  • Add a dcc.Graph element to the layout
  • Create a blank map with Plotly express
# app.py
...

# This makes the actual map
fig = px.line_geo()
fig.update_geos(projection_type="natural earth")
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})

# We added the map as a `Graph` at the end of the layout
app.layout = html.Div([
html.H1('ISS Location Tracker'),
html.Div(id='lat-long-text'),
dcc.Graph(id='map', figure=fig)
])

You can choose from many different types of projections (flat maps, spherical, etc.), and they’re all interactive. Above we use the “natural earth” projection, but you can choose “mercator”, “orthographic”, and more.

Note: The IDs that we pass as arguments (like id=’map’) are HTML ids, which we’ll use later to tell our dashboard-updating functions which HTML element to plug the data into.

Other types of map projections available, such as “orthographic” on the left and right.

Now, we’re reading to marry our data to our graphs and Earth map.

Step 4: Plug the ISS Data into the Dash App

We have to do 3 things:

  1. Add the code that gets data from the Open Notify API (from the previous ISS tutorial)
  2. Set an interval for how often to update the graphs (in milliseconds)
  3. Add 2 new functions for lat/long text and the map, which tell Dash how to plug the data into them

Add the Open Notify API code

The original code to get the ISS’s location was only 4 lines:

import requests

open_notify_api = "http://api.open-notify.org/iss-now.json"
iss_location = requests.get(open_notify_api)
print(iss_location.json())

We’ll put two of these lines in a function, so that our graph-updating functions can use it:

def get_iss_location():
open_notify_api = "http://api.open-notify.org/iss-now.json"
iss_location = requests.get(open_notify_api)

And we’ll add some formatting so that we return only two numbers, latitude and longitude:

def get_iss_location():
# Get ISS lat/long location and return as two decimals. Example: [-10.000, 34.000]
open_notify_api = "http://api.open-notify.org/iss-now.json"
iss_location = requests.get(open_notify_api)
iss_json = iss_location.json()

latitude = float(iss_json["iss_position"]["latitude"])
longitude = float(iss_json["iss_position"]["longitude"])

return [latitude, longitude]

The API gives us the lat/long as two strings in JSON, so we have to extract them and convert them to decimal numbers (with float()).

Set an interval for how often to update the dashboard

Since the Open Notify API updates the ISS’s location every second, we want our dashboard to do the same.

We have to tell Dash how often we want it to update by using dcc.Interval().

app.layout = html.Div([
html.H1('ISS Location Tracker'),
html.Div(id='lat-long-text'),
dcc.Graph(id='map', figure=fig),
dcc.Interval(
id='interval-component',
interval=1000, # in milliseconds
n_intervals=0
)
])

At the bottom, we added the dcc.Interval() function and told it to update once every second (1000 milliseconds). Then we can give its ID ('interval-component') to any other function, and that function will update every second also.

Add 2 new functions to update the lat/long text and map

We’ll make 2 functions, one to update the latitude/longitude raw numbers div and one for the Earth map.

The functions will start like so:

@app.callback()
def update_lat_long_text():
...

@app.callback()
def update_map():
...

These functions will each get the ISS location data then plug it into either the lat/long text or the map.

We specify that these are callback functions with @app.callback(). Callback functions are basically functions that get kicked off when another thing happens — in this case, whenever 1 second passes (which we’ll clarify in a bit). We’ll also give this callback function a few arguments to help Dash plug in the data properly.

Read more about basic callbacks in Plotly Dash and function decorators.

Updating the Latitude/Longitude Text

We’ll fill out:

@app.callback()
def update_lat_long_text():
...

We’ll first use our get_iss_location() function, then feed that data into some HTML elements.

We’ll also use @app.callback() to tell Dash which HTML element to put the new data into and when update_lat_long_text() should be fired.

Use get_iss_location() like so, then return it as a formatted HTML element.

@app.callback(...)
def update_lat_long_text():
# Get ISS location data
iss_location = get_iss_location()
latitude = iss_location[0]
longitude = iss_location[1]

# Put the ISS location data into two HTML "spans" (the <span> element)
return [
html.Span('Latitude: {}'.format(latitude)),
html.Span('Longitude: {}'.format(longitude))
]

But Dash doesn’t know where to plug in these two new HTML <span> elements, so we’ll tell it with the @app.callback decorator:

from dash.dependencies import Input, Output
...

@app.callback(Output('lat-long-text', 'children'),
Input('interval-component', 'n_intervals'))
def update_lat_long_text():
...

We use Input and Output to specify the following:

  • Input tells Dash when to run update_lat_long_text(). In this case, it uses the dcc.Interval() function whose ID is ‘interval-component’, which runs every second. Therefor, update_lat_long_text() will also run every second.
  • Output tells Dash which HTML element to update. In this case, it’s the div that we made with html.Div(id=’lat-long-text’) that we want to show the lat/long as numbers.

Whatever is in the update_lat_long_text() function’s return statement will get fed into the Output. So 2 HTML <span> elements will get fed into the div with the id 'lat-long-text'.

If we run the code up to this point, Latitude and Longitude are now showing in the web app.

The code with Latitude/Longitude text updating:

# app.py

import requests

from dash import Dash, html, dcc
import plotly.express as px
from dash.dependencies import Input, Output

app = Dash(__name__)

def get_iss_location():
# Get ISS lat/long location and return as two decimals. Example: [-10.000, 34.000]
open_notify_api = "http://api.open-notify.org/iss-now.json"
iss_location = requests.get(open_notify_api)
iss_json = iss_location.json()

latitude = float(iss_json["iss_position"]["latitude"])
longitude = float(iss_json["iss_position"]["longitude"])

return [latitude, longitude]

fig = px.line_geo()
fig.update_geos(projection_type="natural earth")
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})

app.layout = html.Div([
html.H1('ISS Location Tracker'),
html.Div(id='lat-long-text'),
dcc.Graph(id='map', figure=fig),
dcc.Interval(
id='interval-component',
interval=1000, # in milliseconds
n_intervals=0
)
])

@app.callback(Output('lat-long-text', 'children'),
Input('interval-component', 'n_intervals'))
def update_lat_long_text(interval):
iss_location = get_iss_location()
latitude = iss_location[0]
longitude = iss_location[1]

style = {'padding': '10px', 'fontSize': '22px'}
return [
html.Span('Latitude: {}'.format(latitude), style=style),
html.Span('Longitude: {}'.format(longitude), style=style)
]


if __name__ == '__main__':
app.run_server(debug=True)

We also added some style to separate the Lat/Long visually on the dashboard.

Updating the Map

Let’s fill out our update_map() function. This function will:

  1. Get ISS location data
  2. Store it in a list
  3. Make a line of the ISS’s path over our map
lat_long = {
'latitude': [],
'longitude': []
}
...

@app.callback(Output('map', 'figure'),
Input('interval-component', 'n_intervals'))
def update_map(interval):
iss_location = get_iss_location()
latitude = float(iss_location["iss_position"]["latitude"])
longitude = float(iss_location["iss_position"]["longitude"])

lat_long['latitude'].append(latitude)
lat_long['longitude'].append(longitude)

fig = px.line_geo(lat=lat_long['latitude'], lon=lat_long['longitude'])
fig.update_geos(projection_type="natural earth")
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})
fig.update_traces(line=dict(color="Red", width=4))

return fig

We’re adding all of our lat/long data to a dictionary called lat_long. That way, we can use all of the coordinates to draw a continuous line of the ISS’s path. lat_long will start out empty once we open our dashboard, but then it will draw a line that grows as our app runs and collects more location data.

The red line was drawn with all of the coordinates stored in `lat_long` over a 20 minute period.

It will take about 15 seconds of data before a red line is clear enough to see. If you’re not sure where you should be looking, plug your latitude and longitude into LatLong.net.

If you run app.py with all of the code below, you will see a red line that tracks the ISS’s location once per second:

We got the above result by letting the dashboard run for an hour. The ISS is traveling at 17,100 mph, so it takes about 90 minutes for it to make a full orbit around the Earth.

The Full Script

# Track the ISS's location in real time with Plotly Dash

import requests

from dash import Dash, html, dcc
import plotly.express as px
from dash.dependencies import Input, Output

app = Dash(__name__)

def get_iss_location():
# Get ISS lat/long location and return as two decimals. Example: [-10.000, 34.000]
open_notify_api = "http://api.open-notify.org/iss-now.json"
iss_location = requests.get(open_notify_api)
iss_json = iss_location.json()

latitude = float(iss_json["iss_position"]["latitude"])
longitude = float(iss_json["iss_position"]["longitude"])

return [latitude, longitude]

lat_long = {
'latitude': [],
'longitude': []
}

fig = px.line_geo()
fig.update_geos(projection_type="natural earth")
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})

app.layout = html.Div([
html.H1('ISS Location Tracker'),
html.Div(id='lat-long-text'),
dcc.Graph(id='map', figure=fig),
dcc.Interval(
id='interval-component',
interval=1000, # in milliseconds
n_intervals=0
)
])

@app.callback(Output('lat-long-text', 'children'),
Input('interval-component', 'n_intervals'))
def update_lat_long_text(interval):
iss_location = get_iss_location()
latitude = iss_location[0]
longitude = iss_location[1]

style = {'padding': '10px', 'fontSize': '22px'}
return [
html.Span('Latitude: {}'.format(latitude), style=style),
html.Span('Longitude: {}'.format(longitude), style=style)
]

@app.callback(Output('map', 'figure'),
Input('interval-component', 'n_intervals'))
def update_map(interval):
iss_location = get_iss_location()

lat_long['latitude'].append(iss_location[0])
lat_long['longitude'].append(iss_location[1])

fig = px.line_geo(lat=lat_long['latitude'], lon=lat_long['longitude'])
fig.update_geos(projection_type="natural earth")
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})
fig.update_traces(line=dict(color="Red", width=4))

return fig


if __name__ == '__main__':
app.run_server(debug=True)

Optimization and Further Reading

Questions and Feedback

If you have questions or feedback, email us at protobioengineering@gmail.com or message us on Instagram (@protobioengineering).

If you liked this article, consider supporting us by donating a coffee.

Related Articles

--

--

Proto Bioengineering

Learn to code for science. “Everything simple is false. Everything complex is unusable.” — Paul Valery