From ff363877e78aebff0c6a07fa0e3dc3a850cf2186 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Mon, 6 Nov 2017 12:23:40 -0500 Subject: [PATCH 01/22] added annotation dict func from facet_grid to utils --- plotly/figure_factory/__init__.py | 1 + plotly/figure_factory/utils.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/plotly/figure_factory/__init__.py b/plotly/figure_factory/__init__.py index 4bdc3ad0e0e..5aa4e97e281 100644 --- a/plotly/figure_factory/__init__.py +++ b/plotly/figure_factory/__init__.py @@ -5,6 +5,7 @@ from plotly.figure_factory._2d_density import create_2d_density from plotly.figure_factory._annotated_heatmap import create_annotated_heatmap +from plotly.figure_factory._bullet import create_bullet from plotly.figure_factory._candlestick import create_candlestick from plotly.figure_factory._dendrogram import create_dendrogram from plotly.figure_factory._distplot import create_distplot diff --git a/plotly/figure_factory/utils.py b/plotly/figure_factory/utils.py index baffeeb68a4..7bd23dc2df8 100644 --- a/plotly/figure_factory/utils.py +++ b/plotly/figure_factory/utils.py @@ -488,3 +488,70 @@ def endpts_to_intervals(endpts): # add +inf to intervals intervals.append([endpts[length - 1], float('inf')]) return intervals + + +def annotation_dict_for_label(text, lane, num_of_lanes, subplot_spacing, + row_col='col', flipped=True, right_side=True, + text_color='#0f0f0f'): + """ + Returns annotation dict for label of n labels of a 1xn or nx1 subplot. + + :param (str) text: the text for a label. + :param (int) lane: the label number for text. From 1 to n inclusive. + :param (int) num_of_lanes: the number 'n' of rows or columns in subplot. + :param (float) subplot_spacing: the value for the horizontal_spacing and + vertical_spacing params in your plotly.tools.make_subplots() call. + :param (str) row_col: choose whether labels are placed along rows or + columns. + :param (bool) flipped: flips text by 90 degrees. Text is printed + horizontally if set to True and row_col='row', or if False and + 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) + if not flipped: + xanchor = 'center' + yanchor = 'middle' + if row_col == 'col': + x = (lane - 1) * (l + subplot_spacing) + 0.5 * l + y = 1.03 + textangle = 0 + elif row_col == 'row': + y = (lane - 1) * (l + subplot_spacing) + 0.5 * l + x = 1.03 + textangle = 90 + else: + if row_col == 'col': + xanchor = 'center' + yanchor = 'bottom' + x = (lane - 1) * (l + subplot_spacing) + 0.5 * l + y = 1.0 + textangle = 270 + elif row_col == 'row': + yanchor = 'middle' + y = (lane - 1) * (l + subplot_spacing) + 0.5 * l + if right_side: + x = 1.0 + xanchor = 'left' + else: + x = -0.01 + xanchor = 'right' + textangle = 0 + + annotation_dict = dict( + textangle=textangle, + xanchor=xanchor, + yanchor=yanchor, + x=x, + y=y, + showarrow=False, + xref='paper', + yref='paper', + text=text, + font=dict( + size=13, + color=axis_title_color + ) + ) + return annotation_dict From 5b9ddaaf1a118fdb69bc11c04cbad726e503246e Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Mon, 6 Nov 2017 12:24:21 -0500 Subject: [PATCH 02/22] add bullet ff --- plotly/figure_factory/_bullet.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 plotly/figure_factory/_bullet.py diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py new file mode 100644 index 00000000000..f6872036df0 --- /dev/null +++ b/plotly/figure_factory/_bullet.py @@ -0,0 +1,29 @@ +from __future__ import absolute_import + +from plotly import colors, exceptions, optional_imports +from plotly.figure_factory import utils +from plotly.graph_objs import graph_objs + +import math +import copy +from numbers import Number + +pd = optional_imports.get_module('pandas') + + +def create_bullet(df): + """ + Returns figure for bullet chart. + + :param (pd.DataFrame) df: + + """ + if not pd: + raise exceptions.ImportError( + "'pandas' must be imported for this figure_factory." + ) + + if not isinstance(df, pd.DataFrame): + raise exceptions.PlotlyError( + "You must input a pandas DataFrame." + ) From 939a8bafa3aaddb6ab85433f1a937bd407dc3f0c Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Mon, 6 Nov 2017 12:24:33 -0500 Subject: [PATCH 03/22] fix text_color name --- plotly/figure_factory/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/figure_factory/utils.py b/plotly/figure_factory/utils.py index 7bd23dc2df8..638e4997251 100644 --- a/plotly/figure_factory/utils.py +++ b/plotly/figure_factory/utils.py @@ -551,7 +551,7 @@ def annotation_dict_for_label(text, lane, num_of_lanes, subplot_spacing, text=text, font=dict( size=13, - color=axis_title_color + color=text_color ) ) return annotation_dict From 5796a9e7a662e431e7e188640740627add98a800 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Mon, 6 Nov 2017 15:33:24 -0500 Subject: [PATCH 04/22] added list_options to utils --- plotly/figure_factory/_bullet.py | 188 +++++++++++++++++++++++++++++-- plotly/figure_factory/utils.py | 15 +++ 2 files changed, 194 insertions(+), 9 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index f6872036df0..b3bbd8e8454 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -1,29 +1,199 @@ from __future__ import absolute_import -from plotly import colors, exceptions, optional_imports +from plotly import exceptions, optional_imports from plotly.figure_factory import utils -from plotly.graph_objs import graph_objs -import math -import copy +import plotly +import plotly.graph_objs as go + from numbers import Number +import pandas as pd pd = optional_imports.get_module('pandas') +DARK_BLUE = 'rgb(31, 119, 180)' +LIGHT_BLUE = 'rgb(176, 196, 221)' +BAD_COLOR ='rgb(204, 204, 204)' +OK_COLOR = 'rgb(221, 221, 221)' +GOOD_COLOR = 'rgb(238, 238, 238)' +SUBPLOT_SPACING = 0.015 +VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] + +# add shapes +def shape(color, x0, x1, y0, y1, xref, yref, layer): + return { + 'fillcolor': color, + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': x0, + 'x1': x1, + 'xref': xref, + 'y0': y0, + 'y1': y1, + 'yref': yref, + 'layer': layer + } + + +def _bullet_rows(df, marker_symbol): + num_of_rows = len(df.columns) + fig = plotly.tools.make_subplots( + num_of_rows, 1, print_grid=False, horizontal_spacing=SUBPLOT_SPACING, + vertical_spacing=SUBPLOT_SPACING + ) + + fig['layout'].update( + dict(shapes=[]), + showlegend=False, + annotations=[], + margin=dict(l=120), + ) + + # add marker symbol + if num_of_rows <= 3: + marker_size = 14 + else: + marker_size = 10 + # marker + for index in range(num_of_rows): + fig['data'].append( + go.Scatter( + x=df.iloc[index]['markers'], + y=[0.5], + marker=dict( + size=marker_size, + color='rgb(0, 0, 0)', + symbol=marker_symbol + ), + xaxis='x{}'.format(index + 1), + yaxis='y{}'.format(index + 1) + ) + ) + + for key in fig['layout'].keys(): + if 'xaxis' in key: + fig['layout'][key]['showgrid'] = False + fig['layout'][key]['zeroline'] = False + fig['layout'][key]['tickwidth'] = 1 + elif 'yaxis' in key: + fig['layout'][key]['showgrid'] = False + fig['layout'][key]['zeroline'] = False + fig['layout'][key]['showticklabels'] = False + fig['layout'][key]['range'] = [0, 1] + + # ranges + y0 = 0.35 + y1 = 0.65 + for axis_num in range(num_of_rows): + fig['layout']['shapes'].append( + shape( + BAD_COLOR, 0, df.iloc[axis_num]['ranges'][0], y0, y1, + 'x{}'.format(axis_num + 1), + 'y{}'.format(axis_num + 1), + 'below' + ) + ) + fig['layout']['shapes'].append( + shape( + OK_COLOR, df.iloc[axis_num]['ranges'][0], + df.iloc[axis_num]['ranges'][1], y0, y1, + 'x{}'.format(axis_num + 1), + 'y{}'.format(axis_num + 1), + 'below' + ) + ) + fig['layout']['shapes'].append( + shape( + GOOD_COLOR, df.iloc[axis_num]['ranges'][1], + df.iloc[axis_num]['ranges'][2], y0, y1, + 'x{}'.format(axis_num + 1), + 'y{}'.format(axis_num + 1), + 'below' + ) + ) + + # measures + y0 = 0.45 + y1 = 0.55 + for axis_num in range(num_of_rows): + darkblue_len = df.iloc[axis_num]['measures'][0] + lightblue_len = df.iloc[axis_num]['measures'][1] + fig['layout']['shapes'].append( + shape( + DARK_BLUE, 0, darkblue_len, y0, y1, + 'x{}'.format(axis_num + 1), + 'y{}'.format(axis_num + 1), + 'below' + ) + ) + fig['layout']['shapes'].append( + shape( + LIGHT_BLUE, darkblue_len, lightblue_len, y0, y1, + 'x{}'.format(axis_num + 1), + 'y{}'.format(axis_num + 1), + 'below' + ) + ) -def create_bullet(df): + # labels + fig['layout']['annotations'] = [] + for k in range(num_of_rows): + title = df.iloc[k]['title'] + subtitle = df.iloc[k]['subtitle'] + + label = '{}
{}'.format(title, subtitle) + annot = utils.annotation_dict_for_label( + label, k + 1, num_of_rows, SUBPLOT_SPACING, 'row', True, False + ) + fig['layout']['annotations'].append(annot) + return fig + +def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', + title='Bullet Chart', height=600, width=1000): """ Returns figure for bullet chart. - :param (pd.DataFrame) df: - + :param (pd.DataFrame | list) df: either a JSON list of dicts or a pandas + DataFrame. Must contain the keys 'title', 'subtitle', 'ranges', + 'measures', and 'markers'. + :param (str) title: title of the bullet chart. """ + # validate df if not pd: raise exceptions.ImportError( "'pandas' must be imported for this figure_factory." ) - if not isinstance(df, pd.DataFrame): + if isinstance(df, list): + if not all(isinstance(item, dict) for item in df): + raise exceptions.PlotlyError( + "If your data is a list, all entries must be dictionaries." + ) + df = pd.DataFrame(df) + + elif not isinstance(df, pd.DataFrame): raise exceptions.PlotlyError( - "You must input a pandas DataFrame." + "You must input a pandas DataFrame or a list of dictionaries." ) + + # check for all keys + #for df_keys in list(df.columns): + if any(key not in VALID_KEYS for key in df.columns): + raise exceptions.PlotlyError( + "The valid keys you need are" + ) + #utils.list_of_options + + if as_rows: + fig = _bullet_rows(df, marker_symbol) + else: + fig = _bullet_cols() + + fig['layout'].update( + title=title, + height=height, + width=width, + ) + + return fig diff --git a/plotly/figure_factory/utils.py b/plotly/figure_factory/utils.py index 638e4997251..f6d4778ab64 100644 --- a/plotly/figure_factory/utils.py +++ b/plotly/figure_factory/utils.py @@ -555,3 +555,18 @@ def annotation_dict_for_label(text, lane, num_of_lanes, subplot_spacing, ) ) return annotation_dict + + +def list_of_options(iterable, conj='and', period=True): + """ + Returns an English listing of objects seperated by commas ',' + + For example, ['foo', 'bar', 'baz'] becomes 'foo, bar and baz' + if the conjunction 'and' is selected. + """ + if len(iterable) < 2: + raise exceptions.PlotlyError( + 'Your list or tuple must contain at least 2 items.' + ) + template = (len(iterable) - 2)*'{}, ' + '{} ' + conj + ' {}' + period*'.' + return template.format(*iterable) From bb15f6af388bd8484cb2a3cb9ee46150ea312a7e Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Mon, 6 Nov 2017 17:12:08 -0500 Subject: [PATCH 05/22] working except variable measures --- plotly/figure_factory/_bullet.py | 141 +++++++++++++++---------------- 1 file changed, 67 insertions(+), 74 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index b3bbd8e8454..d6a131c0421 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -20,7 +20,7 @@ VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] # add shapes -def shape(color, x0, x1, y0, y1, xref, yref, layer): +def rectangle(color, x0, x1, y0, y1, xref, yref, layer): return { 'fillcolor': color, 'line': {'width': 0}, @@ -37,12 +37,13 @@ def shape(color, x0, x1, y0, y1, xref, yref, layer): def _bullet_rows(df, marker_symbol): - num_of_rows = len(df.columns) + num_of_rows = len(df) fig = plotly.tools.make_subplots( num_of_rows, 1, print_grid=False, horizontal_spacing=SUBPLOT_SPACING, vertical_spacing=SUBPLOT_SPACING ) + # layout fig['layout'].update( dict(shapes=[]), showlegend=False, @@ -50,105 +51,97 @@ def _bullet_rows(df, marker_symbol): margin=dict(l=120), ) - # add marker symbol + for key in fig['layout'].keys(): + if 'axis' in key: + fig['layout'][key]['showgrid'] = False + fig['layout'][key]['zeroline'] = False + if 'xaxis' in key: + fig['layout'][key]['tickwidth'] = 1 + if 'yaxis' in key: + fig['layout'][key]['showticklabels'] = False + fig['layout'][key]['range'] = [0, 1] + + # marker symbol size if num_of_rows <= 3: marker_size = 14 else: marker_size = 10 - # marker - for index in range(num_of_rows): + for idx in range(num_of_rows): + # marker fig['data'].append( go.Scatter( - x=df.iloc[index]['markers'], + x=df.iloc[idx]['markers'], y=[0.5], marker=dict( size=marker_size, color='rgb(0, 0, 0)', symbol=marker_symbol ), - xaxis='x{}'.format(index + 1), - yaxis='y{}'.format(index + 1) + xaxis='x{}'.format(idx + 1), + yaxis='y{}'.format(idx + 1) ) ) - for key in fig['layout'].keys(): - if 'xaxis' in key: - fig['layout'][key]['showgrid'] = False - fig['layout'][key]['zeroline'] = False - fig['layout'][key]['tickwidth'] = 1 - elif 'yaxis' in key: - fig['layout'][key]['showgrid'] = False - fig['layout'][key]['zeroline'] = False - fig['layout'][key]['showticklabels'] = False - fig['layout'][key]['range'] = [0, 1] - - # ranges - y0 = 0.35 - y1 = 0.65 - for axis_num in range(num_of_rows): - fig['layout']['shapes'].append( - shape( - BAD_COLOR, 0, df.iloc[axis_num]['ranges'][0], y0, y1, - 'x{}'.format(axis_num + 1), - 'y{}'.format(axis_num + 1), - 'below' - ) - ) - fig['layout']['shapes'].append( - shape( - OK_COLOR, df.iloc[axis_num]['ranges'][0], - df.iloc[axis_num]['ranges'][1], y0, y1, - 'x{}'.format(axis_num + 1), - 'y{}'.format(axis_num + 1), - 'below' - ) - ) - fig['layout']['shapes'].append( - shape( - GOOD_COLOR, df.iloc[axis_num]['ranges'][1], - df.iloc[axis_num]['ranges'][2], y0, y1, - 'x{}'.format(axis_num + 1), - 'y{}'.format(axis_num + 1), - 'below' + # ranges + y0_ranges = 0.35 + y1_ranges = 0.65 + ranges_len = len(df.iloc[idx]['ranges']) + color_incr = 36.0 / max(1, ranges_len - 1) + for range_idx in range(ranges_len): + rgb_val = 198 + range_idx * color_incr + grey_color = 'rgb(' + 3 * '{},'.format(rgb_val) + ')' + if range_idx == 0: + start_range = 0 + else: + start_range = df.iloc[idx]['ranges'][range_idx - 1] + end_range = df.iloc[idx]['ranges'][range_idx] + fig['layout']['shapes'].append( + rectangle( + grey_color, start_range, end_range, y0_ranges, y1_ranges, + 'x{}'.format(idx + 1), + 'y{}'.format(idx + 1), + 'below' + ) ) - ) - # measures - y0 = 0.45 - y1 = 0.55 - for axis_num in range(num_of_rows): - darkblue_len = df.iloc[axis_num]['measures'][0] - lightblue_len = df.iloc[axis_num]['measures'][1] + # measures + y0_measures = 0.45 + y1_measures = 0.55 + darkblue_len = df.iloc[idx]['measures'][0] + lightblue_len = df.iloc[idx]['measures'][1] fig['layout']['shapes'].append( - shape( - DARK_BLUE, 0, darkblue_len, y0, y1, - 'x{}'.format(axis_num + 1), - 'y{}'.format(axis_num + 1), + rectangle( + DARK_BLUE, 0, darkblue_len, y0_measures, y1_measures, + 'x{}'.format(idx + 1), + 'y{}'.format(idx + 1), 'below' ) ) fig['layout']['shapes'].append( - shape( - LIGHT_BLUE, darkblue_len, lightblue_len, y0, y1, - 'x{}'.format(axis_num + 1), - 'y{}'.format(axis_num + 1), + rectangle( + LIGHT_BLUE, darkblue_len, lightblue_len, + y0_measures, y1_measures, + 'x{}'.format(idx + 1), + 'y{}'.format(idx + 1), 'below' ) ) - # labels - fig['layout']['annotations'] = [] - for k in range(num_of_rows): - title = df.iloc[k]['title'] - subtitle = df.iloc[k]['subtitle'] - - label = '{}
{}'.format(title, subtitle) + # labels + title = df.iloc[idx]['title'] + if 'subtitle' in df: + subtitle = '
{}'.format(df.iloc[idx]['subtitle']) + else: + subtitle = '' + label = '{}'.format(title) + subtitle annot = utils.annotation_dict_for_label( - label, k + 1, num_of_rows, SUBPLOT_SPACING, 'row', True, False + label, num_of_rows - idx, num_of_rows, SUBPLOT_SPACING, + 'row', True, False ) fig['layout']['annotations'].append(annot) return fig + def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', title='Bullet Chart', height=600, width=1000): """ @@ -177,13 +170,13 @@ def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', "You must input a pandas DataFrame or a list of dictionaries." ) - # check for all keys - #for df_keys in list(df.columns): + # check for valid keys if any(key not in VALID_KEYS for key in df.columns): raise exceptions.PlotlyError( - "The valid keys you need are" + "Your headers/dict keys must be either {}".format( + utils.list_of_options(VALID_KEYS, 'or') + ) ) - #utils.list_of_options if as_rows: fig = _bullet_rows(df, marker_symbol) From 8a5ea2b6f9cb2b3440977cb78da4f41a681e9c46 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Mon, 6 Nov 2017 17:46:28 -0500 Subject: [PATCH 06/22] enhance intermediate color function in colors.py --- plotly/colors.py | 26 +++++++++++++++++++++----- plotly/figure_factory/_bullet.py | 27 ++++++++++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/plotly/colors.py b/plotly/colors.py index a03128e0121..f4afe8281c2 100644 --- a/plotly/colors.py +++ b/plotly/colors.py @@ -442,21 +442,37 @@ def make_colorscale(colors, scale=None): return colorscale -def find_intermediate_color(lowcolor, highcolor, intermed): +def find_intermediate_color(lowcolor, highcolor, intermed, colortype='tuple'): """ Returns the color at a given distance between two colors This function takes two color tuples, where each element is between 0 and 1, along with a value 0 < intermed < 1 and returns a color that is - intermed-percent from lowcolor to highcolor + intermed-percent from lowcolor to highcolor. If colortype is set to 'rgb', + the function will automatically convert the rgb type to a tuple, find the + intermediate color and return it as an rgb color. """ + if colortype == 'rgb': + # convert to tuple + lowcolor = unlabel_rgb(lowcolor) + highcolor = unlabel_rgb(highcolor) + diff_0 = float(highcolor[0] - lowcolor[0]) diff_1 = float(highcolor[1] - lowcolor[1]) diff_2 = float(highcolor[2] - lowcolor[2]) - return (lowcolor[0] + intermed * diff_0, - lowcolor[1] + intermed * diff_1, - lowcolor[2] + intermed * diff_2) + inter_med_tuple = ( + lowcolor[0] + intermed * diff_0, + lowcolor[1] + intermed * diff_1, + lowcolor[2] + intermed * diff_2 + ) + + if colortype == 'rgb': + # covert back to rgb + inter_med_rgb = label_rgb(inter_med_tuple) + return inter_med_rgb + + return inter_med_tuple def unconvert_from_RGB_255(colors): diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index d6a131c0421..af11f9fb5f3 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -from plotly import exceptions, optional_imports +from plotly import colors, exceptions, optional_imports from plotly.figure_factory import utils import plotly @@ -36,7 +36,7 @@ def rectangle(color, x0, x1, y0, y1, xref, yref, layer): } -def _bullet_rows(df, marker_symbol): +def _bullet_rows(df, marker_symbol, range_colors, measure_colors): num_of_rows = len(df) fig = plotly.tools.make_subplots( num_of_rows, 1, print_grid=False, horizontal_spacing=SUBPLOT_SPACING, @@ -85,6 +85,8 @@ def _bullet_rows(df, marker_symbol): # ranges y0_ranges = 0.35 y1_ranges = 0.65 + if not range_colors: + range_colors = ['rgb(200,200,200)', 'rgb(245,245,245)'] ranges_len = len(df.iloc[idx]['ranges']) color_incr = 36.0 / max(1, ranges_len - 1) for range_idx in range(ranges_len): @@ -143,6 +145,7 @@ def _bullet_rows(df, marker_symbol): def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', + range_colors=None, measure_colors=None, title='Bullet Chart', height=600, width=1000): """ Returns figure for bullet chart. @@ -150,6 +153,11 @@ def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', :param (pd.DataFrame | list) df: either a JSON list of dicts or a pandas DataFrame. Must contain the keys 'title', 'subtitle', 'ranges', 'measures', and 'markers'. + :param (list) range_colors: a list of two colors between which all + the range rectangles are drawn. These rectangles are meant to be + qualitative indicators against which the marker and measre bars are + compared. + Default=['rgb(198, 198, 198)', 'rgb(248, 248, 248)'] :param (str) title: title of the bullet chart. """ # validate df @@ -178,8 +186,21 @@ def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', ) ) + # validate custom colors + for colors_list in [range_colors, measure_colors]: + if colors_list: + if len(colors_list) != 2: + raise exceptions.PlotlyError( + "Both 'range_colors' or 'measure_colors' must be a list " + "of two valid colors." + ) + colors.validate_colors(colors_list) + colors_list = colors.convert_colors_to_same_type( + colors_list, 'rgb' + )[0] + if as_rows: - fig = _bullet_rows(df, marker_symbol) + fig = _bullet_rows(df, marker_symbol, range_colors, measure_colors) else: fig = _bullet_cols() From 4599bc2e9d4a3fe9fe6f059a8c67d3986bdad619 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Tue, 7 Nov 2017 14:27:20 -0500 Subject: [PATCH 07/22] working ff --- plotly/colors.py | 14 ++- plotly/figure_factory/_bullet.py | 169 +++++++++++++++++++++---------- 2 files changed, 126 insertions(+), 57 deletions(-) diff --git a/plotly/colors.py b/plotly/colors.py index f4afe8281c2..776fa7ae4c1 100644 --- a/plotly/colors.py +++ b/plotly/colors.py @@ -514,14 +514,20 @@ def convert_to_RGB_255(colors): return (rgb_components[0], rgb_components[1], rgb_components[2]) -def n_colors(lowcolor, highcolor, n_colors): +def n_colors(lowcolor, highcolor, n_colors, colortype='tuple'): """ Splits a low and high color into a list of n_colors colors in it Accepts two color tuples and returns a list of n_colors colors which form the intermediate colors between lowcolor and highcolor - from linearly interpolating through RGB space + from linearly interpolating through RGB space. If colortype is 'rgb' + the function will return a list of colors in the same form. """ + if colortype == 'rgb': + # convert to tuple + lowcolor = unlabel_rgb(lowcolor) + highcolor = unlabel_rgb(highcolor) + diff_0 = float(highcolor[0] - lowcolor[0]) incr_0 = diff_0/(n_colors - 1) diff_1 = float(highcolor[1] - lowcolor[1]) @@ -536,6 +542,10 @@ def n_colors(lowcolor, highcolor, n_colors): lowcolor[2] + (index * incr_2)) color_tuples.append(new_tuple) + if colortype == 'rgb': + # convert back to rgb + color_tuples = color_parser(color_tuples, label_rgb) + return color_tuples diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index af11f9fb5f3..aa696fa574c 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -1,24 +1,19 @@ from __future__ import absolute_import +import math + from plotly import colors, exceptions, optional_imports from plotly.figure_factory import utils import plotly import plotly.graph_objs as go -from numbers import Number -import pandas as pd - pd = optional_imports.get_module('pandas') -DARK_BLUE = 'rgb(31, 119, 180)' -LIGHT_BLUE = 'rgb(176, 196, 221)' -BAD_COLOR ='rgb(204, 204, 204)' -OK_COLOR = 'rgb(221, 221, 221)' -GOOD_COLOR = 'rgb(238, 238, 238)' SUBPLOT_SPACING = 0.015 VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] + # add shapes def rectangle(color, x0, x1, y0, y1, xref, yref, layer): return { @@ -36,10 +31,14 @@ def rectangle(color, x0, x1, y0, y1, xref, yref, layer): } -def _bullet_rows(df, marker_symbol, range_colors, measure_colors): - num_of_rows = len(df) +def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, + measure_colors): + num_of_lanes = len(df) + num_of_rows = num_of_lanes if as_rows else 1 + num_of_cols = 1 if as_rows else num_of_lanes fig = plotly.tools.make_subplots( - num_of_rows, 1, print_grid=False, horizontal_spacing=SUBPLOT_SPACING, + num_of_rows, num_of_cols, print_grid=False, + horizontal_spacing=SUBPLOT_SPACING, vertical_spacing=SUBPLOT_SPACING ) @@ -48,30 +47,40 @@ def _bullet_rows(df, marker_symbol, range_colors, measure_colors): dict(shapes=[]), showlegend=False, annotations=[], - margin=dict(l=120), + margin=dict(l=120 if as_rows else 80), ) + if as_rows: + length_axis = 'xaxis' + width_axis = 'yaxis' + else: + width_axis = 'xaxis' + length_axis = 'yaxis' + for key in fig['layout'].keys(): if 'axis' in key: fig['layout'][key]['showgrid'] = False fig['layout'][key]['zeroline'] = False - if 'xaxis' in key: + if length_axis in key: fig['layout'][key]['tickwidth'] = 1 - if 'yaxis' in key: + if width_axis in key: fig['layout'][key]['showticklabels'] = False fig['layout'][key]['range'] = [0, 1] # marker symbol size - if num_of_rows <= 3: - marker_size = 14 - else: - marker_size = 10 - for idx in range(num_of_rows): + if not marker_size: + if num_of_lanes <= 3: + marker_size = 18 + else: + marker_size = 12 + for idx in range(num_of_lanes): # marker + x = df.iloc[idx]['markers'] if as_rows else [0.5] + y = [0.5] if as_rows else df.iloc[idx]['markers'] fig['data'].append( go.Scatter( - x=df.iloc[idx]['markers'], - y=[0.5], + x=x, + y=y, marker=dict( size=marker_size, color='rgb(0, 0, 0)', @@ -86,20 +95,29 @@ def _bullet_rows(df, marker_symbol, range_colors, measure_colors): y0_ranges = 0.35 y1_ranges = 0.65 if not range_colors: - range_colors = ['rgb(200,200,200)', 'rgb(245,245,245)'] + range_colors = ['rgb(200, 200, 200)', 'rgb(245, 245, 245)'] ranges_len = len(df.iloc[idx]['ranges']) - color_incr = 36.0 / max(1, ranges_len - 1) + if ranges_len <= 1: + inter_colors = [range_colors[0]] + else: + inter_colors = colors.n_colors( + range_colors[0], range_colors[1], ranges_len, 'rgb' + ) for range_idx in range(ranges_len): - rgb_val = 198 + range_idx * color_incr - grey_color = 'rgb(' + 3 * '{},'.format(rgb_val) + ')' + color = inter_colors[range_idx] if range_idx == 0: start_range = 0 else: start_range = df.iloc[idx]['ranges'][range_idx - 1] end_range = df.iloc[idx]['ranges'][range_idx] + + x0 = start_range if as_rows else y0_ranges + x1 = end_range if as_rows else y1_ranges + y0 = y0_ranges if as_rows else start_range + y1 = y1_ranges if as_rows else end_range fig['layout']['shapes'].append( rectangle( - grey_color, start_range, end_range, y0_ranges, y1_ranges, + color, x0, x1, y0, y1, 'x{}'.format(idx + 1), 'y{}'.format(idx + 1), 'below' @@ -109,25 +127,35 @@ def _bullet_rows(df, marker_symbol, range_colors, measure_colors): # measures y0_measures = 0.45 y1_measures = 0.55 - darkblue_len = df.iloc[idx]['measures'][0] - lightblue_len = df.iloc[idx]['measures'][1] - fig['layout']['shapes'].append( - rectangle( - DARK_BLUE, 0, darkblue_len, y0_measures, y1_measures, - 'x{}'.format(idx + 1), - 'y{}'.format(idx + 1), - 'below' + if not measure_colors: + measure_colors = ['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] + measures_len = len(df.iloc[idx]['measures']) + if measures_len <= 1: + inter_colors = [measure_colors[0]] + else: + inter_colors = colors.n_colors( + measure_colors[0], measure_colors[1], measures_len, 'rgb' ) - ) - fig['layout']['shapes'].append( + for range_idx in range(measures_len): + color = inter_colors[range_idx] + if range_idx == 0: + start_range = 0 + else: + start_range = df.iloc[idx]['measures'][range_idx - 1] + end_range = df.iloc[idx]['measures'][range_idx] + + x0 = start_range if as_rows else y0_measures + x1 = end_range if as_rows else y1_measures + y0 = y0_measures if as_rows else start_range + y1 = y1_measures if as_rows else end_range + fig['layout']['shapes'].append( rectangle( - LIGHT_BLUE, darkblue_len, lightblue_len, - y0_measures, y1_measures, + color, x0, x1, y0, y1, 'x{}'.format(idx + 1), 'y{}'.format(idx + 1), 'below' ) - ) + ) # labels title = df.iloc[idx]['title'] @@ -137,28 +165,42 @@ def _bullet_rows(df, marker_symbol, range_colors, measure_colors): subtitle = '' label = '{}'.format(title) + subtitle annot = utils.annotation_dict_for_label( - label, num_of_rows - idx, num_of_rows, SUBPLOT_SPACING, - 'row', True, False + label, num_of_lanes - idx, num_of_lanes, SUBPLOT_SPACING, + 'row' if as_rows else 'col', + True if as_rows else False, + False ) fig['layout']['annotations'].append(annot) + return fig -def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', - range_colors=None, measure_colors=None, - title='Bullet Chart', height=600, width=1000): +def create_bullet(df, as_rows=True, marker_size=16, + marker_symbol='diamond-tall', range_colors=None, + measure_colors=None, title='Bullet Chart', height=600, + width=1000): """ Returns figure for bullet chart. :param (pd.DataFrame | list) df: either a JSON list of dicts or a pandas - DataFrame. Must contain the keys 'title', 'subtitle', 'ranges', + DataFrame. All keys must be one of 'title', 'subtitle', 'ranges', 'measures', and 'markers'. + :param (bool) as_rows: if True, the bars are placed horizontally as rows. + If False, the bars are placed vertically in the chart. + :param (int) marker_size: sets the size of the markers in the chart. + :param (str | int) marker_symbol: the symbol of the markers in the chart. + Default='diamond-tall' :param (list) range_colors: a list of two colors between which all - the range rectangles are drawn. These rectangles are meant to be - qualitative indicators against which the marker and measre bars are - compared. + the rectangles for the range are drawn. These rectangles are meant to + be qualitative indicators against which the marker and measure bars + are compared. Default=['rgb(198, 198, 198)', 'rgb(248, 248, 248)'] + :param (list) measure_colors: a list of two colors which is used to color + the thin quantitative bars in the bullet chart. + Default=['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] :param (str) title: title of the bullet chart. + :param (float) height: height of the chart. + :param (float) width width of the chart. """ # validate df if not pd: @@ -186,6 +228,25 @@ def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', ) ) + # add necessary columns if missing + for key in VALID_KEYS: + if key not in df: + if key in ['title', 'subtitle']: + element = '' + else: + element = [] + df[key] = [element for _ in range(len(df))] + + # make sure ranges and measures are not NAN or NONE + for needed_key in ['ranges', 'measures']: + for idx, r in enumerate(df[needed_key]): + try: + r_is_nan = math.isnan(r) + if r_is_nan or r is None: + df[needed_key][idx] = [] + except TypeError: + pass + # validate custom colors for colors_list in [range_colors, measure_colors]: if colors_list: @@ -195,14 +256,12 @@ def create_bullet(df, as_rows=True, marker_symbol='diamond-tall', "of two valid colors." ) colors.validate_colors(colors_list) - colors_list = colors.convert_colors_to_same_type( - colors_list, 'rgb' - )[0] + colors_list = colors.convert_colors_to_same_type(colors_list, + 'rgb')[0] - if as_rows: - fig = _bullet_rows(df, marker_symbol, range_colors, measure_colors) - else: - fig = _bullet_cols() + fig = _bullet( + df, as_rows, marker_size, marker_symbol, range_colors, measure_colors + ) fig['layout'].update( title=title, From beb6d189968fa0e2f6e29e16482b7d5854d96bc9 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Tue, 7 Nov 2017 15:22:27 -0500 Subject: [PATCH 08/22] optional tests --- plotly/figure_factory/_bullet.py | 6 +-- .../test_optional/test_figure_factory.py | 50 ++++++++++++++++++- 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index aa696fa574c..892eaa92a42 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -211,19 +211,19 @@ def create_bullet(df, as_rows=True, marker_size=16, if isinstance(df, list): if not all(isinstance(item, dict) for item in df): raise exceptions.PlotlyError( - "If your data is a list, all entries must be dictionaries." + 'If your data is a list, all entries must be dictionaries.' ) df = pd.DataFrame(df) elif not isinstance(df, pd.DataFrame): raise exceptions.PlotlyError( - "You must input a pandas DataFrame or a list of dictionaries." + 'You must input a pandas DataFrame or a list of dictionaries.' ) # check for valid keys if any(key not in VALID_KEYS for key in df.columns): raise exceptions.PlotlyError( - "Your headers/dict keys must be either {}".format( + 'Your headers/dict keys must be either {}'.format( utils.list_of_options(VALID_KEYS, 'or') ) ) diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index 1a12ba8357b..824621c05fa 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -4,6 +4,7 @@ import plotly.tools as tls import plotly.figure_factory as ff +import plotly.figure_factory.utils as utils from plotly.tests.test_optional.optional_utils import NumpyTestUtilsMixin import math from nose.tools import raises @@ -1979,7 +1980,6 @@ def test_x_and_y_for_scatter(self): ff.create_facet_grid, data, 'a') - def test_valid_col_selection(self): data = pd.DataFrame([[0, 0], [1, 1]], columns=['a', 'b']) @@ -2041,7 +2041,6 @@ def test_valid_color_dict(self): data, 'a', 'b', color_name='a', colormap=color_dict) - def test_valid_colorscale_name(self): data = pd.DataFrame([[0, 1, 2], [3, 4, 5]], columns=['a', 'b', 'c']) @@ -2184,3 +2183,50 @@ def test_valid_facet_grid_fig(self): # layout self.assert_dict_equal(test_facet_grid['layout'], exp_facet_grid['layout']) + + +class TestBullet(NumpyTestUtilsMixin, TestCase): + + def test_df_as_list(self): + df = [ + {'title': 'Revenue'}, + 'foo' + ] + + pattern = ('If your data is a list, all entries must be dictionaries.') + self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) + + def test_not_df_or_list(self): + df = 'foo' + + pattern = ('You must input a pandas DataFrame or a list of dictionaries.') + self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) + + def test_valid_keys(self): + df = [{'title': 'Revenue', 'foo': 'bar'}] + VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] + + pattern = ( + 'Your headers/dict keys must be either {}' + ).format(utils.list_of_options(VALID_KEYS, 'or')) + self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) + + def test_valid_color_lists_of_2_rgb_colors(self): + df = [ + {'title': 'Revenue'} + ] + + range_colors = ['rgb(0, 0, 0)'] + measure_colors = ['rgb(0, 0, 0)'] + + pattern = ("Both 'range_colors' or 'measure_colors' must be a list " + "of two valid colors.") + self.assertRaisesRegexp( + PlotlyError, pattern, ff.create_bullet, df, + range_colors=range_colors + ) + + self.assertRaisesRegexp( + PlotlyError, pattern, ff.create_bullet, df, + measure_colors=measure_colors + ) From 8ba62a25329d3241866c5e396d5b269b44cc18bb Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Wed, 8 Nov 2017 11:23:50 -0500 Subject: [PATCH 09/22] added full test for bullet params --- .../test_optional/test_figure_factory.py | 442 ++++++++++++++++++ 1 file changed, 442 insertions(+) diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index 824621c05fa..5203c9373a8 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2230,3 +2230,445 @@ def test_valid_color_lists_of_2_rgb_colors(self): PlotlyError, pattern, ff.create_bullet, df, measure_colors=measure_colors ) + + def test_full_bullet(self): + df = pd.read_json('https://cdn.rawgit.com/plotly/datasets/master/BulletData.json') + + measure_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] + range_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] + + fig = ff.create_bullet( + df, as_rows=False, marker_size=30, marker_symbol='hourglass', + range_colors=range_colors, measure_colors=measure_colors, + title='new title' + ) + + exp_fig = { + 'data': [{'marker': {'color': 'rgb(0, 0, 0)', + 'size': 30, + 'symbol': 'hourglass'}, + 'type': 'scatter', + 'x': [0.5], + 'xaxis': 'x1', + 'y': [250], + 'yaxis': 'y1'}, + {'marker': {'color': 'rgb(0, 0, 0)', + 'size': 30, + 'symbol': 'hourglass'}, + 'type': 'scatter', + 'x': [0.5], + 'xaxis': 'x2', + 'y': [26], + 'yaxis': 'y2'}, + {'marker': {'color': 'rgb(0, 0, 0)', + 'size': 30, + 'symbol': 'hourglass'}, + 'type': 'scatter', + 'x': [0.5], + 'xaxis': 'x3', + 'y': [550], + 'yaxis': 'y3'}, + {'marker': {'color': 'rgb(0, 0, 0)', + 'size': 30, + 'symbol': 'hourglass'}, + 'type': 'scatter', + 'x': [0.5], + 'xaxis': 'x4', + 'y': [2100], + 'yaxis': 'y4'}, + {'marker': {'color': 'rgb(0, 0, 0)', + 'size': 30, + 'symbol': 'hourglass'}, + 'type': 'scatter', + 'x': [0.5], + 'xaxis': 'x5', + 'y': [4.4], + 'yaxis': 'y5'}], + 'layout': {'annotations': [{'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Revenue
US$, in thousands', + 'textangle': 0, + 'x': 0.906, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Profit
%', + 'textangle': 0, + 'x': 0.703, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Order Size
US$, average', + 'textangle': 0, + 'x': 0.5, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'New Customers
count', + 'textangle': 0, + 'x': 0.29700000000000004, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Satisfaction
out of 5', + 'textangle': 0, + 'x': 0.094, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}], + 'height': 600, + 'margin': {'l': 80}, + 'shapes': [{'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x1', + 'y0': 0, + 'y1': 150, + 'yref': 'y1'}, + {'fillcolor': 'rgb(149.5, 143.5, 29.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x1', + 'y0': 150, + 'y1': 225, + 'yref': 'y1'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x1', + 'y0': 225, + 'y1': 300, + 'yref': 'y1'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x1', + 'y0': 0, + 'y1': 220, + 'yref': 'y1'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x1', + 'y0': 220, + 'y1': 270, + 'yref': 'y1'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x2', + 'y0': 0, + 'y1': 20, + 'yref': 'y2'}, + {'fillcolor': 'rgb(149.5, 143.5, 29.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x2', + 'y0': 20, + 'y1': 25, + 'yref': 'y2'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x2', + 'y0': 25, + 'y1': 30, + 'yref': 'y2'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x2', + 'y0': 0, + 'y1': 21, + 'yref': 'y2'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x2', + 'y0': 21, + 'y1': 23, + 'yref': 'y2'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x3', + 'y0': 0, + 'y1': 350, + 'yref': 'y3'}, + {'fillcolor': 'rgb(149.5, 143.5, 29.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x3', + 'y0': 350, + 'y1': 500, + 'yref': 'y3'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x3', + 'y0': 500, + 'y1': 600, + 'yref': 'y3'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x3', + 'y0': 0, + 'y1': 100, + 'yref': 'y3'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x3', + 'y0': 100, + 'y1': 320, + 'yref': 'y3'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x4', + 'y0': 0, + 'y1': 1400, + 'yref': 'y4'}, + {'fillcolor': 'rgb(149.5, 143.5, 29.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x4', + 'y0': 1400, + 'y1': 2000, + 'yref': 'y4'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x4', + 'y0': 2000, + 'y1': 2500, + 'yref': 'y4'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x4', + 'y0': 0, + 'y1': 1000, + 'yref': 'y4'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x4', + 'y0': 1000, + 'y1': 1650, + 'yref': 'y4'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x5', + 'y0': 0, + 'y1': 3.5, + 'yref': 'y5'}, + {'fillcolor': 'rgb(149.5, 143.5, 29.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x5', + 'y0': 3.5, + 'y1': 4.25, + 'yref': 'y5'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.35, + 'x1': 0.65, + 'xref': 'x5', + 'y0': 4.25, + 'y1': 5, + 'yref': 'y5'}, + {'fillcolor': 'rgb(255.0, 127.0, 14.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x5', + 'y0': 0, + 'y1': 3.2, + 'yref': 'y5'}, + {'fillcolor': 'rgb(44.0, 160.0, 44.0)', + 'layer': 'below', + 'line': {'width': 0}, + 'opacity': 1, + 'type': 'rect', + 'x0': 0.45, + 'x1': 0.55, + 'xref': 'x5', + 'y0': 3.2, + 'y1': 4.7, + 'yref': 'y5'}], + 'showlegend': False, + 'title': 'new title', + 'width': 1000, + 'xaxis1': {'anchor': 'y1', + 'domain': [0.0, 0.188], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis2': {'anchor': 'y2', + 'domain': [0.203, 0.391], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis3': {'anchor': 'y3', + 'domain': [0.406, 0.5940000000000001], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis4': {'anchor': 'y4', + 'domain': [0.609, 0.7969999999999999], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis5': {'anchor': 'y5', + 'domain': [0.812, 1.0], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'yaxis1': {'anchor': 'x1', + 'domain': [0.0, 1.0], + 'showgrid': False, + 'tickwidth': 1, + 'zeroline': False}, + 'yaxis2': {'anchor': 'x2', + 'domain': [0.0, 1.0], + 'showgrid': False, + 'tickwidth': 1, + 'zeroline': False}, + 'yaxis3': {'anchor': 'x3', + 'domain': [0.0, 1.0], + 'showgrid': False, + 'tickwidth': 1, + 'zeroline': False}, + 'yaxis4': {'anchor': 'x4', + 'domain': [0.0, 1.0], + 'showgrid': False, + 'tickwidth': 1, + 'zeroline': False}, + 'yaxis5': {'anchor': 'x5', + 'domain': [0.0, 1.0], + 'showgrid': False, + 'tickwidth': 1, + 'zeroline': False}} + } + + self.assert_dict_equal(fig, exp_fig) From 69bd30d82fd1882950b3a333589bb9678037c4f2 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Fri, 10 Nov 2017 16:44:21 -0500 Subject: [PATCH 10/22] reworked bullet charts with bar traces --- plotly/figure_factory/_bullet.py | 193 +++--- .../test_optional/test_figure_factory.py | 595 +++++++++--------- 2 files changed, 392 insertions(+), 396 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 892eaa92a42..3a82f43e2f2 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -10,49 +10,33 @@ pd = optional_imports.get_module('pandas') -SUBPLOT_SPACING = 0.015 VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] -# add shapes -def rectangle(color, x0, x1, y0, y1, xref, yref, layer): - return { - 'fillcolor': color, - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': x0, - 'x1': x1, - 'xref': xref, - 'y0': y0, - 'y1': y1, - 'yref': yref, - 'layer': layer - } - - def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, - measure_colors): + measure_colors, subplot_spacing): num_of_lanes = len(df) num_of_rows = num_of_lanes if as_rows else 1 num_of_cols = 1 if as_rows else num_of_lanes + if not subplot_spacing: + subplot_spacing = 1./num_of_lanes fig = plotly.tools.make_subplots( num_of_rows, num_of_cols, print_grid=False, - horizontal_spacing=SUBPLOT_SPACING, - vertical_spacing=SUBPLOT_SPACING + horizontal_spacing=subplot_spacing, + vertical_spacing=subplot_spacing ) # layout fig['layout'].update( dict(shapes=[]), showlegend=False, - annotations=[], + barmode='stack', margin=dict(l=120 if as_rows else 80), ) if as_rows: - length_axis = 'xaxis' width_axis = 'yaxis' + length_axis = 'xaxis' else: width_axis = 'xaxis' length_axis = 'yaxis' @@ -67,105 +51,102 @@ def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, fig['layout'][key]['showticklabels'] = False fig['layout'][key]['range'] = [0, 1] + # narrow domain if 1 bar + if num_of_lanes <= 1: + fig['layout'][width_axis + '1']['domain'] = [0.4, 0.6] + # marker symbol size if not marker_size: - if num_of_lanes <= 3: + if num_of_lanes <= 4: marker_size = 18 else: - marker_size = 12 - for idx in range(num_of_lanes): - # marker - x = df.iloc[idx]['markers'] if as_rows else [0.5] - y = [0.5] if as_rows else df.iloc[idx]['markers'] - fig['data'].append( - go.Scatter( + marker_size = 8 + + if not range_colors: + range_colors = ['rgb(200, 200, 200)', 'rgb(245, 245, 245)'] + if not measure_colors: + measure_colors = ['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] + + for row in range(num_of_lanes): + # ranges bars + for idx in range(len(df.iloc[row]['ranges'])): + inter_colors = colors.n_colors( + range_colors[0], range_colors[1], + len(df.iloc[row]['ranges']), 'rgb' + ) + x = [sorted(df.iloc[row]['ranges'])[-1 - idx]] if as_rows else [0] + y = [0] if as_rows else [sorted(df.iloc[row]['ranges'])[-1 - idx]] + bar = go.Bar( x=x, y=y, marker=dict( - size=marker_size, - color='rgb(0, 0, 0)', - symbol=marker_symbol + color=inter_colors[-1 - idx] ), - xaxis='x{}'.format(idx + 1), - yaxis='y{}'.format(idx + 1) + name='ranges', + hoverinfo='x' if as_rows else 'y', + orientation='h' if as_rows else 'v', + width=2, + base=0, + xaxis='x{}'.format(row + 1), + yaxis='y{}'.format(row + 1) ) - ) + fig['data'].append(bar) - # ranges - y0_ranges = 0.35 - y1_ranges = 0.65 - if not range_colors: - range_colors = ['rgb(200, 200, 200)', 'rgb(245, 245, 245)'] - ranges_len = len(df.iloc[idx]['ranges']) - if ranges_len <= 1: - inter_colors = [range_colors[0]] - else: + # measures bars + for idx in range(len(df.iloc[row]['measures'])): inter_colors = colors.n_colors( - range_colors[0], range_colors[1], ranges_len, 'rgb' - ) - for range_idx in range(ranges_len): - color = inter_colors[range_idx] - if range_idx == 0: - start_range = 0 - else: - start_range = df.iloc[idx]['ranges'][range_idx - 1] - end_range = df.iloc[idx]['ranges'][range_idx] - - x0 = start_range if as_rows else y0_ranges - x1 = end_range if as_rows else y1_ranges - y0 = y0_ranges if as_rows else start_range - y1 = y1_ranges if as_rows else end_range - fig['layout']['shapes'].append( - rectangle( - color, x0, x1, y0, y1, - 'x{}'.format(idx + 1), - 'y{}'.format(idx + 1), - 'below' - ) + measure_colors[0], measure_colors[1], + len(df.iloc[row]['measures']), 'rgb' ) - - # measures - y0_measures = 0.45 - y1_measures = 0.55 - if not measure_colors: - measure_colors = ['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] - measures_len = len(df.iloc[idx]['measures']) - if measures_len <= 1: - inter_colors = [measure_colors[0]] - else: - inter_colors = colors.n_colors( - measure_colors[0], measure_colors[1], measures_len, 'rgb' + x = ([sorted(df.iloc[row]['measures'])[-1 - idx]] if as_rows + else [0.5]) + y = ([0.5] if as_rows + else [sorted(df.iloc[row]['measures'])[-1 - idx]]) + bar = go.Bar( + x=x, + y=y, + marker=dict( + color=inter_colors[-1 - idx] + ), + name='measures', + hoverinfo='x' if as_rows else 'y', + orientation='h' if as_rows else 'v', + width=0.4, + base=0, + xaxis='x{}'.format(row + 1), + yaxis='y{}'.format(row + 1) ) - for range_idx in range(measures_len): - color = inter_colors[range_idx] - if range_idx == 0: - start_range = 0 - else: - start_range = df.iloc[idx]['measures'][range_idx - 1] - end_range = df.iloc[idx]['measures'][range_idx] + fig['data'].append(bar) - x0 = start_range if as_rows else y0_measures - x1 = end_range if as_rows else y1_measures - y0 = y0_measures if as_rows else start_range - y1 = y1_measures if as_rows else end_range - fig['layout']['shapes'].append( - rectangle( - color, x0, x1, y0, y1, - 'x{}'.format(idx + 1), - 'y{}'.format(idx + 1), - 'below' - ) - ) + # markers + x = df.iloc[row]['markers'] if as_rows else [0.5] + y = [0.5] if as_rows else df.iloc[row]['markers'] + markers = go.Scatter( + x=x, + y=y, + marker=dict( + color='rgb(0, 0, 0)', + symbol=marker_symbol, + size=marker_size + ), + name='markers', + hoverinfo='x' if as_rows else 'y', + xaxis='x{}'.format(row + 1), + yaxis='y{}'.format(row + 1) + ) + fig['data'].append(markers) # labels - title = df.iloc[idx]['title'] + title = df.iloc[row]['title'] if 'subtitle' in df: - subtitle = '
{}'.format(df.iloc[idx]['subtitle']) + subtitle = '
{}'.format(df.iloc[row]['subtitle']) else: subtitle = '' label = '{}'.format(title) + subtitle annot = utils.annotation_dict_for_label( - label, num_of_lanes - idx, num_of_lanes, SUBPLOT_SPACING, + label, + (num_of_lanes - row if as_rows else row + 1), + num_of_lanes, subplot_spacing, 'row' if as_rows else 'col', True if as_rows else False, False @@ -177,8 +158,8 @@ def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, def create_bullet(df, as_rows=True, marker_size=16, marker_symbol='diamond-tall', range_colors=None, - measure_colors=None, title='Bullet Chart', height=600, - width=1000): + measure_colors=None, subplot_spacing=None, + title='Bullet Chart', height=600, width=1000): """ Returns figure for bullet chart. @@ -198,6 +179,9 @@ def create_bullet(df, as_rows=True, marker_size=16, :param (list) measure_colors: a list of two colors which is used to color the thin quantitative bars in the bullet chart. Default=['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] + :param (float) subplot_spacing: set the distance between each bar chart. + If not specified an automatic spacing is assigned based on the number + of bars to be plotted. :param (str) title: title of the bullet chart. :param (float) height: height of the chart. :param (float) width width of the chart. @@ -260,7 +244,8 @@ def create_bullet(df, as_rows=True, marker_size=16, 'rgb')[0] fig = _bullet( - df, as_rows, marker_size, marker_symbol, range_colors, measure_colors + df, as_rows, marker_size, marker_symbol, range_colors, measure_colors, + subplot_spacing ) fig['layout'].update( diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index 5203c9373a8..c0051fdf822 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2244,41 +2244,326 @@ def test_full_bullet(self): ) exp_fig = { - 'data': [{'marker': {'color': 'rgb(0, 0, 0)', - 'size': 30, - 'symbol': 'hourglass'}, + 'data': [{'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x1', + 'y': [300], + 'yaxis': 'y1'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(149.5, 143.5, 29.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x1', + 'y': [225], + 'yaxis': 'y1'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x1', + 'y': [150], + 'yaxis': 'y1'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x1', + 'y': [270], + 'yaxis': 'y1'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x1', + 'y': [220], + 'yaxis': 'y1'}, + {'hoverinfo': 'y', + 'marker': {'color': 'rgb(0, 0, 0)', + 'size': 30, + 'symbol': 'hourglass'}, + 'name': 'markers', 'type': 'scatter', 'x': [0.5], 'xaxis': 'x1', 'y': [250], 'yaxis': 'y1'}, - {'marker': {'color': 'rgb(0, 0, 0)', + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x2', + 'y': [30], + 'yaxis': 'y2'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(149.5, 143.5, 29.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x2', + 'y': [25], + 'yaxis': 'y2'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x2', + 'y': [20], + 'yaxis': 'y2'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x2', + 'y': [23], + 'yaxis': 'y2'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x2', + 'y': [21], + 'yaxis': 'y2'}, + {'hoverinfo': 'y', + 'marker': {'color': 'rgb(0, 0, 0)', 'size': 30, 'symbol': 'hourglass'}, + 'name': 'markers', 'type': 'scatter', 'x': [0.5], 'xaxis': 'x2', 'y': [26], 'yaxis': 'y2'}, - {'marker': {'color': 'rgb(0, 0, 0)', + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x3', + 'y': [600], + 'yaxis': 'y3'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(149.5, 143.5, 29.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x3', + 'y': [500], + 'yaxis': 'y3'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x3', + 'y': [350], + 'yaxis': 'y3'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x3', + 'y': [320], + 'yaxis': 'y3'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x3', + 'y': [100], + 'yaxis': 'y3'}, + {'hoverinfo': 'y', + 'marker': {'color': 'rgb(0, 0, 0)', 'size': 30, 'symbol': 'hourglass'}, + 'name': 'markers', 'type': 'scatter', 'x': [0.5], 'xaxis': 'x3', 'y': [550], 'yaxis': 'y3'}, - {'marker': {'color': 'rgb(0, 0, 0)', + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x4', + 'y': [2500], + 'yaxis': 'y4'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(149.5, 143.5, 29.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x4', + 'y': [2000], + 'yaxis': 'y4'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x4', + 'y': [1400], + 'yaxis': 'y4'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x4', + 'y': [1650], + 'yaxis': 'y4'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x4', + 'y': [1000], + 'yaxis': 'y4'}, + {'hoverinfo': 'y', + 'marker': {'color': 'rgb(0, 0, 0)', 'size': 30, 'symbol': 'hourglass'}, + 'name': 'markers', 'type': 'scatter', 'x': [0.5], 'xaxis': 'x4', 'y': [2100], 'yaxis': 'y4'}, - {'marker': {'color': 'rgb(0, 0, 0)', + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x5', + 'y': [5], + 'yaxis': 'y5'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(149.5, 143.5, 29.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x5', + 'y': [4.25], + 'yaxis': 'y5'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'ranges', + 'orientation': 'v', + 'type': 'bar', + 'width': 2, + 'x': [0], + 'xaxis': 'x5', + 'y': [3.5], + 'yaxis': 'y5'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(44.0, 160.0, 44.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x5', + 'y': [4.7], + 'yaxis': 'y5'}, + {'base': 0, + 'hoverinfo': 'y', + 'marker': {'color': 'rgb(255.0, 127.0, 14.0)'}, + 'name': 'measures', + 'orientation': 'v', + 'type': 'bar', + 'width': 0.4, + 'x': [0.5], + 'xaxis': 'x5', + 'y': [3.2], + 'yaxis': 'y5'}, + {'hoverinfo': 'y', + 'marker': {'color': 'rgb(0, 0, 0)', 'size': 30, 'symbol': 'hourglass'}, + 'name': 'markers', 'type': 'scatter', 'x': [0.5], 'xaxis': 'x5', @@ -2288,7 +2573,7 @@ def test_full_bullet(self): 'showarrow': False, 'text': 'Revenue
US$, in thousands', 'textangle': 0, - 'x': 0.906, + 'x': 0.019999999999999997, 'xanchor': 'center', 'xref': 'paper', 'y': 1.03, @@ -2298,7 +2583,7 @@ def test_full_bullet(self): 'showarrow': False, 'text': 'Profit
%', 'textangle': 0, - 'x': 0.703, + 'x': 0.26, 'xanchor': 'center', 'xref': 'paper', 'y': 1.03, @@ -2318,7 +2603,7 @@ def test_full_bullet(self): 'showarrow': False, 'text': 'New Customers
count', 'textangle': 0, - 'x': 0.29700000000000004, + 'x': 0.74, 'xanchor': 'center', 'xref': 'paper', 'y': 1.03, @@ -2328,318 +2613,45 @@ def test_full_bullet(self): 'showarrow': False, 'text': 'Satisfaction
out of 5', 'textangle': 0, - 'x': 0.094, + 'x': 0.98, 'xanchor': 'center', 'xref': 'paper', 'y': 1.03, 'yanchor': 'middle', 'yref': 'paper'}], + 'barmode': 'stack', 'height': 600, 'margin': {'l': 80}, - 'shapes': [{'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x1', - 'y0': 0, - 'y1': 150, - 'yref': 'y1'}, - {'fillcolor': 'rgb(149.5, 143.5, 29.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x1', - 'y0': 150, - 'y1': 225, - 'yref': 'y1'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x1', - 'y0': 225, - 'y1': 300, - 'yref': 'y1'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x1', - 'y0': 0, - 'y1': 220, - 'yref': 'y1'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x1', - 'y0': 220, - 'y1': 270, - 'yref': 'y1'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x2', - 'y0': 0, - 'y1': 20, - 'yref': 'y2'}, - {'fillcolor': 'rgb(149.5, 143.5, 29.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x2', - 'y0': 20, - 'y1': 25, - 'yref': 'y2'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x2', - 'y0': 25, - 'y1': 30, - 'yref': 'y2'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x2', - 'y0': 0, - 'y1': 21, - 'yref': 'y2'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x2', - 'y0': 21, - 'y1': 23, - 'yref': 'y2'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x3', - 'y0': 0, - 'y1': 350, - 'yref': 'y3'}, - {'fillcolor': 'rgb(149.5, 143.5, 29.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x3', - 'y0': 350, - 'y1': 500, - 'yref': 'y3'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x3', - 'y0': 500, - 'y1': 600, - 'yref': 'y3'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x3', - 'y0': 0, - 'y1': 100, - 'yref': 'y3'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x3', - 'y0': 100, - 'y1': 320, - 'yref': 'y3'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x4', - 'y0': 0, - 'y1': 1400, - 'yref': 'y4'}, - {'fillcolor': 'rgb(149.5, 143.5, 29.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x4', - 'y0': 1400, - 'y1': 2000, - 'yref': 'y4'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x4', - 'y0': 2000, - 'y1': 2500, - 'yref': 'y4'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x4', - 'y0': 0, - 'y1': 1000, - 'yref': 'y4'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x4', - 'y0': 1000, - 'y1': 1650, - 'yref': 'y4'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x5', - 'y0': 0, - 'y1': 3.5, - 'yref': 'y5'}, - {'fillcolor': 'rgb(149.5, 143.5, 29.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x5', - 'y0': 3.5, - 'y1': 4.25, - 'yref': 'y5'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.35, - 'x1': 0.65, - 'xref': 'x5', - 'y0': 4.25, - 'y1': 5, - 'yref': 'y5'}, - {'fillcolor': 'rgb(255.0, 127.0, 14.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x5', - 'y0': 0, - 'y1': 3.2, - 'yref': 'y5'}, - {'fillcolor': 'rgb(44.0, 160.0, 44.0)', - 'layer': 'below', - 'line': {'width': 0}, - 'opacity': 1, - 'type': 'rect', - 'x0': 0.45, - 'x1': 0.55, - 'xref': 'x5', - 'y0': 3.2, - 'y1': 4.7, - 'yref': 'y5'}], + 'shapes': [], 'showlegend': False, 'title': 'new title', 'width': 1000, 'xaxis1': {'anchor': 'y1', - 'domain': [0.0, 0.188], + 'domain': [0.0, 0.039999999999999994], 'range': [0, 1], 'showgrid': False, 'showticklabels': False, 'zeroline': False}, 'xaxis2': {'anchor': 'y2', - 'domain': [0.203, 0.391], + 'domain': [0.24, 0.27999999999999997], 'range': [0, 1], 'showgrid': False, 'showticklabels': False, 'zeroline': False}, 'xaxis3': {'anchor': 'y3', - 'domain': [0.406, 0.5940000000000001], + 'domain': [0.48, 0.52], 'range': [0, 1], 'showgrid': False, 'showticklabels': False, 'zeroline': False}, 'xaxis4': {'anchor': 'y4', - 'domain': [0.609, 0.7969999999999999], + 'domain': [0.72, 0.76], 'range': [0, 1], 'showgrid': False, 'showticklabels': False, 'zeroline': False}, 'xaxis5': {'anchor': 'y5', - 'domain': [0.812, 1.0], + 'domain': [0.96, 1.0], 'range': [0, 1], 'showgrid': False, 'showticklabels': False, @@ -2670,5 +2682,4 @@ def test_full_bullet(self): 'tickwidth': 1, 'zeroline': False}} } - self.assert_dict_equal(fig, exp_fig) From 69a541dc70fe39197b319facf875496379181094 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 16 Nov 2017 11:14:41 -0500 Subject: [PATCH 11/22] not mutating data//pep8 in colors.py --- plotly/colors.py | 14 +-- plotly/figure_factory/_bullet.py | 85 +++++++++++-------- .../test_optional/test_figure_factory.py | 2 +- 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/plotly/colors.py b/plotly/colors.py index 776fa7ae4c1..f037a142a1a 100644 --- a/plotly/colors.py +++ b/plotly/colors.py @@ -453,7 +453,7 @@ def find_intermediate_color(lowcolor, highcolor, intermed, colortype='tuple'): intermediate color and return it as an rgb color. """ if colortype == 'rgb': - # convert to tuple + # convert to tuple color, eg. (1, 0.45, 0.7) lowcolor = unlabel_rgb(lowcolor) highcolor = unlabel_rgb(highcolor) @@ -468,7 +468,7 @@ def find_intermediate_color(lowcolor, highcolor, intermed, colortype='tuple'): ) if colortype == 'rgb': - # covert back to rgb + # back to an rgb string, e.g. rgb(30, 20, 10) inter_med_rgb = label_rgb(inter_med_tuple) return inter_med_rgb @@ -534,19 +534,19 @@ def n_colors(lowcolor, highcolor, n_colors, colortype='tuple'): incr_1 = diff_1/(n_colors - 1) diff_2 = float(highcolor[2] - lowcolor[2]) incr_2 = diff_2/(n_colors - 1) - color_tuples = [] + list_of_colors = [] for index in range(n_colors): new_tuple = (lowcolor[0] + (index * incr_0), lowcolor[1] + (index * incr_1), lowcolor[2] + (index * incr_2)) - color_tuples.append(new_tuple) + list_of_colors.append(new_tuple) if colortype == 'rgb': - # convert back to rgb - color_tuples = color_parser(color_tuples, label_rgb) + # back to an rgb string + list_of_colors = color_parser(list_of_colors, label_rgb) - return color_tuples + return list_of_colors def label_rgb(colors): diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 3a82f43e2f2..86df73a17ef 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -13,17 +13,18 @@ VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] -def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, - measure_colors, subplot_spacing): +def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, + marker_size, marker_symbol, range_colors, measure_colors, + horizontal_spacing, vertical_spacing): num_of_lanes = len(df) num_of_rows = num_of_lanes if as_rows else 1 num_of_cols = 1 if as_rows else num_of_lanes - if not subplot_spacing: - subplot_spacing = 1./num_of_lanes + if not horizontal_spacing and not vertical_spacing: + horizontal_spacing = vertical_spacing = 1./num_of_lanes fig = plotly.tools.make_subplots( num_of_rows, num_of_cols, print_grid=False, - horizontal_spacing=subplot_spacing, - vertical_spacing=subplot_spacing + horizontal_spacing=horizontal_spacing, + vertical_spacing=vertical_spacing ) # layout @@ -146,7 +147,7 @@ def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, annot = utils.annotation_dict_for_label( label, (num_of_lanes - row if as_rows else row + 1), - num_of_lanes, subplot_spacing, + num_of_lanes, vertical_spacing if as_rows else horizontal_spacing, 'row' if as_rows else 'col', True if as_rows else False, False @@ -156,14 +157,16 @@ def _bullet(df, as_rows, marker_size, marker_symbol, range_colors, return fig -def create_bullet(df, as_rows=True, marker_size=16, +def create_bullet(data, markers=None, measures=None, ranges=None, + subtitle=None, title=None, as_rows=True, marker_size=16, marker_symbol='diamond-tall', range_colors=None, - measure_colors=None, subplot_spacing=None, - title='Bullet Chart', height=600, width=1000): + measure_colors=None, horizontal_spacing=None, + vertical_spacing=None, chart_title='Bullet Chart', height=600, + width=1000): """ Returns figure for bullet chart. - :param (pd.DataFrame | list) df: either a JSON list of dicts or a pandas + :param (pd.DataFrame | list) data: either a JSON list of dicts or a pandas DataFrame. All keys must be one of 'title', 'subtitle', 'ranges', 'measures', and 'markers'. :param (bool) as_rows: if True, the bars are placed horizontally as rows. @@ -179,9 +182,10 @@ def create_bullet(df, as_rows=True, marker_size=16, :param (list) measure_colors: a list of two colors which is used to color the thin quantitative bars in the bullet chart. Default=['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] - :param (float) subplot_spacing: set the distance between each bar chart. - If not specified an automatic spacing is assigned based on the number - of bars to be plotted. + :param (float) horizontal_spacing: see the 'horizontal_spacing' param in + plotly.tools.make_subplots. Ranges between 0 and 1. + :param (float) vertical_spacing: see the 'vertical_spacing' param in + plotly.tools.make_subplots. Ranges between 0 and 1. :param (str) title: title of the bullet chart. :param (float) height: height of the chart. :param (float) width width of the chart. @@ -192,34 +196,42 @@ def create_bullet(df, as_rows=True, marker_size=16, "'pandas' must be imported for this figure_factory." ) - if isinstance(df, list): - if not all(isinstance(item, dict) for item in df): + if isinstance(data, list): + if not all(isinstance(item, dict) for item in data): raise exceptions.PlotlyError( 'If your data is a list, all entries must be dictionaries.' ) - df = pd.DataFrame(df) - elif not isinstance(df, pd.DataFrame): + elif not isinstance(data, pd.DataFrame): raise exceptions.PlotlyError( 'You must input a pandas DataFrame or a list of dictionaries.' ) - # check for valid keys - if any(key not in VALID_KEYS for key in df.columns): - raise exceptions.PlotlyError( - 'Your headers/dict keys must be either {}'.format( - utils.list_of_options(VALID_KEYS, 'or') - ) + # make DataFrame from data with correct column headers + col_names = ['title', 'subtitle', 'markers', 'measures', 'ranges'] + if isinstance(data, list): + df = pd.DataFrame( + [ + [d[title] for d in data] if title else [''] * len(data), + [d[subtitle] for d in data] if subtitle else [''] * len(data), + [d[markers] for d in data] if markers else [[]] * len(data), + [d[measures] for d in data] if measures else [[]] * len(data), + [d[ranges] for d in data] if ranges else [[]] * len(data), + ], + index=col_names ) - - # add necessary columns if missing - for key in VALID_KEYS: - if key not in df: - if key in ['title', 'subtitle']: - element = '' - else: - element = [] - df[key] = [element for _ in range(len(df))] + elif isinstance(data, pd.DataFrame): + df = pd.DataFrame( + [ + data[title].tolist() if title else [''] * len(data), + data[subtitle].tolist() if subtitle else [''] * len(data), + data[markers].tolist() if markers else [[]] * len(data), + data[measures].tolist() if measures else [[]] * len(data), + data[ranges].tolist() if ranges else [[]] * len(data), + ], + index=col_names + ) + df = pd.DataFrame.transpose(df) # make sure ranges and measures are not NAN or NONE for needed_key in ['ranges', 'measures']: @@ -244,12 +256,13 @@ def create_bullet(df, as_rows=True, marker_size=16, 'rgb')[0] fig = _bullet( - df, as_rows, marker_size, marker_symbol, range_colors, measure_colors, - subplot_spacing + df, markers, measures, ranges, subtitle, title, as_rows, marker_size, + marker_symbol, range_colors, measure_colors, horizontal_spacing, + vertical_spacing ) fig['layout'].update( - title=title, + title=chart_title, height=height, width=width, ) diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index c0051fdf822..b3052313ec1 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2232,7 +2232,7 @@ def test_valid_color_lists_of_2_rgb_colors(self): ) def test_full_bullet(self): - df = pd.read_json('https://cdn.rawgit.com/plotly/datasets/master/BulletData.json') + df = pd.read_json('BulletData.json') measure_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] range_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] From e74996c3080e57be0fa8c9ed623f75d30657a3f4 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 16 Nov 2017 11:15:08 -0500 Subject: [PATCH 12/22] adding test file to figure_factory repo --- plotly/tests/test_optional/BulletData.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 plotly/tests/test_optional/BulletData.json diff --git a/plotly/tests/test_optional/BulletData.json b/plotly/tests/test_optional/BulletData.json new file mode 100644 index 00000000000..cecdc7d69cb --- /dev/null +++ b/plotly/tests/test_optional/BulletData.json @@ -0,0 +1,7 @@ +[ + {"title":"Revenue","subtitle":"US$, in thousands","ranges":[150,225,300],"measures":[220,270],"markers":[250]}, + {"title":"Profit","subtitle":"%","ranges":[20,25,30],"measures":[21,23],"markers":[26]}, + {"title":"Order Size","subtitle":"US$, average","ranges":[350,500,600],"measures":[100,320],"markers":[550]}, + {"title":"New Customers","subtitle":"count","ranges":[1400,2000,2500],"measures":[1000,1650],"markers":[2100]}, + {"title":"Satisfaction","subtitle":"out of 5","ranges":[3.5,4.25,5],"measures":[3.2,4.7],"markers":[4.4]} +] \ No newline at end of file From 6da599f15162991e97d2055cfeac3a14d4a6c6ea Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 16 Nov 2017 15:44:36 -0500 Subject: [PATCH 13/22] adding **kwargs - broken state --- plotly/figure_factory/_bullet.py | 93 +++++++++++++++++--------------- 1 file changed, 51 insertions(+), 42 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 86df73a17ef..aa85821235d 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -13,12 +13,12 @@ VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] -def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, - marker_size, marker_symbol, range_colors, measure_colors, - horizontal_spacing, vertical_spacing): +def _bullet(df, markers, measures, ranges, subtitle, title, orientation, + range_colors, measure_colors, horizontal_spacing, + vertical_spacing, scatter_options): num_of_lanes = len(df) - num_of_rows = num_of_lanes if as_rows else 1 - num_of_cols = 1 if as_rows else num_of_lanes + num_of_rows = num_of_lanes if orientation == 'h' else 1 + num_of_cols = 1 if orientation == 'h' else num_of_lanes if not horizontal_spacing and not vertical_spacing: horizontal_spacing = vertical_spacing = 1./num_of_lanes fig = plotly.tools.make_subplots( @@ -32,10 +32,10 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, dict(shapes=[]), showlegend=False, barmode='stack', - margin=dict(l=120 if as_rows else 80), + margin=dict(l=120 if orientation == 'h' else 80), ) - if as_rows: + if orientation == 'h': width_axis = 'yaxis' length_axis = 'xaxis' else: @@ -56,13 +56,6 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, if num_of_lanes <= 1: fig['layout'][width_axis + '1']['domain'] = [0.4, 0.6] - # marker symbol size - if not marker_size: - if num_of_lanes <= 4: - marker_size = 18 - else: - marker_size = 8 - if not range_colors: range_colors = ['rgb(200, 200, 200)', 'rgb(245, 245, 245)'] if not measure_colors: @@ -75,8 +68,10 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, range_colors[0], range_colors[1], len(df.iloc[row]['ranges']), 'rgb' ) - x = [sorted(df.iloc[row]['ranges'])[-1 - idx]] if as_rows else [0] - y = [0] if as_rows else [sorted(df.iloc[row]['ranges'])[-1 - idx]] + x = ([sorted(df.iloc[row]['ranges'])[-1 - idx]] if + orientation == 'h' else [0]) + y = ([0] if orientation == 'h' else + [sorted(df.iloc[row]['ranges'])[-1 - idx]]) bar = go.Bar( x=x, y=y, @@ -84,8 +79,8 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, color=inter_colors[-1 - idx] ), name='ranges', - hoverinfo='x' if as_rows else 'y', - orientation='h' if as_rows else 'v', + hoverinfo='x' if orientation == 'h' else 'y', + orientation=orientation, width=2, base=0, xaxis='x{}'.format(row + 1), @@ -99,9 +94,9 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, measure_colors[0], measure_colors[1], len(df.iloc[row]['measures']), 'rgb' ) - x = ([sorted(df.iloc[row]['measures'])[-1 - idx]] if as_rows - else [0.5]) - y = ([0.5] if as_rows + x = ([sorted(df.iloc[row]['measures'])[-1 - idx]] if + orientation == 'h' else [0.5]) + y = ([0.5] if orientation == 'h' else [sorted(df.iloc[row]['measures'])[-1 - idx]]) bar = go.Bar( x=x, @@ -110,8 +105,8 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, color=inter_colors[-1 - idx] ), name='measures', - hoverinfo='x' if as_rows else 'y', - orientation='h' if as_rows else 'v', + hoverinfo='x' if orientation == 'h' else 'y', + orientation=orientation, width=0.4, base=0, xaxis='x{}'.format(row + 1), @@ -120,8 +115,8 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, fig['data'].append(bar) # markers - x = df.iloc[row]['markers'] if as_rows else [0.5] - y = [0.5] if as_rows else df.iloc[row]['markers'] + x = df.iloc[row]['markers'] if orientation == 'h' else [0.5] + y = [0.5] if orientation == 'h' else df.iloc[row]['markers'] markers = go.Scatter( x=x, y=y, @@ -131,7 +126,7 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, size=marker_size ), name='markers', - hoverinfo='x' if as_rows else 'y', + hoverinfo='x' if orientation == 'h' else 'y', xaxis='x{}'.format(row + 1), yaxis='y{}'.format(row + 1) ) @@ -146,10 +141,11 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, label = '{}'.format(title) + subtitle annot = utils.annotation_dict_for_label( label, - (num_of_lanes - row if as_rows else row + 1), - num_of_lanes, vertical_spacing if as_rows else horizontal_spacing, - 'row' if as_rows else 'col', - True if as_rows else False, + (num_of_lanes - row if orientation == 'h' else row + 1), + num_of_lanes, + vertical_spacing if orientation == 'h' else horizontal_spacing, + 'row' if orientation == 'h' else 'col', + True if orientation == 'h' else False, False ) fig['layout']['annotations'].append(annot) @@ -158,19 +154,18 @@ def _bullet(df, markers, measures, ranges, subtitle, title, as_rows, def create_bullet(data, markers=None, measures=None, ranges=None, - subtitle=None, title=None, as_rows=True, marker_size=16, - marker_symbol='diamond-tall', range_colors=None, - measure_colors=None, horizontal_spacing=None, - vertical_spacing=None, chart_title='Bullet Chart', height=600, - width=1000): + subtitle=None, title=None, orientation='h', + range_colors=None, measure_colors=None, horizontal_spacing=None, + vertical_spacing=None, chart_title='Bullet Chart', + height=600, width=1000, **scatter_options): """ Returns figure for bullet chart. :param (pd.DataFrame | list) data: either a JSON list of dicts or a pandas DataFrame. All keys must be one of 'title', 'subtitle', 'ranges', 'measures', and 'markers'. - :param (bool) as_rows: if True, the bars are placed horizontally as rows. - If False, the bars are placed vertically in the chart. + :param (bool) orientation: if 'h', the bars are placed horizontally as + rows. If 'v' the bars are placed vertically in the chart. :param (int) marker_size: sets the size of the markers in the chart. :param (str | int) marker_symbol: the symbol of the markers in the chart. Default='diamond-tall' @@ -233,8 +228,8 @@ def create_bullet(data, markers=None, measures=None, ranges=None, ) df = pd.DataFrame.transpose(df) - # make sure ranges and measures are not NAN or NONE - for needed_key in ['ranges', 'measures']: + # make sure ranges, measures, 'markers' are not NAN or NONE + for needed_key in ['ranges', 'measures', 'markers']: for idx, r in enumerate(df[needed_key]): try: r_is_nan = math.isnan(r) @@ -255,10 +250,24 @@ def create_bullet(data, markers=None, measures=None, ranges=None, colors_list = colors.convert_colors_to_same_type(colors_list, 'rgb')[0] + # scatter options + default_scatter_options = { + 'marker': {'size': 12, + 'symbol': 'diamond-tall', + 'color': 'rgb(0, 0, 0)'} + } + if scatter_options == {}: + scatter_options.update(default_scatter_options) + else: + + + + + fig = _bullet( - df, markers, measures, ranges, subtitle, title, as_rows, marker_size, - marker_symbol, range_colors, measure_colors, horizontal_spacing, - vertical_spacing + df, markers, measures, ranges, subtitle, title, orientation, + range_colors, measure_colors, horizontal_spacing, vertical_spacing, + scatter_options ) fig['layout'].update( From 79c67c74759d2a0b567a41c1af456647adad69c8 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Fri, 17 Nov 2017 13:46:45 -0500 Subject: [PATCH 14/22] finished chris comments and updated tests --- plotly/figure_factory/_bullet.py | 145 +++++++++++++----- .../test_optional/test_figure_factory.py | 28 ++-- 2 files changed, 114 insertions(+), 59 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index aa85821235d..a83eca696bf 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -10,12 +10,11 @@ pd = optional_imports.get_module('pandas') -VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] - -def _bullet(df, markers, measures, ranges, subtitle, title, orientation, +def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, range_colors, measure_colors, horizontal_spacing, - vertical_spacing, scatter_options): + vertical_spacing, scatter_options, layout_options): + num_of_lanes = len(df) num_of_rows = num_of_lanes if orientation == 'h' else 1 num_of_cols = 1 if orientation == 'h' else num_of_lanes @@ -30,11 +29,18 @@ def _bullet(df, markers, measures, ranges, subtitle, title, orientation, # layout fig['layout'].update( dict(shapes=[]), + title='Bullet Chart', + height=600, + width=1000, showlegend=False, barmode='stack', + annotations=[], margin=dict(l=120 if orientation == 'h' else 80), ) + # update layout + fig['layout'].update(layout_options) + if orientation == 'h': width_axis = 'yaxis' length_axis = 'xaxis' @@ -120,22 +126,23 @@ def _bullet(df, markers, measures, ranges, subtitle, title, orientation, markers = go.Scatter( x=x, y=y, - marker=dict( - color='rgb(0, 0, 0)', - symbol=marker_symbol, - size=marker_size - ), + marker=scatter_options['marker'], name='markers', hoverinfo='x' if orientation == 'h' else 'y', xaxis='x{}'.format(row + 1), yaxis='y{}'.format(row + 1) ) + + for k in scatter_options: + if k != 'marker': + markers[k] = scatter_options[k] + fig['data'].append(markers) - # labels - title = df.iloc[row]['title'] - if 'subtitle' in df: - subtitle = '
{}'.format(df.iloc[row]['subtitle']) + # titles and subtitles + title = df.iloc[row]['titles'] + if 'subtitles' in df: + subtitle = '
{}'.format(df.iloc[row]['subtitles']) else: subtitle = '' label = '{}'.format(title) + subtitle @@ -154,16 +161,29 @@ def _bullet(df, markers, measures, ranges, subtitle, title, orientation, def create_bullet(data, markers=None, measures=None, ranges=None, - subtitle=None, title=None, orientation='h', - range_colors=None, measure_colors=None, horizontal_spacing=None, - vertical_spacing=None, chart_title='Bullet Chart', - height=600, width=1000, **scatter_options): + subtitles=None, titles=None, orientation='h', + range_colors=None, measure_colors=None, + horizontal_spacing=None, vertical_spacing=None, + scatter_options={}, **layout_options): """ Returns figure for bullet chart. :param (pd.DataFrame | list) data: either a JSON list of dicts or a pandas - DataFrame. All keys must be one of 'title', 'subtitle', 'ranges', - 'measures', and 'markers'. + DataFrame. + :param (str) markers: the column name or dictionary key for the markers in + each subplot. + :param (str) measures: the column name or dictionary key for the measure + bars in each subplot. This bar usually represents the quantitative + measure of performance, usually a list of two values [a, b] and are + the blue bars in the foreground of each subplot by default. + :param (str) ranges: the column name or dictionary key for the qualitative + ranges of performance, usually a 3-item list [bad, okay, good]. They + correspond to the grey bars in the background of each chart. + :param (str) subtitles: the column name or dictionary key for the subtitle + of each subplot chart. The subplots are displayed right underneath + each title. + :param (str) titles: the column name or dictionary key for the main label + of each subplot chart. :param (bool) orientation: if 'h', the bars are placed horizontally as rows. If 'v' the bars are placed vertically in the chart. :param (int) marker_size: sets the size of the markers in the chart. @@ -181,9 +201,55 @@ def create_bullet(data, markers=None, measures=None, ranges=None, plotly.tools.make_subplots. Ranges between 0 and 1. :param (float) vertical_spacing: see the 'vertical_spacing' param in plotly.tools.make_subplots. Ranges between 0 and 1. - :param (str) title: title of the bullet chart. - :param (float) height: height of the chart. - :param (float) width width of the chart. + :param (dict) scatter_options: describes attributes for the scatter trace + in each subplot such as name 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. + + Example 1: Use a Dictionary + ``` + import plotly + import plotly.plotly as py + import plotly.figure_factory as ff + + data = [ + {"e": "Revenue", "d": "US$, in thousands", "c": [150, 225, 300], + "b": [220,270], "a": [250]}, + {"e": "Profit", "d": "%", "c": [20, 25, 30], "b": [21, 23], "a": [26]}, + {"e": "Order Size", "d":"US$, average","c": [350, 500, 600], + "b": [100,320],"a": [550]}, + {"e": "New Customers", "d": "count", "c": [1400, 2000, 2500], + "b": [1000,1650],"a": [2100]}, + {"e": "Satisfaction", "d": "out of 5","c": [3.5, 4.25, 5], + "b": [3.2,4.7], "a": [4.4]} + ] + + fig = ff.create_bullet( + data, titles='e', subtitles='d', markers='a', measures='b', + ranges='c', orientation='h', title='my simple bullet chart' + ) + py.iplot(fig) + ``` + + Example 2: Use a DataFrame with Custom Colors + ``` + import plotly.plotly as py + import plotly.figure_factory as ff + + import pandas as pd + + data = pd.read_json('https://cdn.rawgit.com/plotly/datasets/master/BulletData.json') + + fig = ff.create_bullet( + data, titles='title', markers='markers', measures='measures', + orientation='v', measure_colors=['rgb(14, 52, 75)', 'rgb(31, 141, 127)'], + scatter_options={'marker': {'symbol': 'circle'}}, width=700 + + ) + py.iplot(fig) + ``` """ # validate df if not pd: @@ -203,12 +269,12 @@ def create_bullet(data, markers=None, measures=None, ranges=None, ) # make DataFrame from data with correct column headers - col_names = ['title', 'subtitle', 'markers', 'measures', 'ranges'] + col_names = ['titles', 'subtitle', 'markers', 'measures', 'ranges'] if isinstance(data, list): df = pd.DataFrame( [ - [d[title] for d in data] if title else [''] * len(data), - [d[subtitle] for d in data] if subtitle else [''] * len(data), + [d[titles] for d in data] if titles else [''] * len(data), + [d[subtitles] for d in data] if subtitles else [''] * len(data), [d[markers] for d in data] if markers else [[]] * len(data), [d[measures] for d in data] if measures else [[]] * len(data), [d[ranges] for d in data] if ranges else [[]] * len(data), @@ -218,8 +284,8 @@ def create_bullet(data, markers=None, measures=None, ranges=None, elif isinstance(data, pd.DataFrame): df = pd.DataFrame( [ - data[title].tolist() if title else [''] * len(data), - data[subtitle].tolist() if subtitle else [''] * len(data), + data[titles].tolist() if titles else [''] * len(data), + data[subtitles].tolist() if subtitles else [''] * len(data), data[markers].tolist() if markers else [[]] * len(data), data[measures].tolist() if measures else [[]] * len(data), data[ranges].tolist() if ranges else [[]] * len(data), @@ -250,30 +316,25 @@ def create_bullet(data, markers=None, measures=None, ranges=None, colors_list = colors.convert_colors_to_same_type(colors_list, 'rgb')[0] - # scatter options - default_scatter_options = { + # default scatter options + default_scatter = { 'marker': {'size': 12, 'symbol': 'diamond-tall', 'color': 'rgb(0, 0, 0)'} } + if scatter_options == {}: - scatter_options.update(default_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']: + if k not in scatter_options['marker']: + scatter_options['marker'][k] = default_scatter['marker'][k] fig = _bullet( - df, markers, measures, ranges, subtitle, title, orientation, + df, markers, measures, ranges, subtitles, titles, orientation, range_colors, measure_colors, horizontal_spacing, vertical_spacing, - scatter_options - ) - - fig['layout'].update( - title=chart_title, - height=height, - width=width, + scatter_options, layout_options, ) return fig diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index b3052313ec1..a0f1be25cd6 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2189,7 +2189,7 @@ class TestBullet(NumpyTestUtilsMixin, TestCase): def test_df_as_list(self): df = [ - {'title': 'Revenue'}, + {'titles': 'Revenue'}, 'foo' ] @@ -2202,15 +2202,6 @@ def test_not_df_or_list(self): pattern = ('You must input a pandas DataFrame or a list of dictionaries.') self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) - def test_valid_keys(self): - df = [{'title': 'Revenue', 'foo': 'bar'}] - VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] - - pattern = ( - 'Your headers/dict keys must be either {}' - ).format(utils.list_of_options(VALID_KEYS, 'or')) - self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) - def test_valid_color_lists_of_2_rgb_colors(self): df = [ {'title': 'Revenue'} @@ -2238,9 +2229,12 @@ def test_full_bullet(self): range_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] fig = ff.create_bullet( - df, as_rows=False, marker_size=30, marker_symbol='hourglass', + df, orientation='v', markers='markers', measures='measures', + ranges='ranges', subtitles='subtitle', titles='title', range_colors=range_colors, measure_colors=measure_colors, - title='new title' + title='new title', + scatter_options={'marker': {'size': 30, + 'symbol': 'hourglass'}} ) exp_fig = { @@ -2571,7 +2565,7 @@ def test_full_bullet(self): 'yaxis': 'y5'}], 'layout': {'annotations': [{'font': {'color': '#0f0f0f', 'size': 13}, 'showarrow': False, - 'text': 'Revenue
US$, in thousands', + 'text': 'Revenue', 'textangle': 0, 'x': 0.019999999999999997, 'xanchor': 'center', @@ -2581,7 +2575,7 @@ def test_full_bullet(self): 'yref': 'paper'}, {'font': {'color': '#0f0f0f', 'size': 13}, 'showarrow': False, - 'text': 'Profit
%', + 'text': 'Profit', 'textangle': 0, 'x': 0.26, 'xanchor': 'center', @@ -2591,7 +2585,7 @@ def test_full_bullet(self): 'yref': 'paper'}, {'font': {'color': '#0f0f0f', 'size': 13}, 'showarrow': False, - 'text': 'Order Size
US$, average', + 'text': 'Order Size', 'textangle': 0, 'x': 0.5, 'xanchor': 'center', @@ -2601,7 +2595,7 @@ def test_full_bullet(self): 'yref': 'paper'}, {'font': {'color': '#0f0f0f', 'size': 13}, 'showarrow': False, - 'text': 'New Customers
count', + 'text': 'New Customers', 'textangle': 0, 'x': 0.74, 'xanchor': 'center', @@ -2611,7 +2605,7 @@ def test_full_bullet(self): 'yref': 'paper'}, {'font': {'color': '#0f0f0f', 'size': 13}, 'showarrow': False, - 'text': 'Satisfaction
out of 5', + 'text': 'Satisfaction', 'textangle': 0, 'x': 0.98, 'xanchor': 'center', From 9b9b578e9c4ea2612e61dccd45ff684a1dee8d68 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Fri, 17 Nov 2017 16:08:11 -0500 Subject: [PATCH 15/22] remove json file and fixed test --- plotly/tests/test_optional/BulletData.json | 7 ---- .../test_optional/test_figure_factory.py | 40 ++++++++++++++++++- 2 files changed, 39 insertions(+), 8 deletions(-) delete mode 100644 plotly/tests/test_optional/BulletData.json diff --git a/plotly/tests/test_optional/BulletData.json b/plotly/tests/test_optional/BulletData.json deleted file mode 100644 index cecdc7d69cb..00000000000 --- a/plotly/tests/test_optional/BulletData.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - {"title":"Revenue","subtitle":"US$, in thousands","ranges":[150,225,300],"measures":[220,270],"markers":[250]}, - {"title":"Profit","subtitle":"%","ranges":[20,25,30],"measures":[21,23],"markers":[26]}, - {"title":"Order Size","subtitle":"US$, average","ranges":[350,500,600],"measures":[100,320],"markers":[550]}, - {"title":"New Customers","subtitle":"count","ranges":[1400,2000,2500],"measures":[1000,1650],"markers":[2100]}, - {"title":"Satisfaction","subtitle":"out of 5","ranges":[3.5,4.25,5],"measures":[3.2,4.7],"markers":[4.4]} -] \ No newline at end of file diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index a0f1be25cd6..6d3565bd0cd 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2223,7 +2223,45 @@ def test_valid_color_lists_of_2_rgb_colors(self): ) def test_full_bullet(self): - df = pd.read_json('BulletData.json') + data = [ + { + "title": "Revenue", + "subtitle": "US$, in thousands", + "ranges": [150, 225, 300], + "measures":[220, 270], + "markers":[250] + }, + { + "title": "Profit", + "subtitle": "%", + "ranges": [20, 25, 30], + "measures": [21, 23], + "markers": [26] + }, + { + "title": "Order Size", + "subtitle": "US$, average", + "ranges": [350, 500, 600], + "measures": [100, 320], + "markers": [550] + }, + { + "title": "New Customers", + "subtitle": "count", + "ranges": [1400, 2000, 2500], + "measures":[1000, 1650], + "markers": [2100] + }, + { + "title": "Satisfaction", + "subtitle": "out of 5", + "ranges": [3.5, 4.25, 5], + "measures": [3.2, 4.7], + "markers": [4.4] + } + ] + + df = pd.DataFrame(data) measure_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] range_colors = ['rgb(255, 127, 14)', 'rgb(44, 160, 44)'] From af9cb1721bf34cc75f609909327da51f9004983b Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Tue, 21 Nov 2017 14:43:24 -0500 Subject: [PATCH 16/22] ensure hor and ver spacing is always float --- plotly/figure_factory/_bullet.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index a83eca696bf..1ee53cf0849 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -18,8 +18,10 @@ def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, num_of_lanes = len(df) num_of_rows = num_of_lanes if orientation == 'h' else 1 num_of_cols = 1 if orientation == 'h' else num_of_lanes - if not horizontal_spacing and not vertical_spacing: - horizontal_spacing = vertical_spacing = 1./num_of_lanes + if not horizontal_spacing: + horizontal_spacing = 1./num_of_lanes + if not vertical_spacing: + vertical_spacing = 1./num_of_lanes fig = plotly.tools.make_subplots( num_of_rows, num_of_cols, print_grid=False, horizontal_spacing=horizontal_spacing, From 6080d80822147b7206e1c9d1bfbac5132546c7b1 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Wed, 22 Nov 2017 13:01:50 -0500 Subject: [PATCH 17/22] changelog and version number bump --- CHANGELOG.md | 4 ++++ plotly/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 930060dc545..6a462538f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [2.2.2] - TBA +### Added +- bullet chart figure factory. Call `help(plotly.figure_factory.create_bullet)` for examples and how to get started making bullet charts with the API. + ## [2.2.1] - 2017-10-26 ### Fixed - presentation objects now added to setup.py diff --git a/plotly/version.py b/plotly/version.py index 36a511eca10..f1edb192fe3 100644 --- a/plotly/version.py +++ b/plotly/version.py @@ -1 +1 @@ -__version__ = '2.2.1' +__version__ = '2.2.2' From 082fdfd7ee4a346b2905d62a51605f4408178be8 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Wed, 22 Nov 2017 16:22:41 -0500 Subject: [PATCH 18/22] few last comments --- plotly/figure_factory/_bullet.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 1ee53cf0849..a3173187d43 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -128,17 +128,13 @@ def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, markers = go.Scatter( x=x, y=y, - marker=scatter_options['marker'], name='markers', hoverinfo='x' if orientation == 'h' else 'y', xaxis='x{}'.format(row + 1), - yaxis='y{}'.format(row + 1) + yaxis='y{}'.format(row + 1), + **scatter_options ) - for k in scatter_options: - if k != 'marker': - markers[k] = scatter_options[k] - fig['data'].append(markers) # titles and subtitles @@ -188,9 +184,6 @@ def create_bullet(data, markers=None, measures=None, ranges=None, of each subplot chart. :param (bool) orientation: if 'h', the bars are placed horizontally as rows. If 'v' the bars are placed vertically in the chart. - :param (int) marker_size: sets the size of the markers in the chart. - :param (str | int) marker_symbol: the symbol of the markers in the chart. - Default='diamond-tall' :param (list) range_colors: a list of two colors between which all the rectangles for the range are drawn. These rectangles are meant to be qualitative indicators against which the marker and measure bars From dfd1b2c0a14f8600c4fa5c279b9521526df734e6 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 23 Nov 2017 15:22:54 -0500 Subject: [PATCH 19/22] pandas imported message to 'installed' --- plotly/figure_factory/_bullet.py | 51 +++++++++++++++------------- plotly/figure_factory/_facet_grid.py | 2 +- 2 files changed, 29 insertions(+), 24 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index a3173187d43..8e851ea1704 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -160,14 +160,15 @@ def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, def create_bullet(data, markers=None, measures=None, ranges=None, subtitles=None, titles=None, orientation='h', - range_colors=None, measure_colors=None, + range_colors=('rgb(200, 200, 200)', 'rgb(245, 245, 245)'), + measure_colors=('rgb(31, 119, 180)', 'rgb(176, 196, 221)'), horizontal_spacing=None, vertical_spacing=None, scatter_options={}, **layout_options): """ Returns figure for bullet chart. - :param (pd.DataFrame | list) data: either a JSON list of dicts or a pandas - DataFrame. + :param (pd.DataFrame | list | tuple) data: either a list/tuple of + dictionaries or a pandas DataFrame. :param (str) markers: the column name or dictionary key for the markers in each subplot. :param (str) measures: the column name or dictionary key for the measure @@ -184,14 +185,14 @@ def create_bullet(data, markers=None, measures=None, ranges=None, of each subplot chart. :param (bool) orientation: if 'h', the bars are placed horizontally as rows. If 'v' the bars are placed vertically in the chart. - :param (list) range_colors: a list of two colors between which all + :param (list) range_colors: a tuple of two colors between which all the rectangles for the range are drawn. These rectangles are meant to be qualitative indicators against which the marker and measure bars are compared. - Default=['rgb(198, 198, 198)', 'rgb(248, 248, 248)'] - :param (list) measure_colors: a list of two colors which is used to color + Default=('rgb(200, 200, 200)', 'rgb(245, 245, 245)') + :param (list) measure_colors: a tuple of two colors which is used to color the thin quantitative bars in the bullet chart. - Default=['rgb(31, 119, 180)', 'rgb(176, 196, 221)'] + Default=('rgb(31, 119, 180)', 'rgb(176, 196, 221)') :param (float) horizontal_spacing: see the 'horizontal_spacing' param in plotly.tools.make_subplots. Ranges between 0 and 1. :param (float) vertical_spacing: see the 'vertical_spacing' param in @@ -210,20 +211,22 @@ def create_bullet(data, markers=None, measures=None, ranges=None, import plotly.figure_factory as ff data = [ - {"e": "Revenue", "d": "US$, in thousands", "c": [150, 225, 300], - "b": [220,270], "a": [250]}, - {"e": "Profit", "d": "%", "c": [20, 25, 30], "b": [21, 23], "a": [26]}, - {"e": "Order Size", "d":"US$, average","c": [350, 500, 600], - "b": [100,320],"a": [550]}, - {"e": "New Customers", "d": "count", "c": [1400, 2000, 2500], - "b": [1000,1650],"a": [2100]}, - {"e": "Satisfaction", "d": "out of 5","c": [3.5, 4.25, 5], - "b": [3.2,4.7], "a": [4.4]} + {"label": "Revenue", "sublabel": "US$, in thousands", + "range": [150, 225, 300], "performance": [220,270], "point": [250]}, + {"label": "Profit", "sublabel": "%", "range": [20, 25, 30], + "performance": [21, 23], "point": [26]}, + {"label": "Order Size", "sublabel":"US$, average","range": [350, 500, 600], + "performance": [100,320],"point": [550]}, + {"label": "New Customers", "sublabel": "count", "range": [1400, 2000, 2500], + "performance": [1000, 1650],"point": [2100]}, + {"label": "Satisfaction", "sublabel": "out of 5","range": [3.5, 4.25, 5], + "performance": [3.2, 4.7], "point": [4.4]} ] fig = ff.create_bullet( - data, titles='e', subtitles='d', markers='a', measures='b', - ranges='c', orientation='h', title='my simple bullet chart' + data, titles='label', subtitles='sublabel', markers='point', + measures='performance', ranges='range', orientation='h', + title='my simple bullet chart' ) py.iplot(fig) ``` @@ -249,23 +252,25 @@ def create_bullet(data, markers=None, measures=None, ranges=None, # validate df if not pd: raise exceptions.ImportError( - "'pandas' must be imported for this figure_factory." + "'pandas' must be installed for this figure factory." ) - if isinstance(data, list): + if isinstance(data, (tuple, list)): if not all(isinstance(item, dict) for item in data): raise exceptions.PlotlyError( - 'If your data is a list, all entries must be dictionaries.' + 'Every entry of the data argument (a list or tuple) must be ' + 'a dictionary.' ) elif not isinstance(data, pd.DataFrame): raise exceptions.PlotlyError( - 'You must input a pandas DataFrame or a list of dictionaries.' + 'You must input a pandas DataFrame, or a list or tuple of ' + 'dictionaries.' ) # make DataFrame from data with correct column headers col_names = ['titles', 'subtitle', 'markers', 'measures', 'ranges'] - if isinstance(data, list): + if isinstance(data, (tuple, list)): df = pd.DataFrame( [ [d[titles] for d in data] if titles else [''] * len(data), diff --git a/plotly/figure_factory/_facet_grid.py b/plotly/figure_factory/_facet_grid.py index 61af2b1ee0d..6d5fe33eedf 100644 --- a/plotly/figure_factory/_facet_grid.py +++ b/plotly/figure_factory/_facet_grid.py @@ -778,7 +778,7 @@ def create_facet_grid(df, x=None, y=None, facet_row=None, facet_col=None, """ if not pd: raise exceptions.ImportError( - "'pandas' must be imported for this figure_factory." + "'pandas' must be installed for this figure_factory." ) if not isinstance(df, pd.DataFrame): From a2b0ca05a53deccf70e80622a1f45a17e057686c Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 23 Nov 2017 16:32:20 -0500 Subject: [PATCH 20/22] rest of chris' comments --- plotly/figure_factory/_bullet.py | 14 ++++++++++---- plotly/tests/test_optional/test_figure_factory.py | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 8e851ea1704..25d82990762 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import collections import math from plotly import colors, exceptions, optional_imports @@ -11,6 +12,11 @@ pd = optional_imports.get_module('pandas') +def is_sequence(obj): + return (isinstance(obj, collections.Sequence) and + not isinstance(obj, basestring)) + + def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, range_colors, measure_colors, horizontal_spacing, vertical_spacing, scatter_options, layout_options): @@ -255,11 +261,11 @@ def create_bullet(data, markers=None, measures=None, ranges=None, "'pandas' must be installed for this figure factory." ) - if isinstance(data, (tuple, list)): + if is_sequence(data): if not all(isinstance(item, dict) for item in data): raise exceptions.PlotlyError( - 'Every entry of the data argument (a list or tuple) must be ' - 'a dictionary.' + 'Every entry of the data argument (list, tuple, etc) must ' + 'be a dictionary.' ) elif not isinstance(data, pd.DataFrame): @@ -270,7 +276,7 @@ def create_bullet(data, markers=None, measures=None, ranges=None, # make DataFrame from data with correct column headers col_names = ['titles', 'subtitle', 'markers', 'measures', 'ranges'] - if isinstance(data, (tuple, list)): + if is_sequence(data): df = pd.DataFrame( [ [d[titles] for d in data] if titles else [''] * len(data), diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index 6d3565bd0cd..364260888e4 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2193,7 +2193,10 @@ def test_df_as_list(self): 'foo' ] - pattern = ('If your data is a list, all entries must be dictionaries.') + pattern = ( + 'Every entry of the data argument (list, tuple, etc) must ' + 'be a dictionary.' + ) self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) def test_not_df_or_list(self): From ca9d37a62bd82c5236e06bc80917ed23ea5c9606 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 23 Nov 2017 16:44:11 -0500 Subject: [PATCH 21/22] fix tests --- plotly/figure_factory/_bullet.py | 5 ++--- plotly/tests/test_optional/test_figure_factory.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 25d82990762..97053b18982 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -264,14 +264,13 @@ def create_bullet(data, markers=None, measures=None, ranges=None, if is_sequence(data): if not all(isinstance(item, dict) for item in data): raise exceptions.PlotlyError( - 'Every entry of the data argument (list, tuple, etc) must ' + 'Every entry of the data argument list, tuple, etc must ' 'be a dictionary.' ) elif not isinstance(data, pd.DataFrame): raise exceptions.PlotlyError( - 'You must input a pandas DataFrame, or a list or tuple of ' - 'dictionaries.' + 'You must input a pandas DataFrame, or a list of dictionaries.' ) # make DataFrame from data with correct column headers diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index 364260888e4..d0a5755b371 100644 --- a/plotly/tests/test_optional/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory.py @@ -2202,7 +2202,7 @@ def test_df_as_list(self): def test_not_df_or_list(self): df = 'foo' - pattern = ('You must input a pandas DataFrame or a list of dictionaries.') + pattern = ('You must input a pandas DataFrame, or a list of dictionaries.') self.assertRaisesRegexp(PlotlyError, pattern, ff.create_bullet, df) def test_valid_color_lists_of_2_rgb_colors(self): From d0f52510002218ee5bfc12b6da1025e6a1d1aa99 Mon Sep 17 00:00:00 2001 From: Adam Kulidjian Date: Thu, 23 Nov 2017 17:17:40 -0500 Subject: [PATCH 22/22] basestring to str --- plotly/figure_factory/_bullet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/figure_factory/_bullet.py b/plotly/figure_factory/_bullet.py index 97053b18982..c23eaeb0e0b 100644 --- a/plotly/figure_factory/_bullet.py +++ b/plotly/figure_factory/_bullet.py @@ -14,7 +14,7 @@ def is_sequence(obj): return (isinstance(obj, collections.Sequence) and - not isinstance(obj, basestring)) + not isinstance(obj, str)) def _bullet(df, markers, measures, ranges, subtitles, titles, orientation,