-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Bullet Chart FF #872
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Bullet Chart FF #872
Changes from 10 commits
ff36387
5b9ddaa
939a8ba
5796a9e
bb15f6a
8a5ea2b
4599bc2
beb6d18
8ba62a2
69bd30d
69a541d
e74996c
6da599f
79c67c7
9b9b578
af9cb17
6080d80
082fdfd
dfd1b2c
a2b0ca0
ca9d37a
d0f5251
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
|
@@ -498,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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this a typo? i thought There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can see how there's confusion. If If the input is
If There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this function will error if you input tuple-colors and colortype is set to |
||
""" | ||
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]) | ||
|
@@ -520,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 | ||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
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 | ||
|
||
pd = optional_imports.get_module('pandas') | ||
|
||
VALID_KEYS = ['title', 'subtitle', 'ranges', 'measures', 'markers'] | ||
|
||
|
||
def _bullet(df, as_rows, marker_size, marker_symbol, range_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 | ||
|
||
) | ||
|
||
# layout | ||
fig['layout'].update( | ||
dict(shapes=[]), | ||
showlegend=False, | ||
barmode='stack', | ||
margin=dict(l=120 if as_rows else 80), | ||
) | ||
|
||
if as_rows: | ||
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] | ||
|
||
# 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: | ||
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( | ||
color=inter_colors[-1 - idx] | ||
), | ||
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) | ||
|
||
# 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 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) | ||
) | ||
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'] | ||
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[row]['title'] | ||
if 'subtitle' in df: | ||
subtitle = '<br>{}'.format(df.iloc[row]['subtitle']) | ||
else: | ||
subtitle = '' | ||
label = '<b>{}</b>'.format(title) + subtitle | ||
annot = utils.annotation_dict_for_label( | ||
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 | ||
) | ||
fig['layout']['annotations'].append(annot) | ||
|
||
return fig | ||
|
||
|
||
def create_bullet(df, 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): | ||
|
||
""" | ||
Returns figure for bullet chart. | ||
:param (pd.DataFrame | list) df: 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 (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 | ||
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 (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. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we include a couple of simple examples in here? |
||
# validate df | ||
if not pd: | ||
raise exceptions.ImportError( | ||
"'pandas' must be imported for this figure_factory." | ||
|
||
) | ||
|
||
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 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') | ||
) | ||
) | ||
|
||
# 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] = [] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similarly here. if you need to change data, make a copy of their data and then mutate the copy |
||
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] | ||
|
||
fig = _bullet( | ||
df, as_rows, marker_size, marker_symbol, range_colors, measure_colors, | ||
subplot_spacing | ||
) | ||
|
||
fig['layout'].update( | ||
title=title, | ||
height=height, | ||
width=width, | ||
|
||
) | ||
|
||
return fig |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
by
rgb
do you mean like a string likergb(30, 20, 10)
? if so, could we sayconvert back to an rgb string, e.g. rgb(30, 20, 10)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's what I mean. More clearer your way.