diff --git a/plotly/figure_factory/__init__.py b/plotly/figure_factory/__init__.py index a8be19872e1..1057a0f40c9 100644 --- a/plotly/figure_factory/__init__.py +++ b/plotly/figure_factory/__init__.py @@ -16,6 +16,7 @@ from plotly.figure_factory._ohlc import create_ohlc from plotly.figure_factory._quiver import create_quiver from plotly.figure_factory._scatterplot import create_scatterplotmatrix +from plotly.figure_factory._sparkline import create_sparkline from plotly.figure_factory._streamline import create_streamline from plotly.figure_factory._table import create_table from plotly.figure_factory._trisurf import create_trisurf diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index bcb5b2541d1..36eb9b0fdd1 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -12,6 +12,7 @@ pd = optional_imports.get_module('pandas') +# TODO: replace by same function in plotly/figure_factory/utils.py def is_sequence(obj): return (isinstance(obj, collections.Sequence) and not isinstance(obj, str)) @@ -164,6 +165,7 @@ def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, return fig +# TODO: replace scatter_options with None in function signature def create_bullet(data, markers=None, measures=None, ranges=None, subtitles=None, titles=None, orientation='h', range_colors=('rgb(200, 200, 200)', 'rgb(245, 245, 245)'), diff --git a/plotly/figure_factory/_facet_grid.py b/plotly/figure_factory/_facet_grid.py index a202599d747..b4bb7b7efe3 100644 --- a/plotly/figure_factory/_facet_grid.py +++ b/plotly/figure_factory/_facet_grid.py @@ -4,8 +4,13 @@ from plotly.figure_factory import utils from plotly.tools import make_subplots +import plotly +import plotly.graph_objs as go + import math from numbers import Number +import numpy as np +import re pd = optional_imports.get_module('pandas') @@ -23,14 +28,38 @@ THRES_FOR_FLIPPED_FACET_TITLES = 10 GRID_WIDTH = 1 -VALID_TRACE_TYPES = ['scatter', 'scattergl', 'histogram', 'bar', 'box'] +X_AND_Y_TRACE_TYPES = ['scatter', 'scattergl', 'line', 'area'] +X_OR_Y_TRACE_TYPES = ['histogram', 'bar', 'box', 'bullet', 'text'] +VALID_TRACE_TYPES = X_AND_Y_TRACE_TYPES + X_OR_Y_TRACE_TYPES CUSTOM_LABEL_ERROR = ( - "If you are using a dictionary for custom labels for the facet row/col, " - "make sure each key in that column of the dataframe is in your facet " - "labels. The keys you need are {}" + 'If you are using a dictionary for custom labels for the facet row/col, ' + 'make sure each key in that column of the dataframe is in your facet ' + 'labels. The keys you need are {}' +) + +BULLET_USING_STRING_DATA_MSG = ( + 'Whoops. You are attempting to create a bullet chart out of an array of ' + 'data with at least one string in it.\n\nBullet charts in the facet grid ' + 'present the mean, standard deviation and maximum value of a 1D dataset. ' + 'Since all of these quantities are quantitiative, they can only be ' + 'generated from numerical data.' ) +def rect(xref, yref, x0, x1, y0, y1, color): + shape = { + 'layer': 'below', + 'xref': xref, + 'yref': yref, + 'x0': x0, + 'x1': x1, + 'y0': y0, + 'y1': y1, + 'fillcolor': color, + 'line': {'width': 0} + } + return shape + def _is_flipped(num): if num >= THRES_FOR_FLIPPED_FACET_TITLES: @@ -185,17 +214,175 @@ def _make_trace_for_scatter(trace, trace_type, color, **kwargs_marker): return trace +def _return_traces_list_for_subplot_cell(trace_type, group, x, y, theme, + trace_dimension, + light_color, dark_color, + marker_color, kwargs_marker, + kwargs_trace): + + mode = None + traces_for_cell = [] + if trace_type == 'bullet': + if 'x' in trace_dimension: + # check if data contains strings + if any(isinstance(e, str) for e in group[x]): + raise exceptions.PlotlyError( + BULLET_USING_STRING_DATA_MSG + ) + std = np.std(group[x]) + rounded_mean = round(np.mean(group[x]), 2) + max_value = max(group[x]) + else: + # check if data contains strings + if any(isinstance(e, str) for e in group[y]): + raise exceptions.PlotlyError( + BULLET_USING_STRING_DATA_MSG + ) + std = np.std(group[y]) + rounded_mean = round(np.mean(group[y]), 2) + max_value = max(group[y]) + + bullet_range_x = std + bullet_measure_x = rounded_mean + bullet_pt_x = max_value + + bullet_range = go.Bar( + x=[bullet_range_x], + y=[0.5], + marker=dict( + color='rgb(230,60,140)', + line=kwargs_marker['line'], + ), + hoverinfo='x', + orientation='h', + width=0.5, + **kwargs_trace + ) + + bullet_measure = go.Bar( + x=[bullet_measure_x], + y=[0.5], + marker=dict( + color=marker_color, + line=kwargs_marker['line'], + ), + hoverinfo='x', + orientation='h', + width=0.14, + offset=-0.07, + **kwargs_trace + ) + + bullet_pt = go.Scatter( + x=[bullet_pt_x], + y=[0.5], + hoverinfo='x', + line=kwargs_marker['line'], + **kwargs_trace + ) + + traces_for_cell.append(bullet_range) + traces_for_cell.append(bullet_measure) + traces_for_cell.append(bullet_pt) + + elif trace_type in ['text']: # grab from a column + pass + + elif trace_type in ['scatter', 'scattergl', 'line', + 'histogram', 'bar', 'box']: + trace = dict( + type=trace_type, + marker=dict( + color=dark_color, + line=kwargs_marker['line'], + ), + **kwargs_trace + ) + + last_pt = dict( + type=trace_type, + marker=dict( + color=light_color, + ), + **kwargs_trace + ) + + if trace_type in ['scatter', 'scattergl']: + trace['mode'] = 'markers' + last_pt['mode'] = 'markers' + + elif trace_type == 'line': + trace['mode'] = 'lines' + trace['type'] = 'scatter' + last_pt['mode'] = 'markers' + last_pt['type'] = 'scatter' + + if trace_dimension == 'x': + # add x + trace['x'] = list(group[x]) if len(group[x]) <= 1 else list(group[x])[:-1] + last_pt['x'] = list(group[x])[-1:] + + #trace['y'] = [None] + #last_pt['y'] = [None] + elif trace_dimension == 'y': + #trace['x'] = [None] + #last_pt['x'] = [None] + + # add y + trace['y'] = list(group[y]) if len(group[y]) <= 1 else list(group[y])[:-1] + last_pt['y'] = list(group[y])[-1:] + else: # 'x+y' + if trace_type in ['scatter', 'scattergl', 'line', 'histogram']: + # add both x and y + trace['x'] = list(group[x]) if len(group[x]) <= 1 else list(group[x])[:-1] + last_pt['x'] = list(group[x])[-1:] + + #trace['y'] = list(group[y]) if len(group[y]) <= 1 else list(group[y])[:-1] + #last_pt['y'] = list(group[y])[-1:] + + elif trace_type in ['box', 'bar']: + # add only x + trace['x'] = list(group[x]) if len(group[x]) <= 1 else list(group[x])[:-1] + last_pt['x'] = list(group[x])[-1:] + + #trace['y'] = [None] + #last_pt['y'] = [None] + + traces_for_cell.append(trace) + traces_for_cell.append(last_pt) + + elif trace_type in ['area']: + trace = dict( + x=range(len(group[y])), + type='scatter', + mode='lines', + fill='tozeroy', + fillcolor=light_color, + line=dict( + color=dark_color + ) + ) + + if y: + trace['y'] = list(group[y]) if len(group[y]) <= 1 else list(group[y])[:-1] + traces_for_cell.append(trace) + + return traces_for_cell + + def _facet_grid_color_categorical(df, x, y, facet_row, facet_col, color_name, colormap, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, marker_color, - kwargs_trace, kwargs_marker): + kwargs_trace, kwargs_marker, column_width, + trace_dimension): fig = make_subplots(rows=num_of_rows, cols=num_of_cols, shared_xaxes=True, shared_yaxes=True, horizontal_spacing=SUBPLOT_SPACING, - vertical_spacing=SUBPLOT_SPACING, print_grid=False) + vertical_spacing=SUBPLOT_SPACING, print_grid=False, + column_width=column_width) annotations = [] if not facet_row and not facet_col: @@ -340,12 +527,14 @@ def _facet_grid_color_numerical(df, x, y, facet_row, facet_col, color_name, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, marker_color, kwargs_trace, - kwargs_marker): + kwargs_marker, column_width, + trace_dimension): fig = make_subplots(rows=num_of_rows, cols=num_of_cols, shared_xaxes=True, shared_yaxes=True, horizontal_spacing=SUBPLOT_SPACING, - vertical_spacing=SUBPLOT_SPACING, print_grid=False) + vertical_spacing=SUBPLOT_SPACING, print_grid=False, + column_width=column_width) annotations = [] if not facet_row and not facet_col: @@ -467,7 +656,8 @@ def _facet_grid_color_numerical(df, x, y, facet_row, facet_col, color_name, facet_row_labels, facet_row) annotations.append( _annotation_dict(row_values[row_count], - num_of_rows - row_count, num_of_rows, SUBPLOT_SPACING, + num_of_rows - row_count, num_of_rows, + SUBPLOT_SPACING, row_col='row', flipped=flipped_rows) ) @@ -477,12 +667,15 @@ def _facet_grid_color_numerical(df, x, y, facet_row, facet_col, color_name, def _facet_grid(df, x, y, facet_row, facet_col, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, - SUBPLOT_SPACING, marker_color, kwargs_trace, kwargs_marker): - + SUBPLOT_SPACING, marker_color, kwargs_trace, kwargs_marker, + row_colors, alternate_row_color, theme, column_width, + trace_dimension, trace_colors_2d): + shared_xaxes = shared_yaxes = False fig = make_subplots(rows=num_of_rows, cols=num_of_cols, - shared_xaxes=True, shared_yaxes=True, + shared_xaxes=shared_xaxes, shared_yaxes=shared_yaxes, horizontal_spacing=SUBPLOT_SPACING, - vertical_spacing=SUBPLOT_SPACING, print_grid=False) + vertical_spacing=SUBPLOT_SPACING, print_grid=False, + column_width=column_width) annotations = [] if not facet_row and not facet_col: trace = dict( @@ -498,10 +691,10 @@ def _facet_grid(df, x, y, facet_row, facet_col, num_of_rows, trace['x'] = df[x] if y: trace['y'] = df[y] - trace = _make_trace_for_scatter( - trace, trace_type, marker_color, **kwargs_marker - ) + if trace_type in ['scatter', 'scattergl']: + trace['mode'] = 'markers' + trace['marker'] = dict(color=marker_color, **kwargs_marker) fig.append_trace(trace, 1, 1) elif (facet_row and not facet_col) or (not facet_row and facet_col): @@ -526,9 +719,11 @@ def _facet_grid(df, x, y, facet_row, facet_col, num_of_rows, trace, trace_type, marker_color, **kwargs_marker ) - fig.append_trace(trace, - j + 1 if facet_row else 1, - 1 if facet_row else j + 1) + fig.append_trace( + trace, + j + 1 if facet_row else 1, + 1 if facet_row else j + 1 + ) label = _return_label( group[0], @@ -537,13 +732,13 @@ def _facet_grid(df, x, y, facet_row, facet_col, num_of_rows, ) annotations.append( - _annotation_dict( + utils.annotation_dict_for_label( label, num_of_rows - j if facet_row else j + 1, num_of_rows if facet_row else num_of_cols, SUBPLOT_SPACING, 'row' if facet_row else 'col', - flipped_rows + flipped_rows, ) ) @@ -554,45 +749,54 @@ def _facet_grid(df, x, y, facet_row, facet_col, num_of_rows, row_values = df[facet_row].unique() col_values = df[facet_col].unique() + + trace_c_idx = 0 for row_count, x_val in enumerate(row_values): for col_count, y_val in enumerate(col_values): try: group = tuple_to_facet_group[(x_val, y_val)] except KeyError: group = pd.DataFrame([[None, None]], columns=[x, y]) - trace = dict( - type=trace_type, - marker=dict( - color=marker_color, - line=kwargs_marker['line'], - ), - **kwargs_trace - ) - if x: - trace['x'] = group[x] - if y: - trace['y'] = group[y] - trace = _make_trace_for_scatter( - trace, trace_type, marker_color, **kwargs_marker + + # set light and dark colors + if trace_c_idx >= len(trace_colors_2d): + trace_c_idx = 0 + light_color = trace_colors_2d[trace_c_idx][1] + dark_color = trace_colors_2d[trace_c_idx][0] + + traces_for_cell = _return_traces_list_for_subplot_cell( + trace_type, group, x, y, theme, trace_dimension, + light_color, dark_color, marker_color, kwargs_marker, + kwargs_trace ) - fig.append_trace(trace, row_count + 1, col_count + 1) + # insert traces in subplot cell + for trace in traces_for_cell: + fig.append_trace(trace, row_count + 1, col_count + 1) + if row_count == 0: label = _return_label(col_values[col_count], facet_col_labels, facet_col) annotations.append( - _annotation_dict(label, col_count + 1, num_of_cols, SUBPLOT_SPACING, - row_col='col', flipped=flipped_cols) + utils.annotation_dict_for_label( + label, col_count + 1, num_of_cols, SUBPLOT_SPACING, + row_col='col', flipped=flipped_cols, + column_width=column_width ) + ) label = _return_label(row_values[row_count], facet_row_labels, facet_row) + annotations.append( - _annotation_dict(label, num_of_rows - row_count, num_of_rows, SUBPLOT_SPACING, - row_col='row', flipped=flipped_rows) + utils.annotation_dict_for_label( + label, num_of_rows - row_count, num_of_rows, + SUBPLOT_SPACING, row_col='row', flipped=flipped_rows + ) ) + trace_c_idx += 1 return fig, annotations @@ -602,7 +806,14 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, facet_row_labels=None, facet_col_labels=None, height=None, width=None, trace_type='scatter', scales='fixed', dtick_x=None, dtick_y=None, - show_boxes=True, ggplot2=False, binsize=1, **kwargs): + show_boxes=True, ggplot2=False, binsize=1, + row_colors=('rgb(247, 247, 242)', + 'rgb(255, 253, 250)'), + alternate_row_color=True, + theme='facet', column_width=None, trace_dimension=None, + trace_colors=None, x_margin_factor=0.4, + y_margin_factor=0.4, chart_types=None, + **kwargs): """ Returns figure for facet grid. @@ -648,6 +859,48 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, http://ggplot2.tidyverse.org/reference/facet_grid.html for reference. Default = False :param (int) binsize: groups all data into bins of a given length. + :param (tuple|list) row_colors: + :param (str) theme: determines the layout style of the plot. The options + are 'facet' (default) and 'sparklines'. + :param (list) column_width: Specify a list that contains numbers where + the amount of numbers in the list is equal to `chart_types`. Call + `help(plotly.tools.make_subplots)` for more info on this subplot param + :param (str) trace_dimension: the trace data for plotting the various + charts. Some charts can only work properly with one variable such as + bullet or histogram, while scatter and area charts can take 'x' and + 'y' data. The valid options are 'x', 'y' and 'x+y'. If trace_dimension + is not set, it will automatically set to: + - 'x' if x=None and y=None OR x='foo' and y='bar' + - 'x' or 'y' if ONLY one of 'x' or 'y' is set + :param (list|tuple) trace_colors: a list of colors or a list of lists of + two colors. Each row in a sparkline chart uses two colors: a darker + one and a lighter one. + Options: + - list of 2 colors: first color is dark color for all traces and + second is light for all traces. + - 1D list of more than 2 colors: the nth color in the list is the + nth dark color for the traces and the associated light color is + just 0.5 times the opacity of the dark color + - lists of lists: each inner list must have exactly 2 colors in it + and the first and second color of the nth inner list represent + the dark and light color for the nth row repsectively + - list of lists and colors: this is a combination of the previous + options + Whenever trace_colors has fewer colors than the number of rows of the + figure, the colors will repeat from the start of the list + Default = [['rgb(62,151,169)', 'rgb(181,221,232)']] + :param (float) x_margin_factor: proportional to how much margin space + along the x axis there is from the data points to the subplot cell + border. The x_margin_factor is multiplied by the standard deviation of + the x-values of the data to yield the actual margin. + Default = 0.2 + :param (float) y_margin_factor: proportional to how much margin space + along the y axis there is from the data points to the subplot cell + border. The y_margin_factor is multiplied by the standard deviation of + the y-values of the data to yield the actual margin. + Default = 0.2 + :param (list|tuple) chart_types: a sequence (list/tuple/etc) of valid + chart names that each column will produce in order from left to right. :param (dict) kwargs: a dictionary of scatterplot arguments. Examples 1: One Way Faceting @@ -780,21 +1033,15 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, # make sure all columns are of homogenous datatype utils.validate_dataframe(df) - if trace_type in ['scatter', 'scattergl']: - if not x or not y: - raise exceptions.PlotlyError( - "You need to input 'x' and 'y' if you are you are using a " - "trace_type of 'scatter' or 'scattergl'." - ) - for key in [x, y, facet_row, facet_col, color_name]: if key is not None: try: df[key] except KeyError: + # TODO: change error message in tests raise exceptions.PlotlyError( "x, y, facet_row, facet_col and color_name must be keys " - "in your dataframe." + "in your dataframe if they are not set to None." ) # autoscale histogram bars if trace_type not in ['scatter', 'scattergl']: @@ -811,10 +1058,19 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, "'trace_type' must be in {}".format(VALID_TRACE_TYPES) ) - if trace_type == 'histogram': - SUBPLOT_SPACING = 0.06 + # theme + if theme not in ['facet', 'sparklines']: + raise exceptions.PlotlyError( + "theme must be 'facet' or 'sparklines'" + ) + + if theme == 'sparklines': + SUBPLOT_SPACING = 0.0 else: - SUBPLOT_SPACING = 0.015 + if trace_type == 'histogram': + SUBPLOT_SPACING = 0.06 + else: + SUBPLOT_SPACING = 0.015 # seperate kwargs for marker and else if 'marker' in kwargs: @@ -848,6 +1104,42 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, else: marker_color = 'rgb(0, 0, 0)' + # set trace dimension if None + if trace_dimension is None: + trace_dimension = 'x' + + # validate trace dimension + if trace_dimension not in ['x', 'y', 'x+y']: + raise exceptions.PlotlyError( + "trace_dimension must be either 'x', 'y' or 'x+y'" + ) + + # validate list/tuple of trace_colors + if not trace_colors: + trace_colors = [['rgb(62,151,169)', 'rgb(181,221,232)']] + if not utils.is_sequence(trace_colors): + raise exceptions.PlotlyError( + 'trace_colors must be a list/tuple' + ) + + trace_colors_2d = [] + for i, item in enumerate(trace_colors): + plotly.colors.validate_colors(item) + if utils.is_sequence(item): + trace_colors_2d.append(item) + else: + # if hex convert to rgb + if '#' in item: + tuple_item = plotly.colors.hex_to_rgb(item) + rgb_item = plotly.colors.label_rgb(tuple_item) + else: + rgb_item = item + light_c = plotly.colors.find_intermediate_color( + rgb_item, 'rgb(255, 255, 255)', 0.5, colortype='rgb' + ) + trace_colors_2d.append([rgb_item, light_c]) + + num_of_rows = 1 num_of_cols = 1 flipped_rows = False @@ -872,6 +1164,26 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, raise exceptions.PlotlyError( CUSTOM_LABEL_ERROR.format(unique_keys) ) + + if column_width is None: + column_width = [1 for _ in range(num_of_cols)] + + # validate chart_types + # TODO: integrate this with trace_type eventually + # and keep backwards compatibility + if chart_types is None: + chart_types = ['scatter' for _ in range(num_of_cols)] + else: + # TODO: use sequence checker, not just list + if not isinstance(chart_types, list): + raise exceptions.PlotlyError( + 'chart_types must be a list' + ) + if len(chart_types) != num_of_cols: + raise exceptions.PlotlyError( + 'number of strings in chart_types must be equal to the ' + 'number of columns' + ) show_legend = False if color_name: if isinstance(df[color_name].iloc[0], str) or color_is_cat: @@ -900,7 +1212,8 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, df, x, y, facet_row, facet_col, color_name, colormap, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, - SUBPLOT_SPACING, marker_color, kwargs_trace, kwargs_marker + SUBPLOT_SPACING, marker_color, kwargs_trace, kwargs_marker, + column_width, trace_dimension ) elif isinstance(df[color_name].iloc[0], Number): @@ -920,7 +1233,8 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, marker_color, - kwargs_trace, kwargs_marker + kwargs_trace, kwargs_marker, column_width, + trace_dimension ) elif isinstance(colormap, list): @@ -932,7 +1246,8 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, colorscale_list, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, - marker_color, kwargs_trace, kwargs_marker + marker_color, kwargs_trace, kwargs_marker, column_width, + trace_dimension ) elif isinstance(colormap, str): if colormap in colors.PLOTLY_SCALES.keys(): @@ -948,7 +1263,8 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, colorscale_list, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, - marker_color, kwargs_trace, kwargs_marker + marker_color, kwargs_trace, kwargs_marker, column_width, + trace_dimension ) else: colorscale_list = colors.PLOTLY_SCALES['Reds'] @@ -957,7 +1273,8 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, colorscale_list, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, - marker_color, kwargs_trace, kwargs_marker + marker_color, kwargs_trace, kwargs_marker, column_width, + trace_dimension ) else: @@ -965,20 +1282,27 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, df, x, y, facet_row, facet_col, num_of_rows, num_of_cols, facet_row_labels, facet_col_labels, trace_type, flipped_rows, flipped_cols, show_boxes, SUBPLOT_SPACING, marker_color, - kwargs_trace, kwargs_marker + kwargs_trace, kwargs_marker, row_colors, alternate_row_color, + theme, column_width, trace_dimension, trace_colors_2d ) + # style the layout depending on theme if not height: height = max(600, 100 * num_of_rows) if not width: width = max(600, 100 * num_of_cols) - fig['layout'].update(height=height, width=width, title='', - paper_bgcolor='rgb(251, 251, 251)') - if ggplot2: - fig['layout'].update(plot_bgcolor=PLOT_BGCOLOR, - paper_bgcolor='rgb(255, 255, 255)', - hovermode='closest') + if theme == 'sparklines': + fig['layout'].update(height=height, width=width, title='') + + else: + fig['layout'].update(height=height, width=width, title='', + paper_bgcolor='rgb(251, 251, 251)') + + if ggplot2: + fig['layout'].update(plot_bgcolor=PLOT_BGCOLOR, + paper_bgcolor='rgb(255, 255, 255)', + hovermode='closest') # axis titles x_title_annot = _axis_title_annotation(x, 'x') @@ -1034,78 +1358,198 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, for axis_name in axis_labels[x_y]: fig['layout'][axis_name]['type'] = 'category' - if scales == 'fixed': + if theme == 'facet': + if scales == 'fixed': + fixed_axes = ['x', 'y'] + elif scales == 'free_x': + fixed_axes = ['y'] + elif scales == 'free_y': + fixed_axes = ['x'] + elif scales == 'free': + fixed_axes = [] + else: fixed_axes = ['x', 'y'] - elif scales == 'free_x': - fixed_axes = ['y'] - elif scales == 'free_y': - fixed_axes = ['x'] - elif scales == 'free': - fixed_axes = [] # fixed ranges - for x_y in fixed_axes: - min_ranges = [] - max_ranges = [] - for trace in fig['data']: - if trace[x_y] is not None and len(trace[x_y]) > 0: - min_ranges.append(min(trace[x_y])) - max_ranges.append(max(trace[x_y])) - while None in min_ranges: - min_ranges.remove(None) - while None in max_ranges: - max_ranges.remove(None) - - min_range = min(min_ranges) - max_range = max(max_ranges) - - range_are_numbers = (isinstance(min_range, Number) and - isinstance(max_range, Number)) - - if range_are_numbers: - min_range = math.floor(min_range) - max_range = math.ceil(max_range) - - # extend widen frame by 5% on each side - min_range -= 0.05 * (max_range - min_range) - max_range += 0.05 * (max_range - min_range) - - if x_y == 'x': - if dtick_x: - dtick = dtick_x - else: - dtick = math.floor( - (max_range - min_range) / MAX_TICKS_PER_AXIS - ) - elif x_y == 'y': - if dtick_y: - dtick = dtick_y - else: - dtick = math.floor( - (max_range - min_range) / MAX_TICKS_PER_AXIS - ) - else: - dtick = 1 - - for axis_title in axis_labels[x_y]: - fig['layout'][axis_title]['dtick'] = dtick - fig['layout'][axis_title]['ticklen'] = 0 - fig['layout'][axis_title]['zeroline'] = False - if ggplot2: - fig['layout'][axis_title]['tickwidth'] = 1 - fig['layout'][axis_title]['ticklen'] = 4 - fig['layout'][axis_title]['gridwidth'] = GRID_WIDTH - - fig['layout'][axis_title]['gridcolor'] = GRID_COLOR - fig['layout'][axis_title]['gridwidth'] = 2 - fig['layout'][axis_title]['tickfont'] = { - 'color': TICK_COLOR, 'size': 10 - } - - # insert ranges into fig - if x_y in fixed_axes: + if theme == 'facet': + for x_y in fixed_axes: + min_ranges = [] + max_ranges = [] + for trace in fig['data']: + if trace[x_y] is not None and len(trace[x_y]) > 0: + min_ranges.append(min(trace[x_y])) + max_ranges.append(max(trace[x_y])) + while None in min_ranges: + min_ranges.remove(None) + while None in max_ranges: + max_ranges.remove(None) + + min_range = min(min_ranges) + max_range = max(max_ranges) + + range_are_numbers = (isinstance(min_range, Number) and + isinstance(max_range, Number)) + + if range_are_numbers: + min_range = math.floor(min_range) + max_range = math.ceil(max_range) + + # widen frame by 5% on each side + min_range -= 0.05 * (max_range - min_range) + max_range += 0.05 * (max_range - min_range) + + if x_y == 'x': + if dtick_x: + dtick = dtick_x + else: + dtick = math.floor( + (max_range - min_range) / MAX_TICKS_PER_AXIS + ) + elif x_y == 'y': + if dtick_y: + dtick = dtick_y + else: + dtick = math.floor( + (max_range - min_range) / MAX_TICKS_PER_AXIS + ) + else: + dtick = 1 + + for axis_title in axis_labels[x_y]: + fig['layout'][axis_title]['dtick'] = dtick + fig['layout'][axis_title]['ticklen'] = 0 + fig['layout'][axis_title]['zeroline'] = False + if ggplot2: + fig['layout'][axis_title]['tickwidth'] = 1 + fig['layout'][axis_title]['ticklen'] = 4 + fig['layout'][axis_title]['gridwidth'] = GRID_WIDTH + + fig['layout'][axis_title]['gridcolor'] = GRID_COLOR + fig['layout'][axis_title]['gridwidth'] = 2 + fig['layout'][axis_title]['tickfont'] = { + 'color': TICK_COLOR, 'size': 10 + } + for key in fig['layout']: if '{}axis'.format(x_y) in key and range_are_numbers: fig['layout'][key]['range'] = [min_range, max_range] + # adjust range for individual subplot + # add bkgd panel colors + else: + # layout styling + for x_y in ['x', 'y']: + for axis_title in axis_labels[x_y]: + fig['layout'][axis_title]['showgrid'] = False + fig['layout'][axis_title]['showticklabels'] = False + fig['layout'][axis_title]['zeroline'] = False + + # set ranges + c_idx = 0 + # extract numbers from axis labels (eg. 'xaxis7' -> '7') + axis_numbers = [] + for xaxis in axis_labels['x']: + num_from_axis = re.findall('[^xaxis]+', xaxis)[0] + axis_numbers.append(int(num_from_axis)) + axis_numbers = sorted(axis_numbers) + + for num in axis_numbers: + # collect all traces with same axes + traces_with_same_axes = [] + for trace in fig['data']: + if trace['xaxis'][1:] == str(num): + traces_with_same_axes.append(trace) + if trace['x'] is not None: + min_x = min( + [min(trace['x']) for trace in traces_with_same_axes] + ) + max_x = max( + [max(trace['x']) for trace in traces_with_same_axes] + ) + else: + min_x = [None] + max_x = [None] + if trace['y'] is not None: + min_y = min( + [min(trace['y']) for trace in traces_with_same_axes] + ) + max_y = max( + [max(trace['y']) for trace in traces_with_same_axes] + ) + else: + min_y = [None] + max_y = [None] + + range_are_numbers = all( + isinstance(n, Number) for n in [min_x, max_x, min_y, max_y] + ) + + # TODO: set x, y ranges when string data + if range_are_numbers: + print('range are numbers') + min_y = math.floor(min_y) + max_y = math.ceil(max_y) + + all_x_data = [] + all_y_data = [] + for l in traces_with_same_axes: + for x in l['x']: + all_x_data.append(x) + for y in l['y']: + all_y_data.append(y) + + # handle x margin for cells + if any(item is None or item != item for item in all_x_data): + xrange_bottom = -1 + xrange_top = 1 + elif min_x == max_x: + xrange_bottom = min_x - 0.5 + xrange_top = max_x + 0.5 + else: + std_x = np.std(all_x_data) + xrange_bottom = min_x - x_margin_factor * std_x + xrange_top = max_x + x_margin_factor * std_x + + # handle y margins for cells + if any(item is None or item != item for item in all_y_data): + yrange_bottom = -1 + yrange_top = 1 + elif min_y == max_y: + yrange_bottom = min_y - 0.5 + yrange_top = max_y + 0.5 + else: + std_y = np.std(all_y_data) + yrange_bottom = min_y - y_margin_factor * std_y + yrange_top = max_y + y_margin_factor * std_y + + else: + xrange_bottom = -2 + xrange_top = 2 + yrange_bottom = -2 + yrange_top = 2 + + some_xaxis = 'xaxis{}'.format(str(num)) + some_yaxis = 'yaxis{}'.format(str(num)) + fig['layout'][some_xaxis]['range'] = [xrange_bottom, xrange_top] + fig['layout'][some_yaxis]['range'] = [yrange_bottom, yrange_top] + + # add shapes to bkgd + if alternate_row_color: + if c_idx >= len(row_colors): + c_idx = 0 + r_color = row_colors[c_idx] + + bkg = rect( + 'x{}'.format(num), + 'y{}'.format(num), + x0=xrange_bottom, x1=xrange_top, + y0=yrange_bottom, y1=yrange_top, + color=( + r_color + ) + ) + fig['layout']['shapes'].append(bkg) + if int(num) % num_of_cols == 0: + c_idx += 1 + return fig diff --git a/plotly/figure_factory/_sparkline.py b/plotly/figure_factory/_sparkline.py new file mode 100644 index 00000000000..e980bee5ba4 --- /dev/null +++ b/plotly/figure_factory/_sparkline.py @@ -0,0 +1,821 @@ +from __future__ import absolute_import + +from plotly import exceptions, optional_imports +from plotly.figure_factory import utils + +import plotly +import plotly.graph_objs as go + +import numpy as np +pd = optional_imports.get_module('pandas') + +VALID_CHART_TYPES = ('label', 'bullet', 'line', 'avg', 'bar', 'area') + + +def rect(xref, yref, x0, x1, y0, y1, color): + shape = { + 'layer': 'below', + 'xref': xref, + 'yref': yref, + 'x0': x0, + 'x1': x1, + 'y0': y0, + 'y1': y1, + 'fillcolor': color, + 'line': {'width': 0} + } + return shape + + +VALID_CHART_TYPE_MESSAGE = ( + 'Invalid chart type. The valid chart types ' + 'are {}'.format(utils.list_of_options(VALID_CHART_TYPES, 'and')) +) + + +def _sparkline_tidy(df, fig, chart_types, num_of_chart_types, trace_colors_2d, + column_width, alternate_row_color, row_colors, xanchor, + line_width, align_x, scatter_options, new_marker_color, + narrow_idxs, show_titles, row, column, x): + shapes = [] + annotations = [] + # create and insert charts + c_idx = 0 + trace_c_idx = 0 + chart_types_keys = [k for d in chart_types for k in d.keys()] + for j, rkey in enumerate(df[row].unique()): + for c, ckey in enumerate(chart_types_keys): + chart = chart_types[c][ckey] + data = list(df[(df[row] == rkey) & (df[column] == ckey)][x]) + + # update indices + if c_idx >= len(row_colors): + c_idx = 0 + r_color = row_colors[c_idx] + + if trace_c_idx >= len(trace_colors_2d): + trace_c_idx = 0 + dark_color = trace_colors_2d[trace_c_idx][0] + light_color = trace_colors_2d[trace_c_idx][1] + + if len(data) <= 0: + if chart in ['label', 'avg']: + if chart == 'label': + text = rkey + else: + text = 'nan' + annotations.append( + dict( + xref='x{}'.format(j * num_of_chart_types + c + 1), + yref='y{}'.format(j * num_of_chart_types + c + 1), + x=align_x, + y=0.5, + xanchor=xanchor, + text=text, + showarrow=False, + font=dict(size=12), + ) + ) + empty_data = go.Bar( + x=[0], + y=[0], + visible=False + ) + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=0, x1=1, + y0=-0.1, y1=1.1, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(empty_data, j + 1, c + 1) + else: + mean = np.mean(data) + rounded_mean = round(mean, 2) + if chart in ['label', 'avg']: + if chart == 'label': + text = rkey + else: + text = '{}'.format(rounded_mean) + annotations.append( + dict( + xref='x{}'.format(j * num_of_chart_types + c + 1), + yref='y{}'.format(j * num_of_chart_types + c + 1), + x=align_x, + y=0.5, + xanchor=xanchor, + text=text, + showarrow=False, + font=dict(size=12), + ) + ) + empty_data = go.Bar( + x=[0], + y=[0], + visible=False + ) + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=0, x1=1, + y0=-0.1, y1=1.1, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(empty_data, j + 1, c + 1) + + elif chart == 'bullet': + bullet_range = go.Bar( + x=[rounded_mean], + y=[0.5], + marker=dict( + color=light_color + ), + hoverinfo='x', + orientation='h', + width=0.5 + ) + + bullet_measure = go.Bar( + x=[data[-1]], + y=[0.5], + marker=dict( + color=dark_color + ), + hoverinfo='x', + orientation='h', + width=0.14, + offset=-0.07 + ) + if not new_marker_color: + scatter_options['marker']['color'] = dark_color + bullet_pt = go.Scatter( + x=[max(data)], + y=[0.5], + hoverinfo='x', + **scatter_options + ) + + xrange_r = max(data) + 0.5 * rounded_mean + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=0, x1=xrange_r, + y0=0, y1=1, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(bullet_range, j + 1, c + 1) + fig.append_trace(bullet_measure, j + 1, c + 1) + fig.append_trace(bullet_pt, j + 1, c + 1) + + fig['layout']['xaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [0, xrange_r] + fig['layout']['yaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [0, 1] + elif chart in ['line', 'area']: + if chart == 'line': + trace_line = go.Scatter( + x=range(len(data)), + y=data, + mode='lines', + marker=dict( + color=dark_color + ) + ) + else: + trace_line = go.Scatter( + x=range(len(data)), + y=data, + mode='lines', + fill='tozeroy', + fillcolor=light_color, + line=dict(width=line_width, color=dark_color) + ) + if not new_marker_color: + scatter_options['marker']['color'] = dark_color + trace_line_pt = go.Scatter( + x=[len(data) - 1], + y=[data[-1]], + **scatter_options + ) + + # invisible pt + pt_hidden = go.Scatter( + x=[-1], + y=[0], + hoverinfo='none' + ) + + std = np.std(data) + x0 = -0.01 + if np.isnan(std): + yrange_top = 1 + yrange_bottom = 0 + elif std == 0: + extra_space = 0.3 * abs(data[0]) + yrange_top = data[0] + extra_space + yrange_bottom = data[0] - extra_space + else: + yrange_top = max(data) + 2*std + yrange_bottom = min(data) - 2*std + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=x0, x1=len(data), + y0=yrange_bottom, y1=yrange_top, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(trace_line, j + 1, c + 1) + fig.append_trace(trace_line_pt, j + 1, c + 1) + fig.append_trace(pt_hidden, j + 1, c + 1) + + fig['layout']['yaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [yrange_bottom, yrange_top] + fig['layout']['xaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [x0, len(data)] + elif chart == 'bar': + std = np.std(data) + if std == 0: + extra_space = 0.3 * abs(data[0]) + if data[0] < 0: + yrange_top = 0 + yrange_bottom = (data[0] - extra_space) + elif data[0] >= 0: + yrange_top = (data[0] + extra_space) + yrange_bottom = 0 + else: + yrange_top = max(data) + std + yrange_bottom = min(data) - std + + trace_bar = go.Bar( + x=range(len(data)), + y=data, + marker=dict( + color=[light_color for k in + range(len(data) - 1)] + [dark_color] + ) + ) + + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=-1, x1=len(data), + y0=yrange_bottom, y1=yrange_top, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(trace_bar, j + 1, c + 1) + + fig['layout']['yaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [yrange_bottom, yrange_top] + for x_y in ['xaxis', 'yaxis']: + fig['layout']['{}{}'.format( + x_y, j * num_of_chart_types + (c + 1) + )]['fixedrange'] = True + + # show titles + if show_titles and j == 0: + label = utils.annotation_dict_for_label( + ckey, c + 1, num_of_chart_types, subplot_spacing=0, + row_col='col', flipped=False, column_width=column_width + ) + annotations.append(label) + + for idx in narrow_idxs: + for axis in ['xaxis', 'yaxis']: + fig['layout']['{}{}'.format( + axis, c * num_of_chart_types + idx + 1 + )]['range'] = [0, 1] + c_idx += 1 + trace_c_idx += 1 + + # add shapes and annotations as tuples + fig['layout']['shapes'] = shapes + fig['layout']['annotations'] = annotations + + +def _sparkline(df, fig, chart_types, num_of_chart_types, trace_colors_2d, + column_width, alternate_row_color, row_colors, xanchor, + line_width, align_x, scatter_options, new_marker_color, + narrow_idxs, show_titles): + shapes = [] + annotations = [] + # create and insert charts + c_idx = 0 + trace_c_idx = 0 + for j, key in enumerate(df): + for c, chart in enumerate(chart_types): + mean = np.mean(df[key]) + rounded_mean = round(mean, 2) + # update indices + if c_idx >= len(row_colors): + c_idx = 0 + r_color = row_colors[c_idx] + + if trace_c_idx >= len(trace_colors_2d): + trace_c_idx = 0 + dark_color = trace_colors_2d[trace_c_idx][0] + light_color = trace_colors_2d[trace_c_idx][1] + + if chart == ['label', 'avg']: + if chart == 'label': + text = key + else: + text = '{}'.format(rounded_mean) + # fig['layout']['annotations'].append( + annotations.append( + dict( + x=align_x, + y=0.5, + xref='x{}'.format(j * num_of_chart_types + c + 1), + yref='y{}'.format(j * num_of_chart_types + c + 1), + xanchor=xanchor, + text=text, + showarrow=False, + font=dict(size=12), + ) + ) + empty_data = go.Bar( + x=[0], + y=[0], + visible=False + ) + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=-0.1, x1=1.1, + y0=-0.1, y1=1.1, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(empty_data, j + 1, c + 1) + + elif chart == 'bullet': + bullet_range = go.Bar( + x=[rounded_mean], + y=[0.5], + marker=dict( + color=light_color + ), + hoverinfo='x', + orientation='h', + width=0.5 + ) + + bullet_measure = go.Bar( + x=[list(df[key])[-1]], + y=[0.5], + marker=dict( + color=dark_color + ), + hoverinfo='x', + orientation='h', + width=0.14, + offset=-0.07 + ) + if not new_marker_color: + scatter_options['marker']['color'] = dark_color + bullet_pt = go.Scatter( + x=[max(df[key])], + y=[0.5], + hoverinfo='x', + **scatter_options + ) + + xrange_r = max(df[key]) + 0.5 * rounded_mean + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=0, x1=xrange_r, + y0=0, y1=1, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(bullet_range, j + 1, c + 1) + fig.append_trace(bullet_measure, j + 1, c + 1) + fig.append_trace(bullet_pt, j + 1, c + 1) + + fig['layout']['xaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [0, xrange_r] + fig['layout']['yaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [0, 1] + + elif chart in ['line', 'area']: + if chart == 'line': + trace_line = go.Scatter( + x=range(len(df[key])), + y=df[key].tolist(), + mode='lines', + marker=dict( + color=dark_color + ) + ) + else: + trace_line = go.Scatter( + x=range(len(df[key])), + y=df[key].tolist(), + mode='lines', + fill='tozeroy', + fillcolor=light_color, + line=dict(width=line_width, color=dark_color) + ) + if not new_marker_color: + scatter_options['marker']['color'] = dark_color + trace_line_pt = go.Scatter( + x=[len(df[key]) - 1], + y=[df[key].tolist()[-1]], + **scatter_options + ) + + std = np.std(df[key]) + if std == 0: + extra_space = 0.3 * abs(df[key][0]) + yrange_top = df[key].tolist()[0] + extra_space + yrange_bottom = df[key].tolist()[0] - extra_space + else: + yrange_top = max(df[key].tolist()) + std + yrange_bottom = min(df[key].tolist()) - std + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=0, x1=len(df[key]), + y0=yrange_bottom, y1=yrange_top, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(trace_line, j + 1, c + 1) + fig.append_trace(trace_line_pt, j + 1, c + 1) + + fig['layout']['yaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [yrange_bottom, yrange_top] + + elif chart == 'bar': + std = np.std(df[key]) + if std == 0: + extra_space = 0.3 * abs(df[key][0]) + if df[key][0] < 0: + yrange_top = 0 + yrange_bottom = (df[key].tolist()[0] - extra_space) + elif df[key][0] >= 0: + yrange_top = (df[key].tolist()[0] + extra_space) + yrange_bottom = 0 + else: + yrange_top = max(df[key].tolist()) + std + yrange_bottom = min(df[key].tolist()) - std + + trace_bar = go.Bar( + x=range(len(df[key])), + y=df[key].tolist(), + marker=dict( + color=[light_color for k in + range(len(df[key]) - 1)] + [dark_color] + ) + ) + + if alternate_row_color: + bkg = rect( + 'x{}'.format(j * num_of_chart_types + c + 1), + 'y{}'.format(j * num_of_chart_types + c + 1), + x0=-1, x1=len(df[key]), + y0=yrange_bottom, y1=yrange_top, + color=( + r_color + ) + ) + # fig['layout']['shapes'].append(bkg) + shapes.append(bkg) + fig.append_trace(trace_bar, j + 1, c + 1) + + fig['layout']['yaxis{}'.format( + j * num_of_chart_types + (c + 1) + )]['range'] = [yrange_bottom, yrange_top] + for x_y in ['xaxis', 'yaxis']: + fig['layout']['{}{}'.format( + x_y, j * num_of_chart_types + (c + 1) + )]['fixedrange'] = True + + # show titles + if show_titles and j == 0: + label = utils.annotation_dict_for_label( + chart, c + 1, num_of_chart_types, subplot_spacing=0, + row_col='col', flipped=False, column_width=column_width + ) + # fig['layout']['annotations'].append(label) + annotations.append(label) + + for idx in narrow_idxs: + for axis in ['xaxis', 'yaxis']: + fig['layout']['{}{}'.format( + axis, c * num_of_chart_types + idx + 1 + )]['range'] = [0, 1] + c_idx += 1 + trace_c_idx += 1 + + fig['layout']['shapes'] = shapes + fig['layout']['annotations'] = annotations + +def create_sparkline(df, chart_types=None, row=None, column=None, + x=None, trace_colors=None, column_width=None, + show_titles=False, text_align='center', + horizontal_spacing=0.0, vertical_spacing=0.0, + alternate_row_color=True, + row_colors=('rgb(247, 247, 242)', + 'rgb(255, 253, 250)'), + line_width=2, scatter_options=None, **layout_options): + """ + Returns figure for sparkline. + + :param (pd.DataFrame | list | tuple) df: either a list/tuple of + dictionaries or a pandas DataFrame. + :param (list|tuple) chart_types: a sequence of any combination of valid + chart types. The valid chart types are 'label', 'bullet', 'line', 'avg' + and 'bar' + Default = ('label', 'bullet', 'line', 'avg', 'bar', 'area') + :param (list|tuple) trace_colors: a list of colors or a list of lists of + two colors. Each row uses two colors: a darker one and a lighter one. + Options: + - list of 2 colors: first color is dark color for all traces and + second is light for all traces. + - 1D list of more than 2 colors: the nth color in the list is the + nth dark color for the traces and the associated light color is + just 0.5 times the opacity of the dark color + - lists of lists: each inner list must have exactly 2 colors in it + and the first and second color of the nth inner list represent + the dark and light color for the nth row repsectively + - list of lists and colors: this is a combination of the previous + options + Whenever trace_colors has fewer colors than the number of rows of the + figure, the colors will repeat from the start of the list + Default = [['rgb(62,151,169)', 'rgb(181,221,232)']] + :param (list) column_width: Specify a list that contains numbers where + the amount of numbers in the list is equal to `chart_types`. Call + `help(plotly.tools.make_subplots)` for more info on this subplot param + :param (bool) show_titles: determines if title of chart type is displayed + above their respective column + :param (str) text_align: aligns label and avg cells. Use either 'center', + 'left', or 'right'. Default='center'. + :param (float) horizontal_spacing: Space between subplot columns. + Applied to all columns + Default = 0.0 + :param (float) vertical_spacing: Space between subplot rows. + Applied to all rows + Default = 0.0 + :param (bool) alternate_row_color: set to True to enable the alternate + row coloring of the chart. Uses the trace_colors from param 'row_colors' + Default = True + :param (list) row_colors: a list/tuple of colors that are used to + alternately color the rows of the chart. If the number of colors in the + list is fewer than the number of rows, the active color for the layout + will be looped back to the first in the list + Default = ('rgb(247, 247, 242)', 'rgb(255, 253, 250)') + :param (float) line_width: sets the width of the lines used in 'area' or + filled area line charts + Default = 2 + :param (dict) scatter_options: describes attributes for the scatter point + in each bullet chart such as label and marker size. Call + help(plotly.graph_objs.Scatter) for more information on valid params. + :param layout_options: describes attributes for the layout of the figure + such as title, height and width. Call help(plotly.graph_objs.Layout) + for more information on valid params + """ + # validate dataframe + if not pd: + raise exceptions.ImportError( + "'pandas' must be installed for this figure factory." + ) + + elif not isinstance(df, pd.DataFrame): + raise exceptions.PlotlyError( + 'df must be a pandas DataFrame' + ) + + # validate list/tuple of trace_colors + if not trace_colors: + trace_colors = [['rgb(62,151,169)', 'rgb(181,221,232)']] + if not utils.is_sequence(trace_colors): + raise exceptions.PlotlyError( + 'trace_colors must be a list/tuple' + ) + + trace_colors_2d = [] + for i, item in enumerate(trace_colors): + plotly.colors.validate_colors(item) + if utils.is_sequence(item): + trace_colors_2d.append(item) + else: + # if hex convert to rgb + if '#' in item: + tuple_item = plotly.colors.hex_to_rgb(item) + rgb_item = plotly.colors.label_rgb(tuple_item) + else: + rgb_item = item + light_c = plotly.colors.find_intermediate_color( + rgb_item, 'rgb(255, 255, 255)', 0.5, colortype='rgb' + ) + trace_colors_2d.append([rgb_item, light_c]) + + # validate chart_types and row, column, x + if row or column or x: + if not (row and column and x): + raise exceptions.PlotlyError( + "You must set the params 'row', 'column' and 'x' to valid " + "column names of your dataframe" + ) + + # confirm valid row, column, x values + for param in [row, column, x]: + if param not in df: + raise exceptions.PlotlyError( + '{} is not a column name of the dataframe'.format(param) + ) + + if not chart_types: + chart_types = [] + for j, val in enumerate(df[column].unique()): + if j == len(VALID_CHART_TYPES): + j = 0 + chart_types.append({val: VALID_CHART_TYPES[j]}) + j += 1 + + if (not utils.is_sequence(chart_types) and + not all(isinstance(i, dict) for i in chart_types)): + raise exceptions.PlotlyError( + "'chart_types' must be a list/tuple of dictionaries if " + "'row', 'column', and 'x' are being used" + ) + chart_types_keys = [k for d in chart_types for k in d.keys()] + for key in chart_types_keys: + if key not in df[column].unique(): + raise exceptions.PlotlyError( + "All the keys of the dictionaries in 'chart_types' must " + "be values in the chosen column. Since your selected " + "column is '{}', the available keys {}".format( + column, + utils.list_of_options(df[column].unique(), 'and') + ) + ) + + # validate chart types + chart_vals = [k for d in chart_types for k in d.values()] + for item in chart_vals: + if item not in VALID_CHART_TYPES: + raise exceptions.PlotlyError(VALID_CHART_TYPE_MESSAGE) + else: + if not chart_types: + chart_types = VALID_CHART_TYPES + for item in chart_types: + if item not in VALID_CHART_TYPES: + raise exceptions.PlotlyError(VALID_CHART_TYPE_MESSAGE) + + num_of_chart_types = len(chart_types) + # narrow columns that are 'label' or 'avg' + narrow_cols = ['label', 'avg'] + narrow_idxs = [] + if not isinstance(chart_types, dict): + for i, chart in enumerate(chart_types): + if chart in narrow_cols: + narrow_idxs.append(i) + else: + for i, chart in enumerate(chart_types): + if chart in narrow_cols: + narrow_idxs.append(i) + + if not column_width: + column_width = [3.0] * num_of_chart_types + for idx in narrow_idxs: + column_width[idx] = 1.0 + + # text alignment + xanchor = text_align + if text_align == 'left': + align_x = 0 + elif text_align == 'center': + align_x = 0.5 + elif text_align == 'right': + align_x = 1 + else: + raise exceptions.PlotlyError( + 'text_align must be left, center or right' + ) + + # scatter options + default_scatter = { + 'mode': 'markers', + 'marker': {'size': 8, + 'symbol': 'diamond-tall'} + } + + if not scatter_options: + scatter_options = {} + + if scatter_options == {}: + scatter_options.update(default_scatter) + else: + # add default options to scatter_options if they are not present + for k in default_scatter['marker']: + try: + if k not in scatter_options['marker']: + scatter_options['marker'][k] = default_scatter['marker'][k] + except KeyError: + scatter_options['marker'] = {} + scatter_options['marker'][k] = default_scatter['marker'][k] + + if 'marker' in scatter_options and 'color' in scatter_options['marker']: + new_marker_color = True + else: + new_marker_color = False + + # create fig + if row and column and x: + fig = plotly.tools.make_subplots( + len(df[row].unique()), num_of_chart_types, print_grid=False, + shared_xaxes=False, shared_yaxes=False, + horizontal_spacing=horizontal_spacing, + vertical_spacing=vertical_spacing, column_width=column_width + ) + else: + fig = plotly.tools.make_subplots( + len(df.columns), num_of_chart_types, print_grid=False, + shared_xaxes=False, shared_yaxes=False, + horizontal_spacing=horizontal_spacing, + vertical_spacing=vertical_spacing, column_width=column_width + ) + + # layout options + fig['layout'].update( + title='', + # annotations=[], + # shapes=[], + showlegend=False + ) + + # update layout + fig['layout'].update(layout_options) + + for key in fig['layout'].to_plotly_json().keys(): + if 'axis' in key: + fig['layout'][key].update( + showgrid=False, + zeroline=False, + showticklabels=False + ) + + if row and column and x: + _sparkline_tidy( + df, fig, chart_types, num_of_chart_types, trace_colors_2d, + column_width, alternate_row_color, row_colors, xanchor, + line_width, align_x, scatter_options, new_marker_color, + narrow_idxs, show_titles, row, column, x + ) + else: + _sparkline( + df, fig, chart_types, num_of_chart_types, trace_colors_2d, + column_width, alternate_row_color, row_colors, xanchor, + line_width, align_x, scatter_options, new_marker_color, + narrow_idxs, show_titles + ) + + return fig diff --git a/plotly/figure_factory/utils.py b/plotly/figure_factory/utils.py index ee19e17cef9..45a2ceedb64 100644 --- a/plotly/figure_factory/utils.py +++ b/plotly/figure_factory/utils.py @@ -386,6 +386,7 @@ def validate_colors_dict(colors, colortype='tuple'): return colors + def colorscale_to_colors(colorscale): """ Extracts the colors from colorscale as a list @@ -499,7 +500,7 @@ def endpts_to_intervals(endpts): def annotation_dict_for_label(text, lane, num_of_lanes, subplot_spacing, row_col='col', flipped=True, right_side=True, - text_color='#0f0f0f'): + text_color='#0f0f0f', column_width=None): """ Returns annotation dict for label of n labels of a 1xn or nx1 subplot. @@ -515,29 +516,42 @@ def annotation_dict_for_label(text, lane, num_of_lanes, subplot_spacing, row_col='col'. :param (bool) right_side: only applicable if row_col is set to 'row'. :param (str) text_color: color of the text. - """ - l = (1 - (num_of_lanes - 1) * subplot_spacing) / (num_of_lanes) + :param (list|tuple) column_width: a sequence of numbers that indicate the + proportions that each subplot lane (row/column) take relative to one + another. + """ + if not column_width: + column_width = [1.0] * num_of_lanes + + cum_sum = float(sum(column_width)) + len_without_spacing = (1.0 - (num_of_lanes - 1) * subplot_spacing) + lane_len = (len_without_spacing * (column_width[lane - 1] / cum_sum)) + len_before_lane = ( + len_without_spacing * (sum(column_width[:lane - 1]) / cum_sum) + + (lane - 1) * subplot_spacing + ) + if not flipped: xanchor = 'center' yanchor = 'middle' if row_col == 'col': - x = (lane - 1) * (l + subplot_spacing) + 0.5 * l + x = len_before_lane + 0.5 * lane_len y = 1.03 textangle = 0 elif row_col == 'row': - y = (lane - 1) * (l + subplot_spacing) + 0.5 * l + y = len_before_lane + 0.5 * lane_len x = 1.03 textangle = 90 else: if row_col == 'col': xanchor = 'center' yanchor = 'bottom' - x = (lane - 1) * (l + subplot_spacing) + 0.5 * l + x = len_before_lane + 0.5 * lane_len y = 1.0 textangle = 270 elif row_col == 'row': yanchor = 'middle' - y = (lane - 1) * (l + subplot_spacing) + 0.5 * l + y = len_before_lane + 0.5 * lane_len if right_side: x = 1.0 xanchor = 'left' diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 32c25113859..49888d614a6 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2662,7 +2662,7 @@ def test_full_bullet(self): 'showarrow': False, 'text': 'New Customers', 'textangle': 0, - 'x': 0.74, + 'x': 0.7400000000000001, 'xanchor': 'center', 'xref': 'paper', 'y': 1.03, @@ -2747,6 +2747,652 @@ def test_full_bullet(self): exp_fig['data'][i]) +class TestSparkline(NumpyTestUtilsMixin, TestCase): + + def test_is_dataframe(self): + df = 'not going to work' + message = 'df must be a pandas DataFrame' + + self.assertRaisesRegexp(PlotlyError, message, ff.create_sparkline, df) + + def test_valid_chart_type(self): + df = pd.DataFrame( + [ + [5, 2, 9], + [6, 5, 4], + [4, 7, 6] + ], + columns=['foo', 'bar', 'baz'], + ) + VALID_CHART_TYPES = ('label', 'bullet', 'line', 'avg', 'bar', 'area') + message = ( + 'Your chart type must be a list and may only contain any ' + 'combination of the keys {}'.format( + utils.list_of_options(VALID_CHART_TYPES, 'or') + ) + ) + + self.assertRaisesRegexp(PlotlyError, message, ff.create_sparkline, + df, chart_types=['love']) + + def test_is_colors_a_list(self): + df = pd.DataFrame( + [ + [5, 2, 9], + [6, 5, 4], + [4, 7, 6] + ], + columns=['foo', 'bar', 'baz'], + ) + + message = 'trace_colors must be a list/tuple' + self.assertRaisesRegexp(PlotlyError, message, ff.create_sparkline, + df, trace_colors='not going to work') + + def test_valid_textalign_value(self): + df = pd.DataFrame( + [ + [5, 2, 9], + [6, 5, 4], + [4, 7, 6] + ], + columns=['foo', 'bar', 'baz'], + ) + + message = 'text_align must be left, center or right' + self.assertRaisesRegexp(PlotlyError, message, ff.create_sparkline, + df, text_align='will not work') + + def test_full_sparkline_fig(self): + + df = pd.DataFrame( + [ + [5, 2, 9], + [6, 5, 4], + [4, 7, 6] + ], + columns=['foo', 'bar', 'baz'], + ) + + fig = ff.create_sparkline( + df, chart_types=('label', 'bullet', 'line', 'avg', 'bar'), + column_width=[1, 2, 3, 4, 5], + scatter_options={'marker': {'color': 'rgb(0,0,0)'}}, + title='this is a test' + ) + + exp_fig = { + 'data': [{'type': 'bar', + 'visible': False, + 'x': [0], + 'xaxis': 'x1', + 'y': [0], + 'yaxis': 'y1'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(181,221,232)'}, + 'orientation': 'h', + 'type': 'bar', + 'width': 0.5, + 'x': [5.0], + 'xaxis': 'x2', + 'y': [0.5], + 'yaxis': 'y2'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(62,151,169)'}, + 'offset': -0.07, + 'orientation': 'h', + 'type': 'bar', + 'width': 0.14, + 'x': [4], + 'xaxis': 'x2', + 'y': [0.5], + 'yaxis': 'y2'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(0,0,0)', 'size': 9, 'symbol': 'diamond-tall'}, + 'type': 'scatter', + 'x': [6], + 'xaxis': 'x2', + 'y': [0.5], + 'yaxis': 'y2'}, + {'marker': {'color': 'rgb(62,151,169)'}, + 'mode': 'lines', + 'type': 'scatter', + 'x': [0, 1, 2], + 'xaxis': 'x3', + 'y': [5, 6, 4], + 'yaxis': 'y3'}, + {'marker': {'color': 'rgb(0,0,0)', 'size': 9, 'symbol': 'diamond-tall'}, + 'type': 'scatter', + 'x': [2], + 'xaxis': 'x3', + 'y': [4], + 'yaxis': 'y3'}, + {'type': 'bar', + 'visible': False, + 'x': [0], + 'xaxis': 'x4', + 'y': [0], + 'yaxis': 'y4'}, + {'marker': {'color': ['rgb(181,221,232)', + 'rgb(181,221,232)', + 'rgb(62,151,169)']}, + 'type': 'bar', + 'x': [0, 1, 2], + 'xaxis': 'x5', + 'y': [5, 6, 4], + 'yaxis': 'y5'}, + {'type': 'bar', + 'visible': False, + 'x': [0], + 'xaxis': 'x6', + 'y': [0], + 'yaxis': 'y6'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(181,221,232)'}, + 'orientation': 'h', + 'type': 'bar', + 'width': 0.5, + 'x': [4.67], + 'xaxis': 'x7', + 'y': [0.5], + 'yaxis': 'y7'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(62,151,169)'}, + 'offset': -0.07, + 'orientation': 'h', + 'type': 'bar', + 'width': 0.14, + 'x': [7], + 'xaxis': 'x7', + 'y': [0.5], + 'yaxis': 'y7'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(0,0,0)', 'size': 9, 'symbol': 'diamond-tall'}, + 'type': 'scatter', + 'x': [7], + 'xaxis': 'x7', + 'y': [0.5], + 'yaxis': 'y7'}, + {'marker': {'color': 'rgb(62,151,169)'}, + 'mode': 'lines', + 'type': 'scatter', + 'x': [0, 1, 2], + 'xaxis': 'x8', + 'y': [2, 5, 7], + 'yaxis': 'y8'}, + {'marker': {'color': 'rgb(0,0,0)', 'size': 9, 'symbol': 'diamond-tall'}, + 'type': 'scatter', + 'x': [2], + 'xaxis': 'x8', + 'y': [7], + 'yaxis': 'y8'}, + {'type': 'bar', + 'visible': False, + 'x': [0], + 'xaxis': 'x9', + 'y': [0], + 'yaxis': 'y9'}, + {'marker': {'color': ['rgb(181,221,232)', + 'rgb(181,221,232)', + 'rgb(62,151,169)']}, + 'type': 'bar', + 'x': [0, 1, 2], + 'xaxis': 'x10', + 'y': [2, 5, 7], + 'yaxis': 'y10'}, + {'type': 'bar', + 'visible': False, + 'x': [0], + 'xaxis': 'x11', + 'y': [0], + 'yaxis': 'y11'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(181,221,232)'}, + 'orientation': 'h', + 'type': 'bar', + 'width': 0.5, + 'x': [6.33], + 'xaxis': 'x12', + 'y': [0.5], + 'yaxis': 'y12'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(62,151,169)'}, + 'offset': -0.07, + 'orientation': 'h', + 'type': 'bar', + 'width': 0.14, + 'x': [6], + 'xaxis': 'x12', + 'y': [0.5], + 'yaxis': 'y12'}, + {'hoverinfo': 'x', + 'marker': {'color': 'rgb(0,0,0)', 'size': 9, 'symbol': 'diamond-tall'}, + 'type': 'scatter', + 'x': [9], + 'xaxis': 'x12', + 'y': [0.5], + 'yaxis': 'y12'}, + {'marker': {'color': 'rgb(62,151,169)'}, + 'mode': 'lines', + 'type': 'scatter', + 'x': [0, 1, 2], + 'xaxis': 'x13', + 'y': [9, 4, 6], + 'yaxis': 'y13'}, + {'marker': {'color': 'rgb(0,0,0)', 'size': 9, 'symbol': 'diamond-tall'}, + 'type': 'scatter', + 'x': [2], + 'xaxis': 'x13', + 'y': [6], + 'yaxis': 'y13'}, + {'type': 'bar', + 'visible': False, + 'x': [0], + 'xaxis': 'x14', + 'y': [0], + 'yaxis': 'y14'}, + {'marker': {'color': ['rgb(181,221,232)', + 'rgb(181,221,232)', + 'rgb(62,151,169)']}, + 'type': 'bar', + 'x': [0, 1, 2], + 'xaxis': 'x15', + 'y': [9, 4, 6], + 'yaxis': 'y15'}], + 'layout': {'annotations': [{'font': {'size': 12}, + 'showarrow': False, + 'text': 'foo', + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'x1', + 'y': 0.5, + 'yref': 'y1'}, + {'font': {'size': 12}, + 'showarrow': False, + 'text': '5.0', + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'x4', + 'y': 0.5, + 'yref': 'y4'}, + {'font': {'size': 12}, + 'showarrow': False, + 'text': 'bar', + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'x6', + 'y': 0.5, + 'yref': 'y6'}, + {'font': {'size': 12}, + 'showarrow': False, + 'text': '4.67', + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'x9', + 'y': 0.5, + 'yref': 'y9'}, + {'font': {'size': 12}, + 'showarrow': False, + 'text': 'baz', + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'x11', + 'y': 0.5, + 'yref': 'y11'}, + {'font': {'size': 12}, + 'showarrow': False, + 'text': '6.33', + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'x14', + 'y': 0.5, + 'yref': 'y14'}], + 'shapes': [{'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': -0.1, + 'x1': 1.1, + 'xref': 'x1', + 'y0': 0, + 'y1': 1.2, + 'yref': 'y1'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 8.5, + 'xref': 'x2', + 'y0': 0, + 'y1': 1, + 'yref': 'y2'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 3, + 'xref': 'x3', + 'y0': 3.1835034190722737, + 'y1': 6.8164965809277263, + 'yref': 'y3'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 2, + 'xref': 'x4', + 'y0': 0, + 'y1': 1.2, + 'yref': 'y4'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': -1, + 'x1': 3, + 'xref': 'x5', + 'y0': 3.1835034190722737, + 'y1': 6.8164965809277263, + 'yref': 'y5'}, + {'fillcolor': 'rgb(255, 253, 250)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': -0.1, + 'x1': 1.1, + 'xref': 'x6', + 'y0': 0, + 'y1': 1.2, + 'yref': 'y6'}, + {'fillcolor': 'rgb(255, 253, 250)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 9.335, + 'xref': 'x7', + 'y0': 0, + 'y1': 1, + 'yref': 'y7'}, + {'fillcolor': 'rgb(255, 253, 250)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 3, + 'xref': 'x8', + 'y0': -0.054804667656325634, + 'y1': 9.0548046676563256, + 'yref': 'y8'}, + {'fillcolor': 'rgb(255, 253, 250)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 2, + 'xref': 'x9', + 'y0': 0, + 'y1': 1.2, + 'yref': 'y9'}, + {'fillcolor': 'rgb(255, 253, 250)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': -1, + 'x1': 3, + 'xref': 'x10', + 'y0': -0.054804667656325634, + 'y1': 9.0548046676563256, + 'yref': 'y10'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': -0.1, + 'x1': 1.1, + 'xref': 'x11', + 'y0': 0, + 'y1': 1.2, + 'yref': 'y11'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 12.165, + 'xref': 'x12', + 'y0': 0, + 'y1': 1, + 'yref': 'y12'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 3, + 'xref': 'x13', + 'y0': 1.9451953323436744, + 'y1': 11.054804667656326, + 'yref': 'y13'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': 0, + 'x1': 2, + 'xref': 'x14', + 'y0': 0, + 'y1': 1.2, + 'yref': 'y14'}, + {'fillcolor': 'rgb(247, 247, 242)', + 'layer': 'below', + 'line': {'width': 0}, + 'x0': -1, + 'x1': 3, + 'xref': 'x15', + 'y0': 1.9451953323436744, + 'y1': 11.054804667656326, + 'yref': 'y15'}], + 'showlegend': False, + 'title': 'this is a test', + 'xaxis1': {'anchor': 'y1', + 'domain': [0.0, 0.06666666666666667], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis10': {'anchor': 'y10', + 'domain': [0.6666666666666667, 1.0], + 'fixedrange': True, + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis11': {'anchor': 'y11', + 'domain': [0.0, 0.06666666666666667], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis12': {'anchor': 'y12', + 'domain': [0.06666666666666667, 0.2], + 'fixedrange': True, + 'range': [0, 12.165], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis13': {'anchor': 'y13', + 'domain': [0.2, 0.4], + 'fixedrange': True, + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis14': {'anchor': 'y14', + 'domain': [0.4, 0.6666666666666667], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis15': {'anchor': 'y15', + 'domain': [0.6666666666666667, 1.0], + 'fixedrange': True, + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis2': {'anchor': 'y2', + 'domain': [0.06666666666666667, 0.2], + 'fixedrange': True, + 'range': [0, 8.5], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis3': {'anchor': 'y3', + 'domain': [0.2, 0.4], + 'fixedrange': True, + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis4': {'anchor': 'y4', + 'domain': [0.4, 0.6666666666666667], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis5': {'anchor': 'y5', + 'domain': [0.6666666666666667, 1.0], + 'fixedrange': True, + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis6': {'anchor': 'y6', + 'domain': [0.0, 0.06666666666666667], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis7': {'anchor': 'y7', + 'domain': [0.06666666666666667, 0.2], + 'fixedrange': True, + 'range': [0, 9.335], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis8': {'anchor': 'y8', + 'domain': [0.2, 0.4], + 'fixedrange': True, + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis9': {'anchor': 'y9', + 'domain': [0.4, 0.6666666666666667], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis1': {'anchor': 'x1', + 'domain': [0.6666666666666666, 1.0], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis10': {'anchor': 'x10', + 'domain': [0.3333333333333333, 0.6666666666666666], + 'fixedrange': True, + 'range': [-0.054804667656325634, 9.0548046676563256], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis11': {'anchor': 'x11', + 'domain': [0.0, 0.3333333333333333], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis12': {'anchor': 'x12', + 'domain': [0.0, 0.3333333333333333], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis13': {'anchor': 'x13', + 'domain': [0.0, 0.3333333333333333], + 'fixedrange': True, + 'range': [1.9451953323436744, 11.054804667656326], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis14': {'anchor': 'x14', + 'domain': [0.0, 0.3333333333333333], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis15': {'anchor': 'x15', + 'domain': [0.0, 0.3333333333333333], + 'fixedrange': True, + 'range': [1.9451953323436744, 11.054804667656326], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis2': {'anchor': 'x2', + 'domain': [0.6666666666666666, 1.0], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis3': {'anchor': 'x3', + 'domain': [0.6666666666666666, 1.0], + 'fixedrange': True, + 'range': [3.1835034190722737, 6.8164965809277263], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis4': {'anchor': 'x4', + 'domain': [0.6666666666666666, 1.0], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis5': {'anchor': 'x5', + 'domain': [0.6666666666666666, 1.0], + 'fixedrange': True, + 'range': [3.1835034190722737, 6.8164965809277263], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis6': {'anchor': 'x6', + 'domain': [0.3333333333333333, 0.6666666666666666], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis7': {'anchor': 'x7', + 'domain': [0.3333333333333333, 0.6666666666666666], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis8': {'anchor': 'x8', + 'domain': [0.3333333333333333, 0.6666666666666666], + 'fixedrange': True, + 'range': [-0.054804667656325634, 9.0548046676563256], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis9': {'anchor': 'x9', + 'domain': [0.3333333333333333, 0.6666666666666666], + 'fixedrange': True, + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}} + } + + self.assertEqual(fig['data'], exp_fig['data']) + self.assertEqual(fig['layout'], exp_fig['layout']) + class TestChoropleth(NumpyTestUtilsMixin, TestCase): # run tests if required packages are installed