268 lines
8.3 KiB
Python
268 lines
8.3 KiB
Python
from collections.abc import Sequence
|
|
|
|
from plotly import exceptions
|
|
from plotly.colors import (
|
|
DEFAULT_PLOTLY_COLORS,
|
|
PLOTLY_SCALES,
|
|
color_parser,
|
|
colorscale_to_colors,
|
|
colorscale_to_scale,
|
|
convert_to_RGB_255,
|
|
find_intermediate_color,
|
|
hex_to_rgb,
|
|
label_rgb,
|
|
n_colors,
|
|
unconvert_from_RGB_255,
|
|
unlabel_rgb,
|
|
validate_colors,
|
|
validate_colors_dict,
|
|
validate_colorscale,
|
|
validate_scale_values,
|
|
)
|
|
|
|
|
|
def is_sequence(obj):
|
|
return isinstance(obj, Sequence) and not isinstance(obj, str)
|
|
|
|
|
|
def validate_index(index_vals):
|
|
"""
|
|
Validates if a list contains all numbers or all strings
|
|
|
|
:raises: (PlotlyError) If there are any two items in the list whose
|
|
types differ
|
|
"""
|
|
from numbers import Number
|
|
|
|
if isinstance(index_vals[0], Number):
|
|
if not all(isinstance(item, Number) for item in index_vals):
|
|
raise exceptions.PlotlyError(
|
|
"Error in indexing column. "
|
|
"Make sure all entries of each "
|
|
"column are all numbers or "
|
|
"all strings."
|
|
)
|
|
|
|
elif isinstance(index_vals[0], str):
|
|
if not all(isinstance(item, str) for item in index_vals):
|
|
raise exceptions.PlotlyError(
|
|
"Error in indexing column. "
|
|
"Make sure all entries of each "
|
|
"column are all numbers or "
|
|
"all strings."
|
|
)
|
|
|
|
|
|
def validate_dataframe(array):
|
|
"""
|
|
Validates all strings or numbers in each dataframe column
|
|
|
|
:raises: (PlotlyError) If there are any two items in any list whose
|
|
types differ
|
|
"""
|
|
from numbers import Number
|
|
|
|
for vector in array:
|
|
if isinstance(vector[0], Number):
|
|
if not all(isinstance(item, Number) for item in vector):
|
|
raise exceptions.PlotlyError(
|
|
"Error in dataframe. "
|
|
"Make sure all entries of "
|
|
"each column are either "
|
|
"numbers or strings."
|
|
)
|
|
elif isinstance(vector[0], str):
|
|
if not all(isinstance(item, str) for item in vector):
|
|
raise exceptions.PlotlyError(
|
|
"Error in dataframe. "
|
|
"Make sure all entries of "
|
|
"each column are either "
|
|
"numbers or strings."
|
|
)
|
|
|
|
|
|
def validate_equal_length(*args):
|
|
"""
|
|
Validates that data lists or ndarrays are the same length.
|
|
|
|
:raises: (PlotlyError) If any data lists are not the same length.
|
|
"""
|
|
length = len(args[0])
|
|
if any(len(lst) != length for lst in args):
|
|
raise exceptions.PlotlyError(
|
|
"Oops! Your data lists or ndarrays " "should be the same length."
|
|
)
|
|
|
|
|
|
def validate_positive_scalars(**kwargs):
|
|
"""
|
|
Validates that all values given in key/val pairs are positive.
|
|
|
|
Accepts kwargs to improve Exception messages.
|
|
|
|
:raises: (PlotlyError) If any value is < 0 or raises.
|
|
"""
|
|
for key, val in kwargs.items():
|
|
try:
|
|
if val <= 0:
|
|
raise ValueError("{} must be > 0, got {}".format(key, val))
|
|
except TypeError:
|
|
raise exceptions.PlotlyError("{} must be a number, got {}".format(key, val))
|
|
|
|
|
|
def flatten(array):
|
|
"""
|
|
Uses list comprehension to flatten array
|
|
|
|
:param (array): An iterable to flatten
|
|
:raises (PlotlyError): If iterable is not nested.
|
|
:rtype (list): The flattened list.
|
|
"""
|
|
try:
|
|
return [item for sublist in array for item in sublist]
|
|
except TypeError:
|
|
raise exceptions.PlotlyError(
|
|
"Your data array could not be "
|
|
"flattened! Make sure your data is "
|
|
"entered as lists or ndarrays!"
|
|
)
|
|
|
|
|
|
def endpts_to_intervals(endpts):
|
|
"""
|
|
Returns a list of intervals for categorical colormaps
|
|
|
|
Accepts a list or tuple of sequentially increasing numbers and returns
|
|
a list representation of the mathematical intervals with these numbers
|
|
as endpoints. For example, [1, 6] returns [[-inf, 1], [1, 6], [6, inf]]
|
|
|
|
:raises: (PlotlyError) If input is not a list or tuple
|
|
:raises: (PlotlyError) If the input contains a string
|
|
:raises: (PlotlyError) If any number does not increase after the
|
|
previous one in the sequence
|
|
"""
|
|
length = len(endpts)
|
|
# Check if endpts is a list or tuple
|
|
if not (isinstance(endpts, (tuple)) or isinstance(endpts, (list))):
|
|
raise exceptions.PlotlyError(
|
|
"The intervals_endpts argument must "
|
|
"be a list or tuple of a sequence "
|
|
"of increasing numbers."
|
|
)
|
|
# Check if endpts contains only numbers
|
|
for item in endpts:
|
|
if isinstance(item, str):
|
|
raise exceptions.PlotlyError(
|
|
"The intervals_endpts argument "
|
|
"must be a list or tuple of a "
|
|
"sequence of increasing "
|
|
"numbers."
|
|
)
|
|
# Check if numbers in endpts are increasing
|
|
for k in range(length - 1):
|
|
if endpts[k] >= endpts[k + 1]:
|
|
raise exceptions.PlotlyError(
|
|
"The intervals_endpts argument "
|
|
"must be a list or tuple of a "
|
|
"sequence of increasing "
|
|
"numbers."
|
|
)
|
|
else:
|
|
intervals = []
|
|
# add -inf to intervals
|
|
intervals.append([float("-inf"), endpts[0]])
|
|
for k in range(length - 1):
|
|
interval = []
|
|
interval.append(endpts[k])
|
|
interval.append(endpts[k + 1])
|
|
intervals.append(interval)
|
|
# 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)
|