diff --git a/README.md b/README.md index 4c2e69d..fd09fdc 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,14 @@ python dashboard.py Then navigate to [http://127.0.0.1:8050/](http://127.0.0.1:8050/) in your browser to see the graphs. -**Note:** For component pieces, see `test_components` folder; they are run similarly. - ### Preview -![image](dashboard_preview.png) + +#### Histogram View +![image](dashboard_preview_hist.png) + +#### Map View +![image](dashboard_preview_map.png) + ### Running Tests diff --git a/components/README.md b/components/README.md deleted file mode 100644 index 662810e..0000000 --- a/components/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Dashboard Prototype Test components -Prototype data dashboard components (histogram, histogram + pie chart, and image selector) using the [Cuthill Gold Standard Dataset](https://datacommons.tdai.osu.edu/dataset.xhtml?persistentId=doi:10.5072/FK2/GZYWNV&version=DRAFT), which was processed from Cuthill, et. al. (original dataset available at [doi:10.5061/dryad.2hp1978](https://doi.org/10.5061/dryad.2hp1978)). - - -### How it works - -Create and activate a new (python) virtual environment. -Then install the required packages (if using `conda`, first run `conda install pip`): - -``` -pip install -r requirements.txt -``` - -and run - -``` -python prototype-multiplot.py -``` - -Then navigate to [http://127.0.0.1:8050/](http://127.0.0.1:8050/) in your browser to see the graphs (histogram and pie chart). - - -**Note:** `proto-img-only.py` and `prototype_histogram.py` can be run in the same manner, but will only produce the image selector or histogram, respectively. - -### Preview -![image](https://github.com/Imageomics/dashboard-prototype/assets/31709066/1a9e5f20-5565-43a4-bd80-fbb2b66bb507) diff --git a/components/divs.py b/components/divs.py new file mode 100644 index 0000000..698d59e --- /dev/null +++ b/components/divs.py @@ -0,0 +1,110 @@ +from dash import html, dcc + +def get_hist_div(cat_list, sort_list, H4_style, div_style): + ''' + Function to generate the histogram options section of the dashboard, including button to select 'Map View'. + Provides choice of variables for distribution and to color by, with options for order to sort x-axis. + + Parameters: + ----------- + cat_list - List of categorical variables to be used for distribution and color-by options. + sort_list - List of options for sorting x-axis of histogram. + H4_style - Style setting for html Header 4. + div_style - Style setting for div containers. + + Returns: + -------- + hist_div - HTML Div containing all user options for histogram (variable for distribution, coloring, and order to sort x-axis), plus and 'Map View' button. + + ''' + hist_div = [ + html.Div([ + html.H4("Show me the distribution of ...", style = H4_style), + # Add dropdown options + # x-axis (feature) distribution options: 'Subspecies', 'Additional Taxa Information', 'Locality' + dcc.RadioItems(cat_list[:2] + cat_list[5:], + 'Subspecies', + id = 'x-variable') + ], style = div_style + ), + + html.Div([ + html.H4("Colored by ...", style = H4_style), + #select color-by option: 'View', 'Sex', 'Hybrid Status' + dcc.RadioItems(cat_list[2:-2], + 'View', + id = 'color-by') + ], style = div_style + ), + + html.Div([ + html.H4("Sort distribution ", style = {'color': 'MidnightBlue', 'margin-top' : 10, 'margin-bottom' : 10}), + dcc.RadioItems(sort_list, + 'alpha', + id = 'sort-by', + inline = True) + ], style = div_style + ), + html.Div([ + # Button to switch to Map View + html.Button("Show Map View", + id = 'dist-view-btn', + n_clicks = 0) + ], style = div_style + ) + ] + return hist_div + +def get_map_div(cat_list, H4_style, div_style): + ''' + Function to generate the mapping options section of the dashboard. + Provides choice of variables to color by and button to switch back to histogram ('Show Histogram'). + + Parameters: + ----------- + cat_list - List of categorical variables to be used for color-by options. + H4_style - Style setting for html Header 4. + div_style - Style setting for div containers. + + Returns: + -------- + map_div - HTML Div containing all user options for map (variables for coloring) and 'Show Histogram' button. + + ''' + map_div = [ + html.Div([ + html.H4(''' + This map shows the distribution of samples by locality, + where the size of the dots is determined by the total number of samples at that location. + ''', + id = 'x-variable', #label to avoid nonexistent callback variable + style = {'color': 'MidnightBlue', 'margin-left': 20, 'margin-right': 20} + ) + ], style = {'width': '48%', 'display': 'inline-block', 'vertical-align': 'bottom'} + ), + + html.Div([ + html.H4("Colored by ...", style = H4_style), + #select color-by option: 'Species', 'Subspecies', 'View', 'Sex', 'Hybrid Status', 'Additional Taxa Information', 'Locality' + dcc.RadioItems(cat_list, + 'View', + id = 'color-by', + style = {'padding-right': '20%', 'display': 'inline-flex', 'flex-wrap': 'wrap', 'flex-direction': 'row', 'justify-content': 'space-between'}) + ], style = {'width': '48%', 'display': 'inline-block', 'margin-bottom': 20} + ), + + html.Div([ + ], + id = 'sort-by', #label sort-by box to avoid non-existent label and generate box so button doesn't move between views + style = div_style + ), + html.Div([ + # Distribution View Type Button + html.Button("Show Histogram", + id = 'dist-view-btn', + n_clicks = 0) + ], style = div_style + ) + ] + + return map_div diff --git a/components/graphs.py b/components/graphs.py index 9f1cc26..ce178e3 100644 --- a/components/graphs.py +++ b/components/graphs.py @@ -1,26 +1,57 @@ import plotly.express as px +def make_hist_plot(df, x_var, color_by, sort_by): + ''' + Generates interactive histogram of selected variable, with option of properties to color by and order in which to sort. + + Parameters: + ----------- + df - DataFrame of specimens. + x_var - Variable to plot distribution. + color_by - Property to color the plot by. + sort_by - Ordering of bar charts (Alphabetical, Ascending, or Descending). -def make_map(df): + Returns: + -------- + fig - Histogram of the distribution of the requested variable. ''' - Generates interactive graph of Species and Subspecies by location. + if sort_by == 'alpha': + fig = px.histogram(df.sort_values(x_var), + x = x_var, + color = color_by, + color_discrete_sequence = px.colors.qualitative.Bold) + else: + fig = px.histogram(df, + x = x_var, + color = color_by, + color_discrete_sequence = px.colors.qualitative.Bold).update_xaxes(categoryorder = sort_by) + + fig.update_layout(title = {'text': f'Distribution of {x_var} Colored by {color_by}'}) + + return fig + +def make_map(df, color_by): + ''' + Generates interactive map of species and subspecies by location. Parameters: ----------- - df - dataframe of specimens + df - DataFrame of specimens. + color_by - Selected categorical variable by which to color. Returns: -------- - fig - Map of their locations + fig - Map of their locations. ''' fig = px.scatter_geo(df, lat = df.lat, lon = df.lon, projection = "natural earth", - hover_data = ["Species", "Subspecies"], - size = df.samples_at_locality, - color = "Subspecies", - color_discrete_sequence = px.colors.qualitative.Bold) + custom_data = ["Samples_at_locality", "Species_at_locality", "Subspecies_at_locality"], + size = df.Samples_at_locality, + color = color_by, + color_discrete_sequence = px.colors.qualitative.Bold, + title = "Distribution of Samples") fig.update_geos(fitbounds = "locations", showcountries = True, countrycolor = "Grey", @@ -29,4 +60,40 @@ def make_map(df): showland = True, landcolor = "wheat", showocean = True, oceancolor = "LightBlue") + fig.update_traces(hovertemplate = + "Latitude: %{lat}
"+ + "Longitude: %{lon}
" + + "Samples at lat/lon: %{customdata[0]}
" + + "Species at lat/lon: %{customdata[1]}
" + + "Subspecies at lat/lon: %{customdata[2]}
" + ) + return fig + +def make_pie_plot(df, var): + ''' + Generates interactive pie chart of dataset specimens with option of properties to color by. + + Parameters: + ----------- + df - DataFrame of specimens. + var - Selected categorical variable by which to color. + + Returns: + -------- + fig - Pie chart of the percentage breakdown of the `var` samples in the dataset. + ''' + if(var == 'Subspecies'): + pie_fig = px.pie(df, + names = var, + color_discrete_sequence = px.colors.qualitative.Bold, + hover_data = ['Species']) + else: + pie_fig = px.pie(df, + names = var, + color_discrete_sequence = px.colors.qualitative.Bold) + pie_fig.update_traces(textposition = 'inside', textinfo = 'percent+label') + + pie_fig.update_layout(title = {'text': f'Percentage Breakdown of {var}'}) + + return pie_fig diff --git a/components/proto-img-only.py b/components/proto-img-only.py deleted file mode 100644 index 7b1c476..0000000 --- a/components/proto-img-only.py +++ /dev/null @@ -1,139 +0,0 @@ -import pandas as pd -from dash import Dash, html, dcc, Input, Output, State -from dash.exceptions import PreventUpdate - -from query import get_species_options - -df = pd.read_csv("test_data/Hoyal_Cuthill_GoldStandard_metadata_cleaned.csv") - -app = Dash(__name__) - -all_species = get_species_options(df) - -app.layout = html.Div([ - html.Div([ - html.H4("Show me sample images of ...", style = {"color":"MidnightBlue", 'marginBottom' : 10}), - #select Species/Subspecies to view (defaul to Any) - # Note: these should be the same type to interact properly, first must not be clearable - dcc.Dropdown(options = list(all_species.keys()), - value = 'Any', - id = 'species-show', - clearable = False), - html.Hr(), - dcc.Dropdown( - multi = True, - id = 'subspecies-show', - placeholder = 'Select Subspecies to View'), - # Further Refine by Features - html.H4("that are ...", style = {"color":"MidnightBlue", 'marginBottom' : 10}), - html.Div([ - dcc.Checklist(df.Sex.unique(), - df.Sex.unique()[0:2], - id = 'which-sex')], - style = {'width': '24%', 'display': 'inline-block'} - ), - html.Div([ - dcc.Checklist(df.View.unique(), - df.View.unique()[0:2], - id = 'which-view')], - style = {'width': '24%', 'display': 'inline-block'} - ), - html.Div([ - dcc.Checklist(df.hybrid_stat.unique(), - df.hybrid_stat.unique()[0:2], - id = 'hybrid?')], - style = {'width': '24%', 'display': 'inline-block'} - ) - ], id = 'dropdown-images'), - - html.Hr(), - - #Button to activate the callback - html.Button('Display Images', - id = 'display-img', - n_clicks = 0), - - # Add some space after the button - html.Br(), - html.Br(), - html.Br(), - html.Br(), - - # Image Should appear - html.Div(id = 'image-1') - -]) - -# Callback for Image Species Selection -@app.callback( - Output(component_id = 'subspecies-show', component_property= 'options'), - Input(component_id = 'species-show', component_property = 'value') -) - -def set_subspecies_options(selected_species): - # Set subspecies options based on selected species - return [{'label': i, 'value': i} for i in all_species[selected_species]] - -# Callback for Image Subspecies Selection -@app.callback( - Output(component_id = 'subspecies-show', component_property= 'value'), - Input(component_id = 'subspecies-show', component_property = 'options') -) - -def set_subspecies_value(available_options): - # Collect selected subspecies - return available_options[0]['value'] - -@app.callback( - Output('image-1', 'children'), - Input('display-img', 'n_clicks'), - #State('species-show', 'value'), - State('subspecies-show', 'value'), - State('which-view', 'value'), - State('which-sex', 'value'), - State('hybrid?', 'value'), - prevent_initial_call = True -) - -# Retrieve a single image -def get_image(n_clicks, subspecies, view, sex, hybrid): - #if n_clicks is None: - # raise PreventUpdate - # else: - filename = get_filename(subspecies, view, sex, hybrid) - if filename == 0: - #print("No Such Images. Please make another selection.") - return html.H4("No Such Images. Please make another selection.", - style = {"color":"MidnightBlue"}) - if 'D_lowres' in filename: - image_directory = "dorsal_images/" - else: - image_directory = "ventral_images/" - #remove 'tif' from filename and replace with 'png' in url - image_path = "https://github.com/Imageomics/dashboard-prototype/blob/feature/image-options/test_data/images/" + image_directory + filename[:-3] + "png?raw=true" - return html.Img(src = image_path) - -def get_filename(subspecies, view, sex, hybrid): - #filter df by subspecies, then view, sex and hybrid - #return filenames for 7 randomly selected images from the filtered dataset - #check for Any-Melpomene, Any-Erato, or Any (general) - if 'Any' in subspecies: - if subspecies == 'Any': - df_sub = df.copy() - else: - subspecies = subspecies.split('-')[1].lower() - df_sub = df.loc[df.Subspecies == subspecies].copy() - else: - df_sub = df.loc[df.Subspecies.isin(subspecies)].copy() - df_sub = df_sub.loc[df_sub.View.isin(view)] - df_sub = df_sub.loc[df_sub.Sex.isin(sex)] - df_filtered = df_sub.loc[df_sub.hybrid_stat.isin(hybrid)] - if len(df_filtered) > 0: - filename = df_filtered.sample().Image_filename.astype('string').values[0] - return filename - else: - return 0 - - -if __name__ == '__main__': - app.run_server(debug=True) diff --git a/components/prototype-map.py b/components/prototype-map.py deleted file mode 100644 index 69f7ff9..0000000 --- a/components/prototype-map.py +++ /dev/null @@ -1,19 +0,0 @@ -import pandas as pd -from dash import Dash, html, dcc -from graphs import make_map - -df = pd.read_csv("test_data/Hoyal_Cuthill_GoldStandard_metadata_cleaned.csv") -df["samples_at_locality"] = df.locality.map(df.locality.value_counts()/2) - -app = Dash(__name__) - -app.layout = html.Div([ - html.H1("Distribution of Samples", style = {'textAlign': 'center', 'color': 'MidnightBlue'}), - - # Add empty plot for map - dcc.Graph(figure = make_map(df), - id = 'map') -]) - -if __name__ == '__main__': - app.run_server(debug=True) diff --git a/components/prototype-multiplot.py b/components/prototype-multiplot.py deleted file mode 100644 index 71d608b..0000000 --- a/components/prototype-multiplot.py +++ /dev/null @@ -1,129 +0,0 @@ -import pandas as pd -import plotly.express as px -from dash import Dash, html, dcc, Input, Output - -df = pd.read_csv("test_data/Hoyal_Cuthill_GoldStandard_metadata_cleaned.csv") - -app = Dash(__name__) - -app.layout = html.Div([ - html.H1("Cuthill Data Distribution Statistics", style = {'textAlign': 'center', 'color': 'MidnightBlue'}), - - #Distribution Options - html.Div([ - html.Div([ - html.H4("Show me the distribution of ...", style = {"color":"MidnightBlue", 'margin-bottom' : 10}), - # Add dropdown options - # x-axis (feature) distribution options: 'Subspecies', 'Additional Taxa Information', 'Locality' - dcc.RadioItems([ - {'label': 'Subspecies', 'value': 'Subspecies'}, - {'label': 'Additional Taxa Information', 'value':'addit_taxa_info'}, - {'label': 'Locality', 'value': 'locality'}], - 'Subspecies', - id = 'x-variable') - ], style = {'width': '48%', 'display': 'inline-block'} - ), - - html.Div([ - html.H4("Colored by ...", style = {'color': 'MidnightBlue', 'margin-bottom' : 10}), - #select color-by option: 'View', 'Sex', 'Hybrid Status' - dcc.RadioItems([ - {'label':'View', 'value': 'View'}, - {'label': 'Sex', 'value': 'Sex'}, - {'label': 'Hybrid Status', 'value':'hybrid_stat'}], - 'View', - id = 'color-by') - ], style = {'width': '48%', 'display': 'inline-block'} - ), - #html.Br(), - html.H4("Sort distribution ", style = {'color': 'MidnightBlue', 'margin-top' : 10, 'margin-bottom' : 10}), - dcc.RadioItems([ - {'label': 'Alphabetical', 'value': 'alpha'}, - {'label': 'Ascending', 'value': 'sum ascending'}, - {'label': 'Descending', 'value': 'sum descending'}], - 'alpha', - id = 'sort-by', - inline = True), - ], style = {'width': '48%', 'display': 'inline-block'} - ), - - #pie chart options - html.Div([ - html.H4("Show me the Percentage Breakdown of ...", style = {'color': 'MidnightBlue', 'margin-bottom' : 10}), - dcc.RadioItems([ - {'label': 'Species', 'value': 'Species'}, - {'label': 'Subspecies', 'value': 'Subspecies'}, - {'label':'View', 'value': 'View'}, - {'label': 'Sex', 'value': 'Sex'}, - {'label': 'Hybrid Status', 'value':'hybrid_stat'}], - 'Species', - id = 'prct-brkdwn' - ), - html.Br(), - ], style = {'width': '48%', 'display': 'inline-block'} - ), - - html.Br(), - html.Br(), - - #Graphs - html.Div([ - dcc.Graph(id = 'hist-plot')], style = {'width': '48%', 'display': 'inline-block'}), - html.Div([ - dcc.Graph(id = 'pie-plot')], style = {'width': '48%', 'display': 'inline-block'}) -]) - -@app.callback( - #hist output - Output(component_id='hist-plot', component_property='figure'), - #input x_var - Input(component_id='x-variable', component_property='value'), - #input color_by - Input(component_id='color-by', component_property='value'), - #input sort_by - Input(component_id='sort-by', component_property='value') -) - -def make_hist_plot(x_var, color_by, sort_by): - #generate histogram - if sort_by == 'alpha': - fig = px.histogram(df.sort_values(x_var), - x = x_var, - color = color_by, - color_discrete_sequence = px.colors.qualitative.Bold) - else: - fig = px.histogram(df, - x = x_var, - color = color_by, - color_discrete_sequence = px.colors.qualitative.Bold).update_xaxes(categoryorder = sort_by) - - fig.update_layout(title = {'text': f'Distribution of {x_var} Colored by {color_by}'}) - - return fig - -@app.callback( - #pie output - Output(component_id='pie-plot', component_property='figure'), - #pie input (var) - Input(component_id='prct-brkdwn', component_property='value') -) - -def make_pie_plot(var): - #generate pie chart - if(var == 'Subspecies'): - pie_fig = px.pie(df, - names = var, - color_discrete_sequence = px.colors.qualitative.Bold, - hover_data = ['Species']) - else: - pie_fig = px.pie(df, - names = var, - color_discrete_sequence = px.colors.qualitative.Bold) - pie_fig.update_traces(textposition = 'inside', textinfo = 'percent+label') - - pie_fig.update_layout(title = {'text': f'Percentage Breakdown of {var}'}) - - return pie_fig - -if __name__ == '__main__': - app.run_server(debug=True) \ No newline at end of file diff --git a/components/prototype_histogram.py b/components/prototype_histogram.py deleted file mode 100644 index 57f6881..0000000 --- a/components/prototype_histogram.py +++ /dev/null @@ -1,44 +0,0 @@ -import pandas as pd -import plotly.express as px -from dash import Dash, html, dcc, Input, Output - -df = pd.read_csv("test_data/Hoyal_Cuthill_GoldStandard_metadata_cleaned.csv") - -app = Dash(__name__) - -app.layout = html.Div([ - html.H1("Choose your distribution", style = {"color":"blue", "justify-content": "center"}), - # Add dropdown options - # x-axis (feature) distribution options: 'Subspecies', 'Additional Taxa Information', 'Locality' - dcc.Dropdown(['Subspecies', 'addit_taxa_info', 'locality'], - 'Subspecies', - id = 'x-variable'), - html.Br(), - #html.Div(id='dd1-output'), - #select color-by option: 'View', 'Sex', 'Hybrid Status' - dcc.RadioItems(['View', 'Sex', 'hybrid_stat'], - 'View', - id = 'color-by'), - html.Br(), - #html.Div(id = 'dd2-output'), - # Add empty plot for histogram - dcc.Graph(id = 'plot') -]) - -@app.callback( - # output - Output(component_id='plot', component_property='figure'), - #input1 - Input(component_id='x-variable', component_property='value'), - #input2 - Input(component_id='color-by', component_property='value') -) - -def make_plot(x_var, color_by): - fig = px.histogram(df.sort_values(x_var), - x = x_var, - color = color_by) - return fig - -if __name__ == '__main__': - app.run_server(debug=True) \ No newline at end of file diff --git a/components/query.py b/components/query.py index 22bb9fc..94eae05 100644 --- a/components/query.py +++ b/components/query.py @@ -1,10 +1,57 @@ +import pandas as pd import numpy as np +from dash import html + +IMAGES_BASE_URL = "https://github.com/Imageomics/dashboard-prototype/raw/main/test_data/images/" # Helper functions for Dashboard +def get_data(): + ''' + Function to read in DataFrame and perform required manipulations: + - add 'lat-lon', `Samples_at_locality`, 'Species_at_locality', and 'Subspecies_at_locality' columns. + - make list of categorical columns. + + Returns: + -------- + df - DataFrame of CSV with added column of number of samples collected at each lat-lon pair. + cat_list - List of categorical variables for RadioItems (pie chart and map). + + ''' + df = pd.read_csv("test_data/Hoyal_Cuthill_GoldStandard_metadata_cleaned.csv") + df['lat-lon'] = df['lat'].astype(str) + '|' + df['lon'].astype(str) + df["Samples_at_locality"] = df['lat-lon'].map(df['lat-lon'].value_counts()/2) + + # Count and record number of species and subspecies at each lat-lon + for lat_lon in df['lat-lon']: + species_list = ['{}'.format(i) for i in df.loc[df['lat-lon'] == lat_lon]['Species'].unique()] + subspecies_list = ['{}'.format(i) for i in df.loc[df['lat-lon'] == lat_lon]['Subspecies'].unique()] + df.loc[df['lat-lon'] == lat_lon, 'Species_at_locality'] = ", ".join(species_list) + df.loc[df['lat-lon'] == lat_lon, 'Subspecies_at_locality'] = ", ".join(subspecies_list) + + # Dictionary of categorical values for graphing options + cat_list = [{'label': 'Species', 'value': 'Species'}, + {'label': 'Subspecies', 'value': 'Subspecies'}, + {'label':'View', 'value': 'View'}, + {'label': 'Sex', 'value': 'Sex'}, + {'label': 'Hybrid Status', 'value':'hybrid_stat'}, + {'label': 'Additional Taxa Information', 'value':'addit_taxa_info'}, + {'label': 'Locality', 'value': 'locality'}] + + return df, cat_list + def get_species_options(df): ''' - Function to pull in dataFrame and produce a dictionary of species options (Melpomene, Erato, and Any) + Function to pull in DataFrame and produce a dictionary of species options (Melpomene, Erato, and Any) + + Parameters: + ----------- + df - DataFrame with image metadata. + + Returns: + -------- + all_species - Dictionary of all potential species options and their subspecies. + ''' melpomene_subspecies = df.loc[df.Species == 'melpomene', 'Subspecies'].unique() erato_subspecies = df.loc[df.Species == 'erato', 'Subspecies'].unique() @@ -16,4 +63,81 @@ def get_species_options(df): 'Erato' : erato_subspecies, 'Any' : all_subspecies } - return all_species \ No newline at end of file + return all_species + +# Retrieve selected number of images + +def get_images(df, subspecies, view, sex, hybrid, num_images): + ''' + Function to retrieve the user-selected number of images. + + Parameters: + ----------- + df - DataFrame with image metadata. + subspecies - String. Subspecies of specimen selected by the user. + view - String. View of specimen selected by the user. + sex - String. Sex of specimen selected by the user. + hybrid - String. Hybrid status of specimen selected by the user. + num_images - Integer. Number of images requested by the user. + + Returns: + -------- + Imgs - List of html image elements with `src` element pointing to paths for the requested number of images matching given parameters. + Returns html header4 "No Such Images. Please make another selection." if no images matching parameters exist. + ''' + filenames = get_filenames(df, subspecies, view, sex, hybrid, num_images) + if filenames == 0: + #print("No Such Images. Please make another selection.") + return html.H4("No Such Images. Please make another selection.", + style = {"color":"MidnightBlue"}) + Imgs = [] + for filename in filenames: + if 'D_lowres' in filename: + image_directory = "dorsal_images/" + else: + image_directory = "ventral_images/" + #remove 'tif' from filename and replace with 'png' in url + image_path = IMAGES_BASE_URL + image_directory + filename[:-3] + "png?raw=true" + Imgs.append(html.Img(src = image_path)) + return Imgs + +def get_filenames(df, subspecies, view, sex, hybrid, num_images): + ''' + Funtion to randomly select the given number of filenames for images adhering to specified filters. + + Parameters: + ----------- + df - DataFrame with image metadata. + subspecies - String. Subspecies of specimen selected by the user. + view - String. View of specimen selected by the user. + sex - String. Sex of specimen selected by the user. + hybrid - String. Hybrid status of specimen selected by the user. + num_images - Integer. Number of images requested by the user. Defaults to 1 if no selection. + + Returns: + -------- + filenames or 0 - List of filenames meeting specified conditions (the lesser of the requested amount or number available). + Returns 0 if no matching values. + ''' + if 'Any' in subspecies: + if subspecies == 'Any': + df_sub = df.copy() + else: + subspecies = subspecies.split('-')[1].lower() + df_sub = df.loc[df.Subspecies == subspecies].copy() + else: + df_sub = df.loc[df.Subspecies.isin(subspecies)].copy() + df_sub = df_sub.loc[df_sub.View.isin(view)] + df_sub = df_sub.loc[df_sub.Sex.isin(sex)] + df_filtered = df_sub.loc[df_sub.hybrid_stat.isin(hybrid)] + max_imgs = len(df_filtered) + if max_imgs > 0: + if num_images == None: + num = 1 + else: + num = min(num_images, max_imgs) + filenames = df_filtered.sample(num).Image_filename.astype('string').values + #return list of filenames for min(user-selected, available) images randomly selected images from the filtered dataset + return list(filenames) + else: + return 0 diff --git a/dashboard.py b/dashboard.py index ca34f66..e8e8776 100644 --- a/dashboard.py +++ b/dashboard.py @@ -1,91 +1,68 @@ -import pandas as pd -import plotly.express as px from dash import Dash, html, dcc, Input, Output, State -from components.query import get_species_options - -IMAGES_BASE_URL = "https://github.com/Imageomics/dashboard-prototype/blob/main/test_data/images/" - -df = pd.read_csv("test_data/Hoyal_Cuthill_GoldStandard_metadata_cleaned.csv") +from components.query import get_data, get_species_options, get_images +from components.graphs import make_hist_plot, make_map, make_pie_plot +from components.divs import get_hist_div, get_map_div + +# Fixed styles and sorting options +H1_STYLE = {'textAlign': 'center', 'color': 'MidnightBlue'} +H4_STYLE = {'color': 'MidnightBlue', 'margin-bottom' : 10} +HALF_DIV_STYLE = {'width': '48%', 'display': 'inline-block'} +QUARTER_DIV_STYLE = {'width': '24%', 'display': 'inline-block'} +SORT_LIST = [{'label': 'Alphabetical', 'value': 'alpha'}, + {'label': 'Ascending', 'value': 'sum ascending'}, + {'label': 'Descending', 'value': 'sum descending'}] + +# get dataset-determined static data: + # the dataframe and categorical features + # all possible species, subspecies + # distribution options (histogram and map options) +df, cat_list = get_data() all_species = get_species_options(df) +hist_div = get_hist_div(cat_list, SORT_LIST, H4_STYLE, HALF_DIV_STYLE) +map_div = get_map_div(cat_list, H4_STYLE, HALF_DIV_STYLE) +# Initialize app/dashboard and set layout app = Dash(__name__) app.layout = html.Div([ - html.H1("Cuthill Data Distribution Statistics", style = {'textAlign': 'center', 'color': 'MidnightBlue'}), + html.H1("Cuthill Data Distribution Statistics", style = H1_STYLE), - #Distribution Options - html.Div([ - html.Div([ - html.H4("Show me the distribution of ...", style = {"color":"MidnightBlue", 'margin-bottom' : 10}), - # Add dropdown options - # x-axis (feature) distribution options: 'Subspecies', 'Additional Taxa Information', 'Locality' - dcc.RadioItems([ - {'label': 'Subspecies', 'value': 'Subspecies'}, - {'label': 'Additional Taxa Information', 'value':'addit_taxa_info'}, - {'label': 'Locality', 'value': 'locality'}], - 'Subspecies', - id = 'x-variable') - ], style = {'width': '48%', 'display': 'inline-block'} - ), - - html.Div([ - html.H4("Colored by ...", style = {'color': 'MidnightBlue', 'margin-bottom' : 10}), - #select color-by option: 'View', 'Sex', 'Hybrid Status' - dcc.RadioItems([ - {'label':'View', 'value': 'View'}, - {'label': 'Sex', 'value': 'Sex'}, - {'label': 'Hybrid Status', 'value':'hybrid_stat'}], - 'View', - id = 'color-by') - ], style = {'width': '48%', 'display': 'inline-block'} - ), - #html.Br(), - html.H4("Sort distribution ", style = {'color': 'MidnightBlue', 'margin-top' : 10, 'margin-bottom' : 10}), - dcc.RadioItems([ - {'label': 'Alphabetical', 'value': 'alpha'}, - {'label': 'Ascending', 'value': 'sum ascending'}, - {'label': 'Descending', 'value': 'sum descending'}], - 'alpha', - id = 'sort-by', - inline = True), - ], style = {'width': '48%', 'display': 'inline-block'} + # Distribution Options, default start on histogram + html.Div(hist_div, + id = 'dist-options', + style = HALF_DIV_STYLE ), - #pie chart options + # Pie chart options: 'Species', 'Subspecies', 'View', 'Sex', 'Hybrid Status' html.Div([ - html.H4("Show me the Percentage Breakdown of ...", style = {'color': 'MidnightBlue', 'margin-bottom' : 10}), - dcc.RadioItems([ - {'label': 'Species', 'value': 'Species'}, - {'label': 'Subspecies', 'value': 'Subspecies'}, - {'label':'View', 'value': 'View'}, - {'label': 'Sex', 'value': 'Sex'}, - {'label': 'Hybrid Status', 'value':'hybrid_stat'}], + html.H4("Show me the Percentage Breakdown of ...", style = H4_STYLE), + dcc.RadioItems(cat_list[:-2], 'Species', id = 'prct-brkdwn' ), html.Br(), - ], style = {'width': '48%', 'display': 'inline-block'} + ], style = HALF_DIV_STYLE ), html.Br(), html.Br(), - #Graphs + # Graphs - Distribution (histogram or map), then pie chart html.Div([ - dcc.Graph(id = 'hist-plot')], style = {'width': '48%', 'display': 'inline-block'}), + dcc.Graph(id = 'dist-plot')], style = HALF_DIV_STYLE), html.Div([ - dcc.Graph(id = 'pie-plot')], style = {'width': '48%', 'display': 'inline-block'}), + dcc.Graph(id = 'pie-plot')], style = HALF_DIV_STYLE), html.Hr(), - html.H1("Cuthill Data Sample Image Selection", style = {'textAlign': 'center', 'color': 'MidnightBlue'}), + html.H1("Cuthill Data Sample Image Selection", style = H1_STYLE), html.Hr(), # Image Selector html.Div([ - html.H4("Show me sample images of ...", style = {"color":"MidnightBlue", 'marginBottom' : 10}), + html.H4("Show me sample images of ...", style = H4_STYLE), #select Species/Subspecies to view (defaul to Any) # Note: these should be the same type to interact properly, first must not be clearable dcc.Dropdown(options = list(all_species.keys()), @@ -98,40 +75,40 @@ id = 'subspecies-show', placeholder = 'Select Subspecies to View'), # Further Refine by Features - html.H4("that are ...", style = {"color":"MidnightBlue", 'marginBottom' : 10}), + html.H4("that are ...", style = H4_STYLE), html.Div([ dcc.Checklist(df.Sex.unique(), df.Sex.unique()[0:2], id = 'which-sex')], - style = {'width': '24%', 'display': 'inline-block'} + style = QUARTER_DIV_STYLE ), html.Div([ dcc.Checklist(df.View.unique(), df.View.unique()[0:2], id = 'which-view')], - style = {'width': '24%', 'display': 'inline-block'} + style = QUARTER_DIV_STYLE ), html.Div([ dcc.Checklist(df.hybrid_stat.unique(), df.hybrid_stat.unique()[0:2], id = 'hybrid?')], - style = {'width': '24%', 'display': 'inline-block'} + style = QUARTER_DIV_STYLE ), html.Div([ - html.H5("How many images?", style = {"color":"MidnightBlue", 'marginBottom' : 10}), + html.H5("How many images?", style = H4_STYLE), dcc.Input(type = 'number', min = 1, max = 100, step = 1, - placeholder = 1, + placeholder = '#', id = 'num-images')], - style = {'width': '24%', 'display': 'inline-block'} + style = QUARTER_DIV_STYLE ) ], id = 'dropdown-images'), html.Hr(), - #Button to activate the callback + # Button to activate the callback html.Button('Display Images', id = 'display-img', n_clicks = 0), @@ -140,7 +117,6 @@ html.Br(), html.Br(), html.Br(), - html.Br(), # Image Should appear html.Div(id = 'image-1'), @@ -153,35 +129,70 @@ ]) -# Histogram Section +# Distribution Section +# Callback to update which options are visible (histogram vs map) +@app.callback( + Output('dist-options', 'children'), + Input('dist-view-btn', 'n_clicks'), + Input('dist-view-btn', 'children') +) +def update_dist_view(n_clicks, children): + ''' + Function to update the upper left distribution options based on selected distribution chart (histogram or map). + Activates on click to change, defaults to histogram view. + + Parameters: + ----------- + n_clicks - Number of clicks. + children - Label on button, determins which distribution options to show. + + Returns: + -------- + hist_div or map_div - The HTML Div corresponding to the selected distribution figure. + ''' + if n_clicks == 0: + return hist_div + if n_clicks > 0: + if children == "Show Histogram": + return hist_div + else: + return map_div + +# Callback to update the distribution figure (histogram or map) @app.callback( - #hist output - Output(component_id='hist-plot', component_property='figure'), + #dist output + Output(component_id='dist-plot', component_property='figure'), #input x_var Input(component_id='x-variable', component_property='value'), #input color_by Input(component_id='color-by', component_property='value'), #input sort_by - Input(component_id='sort-by', component_property='value') + Input(component_id='sort-by', component_property='value'), + #button information + Input(component_id='dist-view-btn', component_property='children') ) -def make_hist_plot(x_var, color_by, sort_by): - #generate histogram - if sort_by == 'alpha': - fig = px.histogram(df.sort_values(x_var), - x = x_var, - color = color_by, - color_discrete_sequence = px.colors.qualitative.Bold) +def update_dist_plot(x_var, color_by, sort_by, btn): + ''' + Function to update distribution figure with either map or histogram based on selections. + Selection is based on current label of the button ('Map View' or 'Show Histogram'), which updates prior to graph. + + Parameters: + ----------- + x_var - User-selected variable to plot distribution. + color_by - User-selected property to color the plot by. + sort_by - User-selected ordering of bar charts (Alphabetical, Ascending, or Descending). + btn - Current label of the button ('Map View' or 'Show Histogram'). + + Returns: + -------- + fig - Figure returned from appropriate function call: histogram or map of the distribution of the requested variable. + ''' + if btn == "Show Histogram": + return make_map(df, color_by) else: - fig = px.histogram(df, - x = x_var, - color = color_by, - color_discrete_sequence = px.colors.qualitative.Bold).update_xaxes(categoryorder = sort_by) - - fig.update_layout(title = {'text': f'Distribution of {x_var} Colored by {color_by}'}) - - return fig + return make_hist_plot(df, x_var, color_by, sort_by) # Pie Section @@ -192,22 +203,19 @@ def make_hist_plot(x_var, color_by, sort_by): Input(component_id='prct-brkdwn', component_property='value') ) -def make_pie_plot(var): - #generate pie chart - if(var == 'Subspecies'): - pie_fig = px.pie(df, - names = var, - color_discrete_sequence = px.colors.qualitative.Bold, - hover_data = ['Species']) - else: - pie_fig = px.pie(df, - names = var, - color_discrete_sequence = px.colors.qualitative.Bold) - pie_fig.update_traces(textposition = 'inside', textinfo = 'percent+label') - - pie_fig.update_layout(title = {'text': f'Percentage Breakdown of {var}'}) +def update_pie_plot(var): + ''' + Updates the pie chart of dataset specimens based on user selection of variable to color by. - return pie_fig + Parameters: + ----------- + var - User-selected categorical variable by which to color. + + Returns: + -------- + fig - Pie chart figure returned from function call: percentage breakdown of `var` samples in the dataset. + ''' + return make_pie_plot(df, var) # Image Section @@ -218,7 +226,7 @@ def make_pie_plot(var): ) def set_subspecies_options(selected_species): - # Set subspecies options based on selected species + # Set subspecies options in dropdown based on user-selected species. return [{'label': i, 'value': i} for i in all_species[selected_species]] # Callback for Image Subspecies Selection @@ -228,7 +236,7 @@ def set_subspecies_options(selected_species): ) def set_subspecies_value(available_options): - # Collect selected subspecies + # Collect selected subspecies to display in multi-select dropdown. return available_options[0]['value'] # Image & Display Images Button Callback @@ -244,60 +252,31 @@ def set_subspecies_value(available_options): prevent_initial_call = True ) +# Retrieve selected number of images def update_display(n_clicks, subspecies, view, sex, hybrid, num_images): - if n_clicks > 0: - return get_images(subspecies, view, sex, hybrid, num_images) - else: - return html.H4("Please make a selection.", - style = {"color":"MidnightBlue"}) + ''' + Function to retrieve the user-selected number of images adhering to their chosen parameters when the 'Display Images' button is pressed. -# Retrieve selected number of images -def get_images(subspecies, view, sex, hybrid, num_images): - #if n_clicks is None: - # raise PreventUpdate - # else: - filenames = get_filenames(subspecies, view, sex, hybrid, num_images) - if filenames == 0: - #print("No Such Images. Please make another selection.") - return html.H4("No Such Images. Please make another selection.", - style = {"color":"MidnightBlue"}) - Imgs = [] - for filename in filenames: - if 'D_lowres' in filename: - image_directory = "dorsal_images/" - else: - image_directory = "ventral_images/" - #remove 'tif' from filename and replace with 'png' in url - image_path = IMAGES_BASE_URL + image_directory + filename[:-3] + "png?raw=true" - Imgs.append(html.Img(src = image_path)) - return Imgs - -def get_filenames(subspecies, view, sex, hybrid, num_images): - #filter df by subspecies, then view, sex and hybrid - #return filenames for num_images randomly selected images from the filtered dataset - #default to 1 if none selected - #check for Any-Melpomene, Any-Erato, or Any (general) - if 'Any' in subspecies: - if subspecies == 'Any': - df_sub = df.copy() - else: - subspecies = subspecies.split('-')[1].lower() - df_sub = df.loc[df.Subspecies == subspecies].copy() - else: - df_sub = df.loc[df.Subspecies.isin(subspecies)].copy() - df_sub = df_sub.loc[df_sub.View.isin(view)] - df_sub = df_sub.loc[df_sub.Sex.isin(sex)] - df_filtered = df_sub.loc[df_sub.hybrid_stat.isin(hybrid)] - max_imgs = len(df_filtered) - if max_imgs > 0: - if num_images == None: - num = 1 - else: - num = min(num_images, max_imgs) - filenames = df_filtered.sample(num).Image_filename.astype('string').values - return list(filenames) + Parameters: + ----------- + n_clicks - Number of times the 'Display Images' button has been pressed. + subspecies - String. Subspecies of specimen selected by the user. + view - String. View of specimen selected by the user. + sex - String. Sex of specimen selected by the user. + hybrid - String. Hybrid status of specimen selected by the user. + num_images - Integer. Number of images requested by the user. Default value is 1 (in get_filename). + + Returns: + -------- + Imgs - (Return of function call) List of html image elements with `src` element pointing to paths for the requested number of images matching given parameters. + Returns html header4 "No Such Images. Please make another selection." if no images matching parameters exist. + Returns html header4 "Please make a selection." If number of images isn't specified. + ''' + if n_clicks > 0 and (view != [] and sex != [] and hybrid != []): + return get_images(df, subspecies, view, sex, hybrid, num_images) else: - return 0 + return html.H4("Please make a selection.", + style = {'color': 'MidnightBlue'}) if __name__ == '__main__': - app.run_server(debug=True) \ No newline at end of file + app.run_server(debug=True) diff --git a/dashboard_preview.png b/dashboard_preview.png deleted file mode 100644 index 9da0d20..0000000 Binary files a/dashboard_preview.png and /dev/null differ diff --git a/dashboard_preview_hist.png b/dashboard_preview_hist.png new file mode 100644 index 0000000..38f2fd7 Binary files /dev/null and b/dashboard_preview_hist.png differ diff --git a/dashboard_preview_map.png b/dashboard_preview_map.png new file mode 100644 index 0000000..dae491f Binary files /dev/null and b/dashboard_preview_map.png differ