from __future__ import absolute_import
import collections
import math
from plotly import exceptions, optional_imports
import plotly.colors as clrs
from plotly.figure_factory import utils
import plotly
import plotly.graph_objs as go
pd = optional_imports.get_module("pandas")
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.0 / num_of_lanes
if not vertical_spacing:
vertical_spacing = 1.0 / num_of_lanes
fig = plotly.subplots.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"]:
if "xaxis" in key or "yaxis" 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 = clrs.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.add_trace(bar)
# measures bars
for idx in range(len(df.iloc[row]["measures"])):
inter_colors = clrs.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.add_trace(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.add_trace(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"] += (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
):
"""
**deprecated**, use instead the plotly.graph_objects trace
:class:`plotly.graph_objects.Indicator`.
: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.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'
... )
>>> fig.show()
Example 2: Use a DataFrame with Custom Colors
>>> 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)
>>> fig.show()
"""
# validate df
if not pd:
raise ImportError("'pandas' must be installed for this figure factory.")
if utils.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 utils.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."
)
clrs.validate_colors(colors_list)
colors_list = clrs.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