Using Basemap to plot geospatial data and other tricks/tools using matplotlib

A notebook developed by Nick Swanson-Hysell (https://github.com/Swanson-Hysell) for a presentation/tutorial at the Berkeley The Hacker Within Group (Berkeley Institute for Data Science, March 16, 2016; http://www.thehackerwithin.org/berkeley/posts/matplotlib-spring-2016).

This notebook was developed using a Python 3 kernel. The first portion of this Jupyter notebook illustrates some functionality using the base matplotlib module within the Jupyter notebook environment. Subsequent portions of the notebook utilize Basemap, mpld3, folium and bokeh which can be pip installed (pip install Basemap) or if you are using the Anaconda distribution of Python (conda install Basemap).


In [1]:
import matplotlib.pyplot as plt
import numpy as np

%matplotlib notebook and %matplotlib inline

Matplotlib supports many GUI backends. Plotting within the notebook can be enabled using the %matplotlib magic both with %matplotlib notebook and %matplotlib inline. Each one results in distinct behavior.


In [2]:
%matplotlib notebook

In [3]:
x_values = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y_values = np.sin(x_values)

plt.plot(x_values, y_values)
plt.xlabel('x values')
plt.ylabel('y values')
plt.show()



In [4]:
%matplotlib inline

In [5]:
x_values = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y_values = np.sin(x_values)

plt.plot(x_values, y_values)
plt.xlabel('x values')
plt.ylabel('y values')
plt.show()



In [6]:
plt.figure()
x_values = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y_values = np.sin(x_values)

plt.plot(x_values, y_values)
plt.xlim(x_values.min()*1.1, x_values.max()*1.1)
plt.ylim(y_values.min()*1.1, y_values.max()*1.1)
plt.xlabel('x values')
plt.ylabel('y values')
plt.show()


styling with matplotlib

The style of matplotlib plots can be changed by importing style. As of matplotlib 1.5 there are many more styles availible such as the 538 style used in the plot generated below.


In [7]:
from matplotlib import style
style.available


Out[7]:
['bmh',
 'seaborn-notebook',
 'seaborn-darkgrid',
 'seaborn-dark',
 'seaborn-colorblind',
 'seaborn-pastel',
 'grayscale',
 'dark_background',
 'ggplot',
 'seaborn-muted',
 'seaborn-white',
 'seaborn-dark-palette',
 'seaborn-paper',
 'seaborn-poster',
 'fivethirtyeight',
 'classic',
 'seaborn-deep',
 'seaborn-ticks',
 'seaborn-bright',
 'seaborn-whitegrid',
 'seaborn-talk']

In [8]:
style.use('fivethirtyeight')
plt.figure()
x_values = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y_values = np.sin(x_values)

plt.plot(x_values, y_values)
plt.xlim(x_values.min()*1.1, x_values.max()*1.1)
plt.ylim(y_values.min()*1.1, y_values.max()*1.1)
plt.xlabel('x values')
plt.ylabel('y values')
plt.savefig('example_plot.svg')
plt.show()


changing the inline backend

Higher resolution png figures or svg vector graphics can be enabled within the notebook environment using the %config IPython magic command.


In [9]:
%config InlineBackend.figure_format='retina'

In [10]:
plt.figure()
x_values = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y_values = np.sin(x_values)

plt.plot(x_values, y_values)
plt.xlim(x_values.min()*1.1, x_values.max()*1.1)
plt.ylim(y_values.min()*1.1, y_values.max()*1.1)
plt.xlabel('x values')
plt.ylabel('y values')
plt.show()



In [11]:
%config InlineBackend.figure_formats = {'svg',}

In [12]:
plt.figure()
x_values = np.linspace(-np.pi, np.pi, 256, endpoint=True)
y_values = np.sin(x_values)

plt.plot(x_values, y_values)
plt.xlim(x_values.min()*1.1, x_values.max()*1.1)
plt.ylim(y_values.min()*1.1, y_values.max()*1.1)
plt.xlabel('x values')
plt.ylabel('y values')
plt.savefig('plot.svg')
plt.show()


built-in support for Pandas (PA Fracking wells plots example)

As of matplotlib 1.5, pandas dataframes can be plotted by column in plots such as plt.scatter(). To illustrate this capability, let's create a dataframe from a .csv of Pennsylvania fracking data. This dataset contains the production of unconventional gas wells for the month of December, 2015. It was downloaded from the Pennsylvania Department of Environmental Protection Oil and Gas Electronic Reporting website: https://www.paoilandgasreporting.state.pa.us/publicreports/

Importing the pandas module gives us a way to nicely import these data as a Dataframe.


In [15]:
import pandas as pd

In [16]:
PA_Frack_Wells_Dec = pd.read_csv('gas_data/Oil_Gas_Well_Historical_Production_Report.csv')
PA_Frack_Wells_Dec.head()


Out[16]:
Well_Permit_Num Period_ID Production_Indicator Well_Status Farm_Name Spud_Date Gas_Quantity Gas_Production_Days Condensate_Quantity Condensate_Production_Days ... Well_Municipality Well_Latitude Well_Longitude Unconventional Well_Configuration Home_Use Reporting_Period Comment_Reason Comment_Text Record_Source
0 003-21994 15DECU Yes ACTIVE FAWN DEVELOPERS INC 1 12/05/2008 2134.00 31 NaN NaN ... FAWN 40.657901 -79.781085 Yes VERTI No Dec 2015 (Unconventional wells) NaN NaN OGRE
1 003-22091 15DECU Yes ACTIVE FAWN DEVELOPERS INC 3H 12/11/2009 33817.00 31 NaN NaN ... FAWN 40.648568 -79.777621 Yes HORIZ No Dec 2015 (Unconventional wells) NaN NaN OGRE
2 003-22092 15DECU Yes ACTIVE FAWN DEVELOPERS INC 4H 12/03/2009 15917.00 31 NaN NaN ... FAWN 40.648704 -79.777674 Yes HORIZ No Dec 2015 (Unconventional wells) NaN NaN OGRE
3 003-22230 15DECU Yes ACTIVE BOVARD DOROTHY UNIT 4H 10/30/2012 43000.43 31 NaN NaN ... FAWN 40.634369 -79.753756 Yes HORIZ No Dec 2015 (Unconventional wells) NaN NaN OGRE
4 003-22231 15DECU Yes ACTIVE BOVARD DOROTHY UNIT 5H 10/30/2012 38414.70 31 NaN NaN ... FAWN 40.634453 -79.753761 Yes HORIZ No Dec 2015 (Unconventional wells) NaN NaN OGRE

5 rows × 26 columns


In [17]:
PA_Frack_Wells_Dec.describe()


Out[17]:
Gas_Quantity Gas_Production_Days Condensate_Quantity Condensate_Production_Days Oil_Quantity Oil_Production_Days Well_Latitude Well_Longitude
count 6619.000000 6619.000000 838.000000 838.000000 39.000000 39 9051.000000 9051.000000
mean 62868.259174 29.399154 534.822303 30.392601 180.765385 31 41.055176 -78.182785
std 74075.234077 5.075785 799.959004 3.023965 218.346404 0 0.738610 1.751591
min 0.110000 1.000000 0.100000 1.000000 1.000000 31 39.721901 -80.516255
25% 16829.500000 31.000000 76.605000 31.000000 62.500000 31 40.244028 -80.043979
50% 36150.000000 31.000000 254.855000 31.000000 114.000000 31 41.343683 -77.575103
75% 82817.500000 31.000000 659.280000 31.000000 189.940000 31 41.721817 -76.568217
max 934826.000000 31.000000 7395.000000 31.000000 1013.000000 31 41.997586 -75.555286

In matplotlib, it is now possible to pass the name of Dataframe columns to be x and y within plt.scatter() as long as we set data equal to the name of the DataFrame which is PA_Frack_Wells_Dec.


In [18]:
style.use('seaborn-notebook')
plt.figure()
plt.scatter(x='Gas_Production_Days',y='Gas_Quantity',data=PA_Frack_Wells_Dec)
plt.xlabel('number of days of gas production (in Dec 2015)')
plt.ylabel('gas production (thousand cubic feet)')
plt.ylim(-20000,PA_Frack_Wells_Dec.Gas_Quantity.max()*1.1)
plt.xlim(0,32)
plt.show()


plotting with Basemap (PA Fracking wells plots example)

The plotting toolkit Basemap can be used to plot this data set of Pennsylvania unconventional gas wells (i.e. fracking) that were producing in December, 2015. To plot these data we need to import Basemap. Basemap is a matplotlib tool kit that you can conda install. The example Basemap gallery (http://matplotlib.org/basemap/users/examples.html) and the list of all availible projections (http://matplotlib.org/basemap/users/mapsetup.html) are quite helpful.


In [19]:
from mpl_toolkits.basemap import Basemap
style.use('seaborn-notebook')

In [20]:
m = Basemap(projection='mill',
            llcrnrlat = 39,
            llcrnrlon = -83,
            urcrnrlat = 43,
            urcrnrlon = -72,
            resolution='l')

core_x,core_y = m(PA_Frack_Wells_Dec.Well_Longitude.tolist(),PA_Frack_Wells_Dec.Well_Latitude.tolist())

m.drawcoastlines()
m.drawcountries()
m.drawstates()
m.fillcontinents(color='tan',lake_color='lightblue',zorder=0)
m.scatter(x=core_x,y=core_y,c=PA_Frack_Wells_Dec.Gas_Quantity.tolist(),cmap='viridis',vmin=0)
m.colorbar(label='gas quantity (Mcf)')
plt.title('Unconventional gas wells in PA (as of Dec 2015)',)
plt.show()


From plotting these gas wells on a map, we can see that there is a geographical clustering of the wells. The majority of gas in PA is produced from the Marcellus Shale. Let's explore the correspondance between the location of the gas wells and the the extent of the Marecellus Shale by plotting a shapefile of the extent of the Marcellus Formation.

Using Basemap to plot shape files


In [21]:
m = Basemap(projection='mill',
            llcrnrlat = 39,
            llcrnrlon = -83,
            urcrnrlat = 43,
            urcrnrlon = -72,
            resolution='l')

core_x,core_y = m(PA_Frack_Wells_Dec.Well_Longitude.tolist(),PA_Frack_Wells_Dec.Well_Latitude.tolist())

m.drawcoastlines()
m.drawcountries()
m.drawstates()
m.fillcontinents(color='tan',lake_color='lightblue',zorder=0)
m.scatter(x=core_x,y=core_y,c=PA_Frack_Wells_Dec.Gas_Quantity.tolist(),cmap='viridis',vmin=0)
m.readshapefile('./gas_data/marcellus_extent', 'marcellus_extent',color='red',linewidth=1)
m.colorbar(label='gas quantity (Mcf)')
plt.title('Unconventional gas wells in PA (with Marcellus Shale extent)')
plt.savefig('Marcellus_Shale_Map.pdf')
plt.show()



In [22]:
high_producing_wells = PA_Frack_Wells_Dec[PA_Frack_Wells_Dec.Gas_Quantity>300000]

m = Basemap(projection='mill',
            llcrnrlat = 39,
            llcrnrlon = -83,
            urcrnrlat = 43,
            urcrnrlon = -72,
            resolution='l')

core_x,core_y = m(high_producing_wells.Well_Longitude.tolist(),high_producing_wells.Well_Latitude.tolist())

m.drawcoastlines()
m.drawcountries()
m.drawstates()
m.fillcontinents(color='tan',lake_color='lightblue',zorder=0)
m.scatter(x=core_x,y=core_y,c=high_producing_wells.Gas_Quantity.tolist(),cmap='viridis',vmin=0)
m.readshapefile('./gas_data/marcellus_extent', 'marcellus_extent',color='red',linewidth=1)
m.colorbar(label='gas quantity (Mcf)')
plt.title('high producing unconventional gas wells in PA (> 300,000 Mcf)')
plt.show()


Plots on a global projection (earthquakes example)

Let's import a dataset of all the earthquakes greater than magnitude 7 between 1900 and 2016 that are in NOAA's National Centers for Environmental Information (NCEI; https://www.ngdc.noaa.gov/) Significant Earthquake Database.


In [23]:
earthquakes_mag7 = pd.read_csv('./earthquake_data/results.tsv',sep='\t')
earthquakes_mag7.drop([358,391,411],inplace=True) #drop lines with empty latitude which are there as empty strings '    '
earthquakes_mag7.columns


Out[23]:
Index(['I_D', 'FLAG_TSUNAMI', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE',
       'SECOND', 'FOCAL_DEPTH', 'EQ_PRIMARY', 'EQ_MAG_MW', 'EQ_MAG_MS',
       'EQ_MAG_MB', 'EQ_MAG_ML', 'EQ_MAG_MFA', 'EQ_MAG_UNK', 'INTENSITY',
       'COUNTRY', 'STATE', 'LOCATION_NAME', 'LATITUDE', 'LONGITUDE',
       'REGION_CODE', 'DEATHS', 'DEATHS_DESCRIPTION', 'MISSING',
       'MISSING_DESCRIPTION', 'INJURIES', 'INJURIES_DESCRIPTION',
       'DAMAGE_MILLIONS_DOLLARS', 'DAMAGE_DESCRIPTION', 'HOUSES_DESTROYED',
       'HOUSES_DESTROYED_DESCRIPTION', 'HOUSES_DAMAGED',
       'HOUSES_DAMAGED_DESCRIPTION', 'TOTAL_DEATHS',
       'TOTAL_DEATHS_DESCRIPTION', 'TOTAL_MISSING',
       'TOTAL_MISSING_DESCRIPTION', 'TOTAL_INJURIES',
       'TOTAL_INJURIES_DESCRIPTION', 'TOTAL_DAMAGE_MILLIONS_DOLLARS',
       'TOTAL_DAMAGE_DESCRIPTION', 'TOTAL_HOUSES_DESTROYED',
       'TOTAL_HOUSES_DESTROYED_DESCRIPTION', 'TOTAL_HOUSES_DAMAGED',
       'TOTAL_HOUSES_DAMAGED_DESCRIPTION'],
      dtype='object')

A nice global projection is the Mollweide elliptical equal-area projection. The earthquake locations can be plotted on this map and let's set up a color bar that corresponds to earthquake magnitude.


In [24]:
# lon_0 is central longitude of projection.
# resolution = 'c' means use crude resolution coastlines.
m = Basemap(projection='moll',lon_0=0,resolution='c')
m.drawcoastlines()
m.drawmapboundary(fill_color='white')

earthquake_lon = earthquakes_mag7.LONGITUDE.astype(float).tolist()
earthquake_lat = earthquakes_mag7.LATITUDE.astype(float).tolist()
earthquake_x,earthquake_y = m(earthquake_lon,earthquake_lat)

m.scatter(earthquake_x,earthquake_y,
          c=earthquakes_mag7.EQ_MAG_MW.astype(float).tolist(),
          cmap='seismic')
m.colorbar(label='Earthquake magnitude')
plt.title("Earthquake locations (M>7) since 1900")
plt.show()


Instead of having the coastlines on the plot for reference, let's plate the boundaries of tectonic plates using the boundaries as compiled in:

Bird, P. (2003), An updated digital model of plate boundaries, Geochem. Geophys. Geosyst., 4, 1027, doi:10.1029/2001GC000252, 3.


In [25]:
m = Basemap(projection='moll',lon_0=0,resolution='c')
m.drawmapboundary(fill_color='white')
m.readshapefile('./earthquake_data/tectonicplates/PB2002_boundaries',
                'PB2002_boundaries',color='black',linewidth=1)

earthquake_lon = earthquakes_mag7.LONGITUDE.astype(float).tolist()
earthquake_lat = earthquakes_mag7.LATITUDE.astype(float).tolist()
earthquake_x,earthquake_y = m(earthquake_lon,earthquake_lat)

m.scatter(earthquake_x,earthquake_y,
          c=earthquakes_mag7.EQ_MAG_MW.astype(float).tolist(),
          cmap='seismic')
m.colorbar(label='Earthquake magnitude')
plt.title("Earthquake locations (M>7) since 1900")
plt.show()


Most of the earthquakes are right at plate boundaries, but some are not. Let's plot polygons of intraplate deformation (mountain belts) to see if those regions might correspond with more of the earthquake locations.


In [26]:
# lon_0 is central longitude of projection.
# resolution = 'c' means use crude resolution coastlines.
m = Basemap(projection='moll',lon_0=0,resolution='c')
m.drawmapboundary(fill_color='white')
m.readshapefile('./earthquake_data/tectonicplates/PB2002_boundaries',
                'PB2002_boundaries',color='black',linewidth=1)
m.readshapefile('./earthquake_data/tectonicplates/PB2002_orogens',
                'PB2002_orogens',color='green',linewidth=1)

earthquake_lon = earthquakes_mag7.LONGITUDE.astype(float).tolist()
earthquake_lat = earthquakes_mag7.LATITUDE.astype(float).tolist()
earthquake_x,earthquake_y = m(earthquake_lon,earthquake_lat)

m.scatter(earthquake_x,earthquake_y,
          c=earthquakes_mag7.EQ_MAG_MW.astype(float).tolist(),
          cmap='seismic')
m.colorbar(label='Earthquake magnitude')
plt.title("Earthquake locations (M>7) since 1900")
plt.show()


Earthquakes that have magnitudes this large can be large disasters with loss of human life. This aspect of the dataset is explored in the figures below.


In [27]:
plt.figure()
plt.scatter(x='EQ_MAG_MW',y='TOTAL_DEATHS',data=earthquakes_mag7)
plt.ylabel('total deaths from Earthquake')
plt.xlabel('magnitude of Earthquake')
plt.show()



In [28]:
earthquakes_mag7_highd = earthquakes_mag7[earthquakes_mag7.TOTAL_DEATHS>500]
earthquakes_mag7_lowd = earthquakes_mag7[earthquakes_mag7.TOTAL_DEATHS<500]

In [29]:
m = Basemap(projection='moll',lon_0=0,resolution='c')
m.drawmapboundary(fill_color='white',zorder=0)
m.drawcoastlines(zorder=1)

badquake_lon = earthquakes_mag7_highd.LONGITUDE.astype(float).tolist()
badquake_lat = earthquakes_mag7_highd.LATITUDE.astype(float).tolist()
badquake_year = earthquakes_mag7_highd.YEAR.tolist()
badquake_casualties = earthquakes_mag7_highd.DEATHS.tolist()
badquake_x,badquake_y = m(badquake_lon,badquake_lat)
m.scatter(badquake_x,badquake_y,c='red',s=30,label='deaths > 5000',zorder=10)

notasbadquake_lon = earthquakes_mag7_lowd.LONGITUDE.astype(float).tolist()
notasbadquake_lat = earthquakes_mag7_lowd.LATITUDE.astype(float).tolist()
notasbadquake_x,notasbadquake_y = m(notasbadquake_lon,notasbadquake_lat)
m.scatter(notasbadquake_x,notasbadquake_y,c='green',s=10,label='deaths < 5000')
plt.legend(loc=4)
plt.title("Earthquake locations (M>7) since 1900")
plt.show()


Using the mpld3 package to make an interactive data visualization (Earthquake data example)

The mpld3 project brings together Matplotlib, the popular Python-based graphing library, and D3js, the popular Javascript library for creating interactive data visualizations for the web. The result is a simple API for exporting your matplotlib graphics to HTML code which can be used within the browser, within standard web pages, blogs, or tools such as the IPython notebook. http://mpld3.github.io/index.html

Using mpld3 the earthquake map can be made interactive with labels associated with point that are activated by a mouse click. A histogram example that plots earthquake depths is also shown.


In [33]:
import mpld3

fig, ax = plt.subplots()

m = Basemap(projection='moll',lon_0=0,resolution='l')
m.drawmapboundary(fill_color='white',zorder=0)
m.drawcoastlines(zorder=1)

scatter = m.scatter(badquake_x,badquake_y,s=30,zorder=2)
ax.set_title("Earthquake locations (M>7) since 1900 with >500 casualties")

labels = earthquakes_mag7_highd.YEAR.astype(float).tolist()
tooltip = mpld3.plugins.PointLabelTooltip(scatter, labels=labels)
mpld3.plugins.connect(fig, tooltip)

mpld3.display()


Out[33]:

In [34]:
bad_quake_w_focal_depth = earthquakes_mag7_highd.dropna(subset = ['FOCAL_DEPTH', 'TOTAL_INJURIES', 'EQ_MAG_MW'])
notasbad_quake_w_focal_depth = earthquakes_mag7_lowd.dropna(subset = ['FOCAL_DEPTH', 'TOTAL_INJURIES', 'EQ_MAG_MW'])

bad_quake_w_focal_depth.head()


Out[34]:
I_D FLAG_TSUNAMI YEAR MONTH DAY HOUR MINUTE SECOND FOCAL_DEPTH EQ_PRIMARY ... TOTAL_MISSING TOTAL_MISSING_DESCRIPTION TOTAL_INJURIES TOTAL_INJURIES_DESCRIPTION TOTAL_DAMAGE_MILLIONS_DOLLARS TOTAL_DAMAGE_DESCRIPTION TOTAL_HOUSES_DESTROYED TOTAL_HOUSES_DESTROYED_DESCRIPTION TOTAL_HOUSES_DAMAGED TOTAL_HOUSES_DAMAGED_DESCRIPTION
259 3227 Tsu 1923 9 1 2 58 37.0 35 7.9 ... 43476 4 47000 4 600 4 695000 4 NaN NaN
283 3315 NaN 1927 5 22 22 32 42.0 27 7.6 ... NaN NaN 524 3 NaN 4 26674 4 NaN NaN
438 3791 Tsu 1944 12 7 4 35 NaN 30 8.1 ... NaN NaN 2135 4 NaN 3 29205 4 46950 4
583 4227 Tsu 1960 5 22 19 11 17.0 33 9.5 ... NaN NaN 3000 4 1000 4 58622 4 NaN NaN
682 4531 Tsu 1970 5 31 20 23 27.3 43 7.9 ... NaN NaN 50000 4 530 4 NaN 3 NaN NaN

5 rows × 47 columns


In [35]:
fig = plt.figure()

ax = fig.add_subplot(111, axisbg='#EEEEEE')
ax.grid(color='white', linestyle='solid')

x = np.random.normal(size=1000)
ax.hist(notasbad_quake_w_focal_depth.FOCAL_DEPTH.tolist(),bins=30,
        histtype='stepfilled', fc='lightblue', alpha=0.5, label='earthquakes with deaths < 500')
ax.hist(bad_quake_w_focal_depth.FOCAL_DEPTH.tolist(),bins=30, 
        histtype='stepfilled', fc='red', alpha=0.5, label='earthquakes with deaths > 500')
plt.legend()
plt.xlabel('depth of earthquake (km)')
plt.ylabel('number of earthquakes')
mpld3.display()


Out[35]:

Using folium to make an interactive map (Earthquake data example)

Folium builds on the data wrangling strengths of the Python ecosystem and the mapping strengths of the Leaflet.js library. Manipulate your data in Python, then visualize it in on a Leaflet map via Folium. https://github.com/python-visualization/folium

OSGeo has put together a number of great example geospatial notebooks in this repository (https://github.com/OSGeo/OSGeoLive-Notebooks) including one with a bunch of Folium examples (https://github.com/OSGeo/OSGeoLive-Notebooks/tree/master/projects/FOLIUM).

One example is shown below where the earthquake data are plotted using folium with a custom popup that brings together year and casuality information.


In [38]:
import folium
map_1 = folium.Map(location=[20, 100], zoom_start=4,
                   tiles='Mapbox Bright')
for n in range(0,len(badquake_lon)):
    map_1.simple_marker([badquake_lat[n],badquake_lon[n]],
                        popup='Year: '+ str(badquake_year[n]) + ' casualties: ' + str(badquake_casualties[n]))
map_1.save(outfile='code_output/earthquakes.html')
map_1


/Users/Laurentia/anaconda/lib/python3.4/site-packages/ipykernel/__main__.py:6: FutureWarning: simple_marker is deprecated. Use add_children(Marker) instead
Out[38]:

Using Bokeh to make an interactive map (Earthquake data example)


In [37]:
from bokeh.io import output_file, show
from bokeh.models import (GMapPlot, GMapOptions, ColumnDataSource, Circle, DataRange1d, PanTool, WheelZoomTool, BoxSelectTool)

map_options = GMapOptions(lat=20, lng=100, map_type="roadmap", zoom=4)

plot = GMapPlot(x_range=DataRange1d(), 
                y_range=DataRange1d(), 
                map_options=map_options, title="Earthquakes")

source = ColumnDataSource(
    data=dict(
        lat=badquake_lat,
        lon=badquake_lon,
    )
)

circle = Circle(x="lon", y="lat", size=15, fill_color="blue", fill_alpha=0.8, line_color=None)
plot.add_glyph(source, circle)

plot.add_tools(PanTool(), WheelZoomTool(), BoxSelectTool())
output_file("code_output/gmap_plot.html")
show(plot)

In [ ]: