|
| 1 | +import numpy as np |
| 2 | +import scipy as scp |
| 3 | +from collections import OrderedDict |
| 4 | +import plotly.graph_objs as go |
| 5 | + |
| 6 | +def plot_dendrogram(Z_dendrogram, cutoff_line=True, value=15, orientation='bottom', hang=30, hide_labels=False, labels=None, |
| 7 | + colorscale=None, hovertext=None, color_threshold=None): |
| 8 | + """ |
| 9 | + Modified version of Plotly _dendrogram.py that returns a dendrogram Plotly figure object with cutoff line. |
| 10 | +
|
| 11 | + :param Z_dendrogram: Matrix of observations as array of arrays |
| 12 | + :type Z_dendrogram: ndarray |
| 13 | + :param cutoff_line: plot distance cutoff line |
| 14 | + :type cutoff_line: boolean |
| 15 | + :param value: dendrogram distance for cutoff line |
| 16 | + :type value: float or int |
| 17 | + :param orientation: 'top', 'right', 'bottom', or 'left' |
| 18 | + :type orientation: str |
| 19 | + :param hang: dendrogram distance of leaf lines |
| 20 | + :type hang: float |
| 21 | + :param hide_labels: show leaf labels |
| 22 | + :type hide_labels: boolean |
| 23 | + :param labels: List of axis category labels(observation labels) |
| 24 | + :type labels: list |
| 25 | + :param colorscale: Optional colorscale for dendrogram tree |
| 26 | + :type colorscale: list |
| 27 | + :param hovertext: List of hovertext for constituent traces of dendrogram |
| 28 | + clusters |
| 29 | + :type hovertext: list[list] |
| 30 | + :param color_threshold: Value at which the separation of clusters will be made |
| 31 | + :type color_threshold: double |
| 32 | + :return: Plotly figure object |
| 33 | +
|
| 34 | + Example:: |
| 35 | +
|
| 36 | + figure = plot_dendrogram(dendro_tree, hang=0.9, cutoff_line=False) |
| 37 | + """ |
| 38 | + |
| 39 | + dendrogram = Dendrogram(Z_dendrogram, orientation, hang, hide_labels, labels, colorscale, hovertext=hovertext, color_threshold=color_threshold) |
| 40 | + |
| 41 | + if cutoff_line == True: |
| 42 | + dendrogram.layout.update({'shapes':[{'type':'line', |
| 43 | + 'xref':'paper', |
| 44 | + 'yref':'y', |
| 45 | + 'x0':0, 'y0':value, |
| 46 | + 'x1':1, 'y1':value, |
| 47 | + 'line':{'color':'red'}}]}) |
| 48 | + |
| 49 | + figure = dict(data=dendrogram.data, layout=dendrogram.layout) |
| 50 | + figure['layout']['template'] = 'plotly_white' |
| 51 | + |
| 52 | + return figure |
| 53 | + |
| 54 | +class Dendrogram(object): |
| 55 | + """Refer to plot_dendrogram() for docstring.""" |
| 56 | + def __init__(self, Z_dendrogram, orientation='bottom', hang=1, hide_labels=False, labels=None, colorscale=None, hovertext=None, |
| 57 | + color_threshold=None, width=np.inf, height=np.inf, xaxis='xaxis', yaxis='yaxis'): |
| 58 | + self.orientation = orientation |
| 59 | + self.labels = labels |
| 60 | + self.xaxis = xaxis |
| 61 | + self.yaxis = yaxis |
| 62 | + self.data = [] |
| 63 | + self.leaves = [] |
| 64 | + self.sign = {self.xaxis: 1, self.yaxis: 1} |
| 65 | + self.layout = {self.xaxis: {}, self.yaxis: {}} |
| 66 | + |
| 67 | + if self.orientation in ['left', 'bottom']: |
| 68 | + self.sign[self.xaxis] = 1 |
| 69 | + else: |
| 70 | + self.sign[self.xaxis] = -1 |
| 71 | + |
| 72 | + if self.orientation in ['right', 'bottom']: |
| 73 | + self.sign[self.yaxis] = 1 |
| 74 | + else: |
| 75 | + self.sign[self.yaxis] = -1 |
| 76 | + |
| 77 | + (dd_traces, xvals, yvals, |
| 78 | + ordered_labels, leaves) = self.get_dendrogram_traces(Z_dendrogram, hang, colorscale, |
| 79 | + hovertext, |
| 80 | + color_threshold) |
| 81 | + |
| 82 | + self.labels = ordered_labels |
| 83 | + self.leaves = leaves |
| 84 | + yvals_flat = yvals.flatten() |
| 85 | + xvals_flat = xvals.flatten() |
| 86 | + |
| 87 | + self.zero_vals = [] |
| 88 | + |
| 89 | + for i in range(len(yvals_flat)): |
| 90 | + if yvals_flat[i] == 0.0 and xvals_flat[i] not in self.zero_vals: |
| 91 | + self.zero_vals.append(xvals_flat[i]) |
| 92 | + |
| 93 | + if len(self.zero_vals) > len(yvals) + 1: |
| 94 | + l_border = int(min(self.zero_vals)) |
| 95 | + r_border = int(max(self.zero_vals)) |
| 96 | + correct_leaves_pos = range(l_border, |
| 97 | + r_border + 1, |
| 98 | + int((r_border - l_border) / len(yvals))) |
| 99 | + self.zero_vals = [v for v in correct_leaves_pos] |
| 100 | + |
| 101 | + self.zero_vals.sort() |
| 102 | + self.layout = self.set_figure_layout(width, height, hide_labels=hide_labels) |
| 103 | + self.data = dd_traces |
| 104 | + |
| 105 | + def get_color_dict(self, colorscale): |
| 106 | + """ |
| 107 | + Returns colorscale used for dendrogram tree clusters. |
| 108 | +
|
| 109 | + :param colorscale: colors to use for the plot in rgb format |
| 110 | + :type colorscale: list |
| 111 | + :return (dict): default colors mapped to the user colorscale |
| 112 | + """ |
| 113 | + |
| 114 | + # These are the color codes returned for dendrograms |
| 115 | + # We're replacing them with nicer colors |
| 116 | + d = {'r': 'red', |
| 117 | + 'g': 'green', |
| 118 | + 'b': 'blue', |
| 119 | + 'c': 'cyan', |
| 120 | + 'm': 'magenta', |
| 121 | + 'y': 'yellow', |
| 122 | + 'k': 'black', |
| 123 | + 'w': 'white'} |
| 124 | + default_colors = OrderedDict(sorted(d.items(), key=lambda t: t[0])) |
| 125 | + |
| 126 | + if colorscale is None: |
| 127 | + colorscale = [ |
| 128 | + 'rgb(0,116,217)', # blue |
| 129 | + 'rgb(35,205,205)', # cyan |
| 130 | + 'rgb(61,153,112)', # green |
| 131 | + 'rgb(40,35,35)', # black |
| 132 | + 'rgb(133,20,75)', # magenta |
| 133 | + 'rgb(255,65,54)', # red |
| 134 | + 'rgb(255,255,255)', # white |
| 135 | + 'rgb(255,220,0)'] # yellow |
| 136 | + |
| 137 | + for i in range(len(default_colors.keys())): |
| 138 | + k = list(default_colors.keys())[i] # PY3 won't index keys |
| 139 | + if i < len(colorscale): |
| 140 | + default_colors[k] = colorscale[i] |
| 141 | + |
| 142 | + return default_colors |
| 143 | + |
| 144 | + def set_axis_layout(self, axis_key, hide_labels): |
| 145 | + """ |
| 146 | + Sets and returns default axis object for dendrogram figure. |
| 147 | +
|
| 148 | + :param axis_key: E.g., 'xaxis', 'xaxis1', 'yaxis', yaxis1', etc. |
| 149 | + :type axis_key: str |
| 150 | + :return (dict): An axis_key dictionary with set parameters. |
| 151 | + """ |
| 152 | + axis_defaults = { |
| 153 | + 'type': 'linear', |
| 154 | + 'ticks': 'outside', |
| 155 | + 'mirror': 'allticks', |
| 156 | + 'rangemode': 'tozero', |
| 157 | + 'showticklabels': True, |
| 158 | + 'zeroline': False, |
| 159 | + 'showgrid': False, |
| 160 | + 'showline': True, |
| 161 | + } |
| 162 | + |
| 163 | + if len(self.labels) != 0: |
| 164 | + axis_key_labels = self.xaxis |
| 165 | + if self.orientation in ['left', 'right']: |
| 166 | + axis_key_labels = self.yaxis |
| 167 | + if axis_key_labels not in self.layout: |
| 168 | + self.layout[axis_key_labels] = {} |
| 169 | + self.layout[axis_key_labels]['tickvals'] = \ |
| 170 | + [zv*self.sign[axis_key] for zv in self.zero_vals] |
| 171 | + self.layout[axis_key_labels]['ticktext'] = self.labels |
| 172 | + self.layout[axis_key_labels]['tickmode'] = 'array' |
| 173 | + |
| 174 | + self.layout[axis_key].update(axis_defaults) |
| 175 | + |
| 176 | + if hide_labels == True: |
| 177 | + self.layout[axis_key].update({'showticklabels': False}) |
| 178 | + else: pass |
| 179 | + |
| 180 | + return self.layout[axis_key] |
| 181 | + |
| 182 | + def set_figure_layout(self, width, height, hide_labels): |
| 183 | + """ |
| 184 | + Sets and returns default layout object for dendrogram figure. |
| 185 | +
|
| 186 | + :param width: plot width |
| 187 | + :type width: int |
| 188 | + :param height: plot height |
| 189 | + :type height: int |
| 190 | + :param hide_labels: show leaf labels |
| 191 | + :type hide_labels: boolean |
| 192 | + :return: Plotly layout |
| 193 | + """ |
| 194 | + self.layout.update({ |
| 195 | + 'showlegend': False, |
| 196 | + 'autosize': False, |
| 197 | + 'hovermode': 'closest', |
| 198 | + 'width': width, |
| 199 | + 'height': height |
| 200 | + }) |
| 201 | + |
| 202 | + self.set_axis_layout(self.xaxis, hide_labels=hide_labels) |
| 203 | + self.set_axis_layout(self.yaxis, hide_labels=False) |
| 204 | + |
| 205 | + return self.layout |
| 206 | + |
| 207 | + |
| 208 | + def get_dendrogram_traces(self, Z_dendrogram, hang, colorscale, hovertext, color_threshold): |
| 209 | + """ |
| 210 | + Calculates all the elements needed for plotting a dendrogram. |
| 211 | + |
| 212 | + :param Z_dendrogram: Matrix of observations as array of arrays |
| 213 | + :type Z_dendrogram: ndarray |
| 214 | + :param hang: dendrogram distance of leaf lines |
| 215 | + :type hang: float |
| 216 | + :param colorscale: Color scale for dendrogram tree clusters |
| 217 | + :type colorscale: list |
| 218 | + :param hovertext: List of hovertext for constituent traces of dendrogram |
| 219 | + :type hovertext: list |
| 220 | + :return (tuple): Contains all the traces in the following order: |
| 221 | + |
| 222 | + a. trace_list: List of Plotly trace objects for dendrogram tree |
| 223 | + b. icoord: All X points of the dendrogram tree as array of arrays with length 4 |
| 224 | + c. dcoord: All Y points of the dendrogram tree as array of arrays with length 4 |
| 225 | + d. ordered_labels: leaf labels in the order they are going to appear on the plot |
| 226 | + e. Z_dendrogram['leaves']: left-to-right traversal of the leaves |
| 227 | + """ |
| 228 | + icoord = scp.array(Z_dendrogram['icoord']) |
| 229 | + dcoord = scp.array(Z_dendrogram['dcoord']) |
| 230 | + ordered_labels = scp.array(Z_dendrogram['ivl']) |
| 231 | + color_list = scp.array(Z_dendrogram['color_list']) |
| 232 | + colors = self.get_color_dict(colorscale) |
| 233 | + |
| 234 | + trace_list = [] |
| 235 | + |
| 236 | + for i in range(len(icoord)): |
| 237 | + if self.orientation in ['top', 'bottom']: |
| 238 | + xs = icoord[i] |
| 239 | + else: |
| 240 | + xs = dcoord[i] |
| 241 | + |
| 242 | + if self.orientation in ['top', 'bottom']: |
| 243 | + ys = dcoord[i] |
| 244 | + else: |
| 245 | + ys = icoord[i] |
| 246 | + color_key = color_list[i] |
| 247 | + hovertext_label = None |
| 248 | + if hovertext: |
| 249 | + hovertext_label = hovertext[i] |
| 250 | + |
| 251 | + coord = [list(a) for a in zip(xs, ys)] |
| 252 | + x_coord = [] |
| 253 | + y_coord = [] |
| 254 | + y_at_x = {} |
| 255 | + for n, seg in enumerate(coord): |
| 256 | + x, y = seg |
| 257 | + if y > 0 and y < y_at_x.get(x, np.inf): |
| 258 | + y_at_x[x] = y |
| 259 | + for n, seg in enumerate(coord): |
| 260 | + x, y = seg |
| 261 | + if y == 0: |
| 262 | + y = max(0, y_at_x.get(x, 0) - hang) |
| 263 | + x_coord.append(x) |
| 264 | + y_coord.append(y) |
| 265 | + |
| 266 | + trace = dict( |
| 267 | + type='scattergl', |
| 268 | + x=np.multiply(self.sign[self.xaxis], x_coord), |
| 269 | + y=np.multiply(self.sign[self.yaxis], y_coord), |
| 270 | + mode='lines', |
| 271 | + marker=dict(color='rgb(40,35,35)'), |
| 272 | + line=dict(color='rgb(40,35,35)', width=1), #dict(color=colors[color_key]), |
| 273 | + text=hovertext_label, |
| 274 | + hoverinfo='text') |
| 275 | + |
| 276 | + try: |
| 277 | + x_index = int(self.xaxis[-1]) |
| 278 | + except ValueError: |
| 279 | + x_index = '' |
| 280 | + |
| 281 | + try: |
| 282 | + y_index = int(self.yaxis[-1]) |
| 283 | + except ValueError: |
| 284 | + y_index = '' |
| 285 | + |
| 286 | + trace['xaxis'] = 'x' + x_index |
| 287 | + trace['yaxis'] = 'y' + y_index |
| 288 | + |
| 289 | + trace_list.append(trace) |
| 290 | + |
| 291 | + return trace_list, icoord, dcoord, ordered_labels, Z_dendrogram['leaves'] |
0 commit comments