|
| 1 | +from __future__ import absolute_import |
| 2 | +import numpy as np |
| 3 | +from scipy.interpolate import griddata |
| 4 | +from plotly.graph_objs import graph_objs as go |
| 5 | +import warnings |
| 6 | + |
| 7 | + |
| 8 | +def _pl_deep(): |
| 9 | + return [[0.0, 'rgb(253, 253, 204)'], |
| 10 | + [0.1, 'rgb(201, 235, 177)'], |
| 11 | + [0.2, 'rgb(145, 216, 163)'], |
| 12 | + [0.3, 'rgb(102, 194, 163)'], |
| 13 | + [0.4, 'rgb(81, 168, 162)'], |
| 14 | + [0.5, 'rgb(72, 141, 157)'], |
| 15 | + [0.6, 'rgb(64, 117, 152)'], |
| 16 | + [0.7, 'rgb(61, 90, 146)'], |
| 17 | + [0.8, 'rgb(65, 64, 123)'], |
| 18 | + [0.9, 'rgb(55, 44, 80)'], |
| 19 | + [1.0, 'rgb(39, 26, 44)']] |
| 20 | + |
| 21 | + |
| 22 | +def _transform_barycentric_cartesian(): |
| 23 | + """ |
| 24 | + Returns the transformation matrix from barycentric to cartesian |
| 25 | + coordinates and conversely. |
| 26 | + """ |
| 27 | + # reference triangle |
| 28 | + tri_verts = np.array([[0.5, np.sqrt(3) / 2], [0, 0], [1, 0]]) |
| 29 | + M = np.array([tri_verts[:, 0], tri_verts[:, 1], np.ones(3)]) |
| 30 | + return M, np.linalg.inv(M) |
| 31 | + |
| 32 | + |
| 33 | +def _contour_trace(x, y, z, tooltip, ncontours=None, colorscale='Viridis', |
| 34 | + showscale=False, linewidth=0.5, |
| 35 | + linecolor='rgb(150,150,150)', |
| 36 | + coloring=None, fontcolor='blue', |
| 37 | + fontsize=12): |
| 38 | + """ |
| 39 | + Contour trace in Cartesian coordinates. |
| 40 | +
|
| 41 | + Parameters |
| 42 | + ========== |
| 43 | +
|
| 44 | + x, y : array-like |
| 45 | + Cartesian coordinates |
| 46 | + z : array-like |
| 47 | + Field to be represented as contours. |
| 48 | + tooltip : list of str |
| 49 | + Annotations to show on hover. |
| 50 | + ncontours : int or None |
| 51 | + Number of contours to display (determined automatically if None). |
| 52 | + colorscale : str o array, optional |
| 53 | + Colorscale to use for contours |
| 54 | + showscale : bool |
| 55 | + If True, a colorbar showing the color scale is displayed. |
| 56 | + linewidth : int |
| 57 | + Line width of contours |
| 58 | + linecolor : color string |
| 59 | + Color on contours |
| 60 | + coloring : None or 'lines' |
| 61 | + How to display contour. Filled contours if None, lines if ``lines``. |
| 62 | + colorscale : None or array-like |
| 63 | + Colorscale of the contours. |
| 64 | + fontcolor : color str |
| 65 | + Color of contour labels. |
| 66 | + fontsize : int |
| 67 | + Font size of contour labels. |
| 68 | + """ |
| 69 | + |
| 70 | + c_dict = dict(type='contour', |
| 71 | + x=x, y=y, z=z, |
| 72 | + text=tooltip, |
| 73 | + hoverinfo='text', |
| 74 | + ncontours=ncontours, |
| 75 | + colorscale=colorscale, |
| 76 | + showscale=showscale, |
| 77 | + line=dict(width=linewidth, color=linecolor), |
| 78 | + colorbar=dict(thickness=20, ticklen=4) |
| 79 | + ) |
| 80 | + if coloring == 'lines': |
| 81 | + contours = dict(coloring=coloring) |
| 82 | + c_dict.update(contours=contours) |
| 83 | + return go.Contour(c_dict) |
| 84 | + |
| 85 | + |
| 86 | +def barycentric_ticks(side): |
| 87 | + """ |
| 88 | + Barycentric coordinates of ticks locations. |
| 89 | +
|
| 90 | + Parameters |
| 91 | + ========== |
| 92 | + side : 0, 1 or 2 |
| 93 | + side j has 0 in the j^th position of barycentric coords of tick |
| 94 | + origin. |
| 95 | + """ |
| 96 | + p = 10 |
| 97 | + if side == 0: # where a=0 |
| 98 | + return np.array([(0, j/p, 1-j/p) for j in range(p - 2, 0, -2)]) |
| 99 | + elif side == 1: # b=0 |
| 100 | + return np.array([(i/p, 0, 1-i/p) for i in range(2, p, 2)]) |
| 101 | + elif side == 2: # c=0 |
| 102 | + return (np.array([(i/p, j/p, 0) |
| 103 | + for i in range(p - 2, 0, -2) |
| 104 | + for j in range(p - i, -1, -1) if i + j == p])) |
| 105 | + else: |
| 106 | + raise ValueError('The side can be only 0, 1, 2') |
| 107 | + |
| 108 | + |
| 109 | +def _side_coord_ticks(side, t=0.01): |
| 110 | + """ |
| 111 | + Cartesian coordinates of ticks loactions for one side (0, 1, 2) |
| 112 | + of ternary diagram. |
| 113 | +
|
| 114 | + Parameters |
| 115 | + ========== |
| 116 | +
|
| 117 | + side : int, 0, 1 or 2 |
| 118 | + Index of side |
| 119 | + t : float, default 0.01 |
| 120 | + Length of tick |
| 121 | +
|
| 122 | + Returns |
| 123 | + ======= |
| 124 | + xt, yt : lists |
| 125 | + Lists of x, resp y-coords of tick segments |
| 126 | + posx, posy : lists |
| 127 | + Lists of ticklabel positions |
| 128 | + """ |
| 129 | + M, invM = _transform_barycentric_cartesian() |
| 130 | + baryc = barycentric_ticks(side) |
| 131 | + xy1 = np.dot(M, baryc.T) |
| 132 | + xs, ys = xy1[:2] |
| 133 | + x_ticks, y_ticks, posx, posy = [], [], [], [] |
| 134 | + if side == 0: |
| 135 | + for i in range(4): |
| 136 | + x_ticks.extend([xs[i], xs[i]+t, None]) |
| 137 | + y_ticks.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) |
| 138 | + posx.extend([xs[i]+t for i in range(4)]) |
| 139 | + posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) |
| 140 | + elif side == 1: |
| 141 | + for i in range(4): |
| 142 | + x_ticks.extend([xs[i], xs[i]+t, None]) |
| 143 | + y_ticks.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) |
| 144 | + posx.extend([xs[i]+t for i in range(4)]) |
| 145 | + posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) |
| 146 | + elif side == 2: |
| 147 | + for i in range(4): |
| 148 | + x_ticks.extend([xs[i], xs[i]-2*t, None]) |
| 149 | + y_ticks.extend([ys[i], ys[i], None]) |
| 150 | + posx.extend([xs[i]-2*t for i in range(4)]) |
| 151 | + posy.extend([ys[i] for i in range(4)]) |
| 152 | + else: |
| 153 | + raise ValueError('Side can be only 0, 1, 2') |
| 154 | + return x_ticks, y_ticks, posx, posy |
| 155 | + |
| 156 | + |
| 157 | +def _cart_coord_ticks(t=0.01): |
| 158 | + """ |
| 159 | + Cartesian coordinates of ticks loactions. |
| 160 | +
|
| 161 | + Parameters |
| 162 | + ========== |
| 163 | +
|
| 164 | + t : float, default 0.01 |
| 165 | + Length of tick |
| 166 | +
|
| 167 | + Returns |
| 168 | + ======= |
| 169 | + xt, yt : lists |
| 170 | + Lists of x, resp y-coords of tick segments (all sides concatenated). |
| 171 | + posx, posy : lists |
| 172 | + Lists of ticklabel positions (all sides concatenated). |
| 173 | + """ |
| 174 | + x_ticks, y_ticks, posx, posy = [], [], [], [] |
| 175 | + for side in range(3): |
| 176 | + xt, yt, px, py = _side_coord_ticks(side, t) |
| 177 | + x_ticks.extend(xt) |
| 178 | + y_ticks.extend(yt) |
| 179 | + posx.extend(px) |
| 180 | + posy.extend(py) |
| 181 | + return x_ticks, y_ticks, posx, posy |
| 182 | + |
| 183 | + |
| 184 | +def _set_ticklabels(annotations, posx, posy, proportions=True): |
| 185 | + """ |
| 186 | +
|
| 187 | + Parameters |
| 188 | + ========== |
| 189 | +
|
| 190 | + annotations : list |
| 191 | + List of annotations previously defined in layout definition |
| 192 | + as a dict, not as an instance of go.Layout. |
| 193 | + posx, posy: lists |
| 194 | + Lists containing ticklabel position coordinates |
| 195 | + proportions : bool |
| 196 | + True when ticklabels are 0.2, 0.4, ... False when they are |
| 197 | + 20%, 40%... |
| 198 | + """ |
| 199 | + if not isinstance(annotations, list): |
| 200 | + raise ValueError('annotations should be a list') |
| 201 | + |
| 202 | + ticklabel = [0.8, 0.6, 0.4, 0.2] if proportions \ |
| 203 | + else ['80%', '60%', '40%', '20%'] |
| 204 | + |
| 205 | + # Annotations for ticklabels on side 0 |
| 206 | + annotations.extend([dict(showarrow=False, |
| 207 | + text=str(ticklabel[j]), |
| 208 | + x=posx[j], |
| 209 | + y=posy[j], |
| 210 | + align='center', |
| 211 | + xanchor='center', |
| 212 | + yanchor='top', |
| 213 | + font=dict(size=12)) for j in range(4)]) |
| 214 | + |
| 215 | + # Annotations for ticklabels on side 1 |
| 216 | + annotations.extend([dict(showarrow=False, |
| 217 | + text=str(ticklabel[j]), |
| 218 | + x=posx[j+4], |
| 219 | + y=posy[j+4], |
| 220 | + align='center', |
| 221 | + xanchor='left', |
| 222 | + yanchor='middle', |
| 223 | + font=dict(size=12)) for j in range(4)]) |
| 224 | + |
| 225 | + # Annotations for ticklabels on side 2 |
| 226 | + annotations.extend([dict(showarrow=False, |
| 227 | + text=str(ticklabel[j]), |
| 228 | + x=posx[j+8], |
| 229 | + y=posy[j+8], |
| 230 | + align='center', |
| 231 | + xanchor='right', |
| 232 | + yanchor='middle', |
| 233 | + font=dict(size=12)) for j in range(4)]) |
| 234 | + return annotations |
| 235 | + |
| 236 | + |
| 237 | +def _styling_traces_ternary(x_ticks, y_ticks): |
| 238 | + """ |
| 239 | + Traces for outer triangle of ternary plot, and corresponding ticks. |
| 240 | +
|
| 241 | + Parameters |
| 242 | + ========== |
| 243 | +
|
| 244 | + x_ticks : array_like, 1D |
| 245 | + x Cartesian coordinate of ticks |
| 246 | + y_ticks : array_like, 1D |
| 247 | + y Cartesian coordinate of ticks |
| 248 | + """ |
| 249 | + side_trace = dict(type='scatter', |
| 250 | + x=[0.5, 0, 1, 0.5], |
| 251 | + y=[np.sqrt(3)/2, 0, 0, np.sqrt(3)/2], |
| 252 | + mode='lines', |
| 253 | + line=dict(width=2, color='#444444'), |
| 254 | + hoverinfo='none') |
| 255 | + |
| 256 | + tick_trace = dict(type='scatter', |
| 257 | + x=x_ticks, |
| 258 | + y=y_ticks, |
| 259 | + mode='lines', |
| 260 | + line=dict(width=1, color='#444444'), |
| 261 | + hoverinfo='none') |
| 262 | + |
| 263 | + return side_trace, tick_trace |
| 264 | + |
| 265 | + |
| 266 | +def _ternary_layout(title='Ternary contour plot', width=550, height=525, |
| 267 | + fontfamily='Balto, sans-serif', colorbar_fontsize=14, |
| 268 | + plot_bgcolor='rgb(240,240,240)', |
| 269 | + pole_labels=['a', 'b', 'c'], label_fontsize=16): |
| 270 | + """ |
| 271 | + Layout of ternary contour plot, to be passed to ``go.FigureWidget`` |
| 272 | + object. |
| 273 | +
|
| 274 | + Parameters |
| 275 | + ========== |
| 276 | + title : str or None |
| 277 | + Title of ternary plot |
| 278 | + width : int |
| 279 | + Figure width. |
| 280 | + height : int |
| 281 | + Figure height. |
| 282 | + fontfamily : str |
| 283 | + Family of fonts |
| 284 | + colorbar_fontsize : int |
| 285 | + Font size of colorbar. |
| 286 | + plot_bgcolor : |
| 287 | + color of figure background |
| 288 | + pole_labels : str, default ['a', 'b', 'c'] |
| 289 | + Names of the three poles of the triangle. |
| 290 | + label_fontsize : int |
| 291 | + Font size of pole labels. |
| 292 | + """ |
| 293 | + return dict(title=title, |
| 294 | + font=dict(family=fontfamily, size=colorbar_fontsize), |
| 295 | + width=width, height=height, |
| 296 | + xaxis=dict(visible=False), |
| 297 | + yaxis=dict(visible=False), |
| 298 | + plot_bgcolor=plot_bgcolor, |
| 299 | + showlegend=False, |
| 300 | + # annotations for strings placed at the triangle vertices |
| 301 | + annotations=[dict(showarrow=False, |
| 302 | + text=pole_labels[0], |
| 303 | + x=0.5, |
| 304 | + y=np.sqrt(3)/2, |
| 305 | + align='center', |
| 306 | + xanchor='center', |
| 307 | + yanchor='bottom', |
| 308 | + font=dict(size=label_fontsize)), |
| 309 | + dict(showarrow=False, |
| 310 | + text=pole_labels[1], |
| 311 | + x=0, |
| 312 | + y=0, |
| 313 | + align='left', |
| 314 | + xanchor='right', |
| 315 | + yanchor='top', |
| 316 | + font=dict(size=label_fontsize)), |
| 317 | + dict(showarrow=False, |
| 318 | + text=pole_labels[2], |
| 319 | + x=1, |
| 320 | + y=0, |
| 321 | + align='right', |
| 322 | + xanchor='left', |
| 323 | + yanchor='top', |
| 324 | + font=dict(size=label_fontsize)) |
| 325 | + ]) |
| 326 | + |
| 327 | + |
| 328 | +def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): |
| 329 | + """ |
| 330 | + Tooltip annotations to be displayed on hover. |
| 331 | +
|
| 332 | + Parameters |
| 333 | + ========== |
| 334 | +
|
| 335 | + N : int |
| 336 | + Number of annotations along each axis. |
| 337 | + bar_coords : array-like |
| 338 | + Barycentric coordinates. |
| 339 | + grid_z : array |
| 340 | + Values (e.g. elevation values) at barycentric coordinates. |
| 341 | + xy1 : array-like |
| 342 | + Cartesian coordinates. |
| 343 | + mode : str, 'proportions' or 'percents' |
| 344 | + Coordinates inside the ternary plot can be displayed either as |
| 345 | + proportions (adding up to 1) or as percents (adding up to 100). |
| 346 | + """ |
| 347 | + if mode == 'proportions' or mode == 'proportion': |
| 348 | + tooltip = [ |
| 349 | + ['a: %.2f' % round(bar_coords[0][i, j], 2) + |
| 350 | + '<br>b: %.2f' % round(bar_coords[1][i, j], 2) + |
| 351 | + '<br>c: %.2f' % (round(1-round(bar_coords[0][i, j], 2) - |
| 352 | + round(bar_coords[1][i, j], 2), 2)) + |
| 353 | + '<br>z: %.2f' % round(grid_z[i, j], 2) |
| 354 | + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] |
| 355 | + for i in range(N)] |
| 356 | + elif mode == 'percents' or mode == 'percent': |
| 357 | + tooltip = [ |
| 358 | + ['a: %d' % int(100*bar_coords[0][i, j] + 0.5) + |
| 359 | + '<br>b: %d' % int(100*bar_coords[1][i, j] + 0.5) + |
| 360 | + '<br>c: %d' % (100-int(100*bar_coords[0][i, j] + 0.5) - |
| 361 | + int(100*bar_coords[1][i, j] + 0.5)) + |
| 362 | + '<br>z: %.2f' % round(grid_z[i, j], 2) |
| 363 | + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] |
| 364 | + for i in range(N)] |
| 365 | + else: |
| 366 | + raise ValueError("""tooltip mode must be either "proportions" or |
| 367 | + "percents".""") |
| 368 | + return tooltip |
| 369 | + |
| 370 | + |
| 371 | +def _prepare_barycentric_coord(b_coords): |
| 372 | + """ |
| 373 | + Check ternary coordinates and return the right barycentric coordinates. |
| 374 | + """ |
| 375 | + if not isinstance(b_coords, (list, np.ndarray)): |
| 376 | + raise ValueError('Data should be either an array of shape (n,m), or a list of n m-lists, m=2 or 3') |
| 377 | + b_coords = np.asarray(b_coords) |
| 378 | + if b_coords.shape[0] not in (2, 3): |
| 379 | + raise ValueError('A point should have 2 (a, b) or 3 (a, b, c) barycentric coordinates') |
| 380 | + if ((len(b_coords) == 3) and |
| 381 | + not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01)): |
| 382 | + msg = "The sum of coordinates should be one for all data points" |
| 383 | + raise ValueError(msg) |
| 384 | + A, B = b_coords[:2] |
| 385 | + C = 1 - (A + B) |
| 386 | + if np.any(np.stack((A, B, C)) < 0): |
| 387 | + raise ValueError('Barycentric coordinates should be positive.') |
| 388 | + return A, B, C |
| 389 | + |
| 390 | + |
| 391 | +def _compute_grid(coordinates, values, tooltip_mode): |
| 392 | + """ |
| 393 | + Compute interpolation of data points on regular grid in Cartesian |
| 394 | + coordinates. |
| 395 | +
|
| 396 | + Parameters |
| 397 | + ========== |
| 398 | +
|
| 399 | + coordinates : array-like |
| 400 | + Barycentric coordinates of data points. |
| 401 | + values : 1-d array-like |
| 402 | + Data points, field to be represented as contours. |
| 403 | + tooltip_mode : str, 'proportions' or 'percents' |
| 404 | + Coordinates inside the ternary plot can be displayed either as |
| 405 | + proportions (adding up to 1) or as percents (adding up to 100). |
| 406 | + """ |
| 407 | + A, B, C = _prepare_barycentric_coord(coordinates) |
| 408 | + M, invM = _transform_barycentric_cartesian() |
| 409 | + cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) |
| 410 | + xx, yy = cartes_coord_points[:2] |
| 411 | + x_min, x_max = xx.min(), xx.max() |
| 412 | + y_min, y_max = yy.min(), yy.max() |
| 413 | + # n_interp = max(100, int(np.sqrt(len(values)))) |
| 414 | + n_interp = 20 |
| 415 | + gr_x = np.linspace(x_min, x_max, n_interp) |
| 416 | + gr_y = np.linspace(y_min, y_max, n_interp) |
| 417 | + grid_x, grid_y = np.meshgrid(gr_x, gr_y) |
| 418 | + grid_z = griddata(cartes_coord_points[:2].T, values, (grid_x, grid_y), |
| 419 | + method='cubic') |
| 420 | + bar_coords = np.einsum('ik, kmn -> imn', invM, |
| 421 | + np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) |
| 422 | + # invalidate the points outside of the reference triangle |
| 423 | + bar_coords[np.where(bar_coords < 0)] = 0 # None |
| 424 | + # recompute back cartesian coordinates with invalid positions |
| 425 | + xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) |
| 426 | + is_nan = np.where(np.isnan(xy1[0])) |
| 427 | + grid_z[is_nan] = 0 # None |
| 428 | + tooltip = _tooltip(n_interp, bar_coords, grid_z, xy1, tooltip_mode) |
| 429 | + return grid_z, gr_x, gr_y, tooltip |
| 430 | + |
| 431 | + |
| 432 | +def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], |
| 433 | + tooltip_mode='proportions', width=500, height=500, |
| 434 | + ncontours=None, |
| 435 | + showscale=False, |
| 436 | + coloring=None, |
| 437 | + colorscale=None, |
| 438 | + plot_bgcolor='rgb(240,240,240)', |
| 439 | + title=None): |
| 440 | + """ |
| 441 | + Ternary contour plot. |
| 442 | +
|
| 443 | + Parameters |
| 444 | + ---------- |
| 445 | +
|
| 446 | + coordinates : list or ndarray |
| 447 | + Barycentric coordinates of shape (2, N) or (3, N) where N is the |
| 448 | + number of data points. The sum of the 3 coordinates is expected |
| 449 | + to be 1 for all data points. |
| 450 | + values : array-like |
| 451 | + Data points of field to be represented as contours. |
| 452 | + pole_labels : str, default ['a', 'b', 'c'] |
| 453 | + Names of the three poles of the triangle. |
| 454 | + tooltip_mode : str, 'proportions' or 'percents' |
| 455 | + Coordinates inside the ternary plot can be displayed either as |
| 456 | + proportions (adding up to 1) or as percents (adding up to 100). |
| 457 | + width : int |
| 458 | + Figure width. |
| 459 | + height : int |
| 460 | + Figure height. |
| 461 | + ncontours : int or None |
| 462 | + Number of contours to display (determined automatically if None). |
| 463 | + showscale : bool, default False |
| 464 | + If True, a colorbar showing the color scale is displayed. |
| 465 | + coloring : None or 'lines' |
| 466 | + How to display contour. Filled contours if None, lines if ``lines``. |
| 467 | + colorscale : None or array-like |
| 468 | + colorscale of the contours. |
| 469 | + plot_bgcolor : |
| 470 | + color of figure background |
| 471 | + title : str or None |
| 472 | + Title of ternary plot |
| 473 | +
|
| 474 | + Examples |
| 475 | + ======== |
| 476 | +
|
| 477 | + Example 1: ternary contour plot with filled contours |
| 478 | +
|
| 479 | + # Define coordinates |
| 480 | + a, b = np.mgrid[0:1:20j, 0:1:20j] |
| 481 | + mask = a + b <= 1 |
| 482 | + a = a[mask].ravel() |
| 483 | + b = b[mask].ravel() |
| 484 | + c = 1 - a - b |
| 485 | + # Values to be displayed as contours |
| 486 | + z = a * b * c |
| 487 | + fig = ff.create_ternary_contour(np.stack((a, b, c)), z) |
| 488 | +
|
| 489 | + It is also possible to give only two barycentric coordinates for each |
| 490 | + point, since the sum of the three coordinates is one: |
| 491 | +
|
| 492 | + fig = ff.create_ternary_contour(np.stack((a, b)), z) |
| 493 | +
|
| 494 | + Example 2: ternary contour plot with line contours |
| 495 | +
|
| 496 | + fig = ff.create_ternary_contour(np.stack((a, b)), z, coloring='lines') |
| 497 | + """ |
| 498 | + grid_z, gr_x, gr_y, tooltip = _compute_grid(coordinates, values, |
| 499 | + tooltip_mode) |
| 500 | + |
| 501 | + x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) |
| 502 | + |
| 503 | + layout = _ternary_layout(pole_labels=pole_labels, |
| 504 | + width=width, height=height, title=title, |
| 505 | + plot_bgcolor=plot_bgcolor) |
| 506 | + |
| 507 | + annotations = _set_ticklabels(layout['annotations'], posx, posy, |
| 508 | + proportions=True) |
| 509 | + if colorscale is None: |
| 510 | + colorscale = _pl_deep() |
| 511 | + |
| 512 | + contour_trace = _contour_trace(gr_x, gr_y, grid_z, tooltip, |
| 513 | + ncontours=ncontours, |
| 514 | + showscale=showscale, |
| 515 | + colorscale=colorscale, |
| 516 | + coloring=coloring) |
| 517 | + side_trace, tick_trace = _styling_traces_ternary(x_ticks, y_ticks) |
| 518 | + fig = go.Figure(data=[contour_trace, tick_trace, side_trace], |
| 519 | + layout=layout) |
| 520 | + fig.layout.annotations = annotations |
| 521 | + return fig |
0 commit comments