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/colors.py b/plotly/colors.py index a03128e0121..f037a142a1a 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 color, eg. (1, 0.45, 0.7) + 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': + # back to an rgb string, e.g. rgb(30, 20, 10) + inter_med_rgb = label_rgb(inter_med_tuple) + return inter_med_rgb + + return inter_med_tuple def unconvert_from_RGB_255(colors): @@ -498,29 +514,39 @@ 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]) 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': + # 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/__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/_bullet.py b/plotly/figure_factory/_bullet.py new file mode 100644 index 00000000000..c23eaeb0e0b --- /dev/null +++ b/plotly/figure_factory/_bullet.py @@ -0,0 +1,345 @@ +from __future__ import absolute_import + +import collections +import math + +from plotly import colors, exceptions, optional_imports +from plotly.figure_factory import utils + +import plotly +import plotly.graph_objs as go + +pd = optional_imports.get_module('pandas') + + +def is_sequence(obj): + return (isinstance(obj, collections.Sequence) and + not isinstance(obj, str)) + + +def _bullet(df, markers, measures, ranges, subtitles, titles, orientation, + range_colors, measure_colors, horizontal_spacing, + 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 + 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, + vertical_spacing=vertical_spacing + ) + + # 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' + 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 length_axis in key: + fig['layout'][key]['tickwidth'] = 1 + if width_axis in key: + 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] + + 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 + orientation == 'h' else [0]) + y = ([0] if orientation == 'h' else + [sorted(df.iloc[row]['ranges'])[-1 - idx]]) + bar = go.Bar( + x=x, + y=y, + marker=dict( + color=inter_colors[-1 - idx] + ), + name='ranges', + hoverinfo='x' if orientation == 'h' else 'y', + orientation=orientation, + width=2, + base=0, + xaxis='x{}'.format(row + 1), + yaxis='y{}'.format(row + 1) + ) + fig['data'].append(bar) + + # measures bars + for idx in range(len(df.iloc[row]['measures'])): + inter_colors = colors.n_colors( + measure_colors[0], measure_colors[1], + len(df.iloc[row]['measures']), 'rgb' + ) + 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, + y=y, + marker=dict( + color=inter_colors[-1 - idx] + ), + name='measures', + hoverinfo='x' if orientation == 'h' else 'y', + orientation=orientation, + width=0.4, + base=0, + xaxis='x{}'.format(row + 1), + yaxis='y{}'.format(row + 1) + ) + fig['data'].append(bar) + + # 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, + name='markers', + hoverinfo='x' if orientation == 'h' else 'y', + xaxis='x{}'.format(row + 1), + yaxis='y{}'.format(row + 1), + **scatter_options + ) + + fig['data'].append(markers) + + # titles and subtitles + title = df.iloc[row]['titles'] + if 'subtitles' in df: + subtitle = '
{}'.format(df.iloc[row]['subtitles']) + else: + subtitle = '' + label = '{}'.format(title) + subtitle + annot = utils.annotation_dict_for_label( + label, + (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) + + return fig + + +def create_bullet(data, markers=None, measures=None, ranges=None, + subtitles=None, titles=None, orientation='h', + range_colors=('rgb(200, 200, 200)', 'rgb(245, 245, 245)'), + 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 | 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 + 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 (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(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)') + :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 (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 = [ + {"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='label', subtitles='sublabel', markers='point', + measures='performance', ranges='range', 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: + raise exceptions.ImportError( + "'pandas' must be installed for this figure factory." + ) + + 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 ' + 'be a dictionary.' + ) + + elif not isinstance(data, pd.DataFrame): + raise exceptions.PlotlyError( + 'You must input a pandas DataFrame, or a list of dictionaries.' + ) + + # make DataFrame from data with correct column headers + col_names = ['titles', 'subtitle', 'markers', 'measures', 'ranges'] + if is_sequence(data): + df = pd.DataFrame( + [ + [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), + ], + index=col_names + ) + elif isinstance(data, pd.DataFrame): + df = pd.DataFrame( + [ + 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), + ], + index=col_names + ) + df = pd.DataFrame.transpose(df) + + # 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) + 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: + 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] + + # default scatter options + default_scatter = { + 'marker': {'size': 12, + 'symbol': 'diamond-tall', + 'color': 'rgb(0, 0, 0)'} + } + + if scatter_options == {}: + scatter_options.update(default_scatter) + else: + # add default options to scatter_options if they are not present + for k in default_scatter['marker']: + if k not in scatter_options['marker']: + scatter_options['marker'][k] = default_scatter['marker'][k] + + fig = _bullet( + df, markers, measures, ranges, subtitles, titles, orientation, + range_colors, measure_colors, horizontal_spacing, vertical_spacing, + scatter_options, layout_options, + ) + + return fig 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): diff --git a/plotly/figure_factory/utils.py b/plotly/figure_factory/utils.py index baffeeb68a4..f6d4778ab64 100644 --- a/plotly/figure_factory/utils.py +++ b/plotly/figure_factory/utils.py @@ -488,3 +488,85 @@ 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=text_color + ) + ) + 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) diff --git a/plotly/tests/test_optional/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory.py index 1a12ba8357b..d0a5755b371 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,538 @@ 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 = [ + {'titles': 'Revenue'}, + 'foo' + ] + + 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): + 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_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 + ) + + def test_full_bullet(self): + 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)'] + + fig = ff.create_bullet( + df, orientation='v', markers='markers', measures='measures', + ranges='ranges', subtitles='subtitle', titles='title', + range_colors=range_colors, measure_colors=measure_colors, + title='new title', + scatter_options={'marker': {'size': 30, + 'symbol': 'hourglass'}} + ) + + exp_fig = { + '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'}, + {'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'}, + {'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'}, + {'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'}, + {'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', + 'y': [4.4], + 'yaxis': 'y5'}], + 'layout': {'annotations': [{'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Revenue', + 'textangle': 0, + 'x': 0.019999999999999997, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Profit', + 'textangle': 0, + 'x': 0.26, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Order Size', + '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', + 'textangle': 0, + 'x': 0.74, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}, + {'font': {'color': '#0f0f0f', 'size': 13}, + 'showarrow': False, + 'text': 'Satisfaction', + 'textangle': 0, + 'x': 0.98, + 'xanchor': 'center', + 'xref': 'paper', + 'y': 1.03, + 'yanchor': 'middle', + 'yref': 'paper'}], + 'barmode': 'stack', + 'height': 600, + 'margin': {'l': 80}, + 'shapes': [], + 'showlegend': False, + 'title': 'new title', + 'width': 1000, + 'xaxis1': {'anchor': 'y1', + 'domain': [0.0, 0.039999999999999994], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis2': {'anchor': 'y2', + 'domain': [0.24, 0.27999999999999997], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis3': {'anchor': 'y3', + 'domain': [0.48, 0.52], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis4': {'anchor': 'y4', + 'domain': [0.72, 0.76], + 'range': [0, 1], + 'showgrid': False, + 'showticklabels': False, + 'zeroline': False}, + 'xaxis5': {'anchor': 'y5', + 'domain': [0.96, 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) 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'