Interactive & Animated Travel Data Visualizations — Mapping NBA Travel

Learn how to create gorgeous interactive and animated visualizations of location and path data with Python and Plotly.

2019–2020 NBA travel paths for all teams

My experience is that people think of putting data on maps to be intimidating. Still, the opportunities for it are very common in any field of work. It is useful in mapping key locations, comparing cities, or in viewing and understanding data related to logistics, travel, or migration.

In this article, I would like to show how easy it is to do create effective, gorgeous mapping visualizations. As many of you know, I am a huge basketball fan. Most NBA teams travel over 40,000 miles over the course of their season across the United States (and Toronto). So, it seemed like an easy, relatable dataset to use — to map their arenas, put some colors and sizes on them, and to map their travel patterns.

Obviously, you do not need any prior basketball knowledge at all to follow this article — I will simply be discussing their travel patterns :).

Without further ado, let’s get started.

Before we get started

Data

I include the code and data in my GitLab repo here, (nba_travel directory) so you should be able to easily follow along by downloading/cloning the repo if you wish.

Packages

I assume you’re familiar with python. Even if you’re relatively new, this tutorial shouldn’t be too tricky, though.

You’ll need pandas and plotly . Install each (in your virtual environment) with a simple pip install [PACKAGE_NAME] .

You can use any graphics packages, obviously, but I personally prefer the simplicity and power you get with Plotly.

Where are these teams, exactly?

First of all, we should put together data on where these teams are located.

Gathering Data

For the purposes of our article, we will assume that each team magically flies from one arena to the venue of the next arena.

Starting from the list of arenas from Wikipedia here, I saved the list as a .csv file and used the free LocationIQ API to get the list of longitudes and latitudes:

This should be pretty straightforward, except for a couple of lines:

LocationIQ has a limit of 2 requests per second for their free account, which is why there is a time.sleep(0.5) restriction.

restriction. I use the .apply method to the full_loc column, which has the effect of looping through every entry through the get_lat_long function. The returning array becomes the arena_loc column.

method to the column, which has the effect of looping through every entry through the function. The returning array becomes the column. Because the returned values are a tuple of (latitude, longitude) , they are split up and converted to floating points.

Map all stadia

Let’s have a look at the data, by putting them on a map using Plotly:

Note: For aesthetics purposes, I am going to be using the Mapbox style here. For this to work, you need to set up a free mapbox account & get a key, which is then loaded to variable mapbox_key . For more details, check out my other tutorial on mapping. If you would prefer not to, change mapbox_accesstoken=mapbox_key to mapbox_style=”open-street-map” .

import plotly.express as px

fig = px.scatter_mapbox(arena_df, lat="lat", lon="lon", zoom=3, hover_name='teamname')

fig.update_layout(mapbox_style="light", mapbox_accesstoken=mapbox_key) # <== Using Mapbox

fig.show()

All 29 NBA stadia on a map

Maybe we could get more information on these stadia. Wikipedia provides stadia capacity size & age, so let’s start with those:

Above, I calculate age from the current year and manipulate the capacity data to remove commas and convert them to numbers from strings. And voila:

NBA stadia, showing age & capacity

If you build your own, you’ll also notice that it is interactive.

Okay, we are quite certain that the stadia information looks right, so let’s get onto obtaining travel data for each team.

Who gets the most frequent flier miles?

Now that we’ve got stadia locations, let’s get travel schedules for each team, collecting where they will be traveling to, and when.

The code here is not so much difficult as it is tedious. The main challenge is converting latitude/longitude differences to distances, for which this wiki page and this website were indispensable.

Broadly, this part requires that I obtain latitude/longitude data for each home arena in the schedule dataframe ( schedule_df ).

Then, loop over for each team and create a temporary dataframe ( team_df ), where the travel distance is calculated for each game (except the first — that’s a freebie). These distances, team names, and dates are added to a new dataframe and subsequently grouped. Simple enough, really. But take a look at this monstrosity:

Luckily, charting it is a breeze as usual.

fig = px.bar(

travel_team_df.sort_values('travel_dist'), x='travel_dist', y='teamname', orientation='h',

labels={'teamname': 'Team', 'travel_dist': 'Total distance (km)', 'km_per_trip': 'km/trip'},

color='km_per_trip', color_continuous_scale=px.colors.sequential.GnBu, hover_name='teamname')

clean_chart_format(fig)

fig.update_layout(title='Total travelled distances - NBA Teams (19-20 season)')

fig.show()

In this chart, I colored the bars to indicate the average distance traveled per trip.

All NBA teams’ travel distances & km per trip for the season

Interestingly, not all teams have the same number of trips, as some teams have multiple home games in a row, etc. As a result, the Jazz travel the furthest over the whole season, while the Kings travel furthest per trip.

If you prefer, we can also visualize it as a scatter, or dot plot. (I do prefer the bar plot myself, with a zero axis crossing.)

fig = px.scatter(

travel_team_df.sort_values('travel_dist'), x='travel_dist', y='teamname',

labels={'teamname': 'Team', 'travel_dist': 'Total distance (km)', 'km_per_trip': 'km/trip'},

color='km_per_trip', color_continuous_scale=px.colors.sequential.GnBu, hover_name='teamname')

clean_chart_format(fig, namelocs=[0.9, 0.1])

fig.update_layout(title='Total travelled distances - NBA Teams (19-20 season)')

fig.show()

An alternative presentation — same data

That’s all good and well, but it would be even better to be able to plot some of this information on maps. Then we can visually see where each team is flying to, and what their schedule looks like.

Let’s stalk a team

I would like to show arenas as well as flight paths in these figures.

For these, we will use the classic Plotly graph_objects module. Although Plotly Express is very powerful in its own right, Plotly (classic) sometimes offers more granular control. Given the different data types, let’s go with the classic Plotly variant.

Get started by importing the module, and setting up a blank canvas/figure.

import plotly.graph_objects as go



fig = go.Figure()

Adding arenas

As we mentioned before, we are going to assume that each team travels between arenas in straight lines. Luckily we have the arena data set up already, and can be added thus:

fig.add_trace(go.Scattergeo(

lon=arena_df['lon'], lat=arena_df['lat'], marker=dict(size=8, color='slategray'),

hoverinfo='text', text=arena_df['teamname'] + ' - ' + arena_df['arena_name'], name='Arenas'

))

We use the Scattergeo object, which works like a scatter plot, but with longitude & latitude data. And the series is set up so that when the mouse moves over the location, a text popup of teamname and arena_name added together is loaded as a string. The name parameter is the name of the series to be shown in the legend.

Adding team flight data

With the Scattergeo object, we can add lines — which will be used to represent flights. Choosing an arbitrary team (the Raptors), we:

Filter the dataframe for Toronto’s flights only,

Add a mode parameter to specify the use of lines, and

parameter to specify the use of lines, and Add n+1 number of points for n lines in between them.

(As two connected lines require three points — just as two connecting flights would start at airport A, go to airport B and end up in airport C.)

teamname = 'TORONTO_RAPTORS'

travel_team_df = travel_df[travel_df.teamname == teamname]

team_col = teamcolor_dict[teamname]



fig.add_trace(go.Scattergeo(

locationmode='USA-states', mode="lines",

lon=np.append(travel_team_df['orig_lon'].values, travel_team_df['dest_lon'].values[-1]),

lat=np.append(travel_team_df['orig_lat'].values, travel_team_df['dest_lat'].values[-1]),

line=dict(width=1, color=team_col), opacity=0.8,

hoverinfo='none', name=teamname

))

Oh, I also set up a dictionary of team colors ( teamcolor_dict ) for each team, rather than to assign them arbitrary colors.

And we generate a map like so:

Our first travel pin board!

Styling

For our finishing touches, let’s change the airport marker to triangles, change the map projection ( projection_type ), add a heading, and automatic zoom ( fitbounds ).

fig['data'][0]['marker']['symbol'] = 'triangle-left'



fig.update_layout(

title_text='NBA Travel paths',

geo=dict(

scope='north america',

projection_type='azimuthal equal area',

showland=True,

fitbounds="locations",

landcolor='rgb(243, 243, 243)',

countrycolor='rgb(204, 204, 204)',

),

)

fig.show()

Updated map

Let’s stalk… all the teams

The same concept can be extended to plot all of the teams on the one map. It’s actually very simple to do. The main changes are for us to loop over the team names, adding a trace for each team, making sure to change team colors.

As the map is also more crowded, I turned the opacity down a little to make it a little less visually jarring.

# NOW FOR THE WHOLE LEAGUE

# Initialise figure

fig = go.Figure()



# Add arenas

fig.add_trace(go.Scattergeo(

lon=arena_df['lon'], lat=arena_df['lat'], marker=dict(size=8, color='slategray'),

hoverinfo='text', text=arena_df['teamname'] + ' - ' + arena_df['arena_name'], name='Arenas'

))



# Add team flight traces

for teamname in travel_df.teamname.unique():

travel_team_df = travel_df[travel_df.teamname == teamname]

team_col = teamcolor_dict[teamname]

fig.add_trace(go.Scattergeo(

locationmode='USA-states', mode="lines",

lon=np.append(travel_team_df['orig_lon'].values, travel_team_df['dest_lon'].values[-1]),

lat=np.append(travel_team_df['orig_lat'].values, travel_team_df['dest_lat'].values[-1]),

line=dict(width=1, color=team_col), opacity=0.4,

hoverinfo='none', name=teamname

))



fig['data'][0]['marker']['symbol'] = 'triangle-left'



fig.update_layout(

title_text='NBA Travel paths',

geo=dict(

scope='north america',

projection_type='azimuthal equal area',

showland=True,

fitbounds="locations",

landcolor='rgb(243, 243, 243)',

countrycolor='rgb(204, 204, 204)',

),

)

fig.show()

All 30 NBA team’s travel schedules — visualized

(I have uploaded the interactive, html version here for you to check out.)

It’s quite a busy map, but this is where Plotly’s interactive capabilities come in handy. By click on one of the traces on the right, they can be turned on/off, allowing you to isolate the data.

Also, once we had the data, it really took only a few lines of code to generate this visualization, which is why I really like high-level libraries like Plotly.

As our last trick, let’s add some pizzaz with animations.

Animated travel map

Plotly’s animation module can be a little bit more tricky than its static plots. It makes perfect sense, as animations obviously require significantly more information.

The key thing to understand is that in graph_objects , animations are based on the idea of specifying a list to the frames parameter to the go. Figure, where each list item is a go.Frame object.

So logically, each go.Frame object becomes the figure that you would like to draw.

For this demo, I chose to plot an animation where each travel path added to the list of existing paths. This meant that each go.Frame object data would become longer and longer, by adding further objects to the list of longitude/latitudes.

Also, the layout parameter is where we would pass information for buttons.

(I highly recommend reading through this tutorial by Plotly.)

Other than that, conceptually nothing changes. It really becomes just a matter of thinking through the idea of what each ‘frame’ needs to have, and putting them into a sequence.

The way that I approached it was to set up a list called frames, and to loop over the travel paths, adding a new segment, as below:

frames = list()

lon_data = np.array(lon_vals[0])

lat_data = np.array(lat_vals[0])



for i in range(len(lon_vals)):

frames.append(

go.Frame(data=[go.Scattergeo(lon=lon_data, lat=lat_data)])

)

lon_data = np.append(lon_data, lon_vals[i])

lat_data = np.append(lat_data, lat_vals[i])

And subsequently, the frames list can be passed onto the Figure object:

fig = go.Figure(

data=[

go.Scattergeo(

locationmode='USA-states', mode="lines",

lon=np.append(lon_vals[0], lon_vals[0]),

lat=np.append(lat_vals[0], lat_vals[0]),

line=dict(width=1, color=team_col), opacity=0.1,

hoverinfo='none', name=teamname

)

],

layout=go.Layout(

title="Start Title",

updatemenus=[dict(

type="buttons",

buttons=[dict(label="Play", method="animate", args=[None])])]

),

frames=frames

You will notice here that the go.Layout object containing button parameters is passed onto the animation.

Putting it all together for the Mavs team, we get:

Animated travel schedule for the Dallas Mavericks’ (LINK)

It’s pretty cool, huh? Check out the animated version online, put together your own for your team, or even better — for your own dataset.

I’m also sure that you can do more interesting things with colors too. At the moment it doesn’t really tell you much, as I am simply using the team color for demonstration purposes. But I’m sure that you could encode some information on it.