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'