from collections import OrderedDict
import numpy as np
import scipy as scp
[docs]
def plot_dendrogram(
Z_dendrogram,
cutoff_line=True,
value=15,
orientation="bottom",
hang=30,
hide_labels=False,
labels=None,
colorscale=None,
hovertext=None,
color_threshold=None,
):
"""
Modified version of Plotly _dendrogram.py that
returns a dendrogram Plotly figure object with cutoff line.
:param Z_dendrogram: Matrix of observations as array of arrays
:type Z_dendrogram: ndarray
:param cutoff_line: plot distance cutoff line
:type cutoff_line: boolean
:param value: dendrogram distance for cutoff line
:type value: float or int
:param orientation: 'top', 'right', 'bottom', or 'left'
:type orientation: str
:param hang: dendrogram distance of leaf lines
:type hang: float
:param hide_labels: show leaf labels
:type hide_labels: boolean
:param labels: List of axis category labels(observation labels)
:type labels: list
:param colorscale: Optional colorscale for dendrogram tree
:type colorscale: list
:param hovertext: List of hovertext for constituent traces of dendrogram
clusters
:type hovertext: list[list]
:param color_threshold: Value at which the separation of clusters will be made
:type color_threshold: double
:return: Plotly figure object
Example::
figure = plot_dendrogram(dendro_tree, hang=0.9, cutoff_line=False)
"""
dendrogram = Dendrogram(
Z_dendrogram,
orientation,
hang,
hide_labels,
labels,
colorscale,
hovertext=hovertext,
color_threshold=color_threshold,
)
if cutoff_line:
dendrogram.layout.update(
{
"shapes": [
{
"type": "line",
"xref": "paper",
"yref": "y",
"x0": 0,
"y0": value,
"x1": 1,
"y1": value,
"line": {"color": "red"},
}
]
}
)
figure = dict(data=dendrogram.data, layout=dendrogram.layout)
figure["layout"]["template"] = "plotly_white"
return figure
[docs]
class Dendrogram(object):
"""Refer to plot_dendrogram() for docstring."""
def __init__(
self,
Z_dendrogram,
orientation="bottom",
hang=1,
hide_labels=False,
labels=None,
colorscale=None,
hovertext=None,
color_threshold=None,
width=np.inf,
height=np.inf,
xaxis="xaxis",
yaxis="yaxis",
):
self.orientation = orientation
self.labels = labels
self.xaxis = xaxis
self.yaxis = yaxis
self.data = []
self.leaves = []
self.sign = {self.xaxis: 1, self.yaxis: 1}
self.layout = {self.xaxis: {}, self.yaxis: {}}
if self.orientation in ["left", "bottom"]:
self.sign[self.xaxis] = 1
else:
self.sign[self.xaxis] = -1
if self.orientation in ["right", "bottom"]:
self.sign[self.yaxis] = 1
else:
self.sign[self.yaxis] = -1
(dd_traces, xvals, yvals, ordered_labels, leaves) = self.get_dendrogram_traces(
Z_dendrogram, hang, colorscale, hovertext, color_threshold
)
self.labels = ordered_labels
self.leaves = leaves
yvals_flat = yvals.flatten()
xvals_flat = xvals.flatten()
self.zero_vals = []
for i in range(len(yvals_flat)):
if yvals_flat[i] == 0.0 and xvals_flat[i] not in self.zero_vals:
self.zero_vals.append(xvals_flat[i])
if len(self.zero_vals) > len(yvals) + 1:
l_border = int(min(self.zero_vals))
r_border = int(max(self.zero_vals))
correct_leaves_pos = range(
l_border, r_border + 1, int((r_border - l_border) / len(yvals))
)
self.zero_vals = [v for v in correct_leaves_pos]
self.zero_vals.sort()
self.layout = self.set_figure_layout(width, height, hide_labels=hide_labels)
self.data = dd_traces
[docs]
def get_color_dict(self, colorscale):
"""
Returns colorscale used for dendrogram tree clusters.
:param colorscale: colors to use for the plot in rgb format
:type colorscale: list
:return (dict): default colors mapped to the user colorscale
"""
# These are the color codes returned for dendrograms
# We're replacing them with nicer colors
d = {
"r": "red",
"g": "green",
"b": "blue",
"c": "cyan",
"m": "magenta",
"y": "yellow",
"k": "black",
"w": "white",
}
default_colors = OrderedDict(sorted(d.items(), key=lambda t: t[0]))
if colorscale is None:
colorscale = [
"rgb(0,116,217)", # blue
"rgb(35,205,205)", # cyan
"rgb(61,153,112)", # green
"rgb(40,35,35)", # black
"rgb(133,20,75)", # magenta
"rgb(255,65,54)", # red
"rgb(255,255,255)", # white
"rgb(255,220,0)",
] # yellow
for i in range(len(default_colors.keys())):
k = list(default_colors.keys())[i] # PY3 won't index keys
if i < len(colorscale):
default_colors[k] = colorscale[i]
return default_colors
[docs]
def set_axis_layout(self, axis_key, hide_labels):
"""
Sets and returns default axis object for dendrogram figure.
:param axis_key: E.g., 'xaxis', 'xaxis1', 'yaxis', yaxis1', etc.
:type axis_key: str
:return (dict): An axis_key dictionary with set parameters.
"""
axis_defaults = {
"type": "linear",
"ticks": "outside",
"mirror": "allticks",
"rangemode": "tozero",
"showticklabels": True,
"zeroline": False,
"showgrid": False,
"showline": True,
}
if len(self.labels) != 0:
axis_key_labels = self.xaxis
if self.orientation in ["left", "right"]:
axis_key_labels = self.yaxis
if axis_key_labels not in self.layout:
self.layout[axis_key_labels] = {}
self.layout[axis_key_labels]["tickvals"] = [
zv * self.sign[axis_key] for zv in self.zero_vals
]
self.layout[axis_key_labels]["ticktext"] = self.labels
self.layout[axis_key_labels]["tickmode"] = "array"
self.layout[axis_key].update(axis_defaults)
if hide_labels:
self.layout[axis_key].update({"showticklabels": False})
else:
pass
return self.layout[axis_key]
[docs]
def get_dendrogram_traces(
self, Z_dendrogram, hang, colorscale, hovertext, color_threshold
):
"""
Calculates all the elements needed for plotting a dendrogram.
:param Z_dendrogram: Matrix of observations as array of arrays
:type Z_dendrogram: ndarray
:param hang: dendrogram distance of leaf lines
:type hang: float
:param colorscale: Color scale for dendrogram tree clusters
:type colorscale: list
:param hovertext: List of hovertext for constituent traces of dendrogram
:type hovertext: list
:return (tuple): Contains all the traces in the following order:
a. trace_list: List of Plotly trace objects for dendrogram tree
b. icoord: All X points of the dendrogram tree as array of arrays with length 4
c. dcoord: All Y points of the dendrogram tree as array of arrays with length 4
d. ordered_labels: leaf labels in the order they are going to appear on the plot
e. Z_dendrogram['leaves']: left-to-right traversal of the leaves
"""
icoord = scp.array(Z_dendrogram["icoord"])
dcoord = scp.array(Z_dendrogram["dcoord"])
ordered_labels = scp.array(Z_dendrogram["ivl"])
# color_list = scp.array(Z_dendrogram["color_list"])
# colors = self.get_color_dict(colorscale)
trace_list = []
for i in range(len(icoord)):
if self.orientation in ["top", "bottom"]:
xs = icoord[i]
else:
xs = dcoord[i]
if self.orientation in ["top", "bottom"]:
ys = dcoord[i]
else:
ys = icoord[i]
# color_key = color_list[i] # not used
hovertext_label = None
if hovertext:
hovertext_label = hovertext[i]
coord = [list(a) for a in zip(xs, ys)]
x_coord = []
y_coord = []
y_at_x = {}
for n, seg in enumerate(coord):
x, y = seg
if y > 0 and y < y_at_x.get(x, np.inf):
y_at_x[x] = y
for n, seg in enumerate(coord):
x, y = seg
if y == 0:
y = max(0, y_at_x.get(x, 0) - hang)
x_coord.append(x)
y_coord.append(y)
trace = dict(
type="scattergl",
x=np.multiply(self.sign[self.xaxis], x_coord),
y=np.multiply(self.sign[self.yaxis], y_coord),
mode="lines",
marker=dict(color="rgb(40,35,35)"),
line=dict(
color="rgb(40,35,35)", width=1
), # dict(color=colors[color_key]),
text=hovertext_label,
hoverinfo="text",
)
try:
x_index = int(self.xaxis[-1])
except ValueError:
x_index = ""
try:
y_index = int(self.yaxis[-1])
except ValueError:
y_index = ""
trace["xaxis"] = "x" + x_index
trace["yaxis"] = "y" + y_index
trace_list.append(trace)
return trace_list, icoord, dcoord, ordered_labels, Z_dendrogram["leaves"]