Skip to content

Commit

Permalink
Archive graphics (#593)
Browse files Browse the repository at this point in the history
* candidate colors/shapes

* map_neighbors no longer assumes the first neighbor in the list is itself

map_neighbors now checks for itself and removes duplicate rows in the neighbors dataframe, keeping the final row as the target

* tweaking colors

* implemented map_neighbors() title with CI test

* map title + CI tests

---------

Co-authored-by: Jordan Landers <[email protected]>
  • Loading branch information
CommonClimate and jordanplanders authored Jun 27, 2024
1 parent 843d61a commit c355874
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 36 deletions.
87 changes: 75 additions & 12 deletions pyleoclim/core/geoseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ def from_Series(lat, lon, elevation=None,sensorType=None,observationType=None,

def map(self, projection='Orthographic', proj_default=True,
background=True, borders=False, coastline=True, rivers=False, lakes=False, ocean=True,
land=True, fig=None, gridspec_slot=None,
land=True, fig=None, gridspec_slot=None, title = None,
figsize=None, marker='archiveType', hue='archiveType', size=None, edgecolor='w',
markersize=None, scatter_kwargs=None, cmap=None, colorbar=False, gridspec_kwargs=None,
legend=True, lgd_kwargs=None, savefig_settings=None):
Expand Down Expand Up @@ -388,17 +388,36 @@ def map(self, projection='Orthographic', proj_default=True,
.. jupyter-execute::
import pyleoclim as pyleo
ts = pyleo.utils.datasets.load_dataset('EDC-dD')
fig, ax = ts.map()
By default, the figure has no title. For a title built from the available labels:
.. jupyter-execute::
fig, ax = ts.map(title=True)
For a custom title, and custom projection:
.. jupyter-execute::
fig, ax = ts.map(title='Insert title here', projection='RotatedPole',
proj_default={'pole_longitude':0.0, 'pole_latitude':-90.0, 'central_rotated_longitude':45.0})
'''
if markersize != None:
scatter_kwargs['markersize'] = markersize

if type(title)==bool:
if title == False:
title = None
else:
if self.label is not None:
title = f"{self.label} location"

fig, ax_d = mapping.scatter_map(self, hue=hue, size=size, marker=marker, projection=projection,
proj_default=proj_default,
proj_default=proj_default, title = title,
background=background, borders=borders, rivers=rivers, lakes=lakes,
ocean=ocean, land=land, coastline=coastline,
figsize=figsize, scatter_kwargs=scatter_kwargs, gridspec_kwargs=gridspec_kwargs,
Expand All @@ -409,7 +428,7 @@ def map(self, projection='Orthographic', proj_default=True,

def map_neighbors(self, mgs, radius=3000, projection='Orthographic', proj_default=True,
background=True, borders=False, rivers=False, lakes=False, ocean=True,
land=True, fig=None, gridspec_slot=None,
land=True, fig=None, gridspec_slot=None, title = None,
figsize=None, marker='archiveType', hue='archiveType', size=None, edgecolor=None,#'w',
markersize=None, scatter_kwargs=None, cmap=None, colorbar=False, gridspec_kwargs=None,
legend=True, lgd_kwargs=None, savefig_settings=None):
Expand Down Expand Up @@ -470,6 +489,10 @@ def map_neighbors(self, mgs, radius=3000, projection='Orthographic', proj_defaul
- "path" must be specified; it can be any existed or non-existed path,
with or without a suffix; if the suffix is not given in "path", it will follow "format"
- "format" can be one of {"pdf", "eps", "png", "ps"}. The default is None.
title : bool or str
the title for the figure. If True or None, made automatically from the objects' labels.
Set to False for an empty title.
Returns
-------
Expand Down Expand Up @@ -500,10 +523,23 @@ def map_neighbors(self, mgs, radius=3000, projection='Orthographic', proj_defaul
archiveType = row['archiveType'], verbose = False,
label=row['dataSetName']+'_'+row['paleoData_variableName']))
mgs = pyleo.MultipleGeoSeries(series_list=ts_list,time_unit='years AD')
mgs = pyleo.MultipleGeoSeries(series_list=ts_list,time_unit='years AD',label='Euro2k')
gs = ts_list[6] # extract one record as the target one
gs.map_neighbors(mgs, radius=4000)
By default, the figure has no title. For a title built from the available labels:
.. jupyter-execute::
gs.map_neighbors(mgs, radius=4000, title=True)
For a custom title:
.. jupyter-execute::
gs.map_neighbors(mgs, radius=4000, title='Insert title here')
'''
from ..core.multiplegeoseries import MultipleGeoSeries
if markersize != None:
Expand All @@ -512,42 +548,69 @@ def map_neighbors(self, mgs, radius=3000, projection='Orthographic', proj_defaul
# find neighbors
lats = [ts.lat for ts in mgs.series_list]
lons = [ts.lon for ts in mgs.series_list]
# compute distance from self to all points in supplied catalog
dist = mapping.compute_dist(self.lat, self.lon, lats, lons)
# identify indices of neighbors within specified radius
neigh_idx = mapping.within_distance(dist, radius)

neighbors =[mgs.series_list[i] for i in neigh_idx if i !=0]
# create a new MultipleGeoSeries object with only the neighbors
neighbors =[mgs.series_list[i] for i in neigh_idx]
dists = [dist[i] for i in neigh_idx]
neighbors = MultipleGeoSeries(neighbors)

# create a dataframe for neighbors and add distance to target
df = mapping.make_df(neighbors, hue=hue, marker=marker, size=size)
df['distance'] = dists

# create a dataframe for the target
df_self = mapping.make_df(self, hue=hue, marker=marker, size=size)
df_self['distance'] = 0

neighborhood = pd.concat([df, df_self], axis=0)

# additional columns are added manually
neighbor_coloring = ['w' for ik in range(len(neighborhood))]
# remove duplicates
# keep = "last" is specified to make sure that if the target was included
# in the potential neighbor catalog, it is kept as the last record
neighborhood = neighborhood.drop_duplicates(keep='last')

# add a column to specify the status of the record as either neighbor or target
neighbor_status = ['neighbor' for ik in range(len(neighborhood))]
neighbor_status[-1] = 'target'
neighborhood['neighbor_status'] = neighbor_status

# neighbors are assigned white as edgecolor
neighbor_coloring = ['w' for ik in range(len(neighborhood))]

# if edgecolor is not specified, use black, otherwise, use the specified color
if edgecolor is None:
edgecolor = 'k'
if isinstance(scatter_kwargs, dict):
edgecolor = scatter_kwargs.pop('edgecolor', 'k')

neighbor_coloring[-1] = edgecolor
neighborhood['original'] =neighbor_coloring
neighborhood['edgecolor'] =neighbor_coloring

# plot neighbors
if type(title)==bool:
if title == False:
title = None
else:
if mgs.label is not None and self.label is not None:
title = f"{mgs.label} neighbors for {self.label} within {radius} km"


# plot neighbors
# in future version, if edgecolor is specified as a dictionary with keys "neighbor" and "target",
# and values that are colors, that mapping will be used to color the edges of the points
fig, ax_d = mapping.scatter_map(neighborhood, fig=fig, gs_slot=gridspec_slot, hue=hue, size=size,
marker=marker, projection=projection,
proj_default=proj_default,
background=background, borders=borders, rivers=rivers, lakes=lakes,
ocean=ocean, land=land,
ocean=ocean, land=land, title = title,
figsize=figsize, scatter_kwargs=scatter_kwargs, lgd_kwargs=lgd_kwargs,
gridspec_kwargs=gridspec_kwargs, colorbar=colorbar,
legend=legend, cmap=cmap, edgecolor=neighborhood['original'].values)
legend=legend, cmap=cmap, edgecolor=neighborhood['edgecolor'].values)


return fig, ax_d

def dashboard(self, figsize=[11, 8], gs=None, plt_kwargs=None, histplt_kwargs=None, spectral_kwargs=None,
Expand Down
53 changes: 50 additions & 3 deletions pyleoclim/tests/test_core_GeoSeries.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_init_dropna(self, evenly_spaced_series):
print(ts2.value)
assert ~np.isnan(ts2.value[0])

@pytest.mark.xfail # will fail until pandas is fixed
#@pytest.mark.xfail # will fail until pandas is fixed
class TestUIGeoSeriesResample():
''' test GeoSeries.Resample()
'''
Expand All @@ -111,8 +111,42 @@ def test_map_neighbors_t1(self, pinkgeoseries):
mgs = multiple_pinkgeoseries()
fig, ax = ts.map_neighbors(mgs, radius=5000)
pyleo.closefig(fig)

@pytest.mark.parametrize('title',[None, False, True, 'Untitled'])
def test_map_neighbors_t2(self, title):
PLOT_DEFAULT = pyleo.utils.lipdutils.PLOT_DEFAULT
ntypes = len(PLOT_DEFAULT)

lat = np.random.uniform(20,70,ntypes)
lon = np.random.uniform(-20,60,ntypes)

dummy = [1, 2, 3]

class TestUiGeoSeriesMap():
ts = pyleo.GeoSeries(time = dummy, value=dummy, lat=lat.mean(), lon=lon.mean(),
auto_time_params=True, verbose=False, archiveType='Wood',
label='Random Tree')
series_list = []
for i, key in enumerate(PLOT_DEFAULT.keys()):
ser = ts.copy()
ser.lat=lat[i]
ser.lon=lon[i]
ser.archiveType=key
ser.label=key
series_list.append(ser)

mgs = pyleo.MultipleGeoSeries(series_list,time_unit='Years CE', label = 'multi-archive maelstrom')

fig, ax = ts.map_neighbors(mgs,radius=5000, title = title)

if title is None or title == False:
assert ax['map'].get_title() == ''
elif title == True:
assert ax['map'].get_title() == 'multi-archive maelstrom neighbors for Random Tree within 5000 km'
else:
ax['map'].get_title() == 'Untitled'
pyleo.closefig(fig)

class TestUIGeoSeriesMap():
''' test GeoSeries.map()
'''

Expand All @@ -121,6 +155,19 @@ def test_map_t0(self, pinkgeoseries):
fig, ax = ts.map()
pyleo.closefig(fig)

@pytest.mark.parametrize('title',[None, False, True, 'Untitled'])
def test_map_t1(self, pinkgeoseries, title):
ts = pinkgeoseries
fig, ax = ts.map(title=title)
if title is None or title == False:
assert ax['map'].get_title() == ''
elif title == True:
assert ax['map'].get_title() == 'pink noise geoseries location'
else:
ax['map'].get_title() == 'Untitled'
pyleo.closefig(fig)
pyleo.closefig(fig)


def test_segment():
'''
Expand All @@ -133,7 +180,7 @@ def test_segment():
assert np.array_equal(mgs.series_list[0].value,gs.value[:4000])
assert np.array_equal(mgs.series_list[1].value,gs.value[5000:])

class TestUiGeoSeriesDashboard():
class TestUIGeoSeriesDashboard():
''' test GeoSeries.Dashboard
'''

Expand Down
62 changes: 44 additions & 18 deletions pyleoclim/utils/lipdutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,53 @@ def __setitem__(self, key, value):
def __getitem__(self, key):
return super().__getitem__(key.lower().replace(" ", ""))

PLOT_DEFAULT = {'GroundIce': ['#86CDFA', 'h'],
'Borehole': ['#00008b', 'h'],
'Coral': ['#FF8B00', 'o'],
'Documents': ['#f8d568', 'p'],
'GlacierIce': ['#86CDFA', 'd'],
'Hybrid': ['#808000', '*'],
'LakeSediment': ['#8A4513', '^'],
'MarineSediment': ['#8A4513', 's'],
'Sclerosponge': ['r', 'o'],
# Old one:
# PLOT_DEFAULT = {'GroundIce': ['#86CDFA', 'h'],
# 'Borehole': ['#00008b', 'h'],
# 'Coral': ['#FF8B00', 'o'],
# 'Documents': ['#f8d568', 'p'],
# 'GlacierIce': ['#86CDFA', 'd'],
# 'Hybrid': ['#808000', '*'],
# 'LakeSediment': ['#8A4513', '^'],
# 'MarineSediment': ['#8A4513', 's'],
# 'Sclerosponge': ['r', 'o'],
# 'Speleothem': ['#FF1492', 'd'],
# 'Wood': ['#32CC32', '^'],
# 'MolluskShell': ['#FFD600', 'h'],
# 'Peat': ['#2F4F4F', '*'],
# 'Midden': ['#824E2B', 'o'],
# 'FluvialSediment': ['#4169E0','o'],
# 'TerrestrialSediment': ['#8A4513','o'],
# 'Shoreline': ['#add8e6','o'],
# 'Instrumental' : ['#8f21d8', '*'],
# 'Model' : ['#b4a7d6', "d"],
# 'Other': ['k', 'o']
# }

PLOT_DEFAULT = {'GlacierIce': ['deepskyblue', '*'],
'GroundIce': ['slategray', '*'],
'Borehole': ['#FFD600', 's'],
'Coral': ['#FF8B00', 'v'],
'Sclerosponge': ['r', 'v'],
'Documents': ['#f8d568', 'p'],
'Hybrid': ['#808000', 'H'],
'LakeSediment': ['#1170aa', 'o'],
'MarineSediment': ['#8A4513', 'o'],
'FluvialSediment': ['#5fa2ce','o'],
'TerrestrialSediment': ['#57606c','o'],
'Speleothem': ['#FF1492', 'd'],
'Wood': ['#32CC32', '^'],
'MolluskShell': ['#FFD600', 'h'],
'Peat': ['#2F4F4F', '*'],
'Midden': ['#824E2B', 'o'],
'FluvialSediment': ['#4169E0','d'],
'TerrestrialSediment': ['#8A4513','o'],
'Shoreline': ['#add8e6','o'],
'Instrumental' : ['#8f21d8', '*'],
'Model' : ['#b4a7d6', "d"],
'Wood': ['#8CD17D', '^'],
'MolluskShell': ['#f8d568', 'h'],
'Peat': ['#8A9A5B', 'X'],
'Midden': ['#824E2B', 'X'],
'Shoreline': ['#40826D','o'],
'Instrumental' : ['#B07AA1', 'D'],
'Model' : ['#E15759', "D"],
'Other': ['k', 'o']
}
# as per lipd convention, communicated by David Edge, 06.25.2024
# var colorPal = {"Borehole":"#FFD600","MolluskShell":"#7b03fc","GlacierIce":"#86CDFA","GroundIce":"#ff6db6","Coral":"#FF8B00","FluvialSediment":"#4169E0","LakeSediment":"#8f8fa1","MarineSediment":"#8A4513","Speleothem":"#FF1492","Midden":"#824E2B","Peat":"#8A9A5B","Sclerosponge":"#D2042D","Shoreline":"#40826D","Wood":"#32CC32","TerrestrialSediment":"#d2b48c"}
# var shapePal ={"Borehole":"square","MolluskShell":"triangle","GlacierIce":"snowflake","GroundIce":"snowflake","Coral":"triangle-down","FluvialSediment":"circle","LakeSediment":"circle","MarineSediment":"circle","Speleothem":"square","Midden":"diamond","Peat":"triangle-down","Sclerosponge":"triangle","Shoreline":"diamond","Wood":"triangle","TerrestrialSediment":"circle"}


"""
Expand Down
11 changes: 8 additions & 3 deletions pyleoclim/utils/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ def make_df(geo_ms, hue=None, marker=None, size=None, cols=None, d=None):


def scatter_map(geos, hue='archiveType', size=None, marker='archiveType', edgecolor='k',
proj_default=True, projection='auto', crit_dist=5000,
proj_default=True, projection='auto', crit_dist=5000, title = None,
background=True, borders=False, coastline=True, rivers=False, lakes=False, ocean=True, land=True,
figsize=None, scatter_kwargs=None, gridspec_kwargs=None, extent='global', edgecolor_var=None,
lgd_kwargs=None, legend=True, colorbar=True, cmap=None, color_scale_type=None,
Expand Down Expand Up @@ -681,7 +681,9 @@ def scatter_map(geos, hue='archiveType', size=None, marker='archiveType', edgeco
- 'missing_val_hue', 'missing_val_marker', 'missing_val_label' can all be used to change the way missing values are represented ('k', '?', are default hue and marker values will be associated with the label: 'missing').
- 'hue_mapping' and 'marker_mapping' can be used to submit dictionaries mapping hue values to colors and marker values to markers. Does not replace passing a string value for hue or marker.
- 'scalar_mappable' can be used to pass a matplotlib scalar mappable. See pyleoclim.utils.plotting.make_scalar_mappable for documentation on using the Pyleoclim utility, or the `Matplotlib tutorial on customizing colorbars <https://matplotlib.org/stable/users/explain/colors/colorbar_only.html>`_.
title : str
the title for the figure
Returns
-------
Expand Down Expand Up @@ -1323,9 +1325,12 @@ def replace_last(source_string, replace_what, replace_with):
x = 'lon'
y = 'lat'
_, ax_d = plot_scatter(df=df, x=x, y=y, hue_var=hue, size_var=size, marker_var=marker, ax_d=ax_d, proj=None,
edgecolor=edgecolor, colorbar=colorbar, color_scale_type=color_scale_type,
edgecolor=edgecolor, colorbar=colorbar, color_scale_type=color_scale_type, title = title,
cmap=cmap, scatter_kwargs=scatter_kwargs, legend=legend, lgd_kwargs=lgd_kwargs,
**kwargs) # , **kwargs)
if title is not None:
ax_d['map'].set_title(title)

return fig, ax_d


Expand Down

0 comments on commit c355874

Please sign in to comment.