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