def generate_time_distribution_by_bib_numbers(data, performance_criteria): ''' This function generates all BIB/performance scatters for each running of Lausanne Marathon. Final Dict has the following pattern: { <performance_criterion_1>: <Plotly figure> [, <performance_criterion_2>: <Plotly figure> , ...] } Parameters - df: DataFrame containing records about runners - performance_criteria: Array containing column name to use for available performance criteria (time and speed) Return - figures: Dict containing all time distribution figures ''' # We define options runnings_names = {10: '10 km', 21: 'Semi-marathon', 42: 'Marathon'} colors = {10: KM_10_COLOR, 21: KM_21_COLOR, 42: KM_42_COLOR} default_options = { 'title': 'Distribution of performance according to BIB numbers over the years', 'x_name': 'BIB numbers', 'x_format': 'f', 'hovermode': 'closest' } time_options = {'y_name': 'Time', 'y_type': 'date', 'y_format': '%H:%M'} speed_options = {'y_name': 'Speed (m/s)'} time_options.update(default_options) speed_options.update(default_options) # We create final dict figures = {} # Loop over performance criteria (time, speed) for performance_criterion in performance_criteria: criterion = performance_criterion.lower() scatters = study_utils.create_plotly_scatters(data=data, x='number', y=criterion, hue='distance (km)', hue_names=runnings_names, text='name', color=colors, use_hue_names=False) if criterion == 'time': figure = study_utils.create_plotly_legends_and_layout( data=scatters, **time_options) else: figure = study_utils.create_plotly_legends_and_layout( data=scatters, **speed_options) figures[performance_criterion] = figure return figures
def generate_all_bib_performance_figure(df): ''' This function generates all BIB/performance scatters for each year of Lausanne Marathon. Parameters - df: DataFrame containing records about runners Return - figure: Plotly figure ''' # We define the considered the years interval, colors and visibility years_range = range(1999, 2017) years = {year: str(year) for year in years_range} colors = study_utils.generate_colors_palette(data=years_range, isDict=False, forceString=True) visibility = {str(year): (True if year > 2015 else 'legendonly') for year in years_range} # We define options default_options = {'title': 'Distribution of performance according to BIB numbers over the years', 'x_name': 'BIB numbers', 'hovermode':'closest'} time_options = {'y_name': 'Time', 'y_type': 'date', 'y_format': '%H:%M'} time_options.update(default_options) scatters = study_utils.create_plotly_scatters(data=df, x='number', y='time', hue='year', hue_names=years, text='name', color=colors, visibility=visibility) figure = study_utils.create_plotly_legends_and_layout(data=scatters, **time_options) plotly.offline.iplot(figure) return figure
def plot_median_age_evolution(data, x=None, y='Median age (all runnings)', title='Evolution of median age over the years', groupby_column='Gender', groupby_attributes=None): ''' This function displays a graph showing evolution of median ages for male and female runners over the years. Parameters - data: DataFrame containing data to use for graph - x: Name of the column to use for x axis (by default None / if None, index will be used) - y: Name of the column to use for y axis (by default, 'Median age (all runnings)') - title: Title of the graph (by default, 'Evolution of median age over the years') - groupby_column: Name of the column to use for grouping data (by default, 'Gender') - groupby_attributes: Dictionary containing options for each unique value in column groupby_column (at present, 'colors' and 'name' are supported / by default, None) Return - figure: Plotly figure ''' lines = [] for key, group in data.groupby([groupby_column]): x_values = group[x] if x else group.index line = go.Scatter(x=x_values, y=group[y], mode='lines', name=(groupby_attributes[key].get('name', key) if groupby_attributes else key), marker={'color': (groupby_attributes[key].get('color', None) if groupby_attributes else None)}) lines.append(line) figure = study_utils.create_plotly_legends_and_layout(lines, title=title, x_name=(x if x else data.index.name), y_name=y) plotly.offline.iplot(figure) return figure
def plot_age_distribution(df, age_column_name='age', sex_column_name='sex'): ''' This function displays the distribution of runners according to their age. Parameters: - df: DataFrame containing information about runners - age_column_name: Name of column containing age of runners ''' # Calculation of age distribution statistics by gender statistics = [] all_genders = ['all'] all_genders.extend(df[sex_column_name].unique()) for sex in all_genders: if sex == 'all': ages = df[age_column_name] else: ages = df[df[sex_column_name] == sex][age_column_name] statistics.append('<b>Mean age of ' + sex + ' runners: ' + str(round(np.mean(ages), 2)) + ' (STD: ' + str(round(np.std(ages), 2)) + ')</b>') data = [go.Histogram(x=df[age_column_name])] annotations = [ Annotation(y=1, x=1, text='<br>'.join(statistics), xref='paper', yref='paper', showarrow=False) ] shapes = [{ 'type': 'line', 'yref': 'paper', 'x0': np.mean(df[age_column_name]), 'y0': 0, 'x1': np.mean(df[age_column_name]), 'y1': 1, 'line': { 'color': '#f44242', 'width': 2, 'dash': 'dash' } }] figure = study_utils.create_plotly_legends_and_layout( data, title='Age distribution of runners', x_name='Age', y_name='Number of runners', barmode='group', bargap=0.25, annotations=annotations, shapes=shapes) plotly.offline.iplot(figure) return figure
def plot_age_evolution_boxplots(df, title='Evolution of age of runners over the years', year_column='year', age_column='age'): ''' This function plots evolution of age of runners over the years (using boxplots). Parameters - df: DataFrame containing records about runners - title: Title of the graph (by default, 'Evolution of age of runners over the years') - year_column: Name of column containing years of event - age_column: Name of column containing age of runners Return - figure: Plotly figure ''' options = {'title': title, 'x_name': year_column.capitalize(), 'y_name': age_column.capitalize()} boxplots = study_utils.create_plotly_boxplots(data=df, x='year', y=age_column) figure = study_utils.create_plotly_legends_and_layout(data=boxplots, **options) plotly.offline.iplot(figure) return figure
def plot_distribution_over_years(data, title='Distribution of runners over the years for Lausanne Marathon'): ''' This function generates a graph representing distribution of runners over the years, given data. Parameters - data: DataFrame containing distribution of runners over the years - title: Title of the graph (by default, 'Distribution of runners over the years for Lausanne Marathon') Return - figure: Plotly figure ''' colors = {'10 km': KM_10_COLOR, 'Semi-marathon': KM_21_COLOR, 'Marathon': KM_42_COLOR} bars = [] for running in data.columns: bars.append(go.Bar(x=[year for year in data.index], y=data[running], name=running, marker={'color': colors[running]})) figure = study_utils.create_plotly_legends_and_layout(bars, title=title, x_name='Years', y_name='Number of runners', barmode='stack') plotly.offline.iplot(figure) return figure
def plot_distribution_between_types_of_participants(df, type_column_name='type'): ''' This functions displays the distribution of runners between types of participants. Parameters - df: DataFrame containing information about runners - type_column_name: Name of column containing type of runners (by default, 'type') Return - figure: Plotly figure ''' x_values, y_values, texts = [[] for i in range(3)] nb_total_participants = len(df) for type_participants in df[type_column_name].unique(): x_values.append(type_participants) nb_participants = len(df[df[type_column_name] == type_participants]) y_values.append(nb_participants) texts.append('<b>' + type_participants.capitalize() + '</b><br>Number of participants: ' + str(nb_participants) + ' (' + '{:.1f}%'.format(nb_participants * 100 / nb_total_participants) + ')') bar = go.Bar(x=x_values, y=y_values, name=type_participants, text=texts, hoverinfo='text') figure = study_utils.create_plotly_legends_and_layout( [bar], title='Distribution by type of runners', x_name='Type of runner', y_name='Number of runners', barmode='group') plotly.offline.iplot(figure) return figure
def generate_all_performance_figures(df, age_categories, sex_categories, performance_criteria): ''' This function generates all performance figures according sets of age categories and sex categories and a set of performance criteria. Final Dict has the following pattern: { <age_category_1: { <sex_1>: { <performance_criterion_1>: <Plotly figure> [, <performance_criterion_2>: <Plotly figure> , ...] } [, <sex_2>: { <performance_criterion_1>: <Plotly figure> [, <performance_criterion_2>: <Plotly figure> , ...] }, ...] } [, <age_category_2: { <sex_1>: { <performance_criterion_1>: <Plotly figure> [, <performance_criterion_2>: <Plotly figure> , ...] } [, <sex_2>: { <performance_criterion_1>: <Plotly figure> [, <performance_criterion_2>: <Plotly figure> , ...] }, ...] }, ...] } Parameters - df: DataFrame containing records about runners - age_categories: Array containing age categories to be displayed (if 'All'/'all', no filter is done on df) - sex_categories: Array containing sex categories to be displayed (if 'All'/'all', no filter is done on df) - performance_criteria: Array containing performance criteria to consider Return - figures: Dict containing all performance figures ''' # We define the considered runnings and the years interval, as colors for boxplots runnings = {10: '10 km', 21: 'Semi-marathon', 42: 'Marathon'} year_values = [year for year in df['year'].unique() if year] colors = {'10 km': KM_10_COLOR, 'Semi-marathon': KM_21_COLOR, 'Marathon': KM_42_COLOR} # We define options and the final Dict figures = {} default_options = {'title': 'Performance over years for runnings of Lausanne Marathon', 'x_name': 'Years', 'x_values': year_values, 'boxmode': 'group'} time_options = {'y_name': 'Time', 'y_type': 'date', 'y_format': '%H:%M:%S'} speed_options = {'y_name': 'Speed (m/s)'} time_options.update(default_options) speed_options.update(default_options) for age_category in age_categories: # We select data according to age category if age_category.lower() == 'all': data = df else: data = df[df['age category'] == age_category] figures[age_category] = {} for sex_category in sex_categories: # We select data according to sex category if sex_category.lower() == 'all': data_final = data else: data_final = data[data['sex'] == sex_category.lower()] figures[age_category][sex_category] = {} annotations = [Annotation(y=1.1, text='Age category: ' + age_category + ' Sex category: ' + sex_category + ' runners', xref='paper', yref='paper', showarrow=False)] # We create a figure for each performance criterion for performance_criterion in performance_criteria: criterion = performance_criterion.lower() boxplots = study_utils.create_plotly_boxplots(data=data_final, x='year', y=criterion, hue='distance (km)', hue_names=runnings, colors=colors) if criterion == 'time': figure = study_utils.create_plotly_legends_and_layout(data=boxplots, **time_options, annotations=annotations) elif criterion == 'speed (m/s)': figure = study_utils.create_plotly_legends_and_layout(data=boxplots, **speed_options, annotations=annotations) else: # By default, two specific criteria are allowed: 'time' and 'speed (m/s)'. If any other criterion is provided, we throw an exception. raise ValueError('Invalid performance criterion encountered. Performance criterion must be either \'Time\' or \'Speed (m/s)\'') figures[age_category][sex_category][performance_criterion] = figure return figures
def generate_all_evolution_figures(df, age_categories, sex_categories): ''' This function generates all evolution figures according sets of age categories and sex categories. Final Dict has the following pattern: { <age_category_1: { <sex_1>: <Plotly figure [, <sex_2>: <Plotly figure, ...] } [, <age_category_2: { <sex_1>: <Plotly figure [, <sex_2>: <Plotly figure, ...] }, ...] } Parameters - df: DataFrame containing records about runners - age_categories: Array containing age categories to be displayed (if 'All'/'all', no filter is done on df) - sex_categories: Array containing sex categories to be displayed (if 'All'/'all', no filter is done on df) Return - figures: Dict containing all evolution figures ''' # We define the considered runnings and the years interval runnings = {'column_name': 'distance (km)', 'values': OrderedDict([(10, {'name': '10 km', 'color': KM_10_COLOR}), (21, {'name': 'Semi-marathon', 'color': KM_21_COLOR}), (42, {'name': 'Marathon', 'color': KM_42_COLOR})])} year_values = [year for year in df['year'].unique() if year] # We define options and the final Dict figures = {} options = {'title': 'Evolution of number of participants over years for runnings of Lausanne Marathon', 'x_name': 'Years', 'x_values': year_values, 'y_format': 'f'} for age_category in age_categories: # We select data according to age category if age_category.lower() == 'all': data = df else: data = df[df['age category'] == age_category] figures[age_category] = {} for sex_category in sex_categories: # We select data according to sex category if sex_category.lower() == 'all': data_final = data else: data_final = data[data['sex'] == sex_category.lower()] lines = [] for km, attributes in runnings['values'].items(): data_running = data_final[data_final[runnings['column_name']] == km] line = go.Scatter(x = year_values, y = [len(data_running[data_running['year'] == y]) for y in year_values], mode = 'lines', name = attributes['name'], marker={'color': attributes['color']}) lines.append(line) annotations = [Annotation(y=1.1, text='Age category: ' + age_category + ' Sex category: ' + sex_category + ' runners', xref='paper', yref='paper', showarrow=False)] figure = study_utils.create_plotly_legends_and_layout(data=lines, **options, annotations=annotations) figures[age_category][sex_category] = figure return figures
def generate_teams_evolution_figures(data, title='Evolution of teams performance over the years', runnings=None, team_column_name='team', year_column_name='year', min_years=6, nb_teams=8, threshold_bins_size=50, display_annotations=True): ''' This function generate teams_evolution figures for all runnings. Final Dict has the following pattern: { <running_1: { <Plotly figure> } [, <running_2: { <Plotly figure> }, ...] } Parameters - data: DataFrame containing results - title: Title of figure - runnings: Dict containing name of column containing runnings (key: column_name) and set of runnings (key: values, value: dict() with key: value in column, value: name of running) By default, None. If None, default values will be set by function. - team_column_name: Name of column containing teams (by default, 'teams') - year_column_name: Name of column containing year associated to a given result (by default, 'year') - min_years: Minimum of participations when considering a team (by default, 6) - nb_teams: Number of teams to consider among teams with number of participations > min_years (by default, 8) Note: Teams are filtered by number of participants. - threshold_bins_size: Maximum size of a bin (by default, 25) Note: Size of bin is related to number of participants of a considered team and for a given year. If None, no limitation is used. - display_annotations: Boolean used to display annotations (by default, True) Return - figures: Dict containing all teams evolution figures ''' # Default runnings if not runnings: runnings = {'column_name': 'distance (km)', 'values': {10: '10 km', 21: 'Semi-marathon', 42: 'Marathon'}} figures = {} # Loop over runnings for key, value in runnings['values'].items(): # We retrieve data related to current running filtered_data = data[data[runnings['column_name']] == key] # We retrieve names of the <nb_teams> most important groups with at least <min_years> participations in Lausanne Marathon top_teams = filtered_data.groupby(team_column_name).filter(lambda x: x[year_column_name].nunique() >= min_years).groupby('team').size().sort_values(ascending=False).nlargest(nb_teams) # We keep only data linked with such groups data_top_teams = filtered_data[filtered_data[team_column_name].isin(top_teams.index.values)] # We finally groupby teams after complete filter groups_top_teams = data_top_teams.groupby(team_column_name) # We generate colors for each group and we initialize array that will contain traces colors = study_utils.generate_colors_palette(groups_top_teams.groups) traces = [] # Loop over groups for name, groups in groups_top_teams: x_values, y_values, size_values, texts = [], [], [], [] # Loop over participation years for current group for year, results in groups.groupby(year_column_name): x_values.append(year) y = study_utils.compute_average_time(results) y_values.append(y) text = '<b>Team: ' + name + '</b><br>Average time: ' + y.strftime('%H:%M:%S') + '<br>Participants: ' + str(len(results)) + '<br>Median age: ' + str(int(results['age'].median())) texts.append(text) size = len(results) if not threshold_bins_size or (len(results) < threshold_bins_size) else threshold_bins_size size_values.append(size) trace = go.Scatter(x=x_values, y=y_values, name=name, mode='lines+markers', hoverinfo='text', text=texts, marker=dict(size=size_values, color=colors[name], line=dict(width = 1.5, color = 'rgb(0, 0, 0)'))) traces.append(trace) # For each running, we create annotations if asked by user, we set multiple options accordingly and we store figure if display_annotations: annotations = [Annotation(y=1.1, text='Running: ' + str(value) + ' | Top teams: ' + str(nb_teams) + ' | Minimum participations: ' + str(min_years) + ' | Maximum bins size: ' + str(threshold_bins_size), xref='paper', yref='paper', showarrow=False)] else: annotations = None options = {'title': title, 'hovermode': 'closest', 'x_name': 'Year', 'y_name': 'Median time', 'y_type': 'time', 'y_format': '%H:%M:%S', 'annotations': annotations} figure = study_utils.create_plotly_legends_and_layout(data=traces, **options) figures[value] = figure return figures
def plot_runners_teams_individual_distribution_according_to_running_type( df, title='Team/individual runners composition', runnings=None, team_column_name='profile'): ''' This function displays the distribution of participants according to their profiles (individual runners/runners in team) for the different runnings. Parameters - df: DataFrame containing data - title: Title of the graph (by default, 'Team/individual runners composition') - runnings: Dict containing name of column containing runnings (key: column_name) and set of runnings (key: values, value: dict() with following keys: name, color) By default, None. If None, default values will be set by function. - team_column_name: Name of column containing type of participants (by default, 'profile') Return - figure: Plotly figure ''' if not runnings: runnings = { 'column_name': 'distance (km)', 'values': OrderedDict([(10, { 'name': '10 km', 'color': KM_10_COLOR }), (21, { 'name': 'Semi-marathon', 'color': KM_21_COLOR }), (42, { 'name': 'Marathon', 'color': KM_42_COLOR })]) } data = [] annotations_texts = [] for key, attributes in runnings['values'].items(): filtered_df = df[df[runnings['column_name']] == key] nb_runners_running = len(filtered_df) x_values, y_values, texts = [[] for i in range(3)] for profile in filtered_df[team_column_name].unique(): x_values.append(profile) nb_runners = len( filtered_df[filtered_df[team_column_name] == profile]) y_values.append(nb_runners) texts.append('<b>' + attributes['name'] + '</b><br>' + profile.capitalize() + ' runners: ' + str(nb_runners) + ' (' + '{:.1f}%'.format(nb_runners * 100 / nb_runners_running) + ')') annotations_texts.append(attributes['name'] + ': ' + str(nb_runners_running) + ' runners') data.append( go.Bar(x=x_values, y=y_values, name=attributes['name'], text=texts, hoverinfo='text', marker={'color': attributes['color']})) annotations = [ Annotation(y=1.1, x=0, text=' | '.join(annotations_texts), xref='paper', yref='paper', showarrow=False) ] figure = study_utils.create_plotly_legends_and_layout( data, title=title, x_name='Composition', y_name='Number of runners', barmode='group', annotations=annotations) plotly.offline.iplot(figure) return figure
def plot_gender_distributions(df): ''' This functions displays graph representing the gender distribution of Canton of Vaud and Lausanne Marathon 2016 for comparison. Parameters - df: DataFrame containing information on runners for Lausanne Marathon 2016 Return - figure: Plotly figure ''' # Building of DataFrame for ploting CANTON_VAUD = 'Canton of Vaud' LAUSANNE_MARATHON = 'Lausanne Marathon' total_runners = len(df) total_runners_male = len(df[df['sex'] == 'male']) total_runners_female = len(df[df['sex'] == 'female']) vaud_information_population = pd.Series({ 'male': TOTAL_RESIDENT_MALE / TOTAL_RESIDENT_VAUD * 100, 'female': TOTAL_RESIDENT_FEMALE / TOTAL_RESIDENT_VAUD * 100 }) marathon_information_runner = pd.Series({ 'male': total_runners_male / total_runners * 100, 'female': total_runners_female / total_runners * 100 }) information_population = pd.DataFrame({ CANTON_VAUD: vaud_information_population, LAUSANNE_MARATHON: marathon_information_runner }) information_population.sort_index(axis=0, level=None, ascending=False, inplace=True) text_vaud = [ '<b>' + CANTON_VAUD + '</b><br>' + str(TOTAL_RESIDENT_MALE) + ' residents', '<b>' + CANTON_VAUD + '</b><br>' + str(TOTAL_RESIDENT_FEMALE) + ' residents' ] text_marathon = [ '<b>' + LAUSANNE_MARATHON + '</b><br>' + str(total_runners_male) + ' runners', '<b>' + LAUSANNE_MARATHON + '</b><br>' + str(total_runners_female) + ' runners' ] vaud_trace = go.Bar(x=information_population.index.values, y=information_population[CANTON_VAUD], name=CANTON_VAUD, hoverinfo='text', text=text_vaud) marathon_trace = go.Bar(x=information_population.index.values, y=information_population[LAUSANNE_MARATHON], name=LAUSANNE_MARATHON, hoverinfo='text', text=text_marathon) data = [vaud_trace, marathon_trace] annotations = [ Annotation(y=1.1, text='Total residents: ' + str(TOTAL_RESIDENT_VAUD) + ' | Total runners: ' + str(total_runners), xref='paper', yref='paper', showarrow=False) ] figure = study_utils.create_plotly_legends_and_layout( data, title='Gender distribution Lausanne Marathon vs Canton of Vaud', x_name='Gender', y_name='Percentage (%)', barmode='group', annotations=annotations) plotly.offline.iplot(figure) return figure
def plot_time_distribution_by_age(data, runnings=None, age_column_name='age'): ''' This function plots the distribution of time for all ages regarding participants of a Lausanne Marathon. 3 subplots are displayed per rows. Parameters - data: DataFrame containing all the information of a Lausanne Marathon - runnings: Dict containing name of column containing runnings (key: column_name) and set of runnings (key: values, value: dict() with following keys: name, color) By default, None. If None, default values will be set by function. - age_column_name: Name of the column containing age of participants('age' or 'age category', by default, 'age') ''' if not runnings: runnings = { 'column_name': 'distance (km)', 'values': { 10: { 'name': '10 km', 'color': KM_10_COLOR }, 21: { 'name': 'Semi-marathon', 'color': KM_21_COLOR }, 42: { 'name': 'Marathon', 'color': KM_42_COLOR } } } groups = data.groupby(age_column_name) figures = {} options = { 'x_name': 'Performance time', 'y_name': 'Number of runners', 'x_type': 'date', 'x_format': '%H:%M:%S', 'barmode': 'overlay', 'bargroupgap': 0.1 } for name, group in groups: histograms, statistics = [], [] for km, attributes_running in runnings['values'].items(): x = group[group[runnings['column_name']] == km]['time'] statistics.append(attributes_running['name'] + ': ' + str(len(x)) + ' runners') histograms.append( go.Histogram(x=x, xbins={ 'start': np.min(group['time']), 'end': np.max(group['time']), 'size': 5 * 60000 }, name=attributes_running['name'], marker={'color': attributes_running['color']}, opacity=0.5)) statistics.append('Total: ' + str(len(group)) + ' runners') annotations = [ Annotation(y=1.1, x=0.9, text=' | '.join(statistics), xref='paper', yref='paper', showarrow=False) ] figure = study_utils.create_plotly_legends_and_layout( data=histograms, title='Time distribution (' + name + ')', **options, annotations=annotations) figures[name] = figure return figures
def plot_time_difference_distribution( df, title='Time difference with the best runner in team', time_difference_column_name='time difference team'): ''' This function displays distribution representing time difference bewteen performance of team members and best performance within the team. Parameters - df: DataFrame containing information on runners - title: Title of the graph (by default, 'Time difference with the best runner in team') - time_difference_column_name: Name of column containing time differencies (by default, 'time_difference_column_name') Return - figure: Plotly figure ''' mean_difference = np.mean(df[time_difference_column_name]) mean_difference_dt = datetime.datetime.strptime( study_utils.convert_seconds_to_time(mean_difference), '%H:%M:%S') max_difference = np.max(df[time_difference_column_name]) statistics = [ 'Mean difference of time: ' + str(study_utils.convert_seconds_to_time(mean_difference)), 'Maximum difference of time: ' + str(study_utils.convert_seconds_to_time(max_difference)) ] data = df.copy() data[time_difference_column_name] = pd.to_datetime([ study_utils.convert_seconds_to_time(t) for t in data[time_difference_column_name] ], format='%H:%M:%S') histogram = [ go.Histogram(x=data[time_difference_column_name], xbins={ 'start': np.min(data[time_difference_column_name]), 'end': np.max(data[time_difference_column_name]), 'size': 5 * 60000 }) ] annotations = [ Annotation(y=1, x=1, text='<br>'.join(statistics), align='right', xref='paper', yref='paper', showarrow=False) ] shapes = [{ 'type': 'line', 'yref': 'paper', 'x0': mean_difference_dt, 'y0': 0, 'x1': mean_difference_dt, 'y1': 1, 'line': { 'color': '#f44242', 'width': 2, 'dash': 'dash' } }] figure = study_utils.create_plotly_legends_and_layout( histogram, title=title, x_name='Performance gap', x_type='date', x_format='%H:%M:%S', y_name='Number of runners', barmode='group', bargap=0.1, annotations=annotations, shapes=shapes) plotly.offline.iplot(figure) return figure