Skip to content

Commit d685040

Browse files
authored
Merge pull request #54 from Imageomics/dev
v1.1.0 dashboard updates
2 parents 8fca070 + 0a148e0 commit d685040

15 files changed

+193
-73
lines changed

README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ For full dashboard functionality, upload a CSV or XLS file with the following co
1111
- `View`: View of the sample (eg., 'ventral' or 'dorsal' for butterflies).
1212
- `Sex`: Sex of each sample.
1313
- `hybrid_stat`: Hybrid status of each sample (eg., 'valid_subspecies', 'subspecies_synonym', or 'unknown').
14-
- `lat`*: Latitude at which image was taken or specimen was collected.
15-
- `lon`*: Longitude at which image was taken or specimen was collected.
14+
- `lat`*: Latitude at which image was taken or specimen was collected: number in [-90,90].
15+
- `lon`*: Longitude at which image was taken or specimen was collected: number in [-180,180]. `long` will also be accepted.
1616
- `file_url`*: URL to access file.
1717

1818
***Note:**
19-
- `lat` and `lon` columns are not required to utilize the dashboard, but there will be no map view if they are not included.
19+
- Column names are **not** case-sensitive.
20+
- `lat` and `lon` columns are not required to utilize the dashboard, but there will be no map view if they are not included. Blank (or null) entries are recorded as `unknown`, and thus excluded from map view.
2021
- `Image_filename` and `file_url` are not required, but there will be no sample images option if either one is not included.
22+
- `locality` may be provided, otherwise it will take on the value `lat|lon` or `unknown` if these are not provided.
2123

2224
## Running Dashboard
2325

components/divs.py

+31-14
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# Fixed styles and sorting options
44
H1_STYLE = {'textAlign': 'center', 'color': 'MidnightBlue'}
55
H4_STYLE = {'color': 'MidnightBlue', 'margin-bottom' : 10}
6-
HALF_DIV_STYLE = {'width': '48%', 'display': 'inline-block'}
6+
HALF_DIV_STYLE = {'height': '48%', 'width': '48%', 'display': 'inline-block'}
77
QUARTER_DIV_STYLE = {'width': '24%', 'display': 'inline-block'}
88
BUTTON_STYLE = {'color': 'MidnightBlue',
99
'background-color': 'BlanchedAlmond',
@@ -18,10 +18,14 @@
1818
{'label': 'Subspecies', 'value': 'Subspecies'},
1919
{'label':'View', 'value': 'View'},
2020
{'label': 'Sex', 'value': 'Sex'},
21-
{'label': 'Hybrid Status', 'value':'hybrid_stat'},
22-
{'label': 'Locality', 'value': 'locality'}
21+
{'label': 'Hybrid Status', 'value':'Hybrid_stat'},
22+
{'label': 'Locality', 'value': 'Locality'}
2323
]
2424
DOCS_URL = "https://github.com/Imageomics/dashboard-prototype#how-it-works"
25+
DOCS_LINK = html.A("documentation",
26+
href=DOCS_URL,
27+
target='_blank',
28+
style = ERROR_STYLE)
2529

2630
def get_hist_div(mapping):
2731
'''
@@ -124,6 +128,12 @@ def get_map_div():
124128
),
125129

126130
html.Div([
131+
html.H4('''
132+
Note: Manual zooming may be required to view all points; the map focuses on the centroid of the data.
133+
''',
134+
id = 'x-variable', #label to avoid nonexistent callback variable
135+
style = {'color': 'MidnightBlue', 'margin-left': 20, 'margin-right': 20}
136+
)
127137
],
128138
id = 'sort-by', #label sort-by box to avoid non-existent label and generate box so button doesn't move between views
129139
style = HALF_DIV_STYLE
@@ -192,8 +202,8 @@ def get_img_div(df, all_species, img_url):
192202
style = QUARTER_DIV_STYLE
193203
),
194204
html.Div([
195-
dcc.Checklist(df.hybrid_stat.unique(),
196-
df.hybrid_stat.unique()[0:2],
205+
dcc.Checklist(df.Hybrid_stat.unique(),
206+
df.Hybrid_stat.unique()[0:2],
197207
id = 'hybrid?')],
198208
style = QUARTER_DIV_STYLE
199209
),
@@ -267,7 +277,10 @@ def get_main_div(hist_div, img_div):
267277

268278
# Graphs - Distribution (histogram or map), then pie chart
269279
html.Div([
270-
dcc.Graph(id = 'dist-plot')], style = HALF_DIV_STYLE),
280+
dcc.Loading(id = 'dist-plot-loading',
281+
type = "circle",
282+
color = 'DarkMagenta',
283+
children = dcc.Graph(id = 'dist-plot'))], style = HALF_DIV_STYLE),
271284
html.Div([
272285
dcc.Graph(id = 'pie-plot')], style = HALF_DIV_STYLE),
273286

@@ -303,20 +316,24 @@ def get_error_div(error_dict):
303316
html.H3("Source data does not have '" + feature + "' column. ",
304317
style = ERROR_STYLE),
305318
html.H4(["Please see the ",
306-
html.A("documentation",
307-
href=DOCS_URL,
308-
target='_blank',
309-
style = ERROR_STYLE),
319+
DOCS_LINK,
310320
" for list of required columns."],
311321
style = ERROR_STYLE)
312322
])
323+
elif 'mapping' in error_dict.keys():
324+
error_msg = error_dict['mapping']
325+
error_div = html.Div([
326+
html.H4("Latitude or longitude columns have non-numeric values: " + error_msg + ".",
327+
style = ERROR_STYLE),
328+
html.H4(["Please see the ",
329+
DOCS_LINK,
330+
"."],
331+
style = ERROR_STYLE)
332+
])
313333
elif 'type' in error_dict.keys():
314334
error_div = html.Div([
315335
html.H4(["The source file is not a valid CSV format, please see the ",
316-
html.A("documentation",
317-
href=DOCS_URL,
318-
target='_blank',
319-
style = ERROR_STYLE),
336+
DOCS_LINK,
320337
"."],
321338
style = ERROR_STYLE)
322339
])

components/graphs.py

+42-15
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ def make_hist_plot(df, x_var, color_by, sort_by):
2626
color = color_by,
2727
color_discrete_sequence = px.colors.qualitative.Bold).update_xaxes(categoryorder = sort_by)
2828

29-
fig.update_layout(title = {'text': f'Distribution of {x_var} Colored by {color_by}'})
29+
fig.update_layout(title = {'text': f'Distribution of {x_var} Colored by {color_by}'},
30+
font = {'size': 16},
31+
margin = {
32+
'l': 30,
33+
'r': 20,
34+
't': 35,
35+
'b': 20
36+
})
3037

3138
return fig
3239

@@ -46,22 +53,17 @@ def make_map(df, color_by):
4653
df = df.copy()
4754
# only use entries that have valid lat & lon for mapping
4855
df = df.loc[df['lat-lon'].str.contains('unknown') == False]
49-
fig = px.scatter_geo(df,
50-
lat = df.lat,
51-
lon = df.lon,
52-
projection = "natural earth",
56+
fig = px.scatter_mapbox(df,
57+
lat = "Lat",
58+
lon = "Lon",
59+
#projection = "natural earth",
5360
custom_data = ["Samples_at_locality", "Species_at_locality", "Subspecies_at_locality"],
54-
size = df.Samples_at_locality,
61+
size = "Samples_at_locality",
5562
color = color_by,
5663
color_discrete_sequence = px.colors.qualitative.Bold,
57-
title = "Distribution of Samples")
58-
59-
fig.update_geos(fitbounds = "locations",
60-
showcountries = True, countrycolor = "Grey",
61-
showrivers = True,
62-
showlakes = True,
63-
showland = True, landcolor = "wheat",
64-
showocean = True, oceancolor = "LightBlue")
64+
title = "Distribution of Samples",
65+
zoom = 1,
66+
mapbox_style = "white-bg")
6567

6668
fig.update_traces(hovertemplate =
6769
"Latitude: %{lat}<br>"+
@@ -71,6 +73,24 @@ def make_map(df, color_by):
7173
"Subspecies at lat/lon: %{customdata[2]}<br>"
7274
)
7375

76+
fig.update_layout(
77+
font = {'size': 16},
78+
margin = {
79+
'l': 20,
80+
'r': 20,
81+
't': 35,
82+
'b': 20
83+
},
84+
mapbox_layers = [{
85+
"below": "traces",
86+
"sourcetype": "raster",
87+
"sourceattribution": "Esri, Maxar, Earthstar Geographics, and the GIS User Community",
88+
"source": ["https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"]
89+
# Usage and Licensing (ArcGIS World Imagery): https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer
90+
# Style: https://roblabs.com/xyz-raster-sources/styles/arcgis-world-imagery.json
91+
}]
92+
)
93+
7494
return fig
7595

7696
def make_pie_plot(df, var):
@@ -97,6 +117,13 @@ def make_pie_plot(df, var):
97117
color_discrete_sequence = px.colors.qualitative.Bold)
98118
pie_fig.update_traces(textposition = 'inside', textinfo = 'percent+label')
99119

100-
pie_fig.update_layout(title = {'text': f'Percentage Breakdown of {var}'})
120+
pie_fig.update_layout(title = {'text': f'Percentage Breakdown of {var}'},
121+
font = {'size': 16},
122+
margin = {
123+
'l': 20,
124+
'r': 20,
125+
't': 35,
126+
'b': 20
127+
})
101128

102129
return pie_fig

components/query.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def get_data(df, mapping, features):
1717
df - DataFrame of the data to visualize.
1818
mapping - Boolean. True when lat/lon are given in dataset.
1919
features - List of features (columns) included in the DataFrame. This is a subset of the suggested columns:
20-
'Species', 'Subspecies', 'View', 'Sex', 'hybrid_stat', 'lat', 'lon', 'file_url', 'Image_filename'
20+
'Species', 'Subspecies', 'View', 'Sex', 'Hybrid_stat', 'Lat', 'Lon', 'File_url', 'Image_filename'
2121
2222
Returns:
2323
--------
@@ -29,24 +29,24 @@ def get_data(df, mapping, features):
2929
# Will likely choose to calculate and return this in later instance
3030
cat_list = [{'label': 'Species', 'value': 'Species'},
3131
{'label': 'Subspecies', 'value': 'Subspecies'},
32-
{'label':'View', 'value': 'View'},
32+
{'label': 'View', 'value': 'View'},
3333
{'label': 'Sex', 'value': 'Sex'},
34-
{'label': 'Hybrid Status', 'value':'hybrid_stat'},
35-
{'label': 'Locality', 'value': 'locality'}
34+
{'label': 'Hybrid Status', 'value':'Hybrid_stat'},
35+
{'label': 'Locality', 'value': 'Locality'}
3636
]
3737

3838
df = df.copy()
3939
df = df.fillna('unknown')
40-
features.append('locality')
40+
features.append('Locality')
4141

4242
# If we don't have lat/lon, just return DataFrame with otherwise required features.
4343
if not mapping:
44-
if 'locality' not in df.columns:
45-
df['locality'] = 'unknown'
44+
if 'Locality' not in df.columns:
45+
df['Locality'] = 'unknown'
4646
return df[features], cat_list
4747

4848
# else lat and lon are in dataset, so process locality information
49-
df['lat-lon'] = df['lat'].astype(str) + '|' + df['lon'].astype(str)
49+
df['lat-lon'] = df['Lat'].astype(str) + '|' + df['Lon'].astype(str)
5050
df["Samples_at_locality"] = df['lat-lon'].map(df['lat-lon'].value_counts()) # will duplicate if multiple views of same sample
5151

5252
# Count and record number of species and subspecies at each lat-lon
@@ -56,8 +56,8 @@ def get_data(df, mapping, features):
5656
df.loc[df['lat-lon'] == lat_lon, "Species_at_locality"] = ", ".join(species_list)
5757
df.loc[df['lat-lon'] == lat_lon, "Subspecies_at_locality"] = ", ".join(subspecies_list)
5858

59-
if 'locality' not in df.columns:
60-
df['locality'] = df['lat-lon'] # contains "unknown" if lat or lon null
59+
if 'Locality' not in df.columns:
60+
df['Locality'] = df['lat-lon'] # contains "unknown" if lat or lon null
6161

6262
new_features = ['lat-lon', "Samples_at_locality", "Species_at_locality", "Subspecies_at_locality"]
6363
for feature in new_features:
@@ -157,12 +157,12 @@ def get_filenames(df, subspecies, view, sex, hybrid, num_images):
157157
df_sub = df.loc[df.Subspecies.isin(subspecies)].copy()
158158
df_sub = df_sub.loc[df_sub.View.isin(view)]
159159
df_sub = df_sub.loc[df_sub.Sex.isin(sex)]
160-
df_sub = df_sub.loc[df_sub.hybrid_stat.isin(hybrid)]
160+
df_sub = df_sub.loc[df_sub.Hybrid_stat.isin(hybrid)]
161161

162162
num_entries = len(df_sub)
163163
# Filter out any entries that have missing filenames or URLs:
164164
df_sub = df_sub.loc[df_sub.Image_filename != 'unknown']
165-
df_sub = df_sub.loc[df_sub.file_url != 'unknown']
165+
df_sub = df_sub.loc[df_sub.File_url != 'unknown']
166166
max_imgs = len(df_sub)
167167
missing_vals = num_entries - max_imgs
168168
if max_imgs > 0:
@@ -172,7 +172,7 @@ def get_filenames(df, subspecies, view, sex, hybrid, num_images):
172172
num = min(num_images, max_imgs)
173173
df_filtered = df_sub.sample(num)
174174
filenames = df_filtered.Image_filename.astype('string').values
175-
filepaths = df_filtered.file_url.astype('string').values
175+
filepaths = df_filtered.File_url.astype('string').values
176176
#return list of filenames for min(user-selected, available) images randomly selected images from the filtered dataset
177177
return list(filenames), list(filepaths)
178178
# If there aren't any images to display, check if there are no such entries or just missing information.

dashboard.py

+25-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import pandas as pd
2+
import numpy as np
23
import base64
34
import io
45
import json
@@ -80,13 +81,21 @@ def parse_contents(contents, filename):
8081
# If no image urls, disable sample image options
8182
mapping = True
8283
img_urls = True
83-
features = ['Species', 'Subspecies', 'View', 'Sex', 'hybrid_stat', 'lat', 'lon', 'file_url', 'Image_filename']
84+
features = ['Species', 'Subspecies', 'View', 'Sex', 'Hybrid_stat', 'Lat', 'Lon', 'File_url', 'Image_filename']
8485
included_features = []
86+
df.columns = df.columns.str.capitalize()
8587
for feature in features:
8688
if feature not in list(df.columns):
87-
if feature == 'lat' or feature == 'lon':
88-
mapping = False
89-
elif feature == 'file_url':
89+
if feature == 'Lat' or feature == 'Lon':
90+
if feature == 'Lon':
91+
if 'Long' not in list(df.columns):
92+
mapping = False
93+
else:
94+
df = df.rename(columns = {"Long": "Lon"})
95+
included_features.append('Lon')
96+
else:
97+
mapping = False
98+
elif feature == 'File_url':
9099
img_urls = False
91100
elif feature == 'Image_filename':
92101
# If 'Image_filename' missing, return missing column if 'file_url' is included.
@@ -97,6 +106,18 @@ def parse_contents(contents, filename):
97106
else:
98107
included_features.append(feature)
99108

109+
# Check for lat/lon bounds & type if columns exist
110+
if mapping:
111+
try:
112+
# Check lat and lon within appropriate ranges (lat: [-90, 90], lon: [-180, 180])
113+
valid_lat = df['Lat'].astype(float).between(-90, 90)
114+
df.loc[~valid_lat, 'Lat'] = 'unknown'
115+
valid_lon = df['Lon'].astype(float).between(-180, 180)
116+
df.loc[~valid_lon, 'Lon'] = 'unknown'
117+
except ValueError as e:
118+
print(e)
119+
return json.dumps({'error': {'mapping': str(e)}})
120+
100121
# get dataset-determined static data:
101122
# the dataframe and categorical features - processed for map view if mapping is True
102123
# all possible species, subspecies

dashboard_preview_hist.png

36.4 KB
Loading

dashboard_preview_map.png

335 KB
Loading

test_data/HCGSD_test_latLonOOB.csv

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
NHM_Specimen,Image_filename,View,Species,Subspecies,Sex,addit_taxa_info,type_stat,hybrid_stat,in_reduced,locality,lat,lon,speciesdesig,file_url
2+
10429021,10429021_V_lowres.png,,erato,notabilis,,f._notabilis,,subspecies synonym,1,,-1.583333333,-77.75,e. notabilis,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/ventral_images/
3+
10428972,10428972_V_lowres.png,ventral,erato,petiverana,male,petiverana,,valid subspecies,1,Songolica (= Zongolica) MEX VC,18.66666667,-96.98333333,e. petiverana,
4+
10429172,,ventral,,petiverana,male,petiverana,,valid subspecies,1,San Ramon NIC ZE,92,-84.68333333,e. petiverana,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/ventral_images/
5+
10428595,10428595_D_lowres.png,dorsal,erato,phyllis,male,f._phyllis,,subspecies synonym,1,Resistencia ARG CH,-27.45,-58.98333333,e. phyllis,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/dorsal_images/
6+
10428140,10428140_V_lowres.png,ventral,,plesseni,male,plesseni,,valid subspecies,1,Banos ECD TU,-1.4,-740,m. plesseni,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/ventral_images/
7+
10428250,10428250_V_lowres.png,ventral,melpomene,,male,ab._rubra,,subspecies synonym,1,Caradoc (Hda) PER CU,-13.36666667,-70.95,m. schunkei,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/ventral_images/
8+
10427979,,dorsal,melpomene,rosina_S,male,rosina_S,,valid subspecies,1,Turrialba CRI CA,9.883333333,-83.63333333,m. rosina,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/dorsal_images/
9+
10428803,10428803_D_lowres.png,dorsal,erato,guarica,female,guarica,,valid subspecies,1,Fusagasuga COL CN,4.35,-74.36666667,e. guarica,
10+
10428169,10428169_V_lowres.png,ventral,melpomene,plesseni,male,f._pura,ST,subspecies synonym,1,Canelos ECD PA,-1.583333333,730,m. plesseni,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/ventral_images/
11+
10428321,10428321_D_lowres.png,,melpomene,nanna,male,nanna,ST,valid subspecies,1,Espirito Santo BRA ES,-20.33333333,-40.28333333,m. nanna,https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/dorsal_images/

0 commit comments

Comments
 (0)