From c6744a0b14dd47df22b9890f5ed00c6a48392010 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sat, 26 Jan 2019 23:00:47 +0100 Subject: [PATCH 01/33] poc of ternarycontour --- plotly/figure_factory/_ternarycontour.py | 301 +++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 plotly/figure_factory/_ternarycontour.py diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py new file mode 100644 index 00000000000..2696f91d8c3 --- /dev/null +++ b/plotly/figure_factory/_ternarycontour.py @@ -0,0 +1,301 @@ +from __future__ import absolute_import +import numpy as np +from scipy.interpolate import griddata +from plotly.graph_objs import graph_objs as go + + +def _pl_deep(): + return [[0.0, 'rgb(253, 253, 204)'], + [0.1, 'rgb(201, 235, 177)'], + [0.2, 'rgb(145, 216, 163)'], + [0.3, 'rgb(102, 194, 163)'], + [0.4, 'rgb(81, 168, 162)'], + [0.5, 'rgb(72, 141, 157)'], + [0.6, 'rgb(64, 117, 152)'], + [0.7, 'rgb(61, 90, 146)'], + [0.8, 'rgb(65, 64, 123)'], + [0.9, 'rgb(55, 44, 80)'], + [1.0, 'rgb(39, 26, 44)']] + + + +def tr_b2c2b(): + """ + Rreturns the transformation matrix from barycentric to cartesian + coordinates and conversely + """ + # reference triangle + tri_verts = np.array([[0.5, np.sqrt(3)/2], [0, 0], [1, 0]]) + M = np.array([tri_verts[:, 0], tri_verts[:, 1], np.ones(3)]) + return M, np.linalg.inv(M) + + +def contour_trace(x, y, z, tooltip, colorscale='Viridis', + reversescale=False, showscale=False, linewidth=0.5, + linecolor='rgb(150,150,150)', smoothing=False, + coloring=None, showlabels=False, fontcolor='blue', + fontsize=12): + + + c_dict = dict(type='contour', + x=x, + y=y, + z=z, + text=tooltip, + hoverinfo='text', + colorscale=colorscale, + reversescale=reversescale, + showscale=showscale, + line=dict(width=linewidth, color=linecolor, + smoothing=smoothing), + colorbar=dict(thickness=20, ticklen=4) + ) + if coloring == 'lines': + contours=dict(coloring =coloring, + showlabels = showlabels,) + if showlabels: + contours.update(labelfont = dict(size=fontsize, + color=fontcolor,)) + c_dict.update(contours=contours) + return go.Contour(c_dict) + + +def barycentric_ticks(side): + # side 0, 1 or 2; side j has 0 in the j^th position of barycentric coords of tick origin + # returns the list of tick origin barycentric coords + p = 10 + if side == 0: #where a=0 + return np.array([(0, j/p, 1-j/p) for j in range(p-2, 0, -2)]) + elif side == 1: # b=0 + return np.array([(i/p, 0, 1-i/p) for i in range( 2, p, 2) ]) + elif side == 2: #c=0 + return np.array([(i/p, j/p, 0) for i in range(p-2, 0, -2) for j in range(p-i, -1, -1) if i+j==p]) + else: + raise ValueError('The side can be only 0, 1, 2') + + + +def cart_coord_ticks(side, xt, yt, posx, posy, t=0.01): + + # side 0, 1 or 2 + # each tick segment is parameterized as (x(s), y(s)), s in [0, t] + # M is the transformation matrix from barycentric to cartesian coords + # xt, yt are the lists of x, resp y-coords of tick segments + # posx, posy are the lists of ticklabel positions for side 0, 1, 2 (concatenated) + + M, invM = tr_b2c2b() + baryc = barycentric_ticks(side) + xy1 = np.dot(M, baryc.T) + xs, ys = xy1[:2] + + if side == 0: + for i in range(4): + xt.extend([xs[i], xs[i]+t, None]) + yt.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) + posx.extend([xs[i]+t for i in range(4)]) + posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) + + elif side == 1: + for i in range(4): + xt.extend([xs[i], xs[i]+t, None]) + yt.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) + posx.extend([xs[i]+t for i in range(4)]) + posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) + + elif side == 2: + for i in range(4): + xt.extend([xs[i], xs[i]-2*t, None]) + yt.extend([ys[i], ys[i], None]) + posx.extend([xs[i]-2*t for i in range(4)]) + posy.extend([ys[i] for i in range(4)]) + else: + raise ValueError('side can be only 0,1,2') + return xt, yt, posx, posy + + +def set_ticklabels(annotations, posx, posy, proportion=True): + """ + annotations: list of annotations previously defined in layout definition as a dict, + not as an instance of go.Layout + posx, posy: lists containing ticklabel position coordinates + proportion - boolean; True when ticklabels are 0.2, 0.4, ... False when they are 20%, 40%... + """ + if not isinstance(annotations, list): + raise ValueError('annotations should be a list') + + ticklabel = [0.8, 0.6, 0.4, 0.2] if proportion else ['80%', '60%', '40%', '20%'] + + annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 0 + text=f'{ticklabel[j]}', + x=posx[j], + y=posy[j], + align='center', + xanchor='center', + yanchor='top', + font=dict(size=12)) for j in range(4)]) + + annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 1 + text=f'{ticklabel[j]}', + x=posx[j+4], + y=posy[j+4], + align='center', + xanchor='left', + yanchor='middle', + font=dict(size=12)) for j in range(4)]) + + annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 2 + text=f'{ticklabel[j]}', + x=posx[j+8], + y=posy[j+8], + align='center', + xanchor='right', + yanchor='middle', + font=dict(size=12)) for j in range(4)]) + return annotations + + + +def styling_traces(xt, yt): + side_trace = dict(type='scatter', + x=[0.5, 0, 1, 0.5], + y=[np.sqrt(3)/2, 0, 0, np.sqrt(3)/2], + mode='lines', + line=dict(width=2, color='#444444'), + hoverinfo='none') + + tick_trace = dict(type='scatter', + x=xt, + y=yt, + mode='lines', + line=dict(width=1, color='#444444'), + hoverinfo='none') + + return side_trace, tick_trace + + + +def ternary_layout(title='Ternary contour plot', width=550, height=525, + fontfamily= 'Balto, sans-serif' , lfontsize=14, + plot_bgcolor='rgb(240,240,240)', + vertex_text=['a', 'b', 'c'], v_fontsize=14): + + return dict(title=title, + font=dict(family=fontfamily, size=lfontsize), + width=width, height=height, + xaxis=dict(visible=False), + yaxis=dict(visible=False), + plot_bgcolor=plot_bgcolor, + showlegend=False, + #annotations for strings placed at the triangle vertices + annotations=[dict(showarrow=False, + text=vertex_text[0], + x=0.5, + y=np.sqrt(3)/2, + align='center', + xanchor='center', + yanchor='bottom', + font=dict(size=v_fontsize)), + dict(showarrow=False, + text=vertex_text[1], + x=0, + y=0, + align='left', + xanchor='right', + yanchor='top', + font=dict(size=v_fontsize)), + dict(showarrow=False, + text=vertex_text[2], + x=1, + y=0, + align='right', + xanchor='left', + yanchor='top', + font=dict(size=v_fontsize)) + ]) + + +def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): + if mode == 'proportions': + tooltip = [ + [f'a: {round(bar_coords[0][i,j], 2)}
b: {round(bar_coords[1][i,j], 2)}'+ + f'
c: {round(1-round(bar_coords[0][i,j], 2) - round(bar_coords[1][i,j], 2), 2)}'+ + f'
z: {round(grid_z[i,j],2)}' + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] + for i in range(N)] + else: + tooltip = [ + [f'a: {int(100*bar_coords[0][i,j]+0.5)}
b: {int(100*bar_coords[1][i,j]+0.5)}'+ + f'
c: {100-int(100*bar_coords[0][i,j]+0.5) -int(100*bar_coords[1][i,j]+0.5)}'+ + f'
z: {round(grid_z[i,j],2)}' + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] + for i in range(N)] + return tooltip + + +def _compute_grid(data, M, N=150): + A, B, C, z = data + M, invM = tr_b2c2b() + cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) + xx, yy = cartes_coord_points[:2] + x_min, x_max = xx.min(), xx.max() + y_min, y_max = yy.min(), yy.max() + gr_x = np.linspace(x_min, x_max, N) + gr_y = np.linspace(y_min, y_max, N) + grid_x, grid_y = np.meshgrid(gr_x, gr_y) + grid_z = griddata(cartes_coord_points[:2].T, z, (grid_x, grid_y), method='cubic') + bar_coords = np.einsum('ik, kmn -> imn', invM, + np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) + # invalidate the points outside of the reference triangle + bar_coords[np.where(bar_coords < 0)] = None + # recompute back the cartesian coordinates of bar_coords with + # invalid positions + xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) + is_nan = np.where(np.isnan(xy1[0])) + grid_z[is_nan] = None + return grid_z + + + + +def create_ternarycontour(data, N=150, tooltip_mode='proportion', + showscale=False, coloring='lines', **kwargs): + A, B, C, z = data + M, invM = tr_b2c2b() + cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) + xx, yy = cartes_coord_points[:2] + x_min, x_max = xx.min(), xx.max() + y_min, y_max = yy.min(), yy.max() + gr_x = np.linspace(x_min, x_max, N) + gr_y = np.linspace(y_min, y_max, N) + grid_x, grid_y = np.meshgrid(gr_x, gr_y) + grid_z = griddata(cartes_coord_points[:2].T, z, (grid_x, grid_y), method='cubic') + bar_coords = np.einsum('ik, kmn -> imn', invM, + np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) + # invalidate the points outside of the reference triangle + bar_coords[np.where(bar_coords < 0)] = None + # recompute back the cartesian coordinates of bar_coords with + # invalid positions + xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) + is_nan = np.where(np.isnan(xy1[0])) + grid_z[is_nan] = None + + xt = [] + yt = [] + posx = [] + posy = [] + for side in [0, 1, 2]: + xt, yt, posx, posy = cart_coord_ticks(side, xt, yt, posx, posy, t=0.01) + + layout = ternary_layout() + annotations = set_ticklabels(layout['annotations'], posx, posy, + proportion=True) + pl_deep = _pl_deep() + tooltip = _tooltip(N, bar_coords, grid_z, xy1, tooltip_mode) + c_trace = contour_trace(gr_x, gr_y, grid_z, tooltip, showscale=showscale, + colorscale=pl_deep, reversescale=True, + coloring=coloring) + side_trace, tick_trace = styling_traces(xt, yt) + fig = go.FigureWidget(data=[c_trace, tick_trace, side_trace], + layout=layout) + fig.layout.annotations = annotations + return fig From 82b600d5a88192a58c5c3678e46b529cc2e92206 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 27 Jan 2019 23:09:25 +0100 Subject: [PATCH 02/33] ternary contour figure factory --- plotly/figure_factory/README.md | 4 + plotly/figure_factory/__init__.py | 1 + plotly/figure_factory/_ternarycontour.py | 488 +++++++++++------- .../test_figure_factory.py | 14 + 4 files changed, 309 insertions(+), 198 deletions(-) diff --git a/plotly/figure_factory/README.md b/plotly/figure_factory/README.md index c88385ad457..53e01b5c79e 100644 --- a/plotly/figure_factory/README.md +++ b/plotly/figure_factory/README.md @@ -142,6 +142,10 @@ It is often not a good idea to put all your code into your `create_foo()` functi It is best to make all other functions besides `create_foo()` secret so a user cannot access them. This is done by placing a `_` before the name of the function, so `_aux_func()` for example. +6. Tests + +Add unit tests in +`plotly/tests/test_optional/test_figure_factory/test_figure_factory.py`. ## Create a Pull Request diff --git a/plotly/figure_factory/__init__.py b/plotly/figure_factory/__init__.py index a8be19872e1..910ffa62f13 100644 --- a/plotly/figure_factory/__init__.py +++ b/plotly/figure_factory/__init__.py @@ -18,6 +18,7 @@ from plotly.figure_factory._scatterplot import create_scatterplotmatrix from plotly.figure_factory._streamline import create_streamline from plotly.figure_factory._table import create_table +from plotly.figure_factory._ternarycontour import create_ternarycontour from plotly.figure_factory._trisurf import create_trisurf from plotly.figure_factory._violin import create_violin if optional_imports.get_module('pandas') is not None: diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 2696f91d8c3..2fd0b8a72d9 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -2,26 +2,26 @@ import numpy as np from scipy.interpolate import griddata from plotly.graph_objs import graph_objs as go +import warnings def _pl_deep(): return [[0.0, 'rgb(253, 253, 204)'], - [0.1, 'rgb(201, 235, 177)'], - [0.2, 'rgb(145, 216, 163)'], - [0.3, 'rgb(102, 194, 163)'], - [0.4, 'rgb(81, 168, 162)'], - [0.5, 'rgb(72, 141, 157)'], - [0.6, 'rgb(64, 117, 152)'], - [0.7, 'rgb(61, 90, 146)'], - [0.8, 'rgb(65, 64, 123)'], - [0.9, 'rgb(55, 44, 80)'], - [1.0, 'rgb(39, 26, 44)']] - - - -def tr_b2c2b(): + [0.1, 'rgb(201, 235, 177)'], + [0.2, 'rgb(145, 216, 163)'], + [0.3, 'rgb(102, 194, 163)'], + [0.4, 'rgb(81, 168, 162)'], + [0.5, 'rgb(72, 141, 157)'], + [0.6, 'rgb(64, 117, 152)'], + [0.7, 'rgb(61, 90, 146)'], + [0.8, 'rgb(65, 64, 123)'], + [0.9, 'rgb(55, 44, 80)'], + [1.0, 'rgb(39, 26, 44)']] + + +def _transform_barycentric_cartesian(): """ - Rreturns the transformation matrix from barycentric to cartesian + Returns the transformation matrix from barycentric to cartesian coordinates and conversely """ # reference triangle @@ -30,132 +30,148 @@ def tr_b2c2b(): return M, np.linalg.inv(M) -def contour_trace(x, y, z, tooltip, colorscale='Viridis', - reversescale=False, showscale=False, linewidth=0.5, - linecolor='rgb(150,150,150)', smoothing=False, - coloring=None, showlabels=False, fontcolor='blue', - fontsize=12): - +def _contour_trace(x, y, z, tooltip, colorscale='Viridis', + reversescale=False, showscale=False, linewidth=0.5, + linecolor='rgb(150,150,150)', smoothing=False, + coloring=None, showlabels=False, fontcolor='blue', + fontsize=12): + """ + Contour trace in Cartesian coordinates. + """ + if showlabels and coloring is not 'lines': + msg = """`showlabels` was set to True, but labels can only be + displayed for `coloring='lines'`""" + warnings.warn(msg) c_dict = dict(type='contour', - x=x, - y=y, - z=z, - text=tooltip, - hoverinfo='text', - colorscale=colorscale, - reversescale=reversescale, - showscale=showscale, - line=dict(width=linewidth, color=linecolor, - smoothing=smoothing), - colorbar=dict(thickness=20, ticklen=4) - ) + x=x, y=y, z=z, + text=tooltip, + hoverinfo='text', + colorscale=colorscale, + reversescale=reversescale, + showscale=showscale, + line=dict(width=linewidth, color=linecolor, + smoothing=smoothing), + colorbar=dict(thickness=20, ticklen=4) + ) if coloring == 'lines': - contours=dict(coloring =coloring, - showlabels = showlabels,) + contours = dict(coloring=coloring, + showlabels=showlabels) if showlabels: - contours.update(labelfont = dict(size=fontsize, + contours.update(labelfont=dict(size=fontsize, color=fontcolor,)) - c_dict.update(contours=contours) - return go.Contour(c_dict) + c_dict.update(contours=contours) + return go.Contour(c_dict) def barycentric_ticks(side): - # side 0, 1 or 2; side j has 0 in the j^th position of barycentric coords of tick origin - # returns the list of tick origin barycentric coords + """ + side 0, 1 or 2; side j has 0 in the j^th position of barycentric + coords of tick origin + returns the list of tick origin barycentric coords + """ p = 10 - if side == 0: #where a=0 - return np.array([(0, j/p, 1-j/p) for j in range(p-2, 0, -2)]) - elif side == 1: # b=0 - return np.array([(i/p, 0, 1-i/p) for i in range( 2, p, 2) ]) - elif side == 2: #c=0 - return np.array([(i/p, j/p, 0) for i in range(p-2, 0, -2) for j in range(p-i, -1, -1) if i+j==p]) + if side == 0: # where a=0 + return np.array([(0, j/p, 1-j/p) for j in range(p - 2, 0, -2)]) + elif side == 1: # b=0 + return np.array([(i/p, 0, 1-i/p) for i in range(2, p, 2)]) + elif side == 2: # c=0 + return (np.array([(i/p, j/p, 0) + for i in range(p-2, 0, -2) + for j in range(p-i, -1, -1) if i+j == p])) else: raise ValueError('The side can be only 0, 1, 2') - -def cart_coord_ticks(side, xt, yt, posx, posy, t=0.01): - - # side 0, 1 or 2 - # each tick segment is parameterized as (x(s), y(s)), s in [0, t] - # M is the transformation matrix from barycentric to cartesian coords - # xt, yt are the lists of x, resp y-coords of tick segments - # posx, posy are the lists of ticklabel positions for side 0, 1, 2 (concatenated) - - M, invM = tr_b2c2b() - baryc = barycentric_ticks(side) +def _cart_coord_ticks(t=0.01): + """ + side 0, 1 or 2 + each tick segment is parameterized as (x(s), y(s)), s in [0, t] + M is the transformation matrix from barycentric to cartesian coords + xt, yt are the lists of x, resp y-coords of tick segments + posx, posy are the lists of ticklabel positions for side 0, 1, 2 + (concatenated) + """ + M, invM = _transform_barycentric_cartesian() + x_ticks, y_ticks, posx, posy = [], [], [], [] + # Side 0 + baryc = barycentric_ticks(0) xy1 = np.dot(M, baryc.T) xs, ys = xy1[:2] - - if side == 0: - for i in range(4): - xt.extend([xs[i], xs[i]+t, None]) - yt.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) - posx.extend([xs[i]+t for i in range(4)]) - posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) - - elif side == 1: - for i in range(4): - xt.extend([xs[i], xs[i]+t, None]) - yt.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) - posx.extend([xs[i]+t for i in range(4)]) - posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) - - elif side == 2: - for i in range(4): - xt.extend([xs[i], xs[i]-2*t, None]) - yt.extend([ys[i], ys[i], None]) - posx.extend([xs[i]-2*t for i in range(4)]) - posy.extend([ys[i] for i in range(4)]) - else: - raise ValueError('side can be only 0,1,2') - return xt, yt, posx, posy + for i in range(4): + x_ticks.extend([xs[i], xs[i]+t, None]) + y_ticks.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) + posx.extend([xs[i]+t for i in range(4)]) + posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) + # Side 1 + baryc = barycentric_ticks(1) + xy1 = np.dot(M, baryc.T) + xs, ys = xy1[:2] + for i in range(4): + x_ticks.extend([xs[i], xs[i]+t, None]) + y_ticks.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) + posx.extend([xs[i]+t for i in range(4)]) + posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) + # Side 2 + baryc = barycentric_ticks(2) + xy1 = np.dot(M, baryc.T) + xs, ys = xy1[:2] + for i in range(4): + x_ticks.extend([xs[i], xs[i]-2*t, None]) + y_ticks.extend([ys[i], ys[i], None]) + posx.extend([xs[i]-2*t for i in range(4)]) + posy.extend([ys[i] for i in range(4)]) + return x_ticks, y_ticks, posx, posy -def set_ticklabels(annotations, posx, posy, proportion=True): +def _set_ticklabels(annotations, posx, posy, proportion=True): """ - annotations: list of annotations previously defined in layout definition as a dict, - not as an instance of go.Layout + annotations: list of annotations previously defined in layout definition + as a dict, not as an instance of go.Layout posx, posy: lists containing ticklabel position coordinates - proportion - boolean; True when ticklabels are 0.2, 0.4, ... False when they are 20%, 40%... + proportion - boolean; True when ticklabels are 0.2, 0.4, ... + False when they are 20%, 40%... """ if not isinstance(annotations, list): raise ValueError('annotations should be a list') - - ticklabel = [0.8, 0.6, 0.4, 0.2] if proportion else ['80%', '60%', '40%', '20%'] - - annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 0 + + ticklabel = [0.8, 0.6, 0.4, 0.2] if proportion \ + else ['80%', '60%', '40%', '20%'] + + # Annotations for ticklabels on side 0 + annotations.extend([dict(showarrow=False, text=f'{ticklabel[j]}', x=posx[j], y=posy[j], align='center', - xanchor='center', + xanchor='center', yanchor='top', font=dict(size=12)) for j in range(4)]) - - annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 1 + + # Annotations for ticklabels on side 1 + annotations.extend([dict(showarrow=False, text=f'{ticklabel[j]}', x=posx[j+4], y=posy[j+4], align='center', - xanchor='left', + xanchor='left', yanchor='middle', font=dict(size=12)) for j in range(4)]) - annotations.extend([dict(showarrow=False, # annotations for ticklabels on side 2 + # Annotations for ticklabels on side 2 + annotations.extend([dict(showarrow=False, text=f'{ticklabel[j]}', x=posx[j+8], y=posy[j+8], align='center', - xanchor='right', + xanchor='right', yanchor='middle', font=dict(size=12)) for j in range(4)]) return annotations - -def styling_traces(xt, yt): +def _styling_traces_ternary(x_ticks, y_ticks): + # Outer triangle side_trace = dict(type='scatter', x=[0.5, 0, 1, 0.5], y=[np.sqrt(3)/2, 0, 0, np.sqrt(3)/2], @@ -164,8 +180,8 @@ def styling_traces(xt, yt): hoverinfo='none') tick_trace = dict(type='scatter', - x=xt, - y=yt, + x=x_ticks, + y=y_ticks, mode='lines', line=dict(width=1, color='#444444'), hoverinfo='none') @@ -173,129 +189,205 @@ def styling_traces(xt, yt): return side_trace, tick_trace - -def ternary_layout(title='Ternary contour plot', width=550, height=525, - fontfamily= 'Balto, sans-serif' , lfontsize=14, +def _ternary_layout(title='Ternary contour plot', width=550, height=525, + fontfamily='Balto, sans-serif', lfontsize=14, plot_bgcolor='rgb(240,240,240)', - vertex_text=['a', 'b', 'c'], v_fontsize=14): - - return dict(title=title, - font=dict(family=fontfamily, size=lfontsize), - width=width, height=height, - xaxis=dict(visible=False), - yaxis=dict(visible=False), - plot_bgcolor=plot_bgcolor, - showlegend=False, - #annotations for strings placed at the triangle vertices - annotations=[dict(showarrow=False, - text=vertex_text[0], - x=0.5, - y=np.sqrt(3)/2, - align='center', - xanchor='center', - yanchor='bottom', - font=dict(size=v_fontsize)), - dict(showarrow=False, - text=vertex_text[1], - x=0, - y=0, - align='left', - xanchor='right', - yanchor='top', - font=dict(size=v_fontsize)), - dict(showarrow=False, - text=vertex_text[2], - x=1, - y=0, - align='right', - xanchor='left', - yanchor='top', - font=dict(size=v_fontsize)) - ]) + pole_labels=['a', 'b', 'c'], v_fontsize=14): + return dict(title=title, + font=dict(family=fontfamily, size=lfontsize), + width=width, height=height, + xaxis=dict(visible=False), + yaxis=dict(visible=False), + plot_bgcolor=plot_bgcolor, + showlegend=False, + # annotations for strings placed at the triangle vertices + annotations=[dict(showarrow=False, + text=pole_labels[0], + x=0.5, + y=np.sqrt(3)/2, + align='center', + xanchor='center', + yanchor='bottom', + font=dict(size=v_fontsize)), + dict(showarrow=False, + text=pole_labels[1], + x=0, + y=0, + align='left', + xanchor='right', + yanchor='top', + font=dict(size=v_fontsize)), + dict(showarrow=False, + text=pole_labels[2], + x=1, + y=0, + align='right', + xanchor='left', + yanchor='top', + font=dict(size=v_fontsize)) + ]) def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): if mode == 'proportions': tooltip = [ - [f'a: {round(bar_coords[0][i,j], 2)}
b: {round(bar_coords[1][i,j], 2)}'+ - f'
c: {round(1-round(bar_coords[0][i,j], 2) - round(bar_coords[1][i,j], 2), 2)}'+ - f'
z: {round(grid_z[i,j],2)}' - if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] - for i in range(N)] - else: + [f'a: {round(bar_coords[0][i,j], 2)}
b: {round(bar_coords[1][i,j], 2)}' + + f'
c: {round(1-round(bar_coords[0][i,j], 2) - round(bar_coords[1][i,j], 2), 2)}' + + f'
z: {round(grid_z[i,j],2)}' + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] + for i in range(N)] + elif mode == 'percents': tooltip = [ - [f'a: {int(100*bar_coords[0][i,j]+0.5)}
b: {int(100*bar_coords[1][i,j]+0.5)}'+ - f'
c: {100-int(100*bar_coords[0][i,j]+0.5) -int(100*bar_coords[1][i,j]+0.5)}'+ - f'
z: {round(grid_z[i,j],2)}' - if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] - for i in range(N)] + [f'a: {int(100*bar_coords[0][i,j]+0.5)}
b: {int(100*bar_coords[1][i,j]+0.5)}' + + f'
c: {100-int(100*bar_coords[0][i,j]+0.5) -int(100*bar_coords[1][i,j]+0.5)}' + + f'
z: {round(grid_z[i,j],2)}' + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] + for i in range(N)] + else: + raise ValueError("""tooltip mode must be either "proportions" or + "percents".""") return tooltip -def _compute_grid(data, M, N=150): - A, B, C, z = data - M, invM = tr_b2c2b() +def _prepare_barycentric_coord(b_coords): + """ + check ternary data and return the right barycentric coordinates + """ + if not isinstance(b_coords, (list, np.ndarray)): + raise ValueError('Data should be either an array of shape (n,m), or a list of n m-lists, m=2 or 3') + b_coords = np.asarray(b_coords) + if b_coords.shape[0] not in (2, 3): + raise ValueError('A point should have 2 (a, b) or 3 (a, b, c) barycentric coordinates') + if ((len(b_coords) == 3) and + not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01)): + msg = "The sum of coordinates is not one for all data points" + warnings.warn(msg) + A, B = b_coords[:2] + C = 1 - (A + B) + return A, B, C + + +def _compute_grid(coordinates, values, tooltip_mode): + A, B, C = _prepare_barycentric_coord(coordinates) + M, invM = _transform_barycentric_cartesian() cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) xx, yy = cartes_coord_points[:2] x_min, x_max = xx.min(), xx.max() y_min, y_max = yy.min(), yy.max() - gr_x = np.linspace(x_min, x_max, N) - gr_y = np.linspace(y_min, y_max, N) + n_interp = max(100, int(np.sqrt(len(values)))) + gr_x = np.linspace(x_min, x_max, n_interp) + gr_y = np.linspace(y_min, y_max, n_interp) grid_x, grid_y = np.meshgrid(gr_x, gr_y) - grid_z = griddata(cartes_coord_points[:2].T, z, (grid_x, grid_y), method='cubic') + grid_z = griddata(cartes_coord_points[:2].T, values, (grid_x, grid_y), + method='cubic') bar_coords = np.einsum('ik, kmn -> imn', invM, - np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) + np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) # invalidate the points outside of the reference triangle - bar_coords[np.where(bar_coords < 0)] = None + bar_coords[np.where(bar_coords < 0)] = None # recompute back the cartesian coordinates of bar_coords with # invalid positions xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) is_nan = np.where(np.isnan(xy1[0])) grid_z[is_nan] = None - return grid_z - + tooltip = _tooltip(n_interp, bar_coords, grid_z, xy1, tooltip_mode) + return grid_z, gr_x, gr_y, tooltip +def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], + tooltip_mode='proportion', width=500, height=500, + showscale=False, coloring=None, + showlabels=False, colorscale=None, + plot_bgcolor='rgb(240,240,240)', + title=None, + smoothing=False): + """ + Ternary contour plot. + + Parameters + ---------- + + coordinates : list or ndarray + Barycentric coordinates of shape (2, N) or (3, N) where N is the + number of points. + values : array-like + Field to be represented as contours. + pole_labels : str, default ['a', 'b', 'c'] + Names of the three poles of the triangle. + tooltip_mode : str, 'proportions' or 'percents' + Coordinates inside the ternary plot can be displayed either as + proportions (adding up to 1) or as percents (adding up to 100). + width : int + Figure width. + height : int + Figure height. + showscale : bool, default False + If True, a colorbar showing the color scale is displayed. + coloring : None or 'lines' + How to display contour. Filled contours if None, lines if ``lines``. + showlabels : bool, default False + For line contours (coloring='lines'), the value of the contour is + displayed if showlabels is True. + colorscale : None or array-like + colorscale of the contours. + plot_bgcolor : + color of figure background + title : str or None + Title of ternary plot + smoothing : bool + If True, contours are smoothed. + + Examples + ======== + + Example 1: ternary contour plot with filled contours + + # Define coordinates + a, b = np.mgrid[0:1:20j, 0:1:20j] + a = a.ravel() + b = b.ravel() + c = 1 - a - b + # Values to be displayed as contours + z = a * b * c + fig = ff.create_ternarycontour(np.stack((a, b, c)), z) + + It is also possible to give only two barycentric coordinates for each + point, since the sum of the three coordinates is one: + + fig = ff.create_ternarycontour(np.stack((a, b)), z) + + Example 2: ternary contour plot with line contours + + fig = ff.create_ternarycontour(np.stack((a, b)), z, coloring='lines') + + Labels of contour plots can be displayed on the contours: + + fig = ff.create_ternarycontour(np.stack((a, b)), z, coloring='lines', + showlabels=True) -def create_ternarycontour(data, N=150, tooltip_mode='proportion', - showscale=False, coloring='lines', **kwargs): - A, B, C, z = data - M, invM = tr_b2c2b() - cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) - xx, yy = cartes_coord_points[:2] - x_min, x_max = xx.min(), xx.max() - y_min, y_max = yy.min(), yy.max() - gr_x = np.linspace(x_min, x_max, N) - gr_y = np.linspace(y_min, y_max, N) - grid_x, grid_y = np.meshgrid(gr_x, gr_y) - grid_z = griddata(cartes_coord_points[:2].T, z, (grid_x, grid_y), method='cubic') - bar_coords = np.einsum('ik, kmn -> imn', invM, - np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) - # invalidate the points outside of the reference triangle - bar_coords[np.where(bar_coords < 0)] = None - # recompute back the cartesian coordinates of bar_coords with - # invalid positions - xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) - is_nan = np.where(np.isnan(xy1[0])) - grid_z[is_nan] = None - - xt = [] - yt = [] - posx = [] - posy = [] - for side in [0, 1, 2]: - xt, yt, posx, posy = cart_coord_ticks(side, xt, yt, posx, posy, t=0.01) - - layout = ternary_layout() - annotations = set_ticklabels(layout['annotations'], posx, posy, - proportion=True) - pl_deep = _pl_deep() - tooltip = _tooltip(N, bar_coords, grid_z, xy1, tooltip_mode) - c_trace = contour_trace(gr_x, gr_y, grid_z, tooltip, showscale=showscale, - colorscale=pl_deep, reversescale=True, - coloring=coloring) - side_trace, tick_trace = styling_traces(xt, yt) - fig = go.FigureWidget(data=[c_trace, tick_trace, side_trace], - layout=layout) + """ + M, invM = _transform_barycentric_cartesian() + grid_z, gr_x, gr_y, tooltip = _compute_grid(coordinates, values, + tooltip_mode) + + x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) + + layout = _ternary_layout(pole_labels=pole_labels, + width=width, height=height, title=title, + plot_bgcolor=plot_bgcolor) + + annotations = _set_ticklabels(layout['annotations'], posx, posy, + proportion=True) + if colorscale is None: + colorscale = _pl_deep() + + contour_trace = _contour_trace(gr_x, gr_y, grid_z, tooltip, + showscale=showscale, + showlabels=showlabels, + colorscale=colorscale, reversescale=True, + coloring=coloring, + smoothing=smoothing) + side_trace, tick_trace = _styling_traces_ternary(x_ticks, y_ticks) + fig = go.FigureWidget(data=[contour_trace, tick_trace, side_trace], + layout=layout) fig.layout.annotations = annotations return fig diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 37834ec3957..382becd4b65 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2858,6 +2858,7 @@ def test_full_choropleth(self): self.assertEqual(fig['data'][2]['x'][:50], exp_fig_head) + class TestQuiver(TestCase): def test_scaleratio_param(self): @@ -2897,3 +2898,16 @@ def test_scaleratio_param(self): self.assertEqual(fig_head, exp_fig_head) +class TestTernarycontour(NumpyTestUtilsMixin, TestCase): + + + def test_simple_ternary_contour(self): + a, b = np.mgrid[0:1:20j, 0:1:20j] + a = a.ravel() + b = b.ravel() + c = 1 - a - b + z = a * b * c + fig = ff.create_ternarycontour(np.stack((a, b, c)), z) + fig2 = ff.create_ternarycontour(np.stack((a, b)), z) + np.testing.assert_array_equal(fig2['data'][0]['z'], + fig['data'][0]['z']) From 0c02bebf36be34d87611b53f5db76eb650d1e94f Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 28 Jan 2019 08:29:18 +0100 Subject: [PATCH 03/33] Changed string formatting to be compatible with Python <3.6 --- plotly/figure_factory/_ternarycontour.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 2fd0b8a72d9..d9b28361f02 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -140,7 +140,7 @@ def _set_ticklabels(annotations, posx, posy, proportion=True): # Annotations for ticklabels on side 0 annotations.extend([dict(showarrow=False, - text=f'{ticklabel[j]}', + text=str(ticklabel[j]), x=posx[j], y=posy[j], align='center', @@ -150,7 +150,7 @@ def _set_ticklabels(annotations, posx, posy, proportion=True): # Annotations for ticklabels on side 1 annotations.extend([dict(showarrow=False, - text=f'{ticklabel[j]}', + text=str(ticklabel[j]), x=posx[j+4], y=posy[j+4], align='center', @@ -160,7 +160,7 @@ def _set_ticklabels(annotations, posx, posy, proportion=True): # Annotations for ticklabels on side 2 annotations.extend([dict(showarrow=False, - text=f'{ticklabel[j]}', + text=str(ticklabel[j]), x=posx[j+8], y=posy[j+8], align='center', From 358b3769be02a98ef120147ba028cc3b38c65697 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 28 Jan 2019 17:35:12 +0100 Subject: [PATCH 04/33] Changed string format to be compatible with Python < 3.6 --- plotly/figure_factory/_ternarycontour.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index d9b28361f02..60ef8166c85 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -231,18 +231,22 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): if mode == 'proportions': tooltip = [ - [f'a: {round(bar_coords[0][i,j], 2)}
b: {round(bar_coords[1][i,j], 2)}' + - f'
c: {round(1-round(bar_coords[0][i,j], 2) - round(bar_coords[1][i,j], 2), 2)}' + - f'
z: {round(grid_z[i,j],2)}' + ['a: %.2f' % round(bar_coords[0][i, j], 2) + + '
b: %.2f' % round(bar_coords[1][i, j], 2) + + '
c: %.2f' % (round(1-round(bar_coords[0][i, j], 2) - + round(bar_coords[1][i, j], 2), 2)) + + '
z: %.2f' % round(grid_z[i, j], 2) if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] for i in range(N)] elif mode == 'percents': tooltip = [ - [f'a: {int(100*bar_coords[0][i,j]+0.5)}
b: {int(100*bar_coords[1][i,j]+0.5)}' + - f'
c: {100-int(100*bar_coords[0][i,j]+0.5) -int(100*bar_coords[1][i,j]+0.5)}' + - f'
z: {round(grid_z[i,j],2)}' - if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] - for i in range(N)] + ['a: %d' % int(100*bar_coords[0][i, j] + 0.5) + + '
b: %d' % int(100*bar_coords[1][i, j] + 0.5) + + '
c: %d' % (100-int(100*bar_coords[0][i, j] + 0.5) - + int(100*bar_coords[1][i, j] + 0.5)) + + '
z: %.2f' % round(grid_z[i, j], 2) + if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] + for i in range(N)] else: raise ValueError("""tooltip mode must be either "proportions" or "percents".""") From 424542316bd94e356b6947d831e13b3a26f80421 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 28 Jan 2019 22:34:48 +0100 Subject: [PATCH 05/33] Improved documentation --- plotly/figure_factory/_ternarycontour.py | 267 +++++++++++++++++------ 1 file changed, 206 insertions(+), 61 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 60ef8166c85..1861b58b0a0 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -22,10 +22,10 @@ def _pl_deep(): def _transform_barycentric_cartesian(): """ Returns the transformation matrix from barycentric to cartesian - coordinates and conversely + coordinates and conversely. """ # reference triangle - tri_verts = np.array([[0.5, np.sqrt(3)/2], [0, 0], [1, 0]]) + tri_verts = np.array([[0.5, np.sqrt(3) / 2], [0, 0], [1, 0]]) M = np.array([tri_verts[:, 0], tri_verts[:, 1], np.ones(3)]) return M, np.linalg.inv(M) @@ -37,8 +37,43 @@ def _contour_trace(x, y, z, tooltip, colorscale='Viridis', fontsize=12): """ Contour trace in Cartesian coordinates. + + Parameters + ========== + + x, y : array-like + Cartesian coordinates + z : array-like + Field to be represented as contours. + tooltip : list of str + Annotations to show on hover. + colorscale : str o array, optional + Colorscale to use for contours + reversescale : bool + Reverses the color mapping if true. If true, `zmin` + will correspond to the last color in the array and + `zmax` will correspond to the first color. + showscale : bool + If True, a colorbar showing the color scale is displayed. + linewidth : int + Line width of contours + linecolor : color string + Color on contours + smoothing : bool + If True, contours are smoothed. + coloring : None or 'lines' + How to display contour. Filled contours if None, lines if ``lines``. + showlabels : bool, default False + For line contours (coloring='lines'), the value of the contour is + displayed if showlabels is True. + colorscale : None or array-like + Colorscale of the contours. + fontcolor : color str + Color of contour labels. + fontsize : int + Font size of contour labels. """ - if showlabels and coloring is not 'lines': + if showlabels and (coloring is not 'lines'): msg = """`showlabels` was set to True, but labels can only be displayed for `coloring='lines'`""" warnings.warn(msg) @@ -66,9 +101,13 @@ def _contour_trace(x, y, z, tooltip, colorscale='Viridis', def barycentric_ticks(side): """ - side 0, 1 or 2; side j has 0 in the j^th position of barycentric - coords of tick origin - returns the list of tick origin barycentric coords + Barycentric coordinates of ticks locations. + + Parameters + ========== + side : 0, 1 or 2 + side j has 0 in the j^th position of barycentric coords of tick + origin. """ p = 10 if side == 0: # where a=0 @@ -77,65 +116,106 @@ def barycentric_ticks(side): return np.array([(i/p, 0, 1-i/p) for i in range(2, p, 2)]) elif side == 2: # c=0 return (np.array([(i/p, j/p, 0) - for i in range(p-2, 0, -2) - for j in range(p-i, -1, -1) if i+j == p])) + for i in range(p - 2, 0, -2) + for j in range(p - i, -1, -1) if i + j == p])) else: raise ValueError('The side can be only 0, 1, 2') -def _cart_coord_ticks(t=0.01): +def _side_coord_ticks(side, t=0.01): """ - side 0, 1 or 2 - each tick segment is parameterized as (x(s), y(s)), s in [0, t] - M is the transformation matrix from barycentric to cartesian coords - xt, yt are the lists of x, resp y-coords of tick segments - posx, posy are the lists of ticklabel positions for side 0, 1, 2 - (concatenated) + Cartesian coordinates of ticks loactions for one side (0, 1, 2) + of ternary diagram. + + Parameters + ========== + + side : int, 0, 1 or 2 + Index of side + t : float, default 0.01 + Length of tick + + Returns + ======= + xt, yt : lists + Lists of x, resp y-coords of tick segments + posx, posy : lists + Lists of ticklabel positions """ M, invM = _transform_barycentric_cartesian() - x_ticks, y_ticks, posx, posy = [], [], [], [] - # Side 0 - baryc = barycentric_ticks(0) + baryc = barycentric_ticks(side) xy1 = np.dot(M, baryc.T) xs, ys = xy1[:2] - for i in range(4): - x_ticks.extend([xs[i], xs[i]+t, None]) - y_ticks.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) - posx.extend([xs[i]+t for i in range(4)]) - posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) - # Side 1 - baryc = barycentric_ticks(1) - xy1 = np.dot(M, baryc.T) - xs, ys = xy1[:2] - for i in range(4): - x_ticks.extend([xs[i], xs[i]+t, None]) - y_ticks.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) - posx.extend([xs[i]+t for i in range(4)]) - posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) - # Side 2 - baryc = barycentric_ticks(2) - xy1 = np.dot(M, baryc.T) - xs, ys = xy1[:2] - for i in range(4): - x_ticks.extend([xs[i], xs[i]-2*t, None]) - y_ticks.extend([ys[i], ys[i], None]) - posx.extend([xs[i]-2*t for i in range(4)]) - posy.extend([ys[i] for i in range(4)]) + x_ticks, y_ticks, posx, posy = [], [], [], [] + if side == 0: + for i in range(4): + x_ticks.extend([xs[i], xs[i]+t, None]) + y_ticks.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) + posx.extend([xs[i]+t for i in range(4)]) + posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) + elif side == 1: + for i in range(4): + x_ticks.extend([xs[i], xs[i]+t, None]) + y_ticks.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) + posx.extend([xs[i]+t for i in range(4)]) + posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) + elif side == 2: + for i in range(4): + x_ticks.extend([xs[i], xs[i]-2*t, None]) + y_ticks.extend([ys[i], ys[i], None]) + posx.extend([xs[i]-2*t for i in range(4)]) + posy.extend([ys[i] for i in range(4)]) + else: + raise ValueError('Side can be only 0, 1, 2') return x_ticks, y_ticks, posx, posy -def _set_ticklabels(annotations, posx, posy, proportion=True): +def _cart_coord_ticks(t=0.01): + """ + Cartesian coordinates of ticks loactions. + + Parameters + ========== + + t : float, default 0.01 + Length of tick + + Returns + ======= + xt, yt : lists + Lists of x, resp y-coords of tick segments (all sides concatenated). + posx, posy : lists + Lists of ticklabel positions (all sides concatenated). """ - annotations: list of annotations previously defined in layout definition - as a dict, not as an instance of go.Layout - posx, posy: lists containing ticklabel position coordinates - proportion - boolean; True when ticklabels are 0.2, 0.4, ... - False when they are 20%, 40%... + x_ticks, y_ticks, posx, posy = [], [], [], [] + for side in range(3): + xt, yt, px, py = _side_coord_ticks(side, t) + x_ticks.extend(xt) + y_ticks.extend(yt) + posx.extend(px) + posy.extend(py) + return x_ticks, y_ticks, posx, posy + + +def _set_ticklabels(annotations, posx, posy, proportions=True): + """ + + Parameters + ========== + + annotations : list + List of annotations previously defined in layout definition + as a dict, not as an instance of go.Layout. + posx, posy: lists + Lists containing ticklabel position coordinates + proportions : bool + True when ticklabels are 0.2, 0.4, ... False when they are + 20%, 40%... """ if not isinstance(annotations, list): raise ValueError('annotations should be a list') - ticklabel = [0.8, 0.6, 0.4, 0.2] if proportion \ + ticklabel = [0.8, 0.6, 0.4, 0.2] if proportions \ else ['80%', '60%', '40%', '20%'] # Annotations for ticklabels on side 0 @@ -171,7 +251,17 @@ def _set_ticklabels(annotations, posx, posy, proportion=True): def _styling_traces_ternary(x_ticks, y_ticks): - # Outer triangle + """ + Traces for outer triangle of ternary plot, and corresponding ticks. + + Parameters + ========== + + x_ticks : array_like, 1D + x Cartesian coordinate of ticks + y_ticks : array_like, 1D + y Cartesian coordinate of ticks + """ side_trace = dict(type='scatter', x=[0.5, 0, 1, 0.5], y=[np.sqrt(3)/2, 0, 0, np.sqrt(3)/2], @@ -190,11 +280,34 @@ def _styling_traces_ternary(x_ticks, y_ticks): def _ternary_layout(title='Ternary contour plot', width=550, height=525, - fontfamily='Balto, sans-serif', lfontsize=14, - plot_bgcolor='rgb(240,240,240)', - pole_labels=['a', 'b', 'c'], v_fontsize=14): + fontfamily='Balto, sans-serif', colorbar_fontsize=14, + plot_bgcolor='rgb(240,240,240)', + pole_labels=['a', 'b', 'c'], label_fontsize=16): + """ + Layout of ternary contour plot, to be passed to ``go.FigureWidget`` + object. + + Parameters + ========== + title : str or None + Title of ternary plot + width : int + Figure width. + height : int + Figure height. + fontfamily : str + Family of fonts + colorbar_fontsize : int + Font size of colorbar. + plot_bgcolor : + color of figure background + pole_labels : str, default ['a', 'b', 'c'] + Names of the three poles of the triangle. + label_fontsize : int + Font size of pole labels. + """ return dict(title=title, - font=dict(family=fontfamily, size=lfontsize), + font=dict(family=fontfamily, size=colorbar_fontsize), width=width, height=height, xaxis=dict(visible=False), yaxis=dict(visible=False), @@ -208,7 +321,7 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, align='center', xanchor='center', yanchor='bottom', - font=dict(size=v_fontsize)), + font=dict(size=label_fontsize)), dict(showarrow=False, text=pole_labels[1], x=0, @@ -216,7 +329,7 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, align='left', xanchor='right', yanchor='top', - font=dict(size=v_fontsize)), + font=dict(size=label_fontsize)), dict(showarrow=False, text=pole_labels[2], x=1, @@ -224,11 +337,29 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, align='right', xanchor='left', yanchor='top', - font=dict(size=v_fontsize)) + font=dict(size=label_fontsize)) ]) def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): + """ + Tooltip annotations to be displayed on hover. + + Parameters + ========== + + N : int + Number of annotations along each axis. + bar_coords : array-like + Barycentric coordinates. + grid_z : array + Values (e.g. elevation values) at barycentric coordinates. + xy1 : array-like + Cartesian coordinates. + mode : str, 'proportions' or 'percents' + Coordinates inside the ternary plot can be displayed either as + proportions (adding up to 1) or as percents (adding up to 100). + """ if mode == 'proportions': tooltip = [ ['a: %.2f' % round(bar_coords[0][i, j], 2) + @@ -255,7 +386,7 @@ def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): def _prepare_barycentric_coord(b_coords): """ - check ternary data and return the right barycentric coordinates + Check ternary coordinates and return the right barycentric coordinates. """ if not isinstance(b_coords, (list, np.ndarray)): raise ValueError('Data should be either an array of shape (n,m), or a list of n m-lists, m=2 or 3') @@ -272,6 +403,21 @@ def _prepare_barycentric_coord(b_coords): def _compute_grid(coordinates, values, tooltip_mode): + """ + Compute interpolation of data points on regular grid in Cartesian + coordinates. + + Parameters + ========== + + coordinates : array-like + Barycentric coordinates of data points. + values : 1-d array-like + Data points, field to be represented as contours. + tooltip_mode : str, 'proportions' or 'percents' + Coordinates inside the ternary plot can be displayed either as + proportions (adding up to 1) or as percents (adding up to 100). + """ A, B, C = _prepare_barycentric_coord(coordinates) M, invM = _transform_barycentric_cartesian() cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) @@ -312,9 +458,9 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], coordinates : list or ndarray Barycentric coordinates of shape (2, N) or (3, N) where N is the - number of points. + number of data points. values : array-like - Field to be represented as contours. + Data points of field to be represented as contours. pole_labels : str, default ['a', 'b', 'c'] Names of the three poles of the triangle. tooltip_mode : str, 'proportions' or 'percents' @@ -369,7 +515,6 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], showlabels=True) """ - M, invM = _transform_barycentric_cartesian() grid_z, gr_x, gr_y, tooltip = _compute_grid(coordinates, values, tooltip_mode) @@ -380,7 +525,7 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], plot_bgcolor=plot_bgcolor) annotations = _set_ticklabels(layout['annotations'], posx, posy, - proportion=True) + proportions=True) if colorscale is None: colorscale = _pl_deep() From bcd35e8081299cc990b80bd881afbcef775b8e82 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 28 Jan 2019 22:38:01 +0100 Subject: [PATCH 06/33] corrected bug, typo in kw argument --- plotly/figure_factory/_ternarycontour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 1861b58b0a0..7276e886e1b 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -444,7 +444,7 @@ def _compute_grid(coordinates, values, tooltip_mode): def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], - tooltip_mode='proportion', width=500, height=500, + tooltip_mode='proportions', width=500, height=500, showscale=False, coloring=None, showlabels=False, colorscale=None, plot_bgcolor='rgb(240,240,240)', From a32f58f4efadee0252dd263794093cf8a8ded614 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Mon, 28 Jan 2019 22:51:56 +0100 Subject: [PATCH 07/33] fixed encoding error --- plotly/figure_factory/_ternarycontour.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 7276e886e1b..60766f1b76f 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -434,8 +434,7 @@ def _compute_grid(coordinates, values, tooltip_mode): np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) # invalidate the points outside of the reference triangle bar_coords[np.where(bar_coords < 0)] = None - # recompute back the cartesian coordinates of bar_coords with - # invalid positions + # recompute back cartesian coordinates with invalid positions xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) is_nan = np.where(np.isnan(xy1[0])) grid_z[is_nan] = None From 3d8f9bff3301abb5ffd19e6282f3cdd8595dc591 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 29 Jan 2019 15:36:07 +0100 Subject: [PATCH 08/33] added tests for ternary contour plot --- plotly/figure_factory/_ternarycontour.py | 30 +++++++-- .../test_figure_factory.py | 67 ++++++++++++++++++- 2 files changed, 88 insertions(+), 9 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 60766f1b76f..4963c5c1b80 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -30,7 +30,7 @@ def _transform_barycentric_cartesian(): return M, np.linalg.inv(M) -def _contour_trace(x, y, z, tooltip, colorscale='Viridis', +def _contour_trace(x, y, z, tooltip, ncontours=None, colorscale='Viridis', reversescale=False, showscale=False, linewidth=0.5, linecolor='rgb(150,150,150)', smoothing=False, coloring=None, showlabels=False, fontcolor='blue', @@ -47,6 +47,8 @@ def _contour_trace(x, y, z, tooltip, colorscale='Viridis', Field to be represented as contours. tooltip : list of str Annotations to show on hover. + ncontours : int or None + Number of contours to display (determined automatically if None). colorscale : str o array, optional Colorscale to use for contours reversescale : bool @@ -82,6 +84,7 @@ def _contour_trace(x, y, z, tooltip, colorscale='Viridis', x=x, y=y, z=z, text=tooltip, hoverinfo='text', + ncontours=ncontours, colorscale=colorscale, reversescale=reversescale, showscale=showscale, @@ -360,7 +363,7 @@ def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): Coordinates inside the ternary plot can be displayed either as proportions (adding up to 1) or as percents (adding up to 100). """ - if mode == 'proportions': + if mode == 'proportions' or mode == 'proportion': tooltip = [ ['a: %.2f' % round(bar_coords[0][i, j], 2) + '
b: %.2f' % round(bar_coords[1][i, j], 2) + @@ -369,7 +372,7 @@ def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): '
z: %.2f' % round(grid_z[i, j], 2) if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] for i in range(N)] - elif mode == 'percents': + elif mode == 'percents' or mode == 'percent': tooltip = [ ['a: %d' % int(100*bar_coords[0][i, j] + 0.5) + '
b: %d' % int(100*bar_coords[1][i, j] + 0.5) + @@ -395,10 +398,12 @@ def _prepare_barycentric_coord(b_coords): raise ValueError('A point should have 2 (a, b) or 3 (a, b, c) barycentric coordinates') if ((len(b_coords) == 3) and not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01)): - msg = "The sum of coordinates is not one for all data points" - warnings.warn(msg) + msg = "The sum of coordinates should be one for all data points" + raise ValueError(msg) A, B = b_coords[:2] C = 1 - (A + B) + if np.any(np.stack((A, B, C)) < 0): + raise ValueError('Barycentric coordinates should be positive.') return A, B, C @@ -444,8 +449,10 @@ def _compute_grid(coordinates, values, tooltip_mode): def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], tooltip_mode='proportions', width=500, height=500, + ncontours=None, showscale=False, coloring=None, showlabels=False, colorscale=None, + reversescale=False, plot_bgcolor='rgb(240,240,240)', title=None, smoothing=False): @@ -457,7 +464,8 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], coordinates : list or ndarray Barycentric coordinates of shape (2, N) or (3, N) where N is the - number of data points. + number of data points. The sum of the 3 coordinates is expected + to be 1 for all data points. values : array-like Data points of field to be represented as contours. pole_labels : str, default ['a', 'b', 'c'] @@ -469,6 +477,8 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], Figure width. height : int Figure height. + ncontours : int or None + Number of contours to display (determined automatically if None). showscale : bool, default False If True, a colorbar showing the color scale is displayed. coloring : None or 'lines' @@ -478,6 +488,10 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], displayed if showlabels is True. colorscale : None or array-like colorscale of the contours. + reversescale : bool + Reverses the color mapping if true. If true, `zmin` + will correspond to the last color in the array and + `zmax` will correspond to the first color. plot_bgcolor : color of figure background title : str or None @@ -529,9 +543,11 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], colorscale = _pl_deep() contour_trace = _contour_trace(gr_x, gr_y, grid_z, tooltip, + ncontours=ncontours, showscale=showscale, showlabels=showlabels, - colorscale=colorscale, reversescale=True, + colorscale=colorscale, + reversescale=reversescale, coloring=coloring, smoothing=smoothing) side_trace, tick_trace = _styling_traces_ternary(x_ticks, y_ticks) diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 382becd4b65..5289fc7057a 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2900,14 +2900,77 @@ def test_scaleratio_param(self): class TestTernarycontour(NumpyTestUtilsMixin, TestCase): - - def test_simple_ternary_contour(self): + def test_wrong_coordinates(self): a, b = np.mgrid[0:1:20j, 0:1:20j] a = a.ravel() b = b.ravel() + z = a * b + with self.assertRaises(ValueError, + msg='Barycentric coordinates should be positive.'): + _ = ff.create_ternarycontour(np.stack((a, b)), z) + mask = a + b < 1. + a = a[mask] + b = b[mask] + with self.assertRaises(ValueError): + _ = ff.create_ternarycontour(np.stack((a, b, a, b)), z) + with self.assertRaises(ValueError, + msg='different number of values and points'): + _ = ff.create_ternarycontour(np.stack((a, b, 1 - a - b)), + np.concatenate((z, [1]))) + # Different sums for different points + c = a + with self.assertRaises(ValueError): + _ = ff.create_ternarycontour(np.stack((a, b, c)), z) + # Sum of coordinates is different from one but is equal + # for all points. + with self.assertRaises(ValueError): + _ = ff.create_ternarycontour(np.stack((a, b, 2 - a - b)), z) + + + def test_tooltip(self): + a, b = np.mgrid[0:1:20j, 0:1:20j] + mask = a + b < 1. + a = a[mask].ravel() + b = b[mask].ravel() + c = 1 - a - b + z = a * b * c + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + tooltip_mode='percents') + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + tooltip_mode='percent') + + with self.assertRaises(ValueError): + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + tooltip_mode='wrong_mode') + + + def test_simple_ternary_contour(self): + a, b = np.mgrid[0:1:20j, 0:1:20j] + mask = a + b < 1. + a = a[mask].ravel() + b = b[mask].ravel() c = 1 - a - b z = a * b * c fig = ff.create_ternarycontour(np.stack((a, b, c)), z) fig2 = ff.create_ternarycontour(np.stack((a, b)), z) np.testing.assert_array_equal(fig2['data'][0]['z'], fig['data'][0]['z']) + + + def test_contour_attributes(self): + a, b = np.mgrid[0:1:20j, 0:1:20j] + mask = a + b < 1. + a = a[mask].ravel() + b = b[mask].ravel() + c = 1 - a - b + z = a * b * c + contour_dict = {'ncontours': 10, + 'showscale': True, + 'reversescale': False} + + + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, **contour_dict) + for key, value in contour_dict.items(): + assert fig['data'][0][key] == value + + From 581b93cf9b6a4bd03664b42e2d8c03e873a94fb9 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 29 Jan 2019 23:18:01 +0100 Subject: [PATCH 09/33] updated docstring --- plotly/figure_factory/_ternarycontour.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 4963c5c1b80..49bea67da57 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -506,8 +506,9 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], # Define coordinates a, b = np.mgrid[0:1:20j, 0:1:20j] - a = a.ravel() - b = b.ravel() + mask = a + b <= 1 + a = a[mask].ravel() + b = b[mask].ravel() c = 1 - a - b # Values to be displayed as contours z = a * b * c From 0c820cf574dbc24bb1e01cb4ec690a93ede0649f Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Wed, 30 Jan 2019 21:30:35 +0100 Subject: [PATCH 10/33] returned Figure instead of FigureWidget --- plotly/figure_factory/_ternarycontour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 49bea67da57..7c329b47f60 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -552,7 +552,7 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], coloring=coloring, smoothing=smoothing) side_trace, tick_trace = _styling_traces_ternary(x_ticks, y_ticks) - fig = go.FigureWidget(data=[contour_trace, tick_trace, side_trace], - layout=layout) + fig = go.Figure(data=[contour_trace, tick_trace, side_trace], + layout=layout) fig.layout.annotations = annotations return fig From e9a2d15365f5f4e61b3fb8a3eaf61bee8b694293 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Thu, 31 Jan 2019 23:06:02 +0100 Subject: [PATCH 11/33] other method for ternary contour plot --- plotly/figure_factory/_ternarycontour.py | 170 +++++++++++++---------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 7c329b47f60..1e3e131e243 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -1,8 +1,11 @@ from __future__ import absolute_import +import warnings import numpy as np from scipy.interpolate import griddata +from skimage import measure +import plotly.colors as clrs from plotly.graph_objs import graph_objs as go -import warnings +from plotly import exceptions def _pl_deep(): @@ -30,11 +33,41 @@ def _transform_barycentric_cartesian(): return M, np.linalg.inv(M) -def _contour_trace(x, y, z, tooltip, ncontours=None, colorscale='Viridis', - reversescale=False, showscale=False, linewidth=0.5, - linecolor='rgb(150,150,150)', smoothing=False, - coloring=None, showlabels=False, fontcolor='blue', - fontsize=12): +def _colors(ncontours, colormap=None): + if isinstance(colormap, str): + if colormap in clrs.PLOTLY_SCALES.keys(): + cmap = clrs.PLOTLY_SCALES[colormap] + else: + raise exceptions.PlotlyError( + "If 'colormap' is a string, it must be the name " + "of a Plotly Colorscale. The available colorscale " + "names are {}".format(clrs.PLOTLY_SCALES.keys())) + elif isinstance(colormap, dict): + cmap = colormap + clrs.validate_colors_dict(cmap, 'rgb') + else: + raise ValueError("""Colormap has to be a dictionary or a valid + Plotly colormap string""") + values = np.linspace(0, 1, ncontours) + keys = np.array([pair[0] for pair in cmap]) + cols = np.array([pair[1] for pair in cmap]) + inds = np.searchsorted(keys, values) + colors = [cols[0]] + for ind, val in zip(inds[1:], values[1:]): + key1, key2 = keys[ind - 1], keys[ind] + interm = (val - key1) / (key2 - key1) + col = clrs.find_intermediate_color(cols[ind - 1], + cols[ind], interm, colortype='rgb') + colors.append(col) + return colors + + + + + +def _contour_trace(x, y, z, ncontours=None, + colorscale='Electric', + linecolor='rgb(150,150,150)'): """ Contour trace in Cartesian coordinates. @@ -45,61 +78,42 @@ def _contour_trace(x, y, z, tooltip, ncontours=None, colorscale='Viridis', Cartesian coordinates z : array-like Field to be represented as contours. - tooltip : list of str - Annotations to show on hover. - ncontours : int or None - Number of contours to display (determined automatically if None). - colorscale : str o array, optional - Colorscale to use for contours - reversescale : bool - Reverses the color mapping if true. If true, `zmin` - will correspond to the last color in the array and - `zmax` will correspond to the first color. - showscale : bool - If True, a colorbar showing the color scale is displayed. - linewidth : int - Line width of contours - linecolor : color string - Color on contours - smoothing : bool - If True, contours are smoothed. - coloring : None or 'lines' - How to display contour. Filled contours if None, lines if ``lines``. - showlabels : bool, default False - For line contours (coloring='lines'), the value of the contour is - displayed if showlabels is True. - colorscale : None or array-like - Colorscale of the contours. - fontcolor : color str - Color of contour labels. - fontsize : int - Font size of contour labels. """ - if showlabels and (coloring is not 'lines'): - msg = """`showlabels` was set to True, but labels can only be - displayed for `coloring='lines'`""" - warnings.warn(msg) - - c_dict = dict(type='contour', - x=x, y=y, z=z, - text=tooltip, - hoverinfo='text', - ncontours=ncontours, - colorscale=colorscale, - reversescale=reversescale, - showscale=showscale, - line=dict(width=linewidth, color=linecolor, - smoothing=smoothing), - colorbar=dict(thickness=20, ticklen=4) - ) - if coloring == 'lines': - contours = dict(coloring=coloring, - showlabels=showlabels) - if showlabels: - contours.update(labelfont=dict(size=fontsize, - color=fontcolor,)) - c_dict.update(contours=contours) - return go.Contour(c_dict) + if ncontours is None: + ncontours = 5 + colors = _colors(ncontours, colorscale) + traces = [] + mask_nan = np.isnan(z) + mask_ok = np.logical_not(mask_nan) + values = np.linspace(z[mask_ok].min(), z[mask_ok].max(), + ncontours + 2)[1:-1] + M, invM = _transform_barycentric_cartesian() + dx = 1./x.size + dy = 1./y.size + zz = np.copy(z) + zz[np.isnan(z)] = (z[mask_ok].min() + - 10. * (z[mask_ok].max() - z[mask_ok].min())) + for i, val in enumerate(values): + contour_level = measure.find_contours(zz, val) + for contour in contour_level: + yy, xx = contour.T + bar_coords = np.dot(invM, + np.stack((dx * xx, dy * yy, np.ones(xx.shape)))) + a = bar_coords[0] + b = bar_coords[1] + c = bar_coords[2] + trace = dict( + type='scatterternary', text=val, + a=a, b=b, c=c, mode='lines', + line=dict(color=colors[i], shape='spline'), + fill='toself', + showlegend=True, + name=str(val), + # fillcolor = colors_iterator.next() + ) + traces.append(trace) + return traces + def barycentric_ticks(side): @@ -534,25 +548,27 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) - layout = _ternary_layout(pole_labels=pole_labels, - width=width, height=height, title=title, - plot_bgcolor=plot_bgcolor) - - annotations = _set_ticklabels(layout['annotations'], posx, posy, - proportions=True) + #layout = _ternary_layout(pole_labels=pole_labels, + # width=width, height=height, title=title, + # plot_bgcolor=plot_bgcolor) + + layout = {'title': 'Ternary Scatter Plot', + 'ternary':{'sum':1, + 'aaxis':{'title': pole_labels[0], + 'min': 0.01, 'linewidth':2, + 'ticks':'outside' }, + 'baxis':{'title': pole_labels[1], 'min': 0.01, 'linewidth':2, 'ticks':'outside' }, + 'caxis':{'title': pole_labels[2], 'min': 0.01, 'linewidth':2, 'ticks':'outside' } + }, + 'showlegend': True + } + #annotations = _set_ticklabels(layout['annotations'], posx, posy, + # proportions=True) if colorscale is None: colorscale = _pl_deep() - contour_trace = _contour_trace(gr_x, gr_y, grid_z, tooltip, + contour_trace = _contour_trace(gr_x, gr_y, grid_z, ncontours=ncontours, - showscale=showscale, - showlabels=showlabels, - colorscale=colorscale, - reversescale=reversescale, - coloring=coloring, - smoothing=smoothing) - side_trace, tick_trace = _styling_traces_ternary(x_ticks, y_ticks) - fig = go.Figure(data=[contour_trace, tick_trace, side_trace], - layout=layout) - fig.layout.annotations = annotations + colorscale=colorscale) + fig = go.Figure(data=contour_trace, layout=layout) return fig From a7c1118388f4edf128893be988e995b6acc855d0 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Fri, 1 Feb 2019 22:18:56 +0100 Subject: [PATCH 12/33] implemented ilr transform --- plotly/figure_factory/_ternarycontour.py | 193 +++++++++++++++++------ 1 file changed, 143 insertions(+), 50 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 1e3e131e243..8ed11991233 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -1,11 +1,12 @@ from __future__ import absolute_import import warnings -import numpy as np -from scipy.interpolate import griddata -from skimage import measure import plotly.colors as clrs from plotly.graph_objs import graph_objs as go -from plotly import exceptions +from plotly import exceptions, optional_imports + +np = optional_imports.get_module('numpy') +sk = optional_imports.get_module('skimage') +scipy_interp = optional_imports.get_module('scipy.interpolate') def _pl_deep(): @@ -22,6 +23,69 @@ def _pl_deep(): [1.0, 'rgb(39, 26, 44)']] +def replace_zero_coords(ternary_data, delta=0.001): + """Replaces zero ternary coordinates with delta + and normalize the new triplets (a, b, c); implements a method + by J. A. Martin-Fernandez, C. Barcelo-Vidal, V. Pawlowsky-Glahn, + Dealing with zeros and missing values in compositional data sets + using nonparametric imputation, + Mathematical Geology 35 (2003), pp 253-278 + """ + # ternary_data: array (n, 3) + # delta: small float (close to zero) + #Returns a new data set of normalized ternary coords + + ternary_data = central_proj(ternary_data) + bool_array = (ternary_data == 0) + + n_comp = ternary_data.shape[-1] + info_row_zero = bool_array.sum(axis=-1, keepdims=True) + + if delta is None: + delta = 0.001 + unity_complement = 1 - delta * info_row_zero + if np.any(unity_complement) < 0: + raise ValueError('Your delta led to negative ternary coords.Set a smaller delta') + ternary_data = np.where(bool_array, delta, unity_complement * ternary_data) + return ternary_data.squeeze() + + +def dir_ilr(barycentric): #ilr: S^2 ---> R^2 identified to x_1+x2+x3=0 in R^3 + barycentric = np.asarray(barycentric) + if barycentric.shape[-1] != 3: + raise ValueError(f'barycentric coordinates are 3 floats, not {barycentric.shape[-1]}') + if len(barycentric.shape) == 1: + barycentric = barycentric.reshape(1,3) + x0 = np.log(barycentric[:,0]/barycentric[:,1]) / np.sqrt(2) + x1 = np.log(barycentric[:,0]*barycentric[:,1]/barycentric[:, 2]**2) / np.sqrt(6) + ilr_tdata = np.stack((x0,x1)).T + return ilr_tdata if barycentric.shape[0] > 1 else ilr_tdata.squeeze() + + +def ilr_inverse (x): #ilr: R^2 -->S^2 (2 simplex) + #x an n list of 2-lists or an array of shape (n,2), + # implementation of a method presented in: + # An algebraic method to compute isometric logratio transformation and back transformation of compositional data + # Jarauta-Bragulat, E.; Buenestado, P.; Hervada-Sala, C. + # in Proc of the Annual Conf of the Intl Assoc for Math Geology, 2003, pp 31-30 + #x should ne an array of shape (n,2) + x = np.array(x) + if x.shape[-1] != 2: + raise ValueError(f'your data must be 2d points, not {x.shape[-1]}-points') + if len(x.shape) == 1: + x = x.reshape(1, 2) + matrix = np.array([[ 0.5, 1 , 1. ], + [-0.5, 1 , 1. ], + [ 0. , 0. , 1. ]]) + s = np.sqrt(2)/2 + t = np.sqrt(3/2) + Sk = np.einsum('ik, kj -> ij', np.array([[s, t],[-s, t]]), x.T) + Z = -np.log(1+np.exp(Sk).sum(axis=0)) + log_barycentric = np.einsum('ik, kj -> ij', matrix, np.stack((2*s*x[:, 0], t*x[:, 1], Z))) + iilr_tdata = np.exp(log_barycentric).T + return iilr_tdata if x.shape[0] > 1 else iilr_tdata.squeeze() + + def _transform_barycentric_cartesian(): """ Returns the transformation matrix from barycentric to cartesian @@ -62,12 +126,9 @@ def _colors(ncontours, colormap=None): return colors - - - def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', - linecolor='rgb(150,150,150)'): + linecolor='rgb(150,150,150)', interp_mode='cartesian'): """ Contour trace in Cartesian coordinates. @@ -88,17 +149,22 @@ def _contour_trace(x, y, z, ncontours=None, values = np.linspace(z[mask_ok].min(), z[mask_ok].max(), ncontours + 2)[1:-1] M, invM = _transform_barycentric_cartesian() - dx = 1./x.size - dy = 1./y.size + dx = (x.max() - x.min())/x.size + dy = (y.max() - y.min())/y.size zz = np.copy(z) zz[np.isnan(z)] = (z[mask_ok].min() - 10. * (z[mask_ok].max() - z[mask_ok].min())) for i, val in enumerate(values): - contour_level = measure.find_contours(zz, val) + contour_level = sk.measure.find_contours(zz, val) + # stop for contour in contour_level: yy, xx = contour.T - bar_coords = np.dot(invM, + if interp_mode == 'cartesian': + bar_coords = np.dot(invM, np.stack((dx * xx, dy * yy, np.ones(xx.shape)))) + elif interp_mode == 'ilr': + bar_coords = ilr_inverse(np.stack((dx * xx + x.min(), + dy * yy + y.min())).T).T a = bar_coords[0] b = bar_coords[1] c = bar_coords[2] @@ -323,6 +389,20 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, label_fontsize : int Font size of pole labels. """ + return dict(title=title, + ternary=dict(sum=1, + aaxis=dict(title=pole_labels[0], + min=0.01, linewidth=2, + ticks='outside'), + baxis=dict(title=pole_labels[1], + min=0.01, linewidth=2, + ticks='outside'), + caxis=dict(title=pole_labels[2], + min=0.01, linewidth=2, + ticks='outside')), + showlegend=True + ) + return dict(title=title, font=dict(family=fontfamily, size=colorbar_fontsize), width=width, height=height, @@ -421,7 +501,31 @@ def _prepare_barycentric_coord(b_coords): return A, B, C -def _compute_grid(coordinates, values, tooltip_mode): +def central_proj(mdata): + + #array of shape(n,3) or a n-list of 3-lists of positive numbers, + #returns for each row [a, b, c]--> np.array([a,b,c])/(a+b+c) + + mdata=np.asarray(mdata) + if mdata.ndim > 2: + raise ValueError("this function requires 2d arrays") + if mdata.shape[-1] != 3: + raise ValueError('data must have 3 coordinates') + + if mdata.ndim == 1: + mdata = np.atleast_2d(mdata) + + if np.any(mdata < 0): + raise ValueError("Data should be positive") + + if np.all(mdata == 0, axis=1).sum() > 0: + raise ValueError("this projection can be applied only to non zero triplets") + barycentric = mdata / mdata.sum(axis=1, keepdims=True) + return barycentric.squeeze() + + + +def _compute_grid(coordinates, values, tooltip_mode, interp_mode='cartesian'): """ Compute interpolation of data points on regular grid in Cartesian coordinates. @@ -438,27 +542,24 @@ def _compute_grid(coordinates, values, tooltip_mode): proportions (adding up to 1) or as percents (adding up to 100). """ A, B, C = _prepare_barycentric_coord(coordinates) - M, invM = _transform_barycentric_cartesian() - cartes_coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) - xx, yy = cartes_coord_points[:2] + if interp_mode == 'cartesian': + M, invM = _transform_barycentric_cartesian() + coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) + elif interp_mode == 'ilr': + mdata = replace_zero_coords(np.stack((A, B, C)).T) + coord_points = dir_ilr(mdata).T + else: + raise ValueError("interp_mode should be cartesian or ilr") + xx, yy = coord_points[:2] x_min, x_max = xx.min(), xx.max() y_min, y_max = yy.min(), yy.max() - n_interp = max(100, int(np.sqrt(len(values)))) + n_interp = max(400, int(np.sqrt(len(values)))) gr_x = np.linspace(x_min, x_max, n_interp) gr_y = np.linspace(y_min, y_max, n_interp) grid_x, grid_y = np.meshgrid(gr_x, gr_y) - grid_z = griddata(cartes_coord_points[:2].T, values, (grid_x, grid_y), - method='cubic') - bar_coords = np.einsum('ik, kmn -> imn', invM, - np.stack((grid_x, grid_y, np.ones(grid_x.shape)))) - # invalidate the points outside of the reference triangle - bar_coords[np.where(bar_coords < 0)] = None - # recompute back cartesian coordinates with invalid positions - xy1 = np.einsum('ik, kmn -> imn', M, bar_coords) - is_nan = np.where(np.isnan(xy1[0])) - grid_z[is_nan] = None - tooltip = _tooltip(n_interp, bar_coords, grid_z, xy1, tooltip_mode) - return grid_z, gr_x, gr_y, tooltip + grid_z = scipy_interp.griddata(coord_points[:2].T, values, (grid_x, grid_y), + method='cubic') + return grid_z, gr_x, gr_y def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], @@ -469,7 +570,8 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], reversescale=False, plot_bgcolor='rgb(240,240,240)', title=None, - smoothing=False): + smoothing=False, + interp_mode='cartesian'): """ Ternary contour plot. @@ -543,25 +645,15 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], showlabels=True) """ - grid_z, gr_x, gr_y, tooltip = _compute_grid(coordinates, values, - tooltip_mode) - - x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) - - #layout = _ternary_layout(pole_labels=pole_labels, - # width=width, height=height, title=title, - # plot_bgcolor=plot_bgcolor) - - layout = {'title': 'Ternary Scatter Plot', - 'ternary':{'sum':1, - 'aaxis':{'title': pole_labels[0], - 'min': 0.01, 'linewidth':2, - 'ticks':'outside' }, - 'baxis':{'title': pole_labels[1], 'min': 0.01, 'linewidth':2, 'ticks':'outside' }, - 'caxis':{'title': pole_labels[2], 'min': 0.01, 'linewidth':2, 'ticks':'outside' } - }, - 'showlegend': True - } + grid_z, gr_x, gr_y = _compute_grid(coordinates, values, + tooltip_mode, + interp_mode=interp_mode) + + #x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) + + layout = _ternary_layout(pole_labels=pole_labels, + width=width, height=height, title=title, + plot_bgcolor=plot_bgcolor) #annotations = _set_ticklabels(layout['annotations'], posx, posy, # proportions=True) if colorscale is None: @@ -569,6 +661,7 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], contour_trace = _contour_trace(gr_x, gr_y, grid_z, ncontours=ncontours, - colorscale=colorscale) + colorscale=colorscale, + interp_mode=interp_mode) fig = go.Figure(data=contour_trace, layout=layout) return fig From 1ba984b215d803e729f798e4847a7bff7d79adb4 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sat, 2 Feb 2019 18:03:26 +0100 Subject: [PATCH 13/33] colormap is now working --- plotly/figure_factory/_ternarycontour.py | 423 ++++++----------------- 1 file changed, 109 insertions(+), 314 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 8ed11991233..9006822b67d 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -1,5 +1,4 @@ from __future__ import absolute_import -import warnings import plotly.colors as clrs from plotly.graph_objs import graph_objs as go from plotly import exceptions, optional_imports @@ -9,60 +8,53 @@ scipy_interp = optional_imports.get_module('scipy.interpolate') -def _pl_deep(): - return [[0.0, 'rgb(253, 253, 204)'], - [0.1, 'rgb(201, 235, 177)'], - [0.2, 'rgb(145, 216, 163)'], - [0.3, 'rgb(102, 194, 163)'], - [0.4, 'rgb(81, 168, 162)'], - [0.5, 'rgb(72, 141, 157)'], - [0.6, 'rgb(64, 117, 152)'], - [0.7, 'rgb(61, 90, 146)'], - [0.8, 'rgb(65, 64, 123)'], - [0.9, 'rgb(55, 44, 80)'], - [1.0, 'rgb(39, 26, 44)']] +def _replace_zero_coords(ternary_data, delta=0.0005): + """ + Replaces zero ternary coordinates with delta and normalize the new + triplets (a, b, c). + + Parameters + ---------- + + ternary_data : ndarray of shape (N, 3) + delta : float + Small float to regularize logarithm. -def replace_zero_coords(ternary_data, delta=0.001): - """Replaces zero ternary coordinates with delta - and normalize the new triplets (a, b, c); implements a method + Notes + ----- + Implements a method by J. A. Martin-Fernandez, C. Barcelo-Vidal, V. Pawlowsky-Glahn, Dealing with zeros and missing values in compositional data sets - using nonparametric imputation, - Mathematical Geology 35 (2003), pp 253-278 + using nonparametric imputation, Mathematical Geology 35 (2003), + pp 253-278. """ - # ternary_data: array (n, 3) - # delta: small float (close to zero) - #Returns a new data set of normalized ternary coords - - ternary_data = central_proj(ternary_data) - bool_array = (ternary_data == 0) - - n_comp = ternary_data.shape[-1] - info_row_zero = bool_array.sum(axis=-1, keepdims=True) + zero_mask = (ternary_data == 0) + info_row_zero = zero_mask.sum(axis=-1, keepdims=True) if delta is None: delta = 0.001 - unity_complement = 1 - delta * info_row_zero + unity_complement = 1 - delta * info_row_zero if np.any(unity_complement) < 0: - raise ValueError('Your delta led to negative ternary coords.Set a smaller delta') - ternary_data = np.where(bool_array, delta, unity_complement * ternary_data) + raise ValueError('The provided value of delta led to negative' + 'ternary coords.Set a smaller delta') + ternary_data = np.where(zero_mask, delta, unity_complement * ternary_data) return ternary_data.squeeze() -def dir_ilr(barycentric): #ilr: S^2 ---> R^2 identified to x_1+x2+x3=0 in R^3 +def _ilr_transform(barycentric): + """ + + """ barycentric = np.asarray(barycentric) - if barycentric.shape[-1] != 3: - raise ValueError(f'barycentric coordinates are 3 floats, not {barycentric.shape[-1]}') - if len(barycentric.shape) == 1: - barycentric = barycentric.reshape(1,3) - x0 = np.log(barycentric[:,0]/barycentric[:,1]) / np.sqrt(2) - x1 = np.log(barycentric[:,0]*barycentric[:,1]/barycentric[:, 2]**2) / np.sqrt(6) - ilr_tdata = np.stack((x0,x1)).T - return ilr_tdata if barycentric.shape[0] > 1 else ilr_tdata.squeeze() + x0 = np.log(barycentric[0] / barycentric[1]) / np.sqrt(2) + x1 = 1. / np.sqrt(6) * np.log(barycentric[0] * barycentric[1] / + barycentric[2] ** 2) + ilr_tdata = np.stack((x0, x1)) + return ilr_tdata -def ilr_inverse (x): #ilr: R^2 -->S^2 (2 simplex) +def _ilr_inverse(x): #ilr: R^2 -->S^2 (2 simplex) #x an n list of 2-lists or an array of shape (n,2), # implementation of a method presented in: # An algebraic method to compute isometric logratio transformation and back transformation of compositional data @@ -98,37 +90,35 @@ def _transform_barycentric_cartesian(): def _colors(ncontours, colormap=None): - if isinstance(colormap, str): - if colormap in clrs.PLOTLY_SCALES.keys(): - cmap = clrs.PLOTLY_SCALES[colormap] - else: - raise exceptions.PlotlyError( - "If 'colormap' is a string, it must be the name " - "of a Plotly Colorscale. The available colorscale " - "names are {}".format(clrs.PLOTLY_SCALES.keys())) - elif isinstance(colormap, dict): - cmap = colormap - clrs.validate_colors_dict(cmap, 'rgb') + if colormap in clrs.PLOTLY_SCALES.keys(): + #cmap = clrs.validate_colors(colormap, colortype='rgb') + cmap = clrs.PLOTLY_SCALES[colormap] else: - raise ValueError("""Colormap has to be a dictionary or a valid - Plotly colormap string""") + raise exceptions.PlotlyError( + "Colorscale must be a valid Plotly Colorscale." + "The available colorscale names are {}".format( + clrs.PLOTLY_SCALES.keys())) values = np.linspace(0, 1, ncontours) keys = np.array([pair[0] for pair in cmap]) cols = np.array([pair[1] for pair in cmap]) inds = np.searchsorted(keys, values) + if '#'in cols[0]: + cols = [clrs.label_rgb(clrs.hex_to_rgb(col)) for col in cols] colors = [cols[0]] for ind, val in zip(inds[1:], values[1:]): key1, key2 = keys[ind - 1], keys[ind] interm = (val - key1) / (key2 - key1) col = clrs.find_intermediate_color(cols[ind - 1], - cols[ind], interm, colortype='rgb') + cols[ind], interm, + colortype='rgb') colors.append(col) return colors -def _contour_trace(x, y, z, ncontours=None, +def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', - linecolor='rgb(150,150,150)', interp_mode='cartesian'): + linecolor='rgb(150,150,150)', interp_mode='cartesian', + coloring=None): """ Contour trace in Cartesian coordinates. @@ -163,205 +153,27 @@ def _contour_trace(x, y, z, ncontours=None, bar_coords = np.dot(invM, np.stack((dx * xx, dy * yy, np.ones(xx.shape)))) elif interp_mode == 'ilr': - bar_coords = ilr_inverse(np.stack((dx * xx + x.min(), - dy * yy + y.min())).T).T - a = bar_coords[0] - b = bar_coords[1] - c = bar_coords[2] + bar_coords = _ilr_inverse(np.stack((dx * xx + x.min(), + dy * yy + y.min())).T).T + a, b, c = bar_coords + tooltip = _tooltip(a, b, c, val) + _col = colors[i] if coloring == 'lines' else linecolor trace = dict( - type='scatterternary', text=val, + type='scatterternary', text=tooltip, a=a, b=b, c=c, mode='lines', - line=dict(color=colors[i], shape='spline'), + line=dict(color=_col, shape='spline', width=1), fill='toself', + fillcolor=colors[i], + hoverinfo='text', showlegend=True, - name=str(val), - # fillcolor = colors_iterator.next() + name='%.2f' % val ) + if coloring == 'lines': + trace['fill'] = None traces.append(trace) return traces - -def barycentric_ticks(side): - """ - Barycentric coordinates of ticks locations. - - Parameters - ========== - side : 0, 1 or 2 - side j has 0 in the j^th position of barycentric coords of tick - origin. - """ - p = 10 - if side == 0: # where a=0 - return np.array([(0, j/p, 1-j/p) for j in range(p - 2, 0, -2)]) - elif side == 1: # b=0 - return np.array([(i/p, 0, 1-i/p) for i in range(2, p, 2)]) - elif side == 2: # c=0 - return (np.array([(i/p, j/p, 0) - for i in range(p - 2, 0, -2) - for j in range(p - i, -1, -1) if i + j == p])) - else: - raise ValueError('The side can be only 0, 1, 2') - - -def _side_coord_ticks(side, t=0.01): - """ - Cartesian coordinates of ticks loactions for one side (0, 1, 2) - of ternary diagram. - - Parameters - ========== - - side : int, 0, 1 or 2 - Index of side - t : float, default 0.01 - Length of tick - - Returns - ======= - xt, yt : lists - Lists of x, resp y-coords of tick segments - posx, posy : lists - Lists of ticklabel positions - """ - M, invM = _transform_barycentric_cartesian() - baryc = barycentric_ticks(side) - xy1 = np.dot(M, baryc.T) - xs, ys = xy1[:2] - x_ticks, y_ticks, posx, posy = [], [], [], [] - if side == 0: - for i in range(4): - x_ticks.extend([xs[i], xs[i]+t, None]) - y_ticks.extend([ys[i], ys[i]-np.sqrt(3)*t, None]) - posx.extend([xs[i]+t for i in range(4)]) - posy.extend([ys[i]-np.sqrt(3)*t for i in range(4)]) - elif side == 1: - for i in range(4): - x_ticks.extend([xs[i], xs[i]+t, None]) - y_ticks.extend([ys[i], ys[i]+np.sqrt(3)*t, None]) - posx.extend([xs[i]+t for i in range(4)]) - posy.extend([ys[i]+np.sqrt(3)*t for i in range(4)]) - elif side == 2: - for i in range(4): - x_ticks.extend([xs[i], xs[i]-2*t, None]) - y_ticks.extend([ys[i], ys[i], None]) - posx.extend([xs[i]-2*t for i in range(4)]) - posy.extend([ys[i] for i in range(4)]) - else: - raise ValueError('Side can be only 0, 1, 2') - return x_ticks, y_ticks, posx, posy - - -def _cart_coord_ticks(t=0.01): - """ - Cartesian coordinates of ticks loactions. - - Parameters - ========== - - t : float, default 0.01 - Length of tick - - Returns - ======= - xt, yt : lists - Lists of x, resp y-coords of tick segments (all sides concatenated). - posx, posy : lists - Lists of ticklabel positions (all sides concatenated). - """ - x_ticks, y_ticks, posx, posy = [], [], [], [] - for side in range(3): - xt, yt, px, py = _side_coord_ticks(side, t) - x_ticks.extend(xt) - y_ticks.extend(yt) - posx.extend(px) - posy.extend(py) - return x_ticks, y_ticks, posx, posy - - -def _set_ticklabels(annotations, posx, posy, proportions=True): - """ - - Parameters - ========== - - annotations : list - List of annotations previously defined in layout definition - as a dict, not as an instance of go.Layout. - posx, posy: lists - Lists containing ticklabel position coordinates - proportions : bool - True when ticklabels are 0.2, 0.4, ... False when they are - 20%, 40%... - """ - if not isinstance(annotations, list): - raise ValueError('annotations should be a list') - - ticklabel = [0.8, 0.6, 0.4, 0.2] if proportions \ - else ['80%', '60%', '40%', '20%'] - - # Annotations for ticklabels on side 0 - annotations.extend([dict(showarrow=False, - text=str(ticklabel[j]), - x=posx[j], - y=posy[j], - align='center', - xanchor='center', - yanchor='top', - font=dict(size=12)) for j in range(4)]) - - # Annotations for ticklabels on side 1 - annotations.extend([dict(showarrow=False, - text=str(ticklabel[j]), - x=posx[j+4], - y=posy[j+4], - align='center', - xanchor='left', - yanchor='middle', - font=dict(size=12)) for j in range(4)]) - - # Annotations for ticklabels on side 2 - annotations.extend([dict(showarrow=False, - text=str(ticklabel[j]), - x=posx[j+8], - y=posy[j+8], - align='center', - xanchor='right', - yanchor='middle', - font=dict(size=12)) for j in range(4)]) - return annotations - - -def _styling_traces_ternary(x_ticks, y_ticks): - """ - Traces for outer triangle of ternary plot, and corresponding ticks. - - Parameters - ========== - - x_ticks : array_like, 1D - x Cartesian coordinate of ticks - y_ticks : array_like, 1D - y Cartesian coordinate of ticks - """ - side_trace = dict(type='scatter', - x=[0.5, 0, 1, 0.5], - y=[np.sqrt(3)/2, 0, 0, np.sqrt(3)/2], - mode='lines', - line=dict(width=2, color='#444444'), - hoverinfo='none') - - tick_trace = dict(type='scatter', - x=x_ticks, - y=y_ticks, - mode='lines', - line=dict(width=1, color='#444444'), - hoverinfo='none') - - return side_trace, tick_trace - - def _ternary_layout(title='Ternary contour plot', width=550, height=525, fontfamily='Balto, sans-serif', colorbar_fontsize=14, plot_bgcolor='rgb(240,240,240)', @@ -390,6 +202,9 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, Font size of pole labels. """ return dict(title=title, + width=width, height=height, + font=dict(family=fontfamily, size=colorbar_fontsize), + plot_bgcolor=plot_bgcolor, ternary=dict(sum=1, aaxis=dict(title=pole_labels[0], min=0.01, linewidth=2, @@ -400,45 +215,11 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, caxis=dict(title=pole_labels[2], min=0.01, linewidth=2, ticks='outside')), - showlegend=True + showlegend=False, ) - return dict(title=title, - font=dict(family=fontfamily, size=colorbar_fontsize), - width=width, height=height, - xaxis=dict(visible=False), - yaxis=dict(visible=False), - plot_bgcolor=plot_bgcolor, - showlegend=False, - # annotations for strings placed at the triangle vertices - annotations=[dict(showarrow=False, - text=pole_labels[0], - x=0.5, - y=np.sqrt(3)/2, - align='center', - xanchor='center', - yanchor='bottom', - font=dict(size=label_fontsize)), - dict(showarrow=False, - text=pole_labels[1], - x=0, - y=0, - align='left', - xanchor='right', - yanchor='top', - font=dict(size=label_fontsize)), - dict(showarrow=False, - text=pole_labels[2], - x=1, - y=0, - align='right', - xanchor='left', - yanchor='top', - font=dict(size=label_fontsize)) - ]) - - -def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): + +def _tooltip(a, b, c, z, mode='proportions'): """ Tooltip annotations to be displayed on hover. @@ -457,24 +238,21 @@ def _tooltip(N, bar_coords, grid_z, xy1, mode='proportions'): Coordinates inside the ternary plot can be displayed either as proportions (adding up to 1) or as percents (adding up to 100). """ + N = len(a) + if np.isscalar(z): + z = z * np.ones(N) if mode == 'proportions' or mode == 'proportion': tooltip = [ - ['a: %.2f' % round(bar_coords[0][i, j], 2) + - '
b: %.2f' % round(bar_coords[1][i, j], 2) + - '
c: %.2f' % (round(1-round(bar_coords[0][i, j], 2) - - round(bar_coords[1][i, j], 2), 2)) + - '
z: %.2f' % round(grid_z[i, j], 2) - if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] - for i in range(N)] + 'a: %.2f' % round(a[i], 2) + + '
b: %.2f' % round(b[i], 2) + + '
c: %.2f' % round(c[i], 2) + + '
z: %.2f' % round(z[i], 2) for i in range(N)] elif mode == 'percents' or mode == 'percent': tooltip = [ - ['a: %d' % int(100*bar_coords[0][i, j] + 0.5) + - '
b: %d' % int(100*bar_coords[1][i, j] + 0.5) + - '
c: %d' % (100-int(100*bar_coords[0][i, j] + 0.5) - - int(100*bar_coords[1][i, j] + 0.5)) + - '
z: %.2f' % round(grid_z[i, j], 2) - if ~np.isnan(xy1[0][i, j]) else '' for j in range(N)] - for i in range(N)] + 'a: %d' % int(100 * a[i] + 0.5) + + '
b: %d' % int(100 * b[i] + 0.5) + + '
c: %d' % int(100 * c[i] + 0.5) + + '
z: %.2f' % round(z[i], 2) for i in range(N)] else: raise ValueError("""tooltip mode must be either "proportions" or "percents".""") @@ -501,7 +279,7 @@ def _prepare_barycentric_coord(b_coords): return A, B, C -def central_proj(mdata): +def _central_proj(mdata): #array of shape(n,3) or a n-list of 3-lists of positive numbers, #returns for each row [a, b, c]--> np.array([a,b,c])/(a+b+c) @@ -546,8 +324,8 @@ def _compute_grid(coordinates, values, tooltip_mode, interp_mode='cartesian'): M, invM = _transform_barycentric_cartesian() coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) elif interp_mode == 'ilr': - mdata = replace_zero_coords(np.stack((A, B, C)).T) - coord_points = dir_ilr(mdata).T + mdata = _replace_zero_coords(np.stack((A, B, C)).T) + coord_points = _ilr_transform(mdata.T) else: raise ValueError("interp_mode should be cartesian or ilr") xx, yy = coord_points[:2] @@ -557,8 +335,9 @@ def _compute_grid(coordinates, values, tooltip_mode, interp_mode='cartesian'): gr_x = np.linspace(x_min, x_max, n_interp) gr_y = np.linspace(y_min, y_max, n_interp) grid_x, grid_y = np.meshgrid(gr_x, gr_y) - grid_z = scipy_interp.griddata(coord_points[:2].T, values, (grid_x, grid_y), - method='cubic') + grid_z = scipy_interp.griddata(coord_points[:2].T, values, + (grid_x, grid_y), + method='cubic') return grid_z, gr_x, gr_y @@ -566,12 +345,12 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], tooltip_mode='proportions', width=500, height=500, ncontours=None, showscale=False, coloring=None, - showlabels=False, colorscale=None, + colorscale='Greens', reversescale=False, plot_bgcolor='rgb(240,240,240)', title=None, - smoothing=False, - interp_mode='cartesian'): + interp_mode='ilr', + showmarkers=False): """ Ternary contour plot. @@ -646,22 +425,38 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], """ grid_z, gr_x, gr_y = _compute_grid(coordinates, values, - tooltip_mode, - interp_mode=interp_mode) - - #x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) + tooltip_mode, + interp_mode=interp_mode) layout = _ternary_layout(pole_labels=pole_labels, width=width, height=height, title=title, plot_bgcolor=plot_bgcolor) - #annotations = _set_ticklabels(layout['annotations'], posx, posy, - # proportions=True) - if colorscale is None: - colorscale = _pl_deep() contour_trace = _contour_trace(gr_x, gr_y, grid_z, ncontours=ncontours, colorscale=colorscale, - interp_mode=interp_mode) + interp_mode=interp_mode, + coloring=coloring) fig = go.Figure(data=contour_trace, layout=layout) + + if showmarkers: + a, b, c = coordinates + tooltip = _tooltip(a, b, c, values) + fig.add_scatterternary(a=a, b=b, c=c, + mode='markers', + marker={'color': values, + 'colorscale': colorscale}, + text=tooltip, + hoverinfo='text') + if showscale: + colorbar = dict({'type': 'scatterternary', + 'a': [None], 'b': [None], + 'c': [None], + 'marker': {'cmin': values.min(), + 'cmax': values.max(), + 'colorscale': colorscale, + 'showscale': True}, + 'mode': 'markers'}) + fig.add_trace(colorbar) + return fig From d9c4bf11c9d2d8f3ecced746d5893ff80ae10215 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Feb 2019 16:28:16 +0100 Subject: [PATCH 14/33] Improved documentation and consistency of kw arguments --- plotly/figure_factory/__init__.py | 2 +- plotly/figure_factory/_ternarycontour.py | 324 +++++++++++++---------- 2 files changed, 184 insertions(+), 142 deletions(-) diff --git a/plotly/figure_factory/__init__.py b/plotly/figure_factory/__init__.py index 910ffa62f13..4d2a501061e 100644 --- a/plotly/figure_factory/__init__.py +++ b/plotly/figure_factory/__init__.py @@ -18,7 +18,7 @@ from plotly.figure_factory._scatterplot import create_scatterplotmatrix from plotly.figure_factory._streamline import create_streamline from plotly.figure_factory._table import create_table -from plotly.figure_factory._ternarycontour import create_ternarycontour +from plotly.figure_factory._ternarycontour import create_ternary_contour from plotly.figure_factory._trisurf import create_trisurf from plotly.figure_factory._violin import create_violin if optional_imports.get_module('pandas') is not None: diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index 9006822b67d..e5adbb348d9 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -30,57 +30,75 @@ def _replace_zero_coords(ternary_data, delta=0.0005): pp 253-278. """ zero_mask = (ternary_data == 0) - info_row_zero = zero_mask.sum(axis=-1, keepdims=True) + is_any_coord_zero = np.any(zero_mask, axis=0) - if delta is None: - delta = 0.001 - unity_complement = 1 - delta * info_row_zero + unity_complement = 1 - delta * is_any_coord_zero if np.any(unity_complement) < 0: raise ValueError('The provided value of delta led to negative' 'ternary coords.Set a smaller delta') ternary_data = np.where(zero_mask, delta, unity_complement * ternary_data) - return ternary_data.squeeze() + return ternary_data def _ilr_transform(barycentric): """ + Perform Isometric Log-Ratio on barycentric (compositional) data. + Parameters + ---------- + barycentric: ndarray of shape (3, N) + Barycentric coordinates. + + References + ---------- + "An algebraic method to compute isometric logratio transformation and + back transformation of compositional data", Jarauta-Bragulat, E., + Buenestado, P.; Hervada-Sala, C., in Proc. of the Annual Conf. of the + Intl Assoc for Math Geology, 2003, pp 31-30. """ barycentric = np.asarray(barycentric) - x0 = np.log(barycentric[0] / barycentric[1]) / np.sqrt(2) - x1 = 1. / np.sqrt(6) * np.log(barycentric[0] * barycentric[1] / - barycentric[2] ** 2) - ilr_tdata = np.stack((x0, x1)) + x_0 = np.log(barycentric[0] / barycentric[1]) / np.sqrt(2) + x_1 = 1. / np.sqrt(6) * np.log(barycentric[0] * barycentric[1] / + barycentric[2] ** 2) + ilr_tdata = np.stack((x_0, x_1)) return ilr_tdata -def _ilr_inverse(x): #ilr: R^2 -->S^2 (2 simplex) - #x an n list of 2-lists or an array of shape (n,2), - # implementation of a method presented in: - # An algebraic method to compute isometric logratio transformation and back transformation of compositional data - # Jarauta-Bragulat, E.; Buenestado, P.; Hervada-Sala, C. - # in Proc of the Annual Conf of the Intl Assoc for Math Geology, 2003, pp 31-30 - #x should ne an array of shape (n,2) +def _ilr_inverse(x): + """ + Perform inverse Isometric Log-Ratio (ILR) transform to retrieve + barycentric (compositional) data. + + Parameters + ---------- + x : array of shape (2, N) + Coordinates in ILR space. + + References + ---------- + "An algebraic method to compute isometric logratio transformation and + back transformation of compositional data", Jarauta-Bragulat, E., + Buenestado, P.; Hervada-Sala, C., in Proc. of the Annual Conf. of the + Intl Assoc for Math Geology, 2003, pp 31-30. + """ x = np.array(x) - if x.shape[-1] != 2: - raise ValueError(f'your data must be 2d points, not {x.shape[-1]}-points') - if len(x.shape) == 1: - x = x.reshape(1, 2) - matrix = np.array([[ 0.5, 1 , 1. ], - [-0.5, 1 , 1. ], - [ 0. , 0. , 1. ]]) + matrix = np.array([[0.5, 1, 1.], + [-0.5, 1, 1.], + [0., 0., 1.]]) s = np.sqrt(2)/2 t = np.sqrt(3/2) - Sk = np.einsum('ik, kj -> ij', np.array([[s, t],[-s, t]]), x.T) - Z = -np.log(1+np.exp(Sk).sum(axis=0)) - log_barycentric = np.einsum('ik, kj -> ij', matrix, np.stack((2*s*x[:, 0], t*x[:, 1], Z))) - iilr_tdata = np.exp(log_barycentric).T - return iilr_tdata if x.shape[0] > 1 else iilr_tdata.squeeze() + Sk = np.einsum('ik, kj -> ij', np.array([[s, t], [-s, t]]), x) + Z = -np.log(1 + np.exp(Sk).sum(axis=0)) + log_barycentric = np.einsum('ik, kj -> ij', + matrix, + np.stack((2*s*x[0], t*x[1], Z))) + iilr_tdata = np.exp(log_barycentric) + return iilr_tdata def _transform_barycentric_cartesian(): """ - Returns the transformation matrix from barycentric to cartesian + Returns the transformation matrix from barycentric to Cartesian coordinates and conversely. """ # reference triangle @@ -90,24 +108,27 @@ def _transform_barycentric_cartesian(): def _colors(ncontours, colormap=None): + """ + Return a list of ``ncontours`` colors from the ``colormap`` colorscale. + """ if colormap in clrs.PLOTLY_SCALES.keys(): - #cmap = clrs.validate_colors(colormap, colortype='rgb') cmap = clrs.PLOTLY_SCALES[colormap] else: raise exceptions.PlotlyError( - "Colorscale must be a valid Plotly Colorscale." - "The available colorscale names are {}".format( - clrs.PLOTLY_SCALES.keys())) + "Colorscale must be a valid Plotly Colorscale." + "The available colorscale names are {}".format( + clrs.PLOTLY_SCALES.keys())) values = np.linspace(0, 1, ncontours) - keys = np.array([pair[0] for pair in cmap]) + vals_cmap = np.array([pair[0] for pair in cmap]) cols = np.array([pair[1] for pair in cmap]) - inds = np.searchsorted(keys, values) - if '#'in cols[0]: + inds = np.searchsorted(vals_cmap, values) + if '#'in cols[0]: # for Viridis cols = [clrs.label_rgb(clrs.hex_to_rgb(col)) for col in cols] + colors = [cols[0]] for ind, val in zip(inds[1:], values[1:]): - key1, key2 = keys[ind - 1], keys[ind] - interm = (val - key1) / (key2 - key1) + val1, val2 = vals_cmap[ind - 1], vals_cmap[ind] + interm = (val - val1) / (val2 - val1) col = clrs.find_intermediate_color(cols[ind - 1], cols[ind], interm, colortype='rgb') @@ -117,8 +138,8 @@ def _colors(ncontours, colormap=None): def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', - linecolor='rgb(150,150,150)', interp_mode='cartesian', - coloring=None): + linecolor='rgb(150,150,150)', interp_mode='llr', + coloring=None, tooltip_mode='proportions'): """ Contour trace in Cartesian coordinates. @@ -129,35 +150,60 @@ def _contour_trace(x, y, z, ncontours=None, Cartesian coordinates z : array-like Field to be represented as contours. + ncontours : int or None + Number of contours to display (determined automatically if None). + colorscale : None or str (Plotly colormap) + colorscale of the contours. + linecolor : rgb color + Color used for lines. If ``colorscale`` is not None, line colors are + determined from ``colorscale`` instead. + interp_mode : 'ilr' (default) or 'cartesian' + Defines how data are interpolated to compute contours. If 'irl', + ILR (Isometric Log-Ratio) of compositional data is performed. If + 'cartesian', contours are determined in Cartesian space. + coloring : None or 'lines' + How to display contour. Filled contours if None, lines if ``lines``. + tooltip_mode : str, 'proportions' or 'percents' + Coordinates inside the ternary plot can be displayed either as + proportions (adding up to 1) or as percents (adding up to 100). """ if ncontours is None: ncontours = 5 - colors = _colors(ncontours, colorscale) + if colorscale is not None: + colors = _colors(ncontours, colorscale) + if linecolor is None: + linecolor = 'rgb(150, 150, 150)' + else: + colors = [linecolor] * ncontours traces = [] mask_nan = np.isnan(z) mask_ok = np.logical_not(mask_nan) - values = np.linspace(z[mask_ok].min(), z[mask_ok].max(), - ncontours + 2)[1:-1] + values = np.linspace(z[mask_ok].min(), z[mask_ok].max(), + ncontours + 2)[1:-1] M, invM = _transform_barycentric_cartesian() - dx = (x.max() - x.min())/x.size - dy = (y.max() - y.min())/y.size + dx = (x.max() - x.min()) / x.size + dy = (y.max() - y.min()) / y.size zz = np.copy(z) zz[np.isnan(z)] = (z[mask_ok].min() - 10. * (z[mask_ok].max() - z[mask_ok].min())) for i, val in enumerate(values): contour_level = sk.measure.find_contours(zz, val) - # stop - for contour in contour_level: - yy, xx = contour.T + for contour in contour_level: # several closed contours for 1 value + y_contour, x_contour = contour.T if interp_mode == 'cartesian': bar_coords = np.dot(invM, - np.stack((dx * xx, dy * yy, np.ones(xx.shape)))) + np.stack((dx * x_contour, + dy * y_contour, + np.ones(x_contour.shape)))) elif interp_mode == 'ilr': - bar_coords = _ilr_inverse(np.stack((dx * xx + x.min(), - dy * yy + y.min())).T).T + bar_coords = _ilr_inverse(np.stack((dx * x_contour + x.min(), + dy * y_contour + + y.min()))) a, b, c = bar_coords - tooltip = _tooltip(a, b, c, val) + tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) + _col = colors[i] if coloring == 'lines' else linecolor + trace = dict( type='scatterternary', text=tooltip, a=a, b=b, c=c, mode='lines', @@ -175,7 +221,7 @@ def _contour_trace(x, y, z, ncontours=None, def _ternary_layout(title='Ternary contour plot', width=550, height=525, - fontfamily='Balto, sans-serif', colorbar_fontsize=14, + fontfamily='Balto, sans-serif', plot_bgcolor='rgb(240,240,240)', pole_labels=['a', 'b', 'c'], label_fontsize=16): """ @@ -192,8 +238,6 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, Figure height. fontfamily : str Family of fonts - colorbar_fontsize : int - Font size of colorbar. plot_bgcolor : color of figure background pole_labels : str, default ['a', 'b', 'c'] @@ -203,20 +247,23 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, """ return dict(title=title, width=width, height=height, - font=dict(family=fontfamily, size=colorbar_fontsize), + font=dict(family=fontfamily), plot_bgcolor=plot_bgcolor, ternary=dict(sum=1, - aaxis=dict(title=pole_labels[0], + aaxis=dict(title=dict(text=pole_labels[0], + font=dict(size=label_fontsize)), min=0.01, linewidth=2, ticks='outside'), - baxis=dict(title=pole_labels[1], + baxis=dict(title=dict(text=pole_labels[1], + font=dict(size=label_fontsize)), min=0.01, linewidth=2, ticks='outside'), - caxis=dict(title=pole_labels[2], + caxis=dict(title=dict(text=pole_labels[2], + font=dict(size=label_fontsize)), min=0.01, linewidth=2, ticks='outside')), showlegend=False, - ) + ) def _tooltip(a, b, c, z, mode='proportions'): @@ -226,14 +273,10 @@ def _tooltip(a, b, c, z, mode='proportions'): Parameters ========== - N : int - Number of annotations along each axis. - bar_coords : array-like + a, b, c : 1-D array-like Barycentric coordinates. - grid_z : array + z : 1-D array Values (e.g. elevation values) at barycentric coordinates. - xy1 : array-like - Cartesian coordinates. mode : str, 'proportions' or 'percents' Coordinates inside the ternary plot can be displayed either as proportions (adding up to 1) or as percents (adding up to 100). @@ -242,17 +285,15 @@ def _tooltip(a, b, c, z, mode='proportions'): if np.isscalar(z): z = z * np.ones(N) if mode == 'proportions' or mode == 'proportion': - tooltip = [ - 'a: %.2f' % round(a[i], 2) + - '
b: %.2f' % round(b[i], 2) + - '
c: %.2f' % round(c[i], 2) + - '
z: %.2f' % round(z[i], 2) for i in range(N)] + tooltip = ['a: %.2f' % round(a[i], 2) + + '
b: %.2f' % round(b[i], 2) + + '
c: %.2f' % round(c[i], 2) + + '
z: %.2f' % round(z[i], 2) for i in range(N)] elif mode == 'percents' or mode == 'percent': - tooltip = [ - 'a: %d' % int(100 * a[i] + 0.5) + - '
b: %d' % int(100 * b[i] + 0.5) + - '
c: %d' % int(100 * c[i] + 0.5) + - '
z: %.2f' % round(z[i], 2) for i in range(N)] + tooltip = ['a: %d' % int(100 * a[i] + 0.5) + + '
b: %d' % int(100 * b[i] + 0.5) + + '
c: %d' % int(100 * c[i] + 0.5) + + '
z: %.2f' % round(z[i], 2) for i in range(N)] else: raise ValueError("""tooltip mode must be either "proportions" or "percents".""") @@ -269,44 +310,25 @@ def _prepare_barycentric_coord(b_coords): if b_coords.shape[0] not in (2, 3): raise ValueError('A point should have 2 (a, b) or 3 (a, b, c) barycentric coordinates') if ((len(b_coords) == 3) and - not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01)): - msg = "The sum of coordinates should be one for all data points" + not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01) and + not np.allclose(b_coords.sum(axis=0), 100, rtol=0.01)): + msg = "The sum of coordinates should be 1 or 100 for all data points" raise ValueError(msg) - A, B = b_coords[:2] - C = 1 - (A + B) + + if len(b_coords) == 2: + A, B = b_coords + C = 1 - (A + B) + else: + A, B, C = b_coords / b_coords.sum(axis=0) if np.any(np.stack((A, B, C)) < 0): raise ValueError('Barycentric coordinates should be positive.') - return A, B, C - - -def _central_proj(mdata): - - #array of shape(n,3) or a n-list of 3-lists of positive numbers, - #returns for each row [a, b, c]--> np.array([a,b,c])/(a+b+c) - - mdata=np.asarray(mdata) - if mdata.ndim > 2: - raise ValueError("this function requires 2d arrays") - if mdata.shape[-1] != 3: - raise ValueError('data must have 3 coordinates') - - if mdata.ndim == 1: - mdata = np.atleast_2d(mdata) + return np.stack((A, B, C)) - if np.any(mdata < 0): - raise ValueError("Data should be positive") - if np.all(mdata == 0, axis=1).sum() > 0: - raise ValueError("this projection can be applied only to non zero triplets") - barycentric = mdata / mdata.sum(axis=1, keepdims=True) - return barycentric.squeeze() - - - -def _compute_grid(coordinates, values, tooltip_mode, interp_mode='cartesian'): +def _compute_grid(coordinates, values, interp_mode='ilr'): """ - Compute interpolation of data points on regular grid in Cartesian - coordinates. + Transform data points with Cartesian or ILR mapping, then Compute + interpolation on a regular grid. Parameters ========== @@ -315,23 +337,21 @@ def _compute_grid(coordinates, values, tooltip_mode, interp_mode='cartesian'): Barycentric coordinates of data points. values : 1-d array-like Data points, field to be represented as contours. - tooltip_mode : str, 'proportions' or 'percents' - Coordinates inside the ternary plot can be displayed either as - proportions (adding up to 1) or as percents (adding up to 100). + interp_mode : 'ilr' (default) or 'cartesian' + Defines how data are interpolated to compute contours. """ - A, B, C = _prepare_barycentric_coord(coordinates) if interp_mode == 'cartesian': M, invM = _transform_barycentric_cartesian() - coord_points = np.einsum('ik, kj -> ij', M, np.stack((A, B, C))) + coord_points = np.einsum('ik, kj -> ij', M, coordinates) elif interp_mode == 'ilr': - mdata = _replace_zero_coords(np.stack((A, B, C)).T) - coord_points = _ilr_transform(mdata.T) + coordinates = _replace_zero_coords(coordinates) + coord_points = _ilr_transform(coordinates) else: raise ValueError("interp_mode should be cartesian or ilr") xx, yy = coord_points[:2] x_min, x_max = xx.min(), xx.max() y_min, y_max = yy.min(), yy.max() - n_interp = max(400, int(np.sqrt(len(values)))) + n_interp = max(200, int(np.sqrt(len(values)))) gr_x = np.linspace(x_min, x_max, n_interp) gr_y = np.linspace(y_min, y_max, n_interp) grid_x, grid_y = np.meshgrid(gr_x, gr_y) @@ -341,16 +361,17 @@ def _compute_grid(coordinates, values, tooltip_mode, interp_mode='cartesian'): return grid_z, gr_x, gr_y -def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], - tooltip_mode='proportions', width=500, height=500, - ncontours=None, - showscale=False, coloring=None, - colorscale='Greens', - reversescale=False, - plot_bgcolor='rgb(240,240,240)', - title=None, - interp_mode='ilr', - showmarkers=False): +def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], + tooltip_mode='proportions', width=500, height=500, + ncontours=None, + showscale=False, coloring=None, + colorscale='BlueRed', + linecolor=None, + plot_bgcolor='rgb(240,240,240)', + title=None, + interp_mode='ilr', + showmarkers=False, + label_fontsize=16): """ Ternary contour plot. @@ -381,18 +402,24 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], showlabels : bool, default False For line contours (coloring='lines'), the value of the contour is displayed if showlabels is True. - colorscale : None or array-like + colorscale : None or str (Plotly colormap) colorscale of the contours. - reversescale : bool - Reverses the color mapping if true. If true, `zmin` - will correspond to the last color in the array and - `zmax` will correspond to the first color. + linecolor : None or rgb color + Color used for lines. ``colorscale`` has to be set to None, otherwise + line colors are determined from ``colorscale``. plot_bgcolor : color of figure background title : str or None Title of ternary plot - smoothing : bool - If True, contours are smoothed. + interp_mode : 'ilr' (default) or 'cartesian' + Defines how data are interpolated to compute contours. If 'irl', + ILR (Isometric Log-Ratio) of compositional data is performed. If + 'cartesian', contours are determined in Cartesian space. + showmarkers : bool, default False + If True, markers corresponding to input compositional points are + superimposed on contours, using the same colorscale. + label_fontsize : int + Font size of pole labels. Examples ======== @@ -413,35 +440,50 @@ def create_ternarycontour(coordinates, values, pole_labels=['a', 'b', 'c'], point, since the sum of the three coordinates is one: fig = ff.create_ternarycontour(np.stack((a, b)), z) + plotly.iplot(fig) Example 2: ternary contour plot with line contours - fig = ff.create_ternarycontour(np.stack((a, b)), z, coloring='lines') + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, coloring='lines') + + Example 3: customize number of contours + + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, ncontours=8) + + Example 4: superimpose contour plot and original data as markers - Labels of contour plots can be displayed on the contours: + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, coloring='lines', + showmarkers=True) - fig = ff.create_ternarycontour(np.stack((a, b)), z, coloring='lines', - showlabels=True) + Example 5: customize title and pole labels + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + title='Ternary plot', + pole_labels=['clay', 'quartz', 'fledspar']) """ + if colorscale is None: + showscale = False + coordinates = _prepare_barycentric_coord(coordinates) grid_z, gr_x, gr_y = _compute_grid(coordinates, values, - tooltip_mode, interp_mode=interp_mode) layout = _ternary_layout(pole_labels=pole_labels, width=width, height=height, title=title, - plot_bgcolor=plot_bgcolor) + plot_bgcolor=plot_bgcolor, + label_fontsize=label_fontsize) contour_trace = _contour_trace(gr_x, gr_y, grid_z, ncontours=ncontours, colorscale=colorscale, + linecolor=linecolor, interp_mode=interp_mode, - coloring=coloring) + coloring=coloring, + tooltip_mode=tooltip_mode) fig = go.Figure(data=contour_trace, layout=layout) if showmarkers: a, b, c = coordinates - tooltip = _tooltip(a, b, c, values) + tooltip = _tooltip(a, b, c, values, mode=tooltip_mode) fig.add_scatterternary(a=a, b=b, c=c, mode='markers', marker={'color': values, From e632ec22aebbe6fee8eef0b14d8305e57c466321 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Feb 2019 23:03:03 +0100 Subject: [PATCH 15/33] fixed tests with ternary contour plot --- plotly/figure_factory/_ternarycontour.py | 317 ++++++++++-------- .../test_figure_factory.py | 55 +-- 2 files changed, 200 insertions(+), 172 deletions(-) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternarycontour.py index e5adbb348d9..22400e24dec 100644 --- a/plotly/figure_factory/_ternarycontour.py +++ b/plotly/figure_factory/_ternarycontour.py @@ -4,10 +4,95 @@ from plotly import exceptions, optional_imports np = optional_imports.get_module('numpy') -sk = optional_imports.get_module('skimage') +sk_measure = optional_imports.get_module('skimage.measure') scipy_interp = optional_imports.get_module('scipy.interpolate') +# ----------- Layout and tooltip ------------------------------ + + +def _ternary_layout(title='Ternary contour plot', width=550, height=525, + fontfamily='Balto, sans-serif', + plot_bgcolor='rgb(240,240,240)', + pole_labels=['a', 'b', 'c'], label_fontsize=16): + """ + Layout of ternary contour plot, to be passed to ``go.FigureWidget`` + object. + + Parameters + ========== + title : str or None + Title of ternary plot + width : int + Figure width. + height : int + Figure height. + fontfamily : str + Family of fonts + plot_bgcolor : + color of figure background + pole_labels : str, default ['a', 'b', 'c'] + Names of the three poles of the triangle. + label_fontsize : int + Font size of pole labels. + """ + return dict(title=title, + width=width, height=height, + font=dict(family=fontfamily), + plot_bgcolor=plot_bgcolor, + ternary=dict(sum=1, + aaxis=dict(title=dict(text=pole_labels[0], + font=dict(size=label_fontsize)), + min=0.01, linewidth=2, + ticks='outside'), + baxis=dict(title=dict(text=pole_labels[1], + font=dict(size=label_fontsize)), + min=0.01, linewidth=2, + ticks='outside'), + caxis=dict(title=dict(text=pole_labels[2], + font=dict(size=label_fontsize)), + min=0.01, linewidth=2, + ticks='outside')), + showlegend=False, + ) + + +def _tooltip(a, b, c, z, mode='proportions'): + """ + Tooltip annotations to be displayed on hover. + + Parameters + ========== + + a, b, c : 1-D array-like + Barycentric coordinates. + z : 1-D array + Values (e.g. elevation values) at barycentric coordinates. + mode : str, 'proportions' or 'percents' + Coordinates inside the ternary plot can be displayed either as + proportions (adding up to 1) or as percents (adding up to 100). + """ + N = len(a) + if np.isscalar(z): + z = z * np.ones(N) + if mode == 'proportions' or mode == 'proportion': + tooltip = ['a: %.2f' % round(a[i], 2) + + '
b: %.2f' % round(b[i], 2) + + '
c: %.2f' % round(c[i], 2) + + '
z: %.2f' % round(z[i], 2) for i in range(N)] + elif mode == 'percents' or mode == 'percent': + tooltip = ['a: %d' % int(100 * a[i] + 0.5) + + '
b: %d' % int(100 * b[i] + 0.5) + + '
c: %d' % int(100 * c[i] + 0.5) + + '
z: %.2f' % round(z[i], 2) for i in range(N)] + else: + raise ValueError("""tooltip mode must be either "proportions" or + "percents".""") + return tooltip + +# ------------- Transformations of coordinates ------------------- + + def _replace_zero_coords(ternary_data, delta=0.0005): """ Replaces zero ternary coordinates with delta and normalize the new @@ -107,6 +192,78 @@ def _transform_barycentric_cartesian(): return M, np.linalg.inv(M) +def _prepare_barycentric_coord(b_coords): + """ + Check ternary coordinates and return the right barycentric coordinates. + """ + if not isinstance(b_coords, (list, np.ndarray)): + raise ValueError('Data should be either an array of shape (n,m),' + 'or a list of n m-lists, m=2 or 3') + b_coords = np.asarray(b_coords) + if b_coords.shape[0] not in (2, 3): + raise ValueError('A point should have 2 (a, b) or 3 (a, b, c)' + 'barycentric coordinates') + if ((len(b_coords) == 3) and + not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01) and + not np.allclose(b_coords.sum(axis=0), 100, rtol=0.01)): + msg = "The sum of coordinates should be 1 or 100 for all data points" + raise ValueError(msg) + + if len(b_coords) == 2: + A, B = b_coords + C = 1 - (A + B) + else: + A, B, C = b_coords / b_coords.sum(axis=0) + if np.any(np.stack((A, B, C)) < 0): + raise ValueError('Barycentric coordinates should be positive.') + return np.stack((A, B, C)) + + +def _compute_grid(coordinates, values, interp_mode='ilr'): + """ + Transform data points with Cartesian or ILR mapping, then Compute + interpolation on a regular grid. + + Parameters + ========== + + coordinates : array-like + Barycentric coordinates of data points. + values : 1-d array-like + Data points, field to be represented as contours. + interp_mode : 'ilr' (default) or 'cartesian' + Defines how data are interpolated to compute contours. + """ + if interp_mode == 'cartesian': + M, invM = _transform_barycentric_cartesian() + coord_points = np.einsum('ik, kj -> ij', M, coordinates) + elif interp_mode == 'ilr': + coordinates = _replace_zero_coords(coordinates) + coord_points = _ilr_transform(coordinates) + else: + raise ValueError("interp_mode should be cartesian or ilr") + xx, yy = coord_points[:2] + x_min, x_max = xx.min(), xx.max() + y_min, y_max = yy.min(), yy.max() + n_interp = max(200, int(np.sqrt(len(values)))) + gr_x = np.linspace(x_min, x_max, n_interp) + gr_y = np.linspace(y_min, y_max, n_interp) + grid_x, grid_y = np.meshgrid(gr_x, gr_y) + # We use cubic interpolation, except outside of the convex hull + # of data points where we use nearest neighbor values. + grid_z = scipy_interp.griddata(coord_points[:2].T, values, + (grid_x, grid_y), + method='cubic') + grid_z_other = scipy_interp.griddata(coord_points[:2].T, values, + (grid_x, grid_y), + method='nearest') + mask_nan = np.isnan(grid_z) + grid_z[mask_nan] = grid_z_other[mask_nan] + return grid_z, gr_x, gr_y + +# ----------------------- Contour traces ---------------------- + + def _colors(ncontours, colormap=None): """ Return a list of ``ncontours`` colors from the ``colormap`` colorscale. @@ -136,6 +293,10 @@ def _colors(ncontours, colormap=None): return colors +def _is_invalid_contour(x, y): + return (np.all(np.abs(x - x[0]) < 2) and np.all(np.abs(y - y[0]) < 2)) + + def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', linecolor='rgb(150,150,150)', interp_mode='llr', @@ -183,13 +344,13 @@ def _contour_trace(x, y, z, ncontours=None, M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size dy = (y.max() - y.min()) / y.size - zz = np.copy(z) - zz[np.isnan(z)] = (z[mask_ok].min() - - 10. * (z[mask_ok].max() - z[mask_ok].min())) for i, val in enumerate(values): - contour_level = sk.measure.find_contours(zz, val) + contour_level = sk_measure.find_contours(z, val) + fill = 'toself' for contour in contour_level: # several closed contours for 1 value y_contour, x_contour = contour.T + if _is_invalid_contour(x_contour, y_contour): + continue if interp_mode == 'cartesian': bar_coords = np.dot(invM, np.stack((dx * x_contour, @@ -208,7 +369,7 @@ def _contour_trace(x, y, z, ncontours=None, type='scatterternary', text=tooltip, a=a, b=b, c=c, mode='lines', line=dict(color=_col, shape='spline', width=1), - fill='toself', + fill=fill, fillcolor=colors[i], hoverinfo='text', showlegend=True, @@ -219,153 +380,13 @@ def _contour_trace(x, y, z, ncontours=None, traces.append(trace) return traces - -def _ternary_layout(title='Ternary contour plot', width=550, height=525, - fontfamily='Balto, sans-serif', - plot_bgcolor='rgb(240,240,240)', - pole_labels=['a', 'b', 'c'], label_fontsize=16): - """ - Layout of ternary contour plot, to be passed to ``go.FigureWidget`` - object. - - Parameters - ========== - title : str or None - Title of ternary plot - width : int - Figure width. - height : int - Figure height. - fontfamily : str - Family of fonts - plot_bgcolor : - color of figure background - pole_labels : str, default ['a', 'b', 'c'] - Names of the three poles of the triangle. - label_fontsize : int - Font size of pole labels. - """ - return dict(title=title, - width=width, height=height, - font=dict(family=fontfamily), - plot_bgcolor=plot_bgcolor, - ternary=dict(sum=1, - aaxis=dict(title=dict(text=pole_labels[0], - font=dict(size=label_fontsize)), - min=0.01, linewidth=2, - ticks='outside'), - baxis=dict(title=dict(text=pole_labels[1], - font=dict(size=label_fontsize)), - min=0.01, linewidth=2, - ticks='outside'), - caxis=dict(title=dict(text=pole_labels[2], - font=dict(size=label_fontsize)), - min=0.01, linewidth=2, - ticks='outside')), - showlegend=False, - ) - - -def _tooltip(a, b, c, z, mode='proportions'): - """ - Tooltip annotations to be displayed on hover. - - Parameters - ========== - - a, b, c : 1-D array-like - Barycentric coordinates. - z : 1-D array - Values (e.g. elevation values) at barycentric coordinates. - mode : str, 'proportions' or 'percents' - Coordinates inside the ternary plot can be displayed either as - proportions (adding up to 1) or as percents (adding up to 100). - """ - N = len(a) - if np.isscalar(z): - z = z * np.ones(N) - if mode == 'proportions' or mode == 'proportion': - tooltip = ['a: %.2f' % round(a[i], 2) + - '
b: %.2f' % round(b[i], 2) + - '
c: %.2f' % round(c[i], 2) + - '
z: %.2f' % round(z[i], 2) for i in range(N)] - elif mode == 'percents' or mode == 'percent': - tooltip = ['a: %d' % int(100 * a[i] + 0.5) + - '
b: %d' % int(100 * b[i] + 0.5) + - '
c: %d' % int(100 * c[i] + 0.5) + - '
z: %.2f' % round(z[i], 2) for i in range(N)] - else: - raise ValueError("""tooltip mode must be either "proportions" or - "percents".""") - return tooltip - - -def _prepare_barycentric_coord(b_coords): - """ - Check ternary coordinates and return the right barycentric coordinates. - """ - if not isinstance(b_coords, (list, np.ndarray)): - raise ValueError('Data should be either an array of shape (n,m), or a list of n m-lists, m=2 or 3') - b_coords = np.asarray(b_coords) - if b_coords.shape[0] not in (2, 3): - raise ValueError('A point should have 2 (a, b) or 3 (a, b, c) barycentric coordinates') - if ((len(b_coords) == 3) and - not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01) and - not np.allclose(b_coords.sum(axis=0), 100, rtol=0.01)): - msg = "The sum of coordinates should be 1 or 100 for all data points" - raise ValueError(msg) - - if len(b_coords) == 2: - A, B = b_coords - C = 1 - (A + B) - else: - A, B, C = b_coords / b_coords.sum(axis=0) - if np.any(np.stack((A, B, C)) < 0): - raise ValueError('Barycentric coordinates should be positive.') - return np.stack((A, B, C)) - - -def _compute_grid(coordinates, values, interp_mode='ilr'): - """ - Transform data points with Cartesian or ILR mapping, then Compute - interpolation on a regular grid. - - Parameters - ========== - - coordinates : array-like - Barycentric coordinates of data points. - values : 1-d array-like - Data points, field to be represented as contours. - interp_mode : 'ilr' (default) or 'cartesian' - Defines how data are interpolated to compute contours. - """ - if interp_mode == 'cartesian': - M, invM = _transform_barycentric_cartesian() - coord_points = np.einsum('ik, kj -> ij', M, coordinates) - elif interp_mode == 'ilr': - coordinates = _replace_zero_coords(coordinates) - coord_points = _ilr_transform(coordinates) - else: - raise ValueError("interp_mode should be cartesian or ilr") - xx, yy = coord_points[:2] - x_min, x_max = xx.min(), xx.max() - y_min, y_max = yy.min(), yy.max() - n_interp = max(200, int(np.sqrt(len(values)))) - gr_x = np.linspace(x_min, x_max, n_interp) - gr_y = np.linspace(y_min, y_max, n_interp) - grid_x, grid_y = np.meshgrid(gr_x, gr_y) - grid_z = scipy_interp.griddata(coord_points[:2].T, values, - (grid_x, grid_y), - method='cubic') - return grid_z, gr_x, gr_y - +# -------------------- Figure Factory for ternary contour ------------- def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], tooltip_mode='proportions', width=500, height=500, ncontours=None, showscale=False, coloring=None, - colorscale='BlueRed', + colorscale='Bluered', linecolor=None, plot_bgcolor='rgb(240,240,240)', title=None, diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 5289fc7057a..829b428777d 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2907,40 +2907,40 @@ def test_wrong_coordinates(self): z = a * b with self.assertRaises(ValueError, msg='Barycentric coordinates should be positive.'): - _ = ff.create_ternarycontour(np.stack((a, b)), z) - mask = a + b < 1. + _ = ff.create_ternary_contour(np.stack((a, b)), z) + mask = a + b <= 1. a = a[mask] b = b[mask] with self.assertRaises(ValueError): - _ = ff.create_ternarycontour(np.stack((a, b, a, b)), z) + _ = ff.create_ternary_contour(np.stack((a, b, a, b)), z) with self.assertRaises(ValueError, msg='different number of values and points'): - _ = ff.create_ternarycontour(np.stack((a, b, 1 - a - b)), + _ = ff.create_ternary_contour(np.stack((a, b, 1 - a - b)), np.concatenate((z, [1]))) # Different sums for different points c = a with self.assertRaises(ValueError): - _ = ff.create_ternarycontour(np.stack((a, b, c)), z) + _ = ff.create_ternary_contour(np.stack((a, b, c)), z) # Sum of coordinates is different from one but is equal # for all points. with self.assertRaises(ValueError): - _ = ff.create_ternarycontour(np.stack((a, b, 2 - a - b)), z) + _ = ff.create_ternary_contour(np.stack((a, b, 2 - a - b)), z) def test_tooltip(self): a, b = np.mgrid[0:1:20j, 0:1:20j] - mask = a + b < 1. + mask = a + b <= 1. a = a[mask].ravel() b = b[mask].ravel() c = 1 - a - b z = a * b * c - fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, tooltip_mode='percents') - fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, tooltip_mode='percent') with self.assertRaises(ValueError): - fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, tooltip_mode='wrong_mode') @@ -2951,26 +2951,33 @@ def test_simple_ternary_contour(self): b = b[mask].ravel() c = 1 - a - b z = a * b * c - fig = ff.create_ternarycontour(np.stack((a, b, c)), z) - fig2 = ff.create_ternarycontour(np.stack((a, b)), z) - np.testing.assert_array_equal(fig2['data'][0]['z'], - fig['data'][0]['z']) + fig = ff.create_ternary_contour(np.stack((a, b, c)), z) + fig2 = ff.create_ternary_contour(np.stack((a, b)), z) + np.testing.assert_array_almost_equal(fig2['data'][0]['a'], + fig['data'][0]['a'], + decimal=3) - def test_contour_attributes(self): + def test_optional_arguments(self): a, b = np.mgrid[0:1:20j, 0:1:20j] - mask = a + b < 1. + mask = a + b <= 1. a = a[mask].ravel() b = b[mask].ravel() c = 1 - a - b z = a * b * c - contour_dict = {'ncontours': 10, - 'showscale': True, - 'reversescale': False} - - - fig = ff.create_ternarycontour(np.stack((a, b, c)), z, **contour_dict) - for key, value in contour_dict.items(): - assert fig['data'][0][key] == value + ncontours = 7 + args = [dict(showmarkers=False, showscale=False), + dict(showmarkers=True, showscale=False), + dict(showmarkers=False, showscale=True), + dict(showmarkers=True, showscale=True)] + + for arg_set in args: + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, + interp_mode='cartesian', + ncontours=ncontours, + **arg_set) + print(len(fig.data)) + assert (len(fig.data) == ncontours + arg_set['showmarkers'] + + arg_set['showscale']) From cb3f62f7b1b0cc11a43f1a93eda735540a37119a Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Feb 2019 23:08:49 +0100 Subject: [PATCH 16/33] renamed file --- plotly/figure_factory/{_ternarycontour.py => _ternary_contour.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plotly/figure_factory/{_ternarycontour.py => _ternary_contour.py} (100%) diff --git a/plotly/figure_factory/_ternarycontour.py b/plotly/figure_factory/_ternary_contour.py similarity index 100% rename from plotly/figure_factory/_ternarycontour.py rename to plotly/figure_factory/_ternary_contour.py From 5a64a5af83b20ce0e7dd74ac4a816b0bb9ce20a5 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Feb 2019 23:10:09 +0100 Subject: [PATCH 17/33] fixed optional import --- .../test_optional/test_figure_factory/test_figure_factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 829b428777d..a9c6f17ae9c 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -13,6 +13,7 @@ shapely = optional_imports.get_module('shapely') shapefile = optional_imports.get_module('shapefile') gp = optional_imports.get_module('geopandas') +sk_measure = optional_imports.get_module('skimage.measure') class TestDistplot(NumpyTestUtilsMixin, TestCase): From ab28c698778e3917fe5b8204a6cf658816b4d790 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Feb 2019 23:11:57 +0100 Subject: [PATCH 18/33] fixed name bug --- plotly/figure_factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/figure_factory/__init__.py b/plotly/figure_factory/__init__.py index 4d2a501061e..b7dd72c21bd 100644 --- a/plotly/figure_factory/__init__.py +++ b/plotly/figure_factory/__init__.py @@ -18,7 +18,7 @@ from plotly.figure_factory._scatterplot import create_scatterplotmatrix from plotly.figure_factory._streamline import create_streamline from plotly.figure_factory._table import create_table -from plotly.figure_factory._ternarycontour import create_ternary_contour +from plotly.figure_factory._ternary_contour import create_ternary_contour from plotly.figure_factory._trisurf import create_trisurf from plotly.figure_factory._violin import create_violin if optional_imports.get_module('pandas') is not None: From f85435fbeade8eecfd62f38e8a370051992a7aee Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 5 Feb 2019 10:06:04 +0100 Subject: [PATCH 19/33] better handling of colorspace interval + optional imports --- plotly/figure_factory/_ternary_contour.py | 34 ++++++++++++------- .../test_figure_factory.py | 27 +-------------- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 97e96a8a680..03993f4a143 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -5,8 +5,6 @@ from plotly import optional_imports from plotly.graph_objs import graph_objs as go -interpolate = optional_imports.get_module('scipy.interpolate') - np = optional_imports.get_module('numpy') sk_measure = optional_imports.get_module('skimage.measure') scipy_interp = optional_imports.get_module('scipy.interpolate') @@ -304,7 +302,8 @@ def _is_invalid_contour(x, y): def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', linecolor='rgb(150,150,150)', interp_mode='llr', - coloring=None, tooltip_mode='proportions'): + coloring=None, tooltip_mode='proportions', + v_min=0, v_max=1, fill_mode='toself'): """ Contour trace in Cartesian coordinates. @@ -331,6 +330,10 @@ def _contour_trace(x, y, z, ncontours=None, tooltip_mode : str, 'proportions' or 'percents' Coordinates inside the ternary plot can be displayed either as proportions (adding up to 1) or as percents (adding up to 100). + vmin, vmax : float + Bounds of interval of values used for the colorspace + fill_mode : 'toself' or 'tonext' + Mode used for filling contours when coloring=None """ if ncontours is None: ncontours = 5 @@ -341,16 +344,12 @@ def _contour_trace(x, y, z, ncontours=None, else: colors = [linecolor] * ncontours traces = [] - mask_nan = np.isnan(z) - mask_ok = np.logical_not(mask_nan) - values = np.linspace(z[mask_ok].min(), z[mask_ok].max(), - ncontours + 2)[1:-1] + values = np.linspace(v_min, v_max, ncontours) M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size dy = (y.max() - y.min()) / y.size for i, val in enumerate(values): contour_level = sk_measure.find_contours(z, val) - fill = 'toself' for contour in contour_level: # several closed contours for 1 value y_contour, x_contour = contour.T if _is_invalid_contour(x_contour, y_contour): @@ -373,7 +372,7 @@ def _contour_trace(x, y, z, ncontours=None, type='scatterternary', text=tooltip, a=a, b=b, c=c, mode='lines', line=dict(color=_col, shape='spline', width=1), - fill=fill, + fill=fill_mode, fillcolor=colors[i], hoverinfo='text', showlegend=True, @@ -397,7 +396,8 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], title=None, interp_mode='ilr', showmarkers=False, - label_fontsize=16): + label_fontsize=16, + fill_mode='toself'): """ Ternary contour plot. @@ -446,6 +446,8 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], superimposed on contours, using the same colorscale. label_fontsize : int Font size of pole labels. + fill_mode : 'toself' or 'tonext' + Mode used for filling contours when coloring=None Examples ======== @@ -487,12 +489,17 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], title='Ternary plot', pole_labels=['clay', 'quartz', 'fledspar']) """ - if interpolate is None: + if scipy_interp is None: raise ImportError("""\ The create_ternary_contour figure factory requires the scipy package""") + if sk_measure is None: + raise ImportError("""\ + The create_ternary_contour figure factory requires the scikit-image + package""") if colorscale is None: showscale = False coordinates = _prepare_barycentric_coord(coordinates) + v_min, v_max = values.min(), values.max() grid_z, gr_x, gr_y = _compute_grid(coordinates, values, interp_mode=interp_mode) @@ -507,7 +514,10 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], linecolor=linecolor, interp_mode=interp_mode, coloring=coloring, - tooltip_mode=tooltip_mode) + tooltip_mode=tooltip_mode, + v_min=v_min, + v_max=v_max, + fill_mode=fill_mode) fig = go.Figure(data=contour_trace, layout=layout) if showmarkers: diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index a9c6f17ae9c..3ccdd9c5e12 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2955,30 +2955,5 @@ def test_simple_ternary_contour(self): fig = ff.create_ternary_contour(np.stack((a, b, c)), z) fig2 = ff.create_ternary_contour(np.stack((a, b)), z) np.testing.assert_array_almost_equal(fig2['data'][0]['a'], - fig['data'][0]['a'], + fig['data'][0]['a'], decimal=3) - - - def test_optional_arguments(self): - a, b = np.mgrid[0:1:20j, 0:1:20j] - mask = a + b <= 1. - a = a[mask].ravel() - b = b[mask].ravel() - c = 1 - a - b - z = a * b * c - ncontours = 7 - args = [dict(showmarkers=False, showscale=False), - dict(showmarkers=True, showscale=False), - dict(showmarkers=False, showscale=True), - dict(showmarkers=True, showscale=True)] - - for arg_set in args: - fig = ff.create_ternary_contour(np.stack((a, b, c)), z, - interp_mode='cartesian', - ncontours=ncontours, - **arg_set) - print(len(fig.data)) - assert (len(fig.data) == ncontours + arg_set['showmarkers'] + - arg_set['showscale']) - - From 258fb09f47b9792b27f52fbcfd757ef6ceea0980 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 5 Feb 2019 10:13:52 +0100 Subject: [PATCH 20/33] attempt to fix CI with scikit-image import --- .circleci/create_conda_optional_env.sh | 2 +- tox.ini | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/create_conda_optional_env.sh b/.circleci/create_conda_optional_env.sh index d26a6cbdfb3..ad20809d5fb 100755 --- a/.circleci/create_conda_optional_env.sh +++ b/.circleci/create_conda_optional_env.sh @@ -16,7 +16,7 @@ if [ ! -d $HOME/miniconda/envs/circle_optional ]; then # Create environment # PYTHON_VERSION=3.6 $HOME/miniconda/bin/conda create -n circle_optional --yes python=$PYTHON_VERSION \ -requests six pytz retrying psutil pandas decorator pytest mock nose poppler xarray +requests six pytz retrying psutil pandas decorator pytest mock nose poppler xarray scikit-image # Install orca into environment $HOME/miniconda/bin/conda install --yes -n circle_optional -c plotly plotly-orca diff --git a/tox.ini b/tox.ini index 511f1973ff3..cb6b01bec3b 100644 --- a/tox.ini +++ b/tox.ini @@ -73,6 +73,7 @@ deps= optional: pillow==5.2.0 optional: matplotlib==2.2.3 optional: xarray==0.10.9 + optional: scikit-image==0.14.2 ; CORE ENVIRONMENTS [testenv:py27-core] From 335e2d2a7db30337d9282014acbfb39dfcdfeb3f Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 5 Feb 2019 10:44:42 +0100 Subject: [PATCH 21/33] Better handling of contour values --- plotly/figure_factory/_ternary_contour.py | 3 ++- .../test_figure_factory.py | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 03993f4a143..8494fb79a9a 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -344,10 +344,11 @@ def _contour_trace(x, y, z, ncontours=None, else: colors = [linecolor] * ncontours traces = [] - values = np.linspace(v_min, v_max, ncontours) + values = np.linspace(v_min, v_max, ncontours + 2)[1:-1] M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size dy = (y.max() - y.min()) / y.size + #stop for i, val in enumerate(values): contour_level = sk_measure.find_contours(z, val) for contour in contour_level: # several closed contours for 1 value diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 3ccdd9c5e12..b77070c9f28 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2957,3 +2957,26 @@ def test_simple_ternary_contour(self): np.testing.assert_array_almost_equal(fig2['data'][0]['a'], fig['data'][0]['a'], decimal=3) + + def test_optional_arguments(self): + a, b = np.mgrid[0:1:20j, 0:1:20j] + mask = a + b <= 1. + a = a[mask].ravel() + b = b[mask].ravel() + c = 1 - a - b + z = a * b * c + ncontours = 7 + args = [dict(showmarkers=False, showscale=False), + dict(showmarkers=True, showscale=False), + dict(showmarkers=False, showscale=True), + dict(showmarkers=True, showscale=True)] + + for arg_set in args: + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, + interp_mode='cartesian', + ncontours=ncontours, + **arg_set) + # This test does not work for ilr interpolation + assert (len(fig.data) == ncontours + arg_set['showmarkers'] + + arg_set['showscale']) + From 222f303dfe51db61b096578d9484e2fb5f16964c Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 5 Feb 2019 10:53:23 +0100 Subject: [PATCH 22/33] changed skimage version --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index cb6b01bec3b..ae79862938a 100644 --- a/tox.ini +++ b/tox.ini @@ -73,7 +73,7 @@ deps= optional: pillow==5.2.0 optional: matplotlib==2.2.3 optional: xarray==0.10.9 - optional: scikit-image==0.14.2 + optional: scikit-image==0.13.1 ; CORE ENVIRONMENTS [testenv:py27-core] From a6af167b6c1c9814b1ddb4b80ed28b96a2a9146c Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Tue, 5 Feb 2019 23:12:16 +0100 Subject: [PATCH 23/33] fill contours after sorting them by decreasing enclosed area --- plotly/figure_factory/_ternary_contour.py | 78 +++++++++++++---------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 8494fb79a9a..5668b361a22 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -265,6 +265,10 @@ def _compute_grid(coordinates, values, interp_mode='ilr'): # ----------------------- Contour traces ---------------------- +def _polygon_area(x, y): + return (0.5 * np.abs(np.dot(x, np.roll(y, 1)) + - np.dot(y, np.roll(x, 1)))) + def _colors(ncontours, colormap=None): """ @@ -348,40 +352,50 @@ def _contour_trace(x, y, z, ncontours=None, M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size dy = (y.max() - y.min()) / y.size - #stop + all_contours, all_values, all_areas, all_colors = [], [], [], [] + for i, val in enumerate(values): contour_level = sk_measure.find_contours(z, val) - for contour in contour_level: # several closed contours for 1 value - y_contour, x_contour = contour.T - if _is_invalid_contour(x_contour, y_contour): - continue - if interp_mode == 'cartesian': - bar_coords = np.dot(invM, - np.stack((dx * x_contour, - dy * y_contour, - np.ones(x_contour.shape)))) - elif interp_mode == 'ilr': - bar_coords = _ilr_inverse(np.stack((dx * x_contour + x.min(), - dy * y_contour + - y.min()))) - a, b, c = bar_coords - tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) - - _col = colors[i] if coloring == 'lines' else linecolor - - trace = dict( - type='scatterternary', text=tooltip, - a=a, b=b, c=c, mode='lines', - line=dict(color=_col, shape='spline', width=1), - fill=fill_mode, - fillcolor=colors[i], - hoverinfo='text', - showlegend=True, - name='%.2f' % val - ) - if coloring == 'lines': - trace['fill'] = None - traces.append(trace) + all_contours.extend(contour_level) + all_values.extend([val] * len(contour_level)) + all_areas.extend([_polygon_area(contour.T[1], contour.T[0]) + for contour in contour_level]) + all_colors.extend([colors[i]] * len(contour_level)) + + # Now sort contours by decreasing area + order = np.argsort(all_areas)[::-1] + for index in order: + y_contour, x_contour = all_contours[index].T + val = all_values[index] + if _is_invalid_contour(x_contour, y_contour): + continue + if interp_mode == 'cartesian': + bar_coords = np.dot(invM, + np.stack((dx * x_contour, + dy * y_contour, + np.ones(x_contour.shape)))) + elif interp_mode == 'ilr': + bar_coords = _ilr_inverse(np.stack((dx * x_contour + x.min(), + dy * y_contour + + y.min()))) + a, b, c = bar_coords + tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) + + _col = all_colors[index] if coloring == 'lines' else linecolor + + trace = dict( + type='scatterternary', text=tooltip, + a=a, b=b, c=c, mode='lines', + line=dict(color=_col, shape='spline', width=1), + fill='toself', + fillcolor=all_colors[index], + hoverinfo='text', + showlegend=True, + name='%.2f' % val + ) + if coloring == 'lines': + trace['fill'] = None + traces.append(trace) return traces # -------------------- Figure Factory for ternary contour ------------- From 4d8520fdb24db8dc4eb6066e476f2a6d8939d05f Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Wed, 6 Feb 2019 23:05:24 +0100 Subject: [PATCH 24/33] play with contour filling --- plotly/figure_factory/_ternary_contour.py | 36 +++++++++++++++---- .../test_figure_factory.py | 5 +-- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 5668b361a22..9e62018c558 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -259,8 +259,8 @@ def _compute_grid(coordinates, values, interp_mode='ilr'): grid_z_other = scipy_interp.griddata(coord_points[:2].T, values, (grid_x, grid_y), method='nearest') - mask_nan = np.isnan(grid_z) - grid_z[mask_nan] = grid_z_other[mask_nan] + #mask_nan = np.isnan(grid_z) + #grid_z[mask_nan] = grid_z_other[mask_nan] return grid_z, gr_x, gr_y # ----------------------- Contour traces ---------------------- @@ -342,20 +342,25 @@ def _contour_trace(x, y, z, ncontours=None, if ncontours is None: ncontours = 5 if colorscale is not None: - colors = _colors(ncontours, colorscale) + colors = _colors(ncontours + 2, colorscale) + color_min, color_max = colors[0], colors[-1] + colors = colors[1:-1] if linecolor is None: linecolor = 'rgb(150, 150, 150)' else: colors = [linecolor] * ncontours traces = [] - values = np.linspace(v_min, v_max, ncontours + 2)[1:-1] + values = np.linspace(v_min, v_max, ncontours + 2)[2:-2] M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size dy = (y.max() - y.min()) / y.size all_contours, all_values, all_areas, all_colors = [], [], [], [] - + mask_nan = np.isnan(z) + z_max = z[np.logical_not(mask_nan)].max() + zz = np.copy(z) + zz[mask_nan] = 2 * z_max for i, val in enumerate(values): - contour_level = sk_measure.find_contours(z, val) + contour_level = sk_measure.find_contours(zz, val) all_contours.extend(contour_level) all_values.extend([val] * len(contour_level)) all_areas.extend([_polygon_area(contour.T[1], contour.T[0]) @@ -364,6 +369,18 @@ def _contour_trace(x, y, z, ncontours=None, # Now sort contours by decreasing area order = np.argsort(all_areas)[::-1] + outer_contour = z.shape[0] * np.array([[0, 0, 1], [0, 1, 0.5]]).T + all_contours = [outer_contour] + all_contours + if all_values[order[0]] == values[0]: + all_values = [v_min] + all_values + all_colors = [color_min] + all_colors + elif all_values[order[0]] == values[-1]: + all_values = [v_max] + all_values + all_colors = [color_max] + all_colors + else: + print("problem!!!") + all_areas = [0] + all_areas + order = np.concatenate(([0], order + 1)) for index in order: y_contour, x_contour = all_contours[index].T val = all_values[index] @@ -378,7 +395,12 @@ def _contour_trace(x, y, z, ncontours=None, bar_coords = _ilr_inverse(np.stack((dx * x_contour + x.min(), dy * y_contour + y.min()))) - a, b, c = bar_coords + if index == 0: + a = np.array([1, 0, 0]) + b = np.array([0, 1, 0]) + c = np.array([0, 0, 1]) + else: + a, b, c = bar_coords tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) _col = all_colors[index] if coloring == 'lines' else linecolor diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index b77070c9f28..3b3e5d93d1a 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2977,6 +2977,7 @@ def test_optional_arguments(self): ncontours=ncontours, **arg_set) # This test does not work for ilr interpolation - assert (len(fig.data) == ncontours + arg_set['showmarkers'] + - arg_set['showscale']) + #print(len(fig.data)) + #assert (len(fig.data) == ncontours + 1 + arg_set['showmarkers'] + + # arg_set['showscale']) From fd8765c6f6ddfd47dfdea6f64118574d163a1b4c Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 10 Feb 2019 16:34:24 +0100 Subject: [PATCH 25/33] Better behavior of outer contour and color of background --- plotly/figure_factory/_ternary_contour.py | 141 +++++++++++++++------- 1 file changed, 96 insertions(+), 45 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 9e62018c558..a347156bfdd 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -299,15 +299,79 @@ def _colors(ncontours, colormap=None): return colors -def _is_invalid_contour(x, y): - return (np.all(np.abs(x - x[0]) < 2) and np.all(np.abs(y - y[0]) < 2)) +def _is_invalid_contour(x, y, a, b, c): + """ + Utility function for _contour_trace + """ + too_small = (np.all(np.abs(x - x[0]) < 2) and + np.all(np.abs(y - y[0]) < 2)) + ternary = np.stack((a, b, c)) + is_neg = np.any(ternary < 0, axis=0).mean() + return too_small or is_neg > 0.5 + + + + +def _extract_contours(im, values, colors): + """ + Utility function for _contour_trace + """ + mask_nan = np.isnan(im) + im_min, im_max = (im[np.logical_not(mask_nan)].min(), + im[np.logical_not(mask_nan)].max()) + zz_min = np.copy(im) + zz_min[mask_nan] = 2 * im_min + zz_max = np.copy(im) + zz_max[mask_nan] = 2 * im_max + all_contours1, all_values1, all_areas1, all_colors1 = [], [], [], [] + all_contours2, all_values2, all_areas2, all_colors2 = [], [], [], [] + for i, val in enumerate(values): + contour_level1 = sk_measure.find_contours(zz_min, val) + contour_level2 = sk_measure.find_contours(zz_max, val) + all_contours1.extend(contour_level1) + all_contours2.extend(contour_level2) + all_values1.extend([val] * len(contour_level1)) + all_values2.extend([val] * len(contour_level2)) + all_areas1.extend([_polygon_area(contour.T[1], contour.T[0]) + for contour in contour_level1]) + all_areas2.extend([_polygon_area(contour.T[1], contour.T[0]) + for contour in contour_level2]) + all_colors1.extend([colors[i]] * len(contour_level1)) + all_colors2.extend([colors[i]] * len(contour_level2)) + if len(all_contours1) <= len(all_contours2): + return all_contours1, all_values1, all_areas1, all_colors1 + else: + return all_contours2, all_values2, all_areas2, all_colors2 + + +def _add_outer_contour(all_contours, all_values, all_areas, all_colors, + values, val_outer, v_min, v_max, + colors, color_min, color_max): + """ + Utility function for _contour_trace + """ + outer_contour = 20 * np.array([[0, 0, 1], [0, 1, 0.5]]).T + all_contours = [outer_contour] + all_contours + values = np.concatenate(([np.nan], values, [np.nan])) + colors = np.concatenate(([color_min], colors, [color_max])) + index = np.nonzero(values == val_outer)[0][0] + if index < len(values)/2: + index -= 1 + else: + index += 1 + all_colors = [colors[index]] + all_colors + all_values = [values[index]] + all_values + all_areas = [0] + all_areas + return all_contours, all_values, all_areas, all_colors + + def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', linecolor='rgb(150,150,150)', interp_mode='llr', coloring=None, tooltip_mode='proportions', - v_min=0, v_max=1, fill_mode='toself'): + v_min=0, v_max=1): """ Contour trace in Cartesian coordinates. @@ -336,56 +400,42 @@ def _contour_trace(x, y, z, ncontours=None, proportions (adding up to 1) or as percents (adding up to 100). vmin, vmax : float Bounds of interval of values used for the colorspace - fill_mode : 'toself' or 'tonext' - Mode used for filling contours when coloring=None """ - if ncontours is None: - ncontours = 5 + # Prepare colors if colorscale is not None: colors = _colors(ncontours + 2, colorscale) color_min, color_max = colors[0], colors[-1] colors = colors[1:-1] + if linecolor is None: linecolor = 'rgb(150, 150, 150)' else: colors = [linecolor] * ncontours - traces = [] + values = np.linspace(v_min, v_max, ncontours + 2)[2:-2] - M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size dy = (y.max() - y.min()) / y.size - all_contours, all_values, all_areas, all_colors = [], [], [], [] - mask_nan = np.isnan(z) - z_max = z[np.logical_not(mask_nan)].max() - zz = np.copy(z) - zz[mask_nan] = 2 * z_max - for i, val in enumerate(values): - contour_level = sk_measure.find_contours(zz, val) - all_contours.extend(contour_level) - all_values.extend([val] * len(contour_level)) - all_areas.extend([_polygon_area(contour.T[1], contour.T[0]) - for contour in contour_level]) - all_colors.extend([colors[i]] * len(contour_level)) + + # Retrieve all contours + all_contours, all_values, all_areas, all_colors = _extract_contours( + z, values, colors) # Now sort contours by decreasing area order = np.argsort(all_areas)[::-1] - outer_contour = z.shape[0] * np.array([[0, 0, 1], [0, 1, 0.5]]).T - all_contours = [outer_contour] + all_contours - if all_values[order[0]] == values[0]: - all_values = [v_min] + all_values - all_colors = [color_min] + all_colors - elif all_values[order[0]] == values[-1]: - all_values = [v_max] + all_values - all_colors = [color_max] + all_colors - else: - print("problem!!!") - all_areas = [0] + all_areas + + # Add Outer contour + all_contours, all_values, all_areas, all_colors = _add_outer_contour( + all_contours, all_values, all_areas, all_colors, + values, all_values[order[0]], v_min, v_max, + colors, color_min, color_max) order = np.concatenate(([0], order + 1)) + + # Compute traces, in the order of decreasing area + traces = [] + M, invM = _transform_barycentric_cartesian() for index in order: y_contour, x_contour = all_contours[index].T val = all_values[index] - if _is_invalid_contour(x_contour, y_contour): - continue if interp_mode == 'cartesian': bar_coords = np.dot(invM, np.stack((dx * x_contour, @@ -395,29 +445,30 @@ def _contour_trace(x, y, z, ncontours=None, bar_coords = _ilr_inverse(np.stack((dx * x_contour + x.min(), dy * y_contour + y.min()))) - if index == 0: + if index == 0: # outer triangle a = np.array([1, 0, 0]) b = np.array([0, 1, 0]) c = np.array([0, 0, 1]) else: a, b, c = bar_coords + if _is_invalid_contour(x_contour, y_contour, a, b, c): + print("invalid!") + continue tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) _col = all_colors[index] if coloring == 'lines' else linecolor - trace = dict( type='scatterternary', text=tooltip, a=a, b=b, c=c, mode='lines', line=dict(color=_col, shape='spline', width=1), - fill='toself', - fillcolor=all_colors[index], - hoverinfo='text', - showlegend=True, - name='%.2f' % val + fill='toself', fillcolor=all_colors[index], + hoverinfo='text', showlegend=True, + name='%.3f' % val ) if coloring == 'lines': trace['fill'] = None traces.append(trace) + return traces # -------------------- Figure Factory for ternary contour ------------- @@ -483,8 +534,6 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], superimposed on contours, using the same colorscale. label_fontsize : int Font size of pole labels. - fill_mode : 'toself' or 'tonext' - Mode used for filling contours when coloring=None Examples ======== @@ -535,6 +584,8 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], package""") if colorscale is None: showscale = False + if ncontours is None: + ncontours = 5 coordinates = _prepare_barycentric_coord(coordinates) v_min, v_max = values.min(), values.max() grid_z, gr_x, gr_y = _compute_grid(coordinates, values, @@ -553,8 +604,8 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], coloring=coloring, tooltip_mode=tooltip_mode, v_min=v_min, - v_max=v_max, - fill_mode=fill_mode) + v_max=v_max) + fig = go.Figure(data=contour_trace, layout=layout) if showmarkers: From e8567ea70ddb4e7c34c5e8db62079bfd78401983 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 10 Feb 2019 17:12:04 +0100 Subject: [PATCH 26/33] Polished doc of ternary contour, ready for a review! --- plotly/figure_factory/_ternary_contour.py | 72 +++++++++++++------ .../test_figure_factory.py | 6 +- 2 files changed, 54 insertions(+), 24 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index a347156bfdd..04459fd1a9c 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -265,6 +265,7 @@ def _compute_grid(coordinates, values, interp_mode='ilr'): # ----------------------- Contour traces ---------------------- + def _polygon_area(x, y): return (0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))) @@ -299,22 +300,34 @@ def _colors(ncontours, colormap=None): return colors -def _is_invalid_contour(x, y, a, b, c): +def _is_invalid_contour(x, y): """ Utility function for _contour_trace + + Contours with an area of the order as 1 pixel are considered spurious. """ - too_small = (np.all(np.abs(x - x[0]) < 2) and + too_small = (np.all(np.abs(x - x[0]) < 2) and np.all(np.abs(y - y[0]) < 2)) - ternary = np.stack((a, b, c)) - is_neg = np.any(ternary < 0, axis=0).mean() - return too_small or is_neg > 0.5 - - + return too_small def _extract_contours(im, values, colors): """ - Utility function for _contour_trace + Utility function for _contour_trace. + + In ``im`` only one part of the domain has valid values (corresponding + to a subdomain where barycentric coordinates are well defined). When + computing contours, we need to assign values outside of this domain. + We can choose a value either smaller than all the values inside the + valid domain, or larger. This value must be chose with caution so that + no spurious contours are added. For example, if the boundary of the valid + domain has large values and the outer value is set to a small one, all + intermediate contours will be added at the boundary. + + Therefore, we compute the two sets of contours (with an outer value + smaller of larger than all values in the valid domain), and choose + the value resulting in a smaller total number of contours. There might + be a faster way to do this, but it works... """ mask_nan = np.isnan(im) im_min, im_max = (im[np.logical_not(mask_nan)].min(), @@ -333,9 +346,9 @@ def _extract_contours(im, values, colors): all_values1.extend([val] * len(contour_level1)) all_values2.extend([val] * len(contour_level2)) all_areas1.extend([_polygon_area(contour.T[1], contour.T[0]) - for contour in contour_level1]) + for contour in contour_level1]) all_areas2.extend([_polygon_area(contour.T[1], contour.T[0]) - for contour in contour_level2]) + for contour in contour_level2]) all_colors1.extend([colors[i]] * len(contour_level1)) all_colors2.extend([colors[i]] * len(contour_level2)) if len(all_contours1) <= len(all_contours2): @@ -349,10 +362,22 @@ def _add_outer_contour(all_contours, all_values, all_areas, all_colors, colors, color_min, color_max): """ Utility function for _contour_trace + + Adds the background color to fill gaps outside of computed contours. + + To compute the background color, the color of the contour with largest + area (``val_outer``) is used. As background color, we choose the next + color value in the direction of the extrema of the colormap. + + Then we add information for the outer contour for the different lists + provided as arguments. """ + # The exact value of outer contour is not used when defining the trace outer_contour = 20 * np.array([[0, 0, 1], [0, 1, 0.5]]).T all_contours = [outer_contour] + all_contours - values = np.concatenate(([np.nan], values, [np.nan])) + delta_values = np.diff(values)[0] + values = np.concatenate(([values[0] - delta_values], values, + [values[-1] + delta_values])) colors = np.concatenate(([color_min], colors, [color_max])) index = np.nonzero(values == val_outer)[0][0] if index < len(values)/2: @@ -365,8 +390,6 @@ def _add_outer_contour(all_contours, all_values, all_areas, all_colors, return all_contours, all_values, all_areas, all_colors - - def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', linecolor='rgb(150,150,150)', interp_mode='llr', @@ -400,21 +423,28 @@ def _contour_trace(x, y, z, ncontours=None, proportions (adding up to 1) or as percents (adding up to 100). vmin, vmax : float Bounds of interval of values used for the colorspace + + Notes + ===== """ # Prepare colors if colorscale is not None: + # We do not take extrema, for example for one single contour + # the color will be the middle point of the colormap colors = _colors(ncontours + 2, colorscale) color_min, color_max = colors[0], colors[-1] colors = colors[1:-1] + # Color of line contours if linecolor is None: linecolor = 'rgb(150, 150, 150)' else: colors = [linecolor] * ncontours - values = np.linspace(v_min, v_max, ncontours + 2)[2:-2] - dx = (x.max() - x.min()) / x.size - dy = (y.max() - y.min()) / y.size + # Values used for contours, extrema are not used + # For example for a binary array [0, 1], the value of + # the contour for ncontours=1 is 0.5. + values = np.linspace(v_min, v_max, ncontours + 2)[1:-1] # Retrieve all contours all_contours, all_values, all_areas, all_colors = _extract_contours( @@ -423,7 +453,7 @@ def _contour_trace(x, y, z, ncontours=None, # Now sort contours by decreasing area order = np.argsort(all_areas)[::-1] - # Add Outer contour + # Add outer contour all_contours, all_values, all_areas, all_colors = _add_outer_contour( all_contours, all_values, all_areas, all_colors, values, all_values[order[0]], v_min, v_max, @@ -433,6 +463,8 @@ def _contour_trace(x, y, z, ncontours=None, # Compute traces, in the order of decreasing area traces = [] M, invM = _transform_barycentric_cartesian() + dx = (x.max() - x.min()) / x.size + dy = (y.max() - y.min()) / y.size for index in order: y_contour, x_contour = all_contours[index].T val = all_values[index] @@ -451,8 +483,7 @@ def _contour_trace(x, y, z, ncontours=None, c = np.array([0, 0, 1]) else: a, b, c = bar_coords - if _is_invalid_contour(x_contour, y_contour, a, b, c): - print("invalid!") + if _is_invalid_contour(x_contour, y_contour): continue tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) @@ -484,8 +515,7 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], title=None, interp_mode='ilr', showmarkers=False, - label_fontsize=16, - fill_mode='toself'): + label_fontsize=16): """ Ternary contour plot. diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index 3b3e5d93d1a..df3a98959ab 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2977,7 +2977,7 @@ def test_optional_arguments(self): ncontours=ncontours, **arg_set) # This test does not work for ilr interpolation - #print(len(fig.data)) - #assert (len(fig.data) == ncontours + 1 + arg_set['showmarkers'] + - # arg_set['showscale']) + print(len(fig.data)) + assert (len(fig.data) == ncontours + 1 + arg_set['showmarkers'] + + arg_set['showscale']) From 0182f3d5a973ddcc14837f09659a144cc0df7b9b Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Thu, 28 Feb 2019 17:39:13 +0100 Subject: [PATCH 27/33] Removed bgcolor and font configuration. Added marker line --- plotly/figure_factory/_ternary_contour.py | 41 +++++++---------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 04459fd1a9c..51af04b4786 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -14,9 +14,7 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, - fontfamily='Balto, sans-serif', - plot_bgcolor='rgb(240,240,240)', - pole_labels=['a', 'b', 'c'], label_fontsize=16): + pole_labels=['a', 'b', 'c']): """ Layout of ternary contour plot, to be passed to ``go.FigureWidget`` object. @@ -29,34 +27,23 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, Figure width. height : int Figure height. - fontfamily : str - Family of fonts - plot_bgcolor : - color of figure background pole_labels : str, default ['a', 'b', 'c'] Names of the three poles of the triangle. - label_fontsize : int - Font size of pole labels. """ return dict(title=title, width=width, height=height, - font=dict(family=fontfamily), - plot_bgcolor=plot_bgcolor, ternary=dict(sum=1, - aaxis=dict(title=dict(text=pole_labels[0], - font=dict(size=label_fontsize)), + aaxis=dict(title=dict(text=pole_labels[0]), min=0.01, linewidth=2, ticks='outside'), - baxis=dict(title=dict(text=pole_labels[1], - font=dict(size=label_fontsize)), + baxis=dict(title=dict(text=pole_labels[1]), min=0.01, linewidth=2, ticks='outside'), - caxis=dict(title=dict(text=pole_labels[2], - font=dict(size=label_fontsize)), + caxis=dict(title=dict(text=pole_labels[2]), min=0.01, linewidth=2, ticks='outside')), showlegend=False, - ) + ) def _tooltip(a, b, c, z, mode='proportions'): @@ -511,11 +498,9 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], showscale=False, coloring=None, colorscale='Bluered', linecolor=None, - plot_bgcolor='rgb(240,240,240)', title=None, interp_mode='ilr', - showmarkers=False, - label_fontsize=16): + showmarkers=False): """ Ternary contour plot. @@ -551,8 +536,6 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], linecolor : None or rgb color Color used for lines. ``colorscale`` has to be set to None, otherwise line colors are determined from ``colorscale``. - plot_bgcolor : - color of figure background title : str or None Title of ternary plot interp_mode : 'ilr' (default) or 'cartesian' @@ -562,8 +545,6 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], showmarkers : bool, default False If True, markers corresponding to input compositional points are superimposed on contours, using the same colorscale. - label_fontsize : int - Font size of pole labels. Examples ======== @@ -622,9 +603,7 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], interp_mode=interp_mode) layout = _ternary_layout(pole_labels=pole_labels, - width=width, height=height, title=title, - plot_bgcolor=plot_bgcolor, - label_fontsize=label_fontsize) + width=width, height=height, title=title) contour_trace = _contour_trace(gr_x, gr_y, grid_z, ncontours=ncontours, @@ -644,7 +623,11 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], fig.add_scatterternary(a=a, b=b, c=c, mode='markers', marker={'color': values, - 'colorscale': colorscale}, + 'colorscale': colorscale, + 'line':{'color':'rgb(120, 120, 120)', + 'width':1, + }, + }, text=tooltip, hoverinfo='text') if showscale: From 2c1857c3337f48acf223864b64b70ed5bbde74c1 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Mar 2019 16:11:48 +0100 Subject: [PATCH 28/33] modified hover behavior --- plotly/figure_factory/_ternary_contour.py | 71 ++++++----------------- 1 file changed, 19 insertions(+), 52 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 51af04b4786..5d256ea1f7b 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -46,39 +46,6 @@ def _ternary_layout(title='Ternary contour plot', width=550, height=525, ) -def _tooltip(a, b, c, z, mode='proportions'): - """ - Tooltip annotations to be displayed on hover. - - Parameters - ========== - - a, b, c : 1-D array-like - Barycentric coordinates. - z : 1-D array - Values (e.g. elevation values) at barycentric coordinates. - mode : str, 'proportions' or 'percents' - Coordinates inside the ternary plot can be displayed either as - proportions (adding up to 1) or as percents (adding up to 100). - """ - N = len(a) - if np.isscalar(z): - z = z * np.ones(N) - if mode == 'proportions' or mode == 'proportion': - tooltip = ['a: %.2f' % round(a[i], 2) + - '
b: %.2f' % round(b[i], 2) + - '
c: %.2f' % round(c[i], 2) + - '
z: %.2f' % round(z[i], 2) for i in range(N)] - elif mode == 'percents' or mode == 'percent': - tooltip = ['a: %d' % int(100 * a[i] + 0.5) + - '
b: %d' % int(100 * b[i] + 0.5) + - '
c: %d' % int(100 * c[i] + 0.5) + - '
z: %.2f' % round(z[i], 2) for i in range(N)] - else: - raise ValueError("""tooltip mode must be either "proportions" or - "percents".""") - return tooltip - # ------------- Transformations of coordinates ------------------- @@ -380,7 +347,7 @@ def _add_outer_contour(all_contours, all_values, all_areas, all_colors, def _contour_trace(x, y, z, ncontours=None, colorscale='Electric', linecolor='rgb(150,150,150)', interp_mode='llr', - coloring=None, tooltip_mode='proportions', + coloring=None, v_min=0, v_max=1): """ Contour trace in Cartesian coordinates. @@ -472,15 +439,14 @@ def _contour_trace(x, y, z, ncontours=None, a, b, c = bar_coords if _is_invalid_contour(x_contour, y_contour): continue - tooltip = _tooltip(a, b, c, val, mode=tooltip_mode) _col = all_colors[index] if coloring == 'lines' else linecolor trace = dict( - type='scatterternary', text=tooltip, + type='scatterternary', a=a, b=b, c=c, mode='lines', line=dict(color=_col, shape='spline', width=1), fill='toself', fillcolor=all_colors[index], - hoverinfo='text', showlegend=True, + showlegend=True, name='%.3f' % val ) if coloring == 'lines': @@ -611,25 +577,26 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], linecolor=linecolor, interp_mode=interp_mode, coloring=coloring, - tooltip_mode=tooltip_mode, v_min=v_min, v_max=v_max) fig = go.Figure(data=contour_trace, layout=layout) - - if showmarkers: - a, b, c = coordinates - tooltip = _tooltip(a, b, c, values, mode=tooltip_mode) - fig.add_scatterternary(a=a, b=b, c=c, - mode='markers', - marker={'color': values, - 'colorscale': colorscale, - 'line':{'color':'rgb(120, 120, 120)', - 'width':1, - }, - }, - text=tooltip, - hoverinfo='text') + opacity = 1 if showmarkers else 0 + a, b, c = coordinates + hovertemplate = (pole_labels[0] + ": %{a:.3f}
" + + pole_labels[1] + ": %{b:.3f}
" + + pole_labels[2] + ": %{c:.3f}
" + "z: %{marker.color:.3f}") + fig.add_scatterternary(a=a, b=b, c=c, + mode='markers', + marker={'color': values, + 'colorscale': colorscale, + 'line':{'color':'rgb(120, 120, 120)', + 'width':1, + }, + }, + opacity=opacity, + hovertemplate=hovertemplate) if showscale: colorbar = dict({'type': 'scatterternary', 'a': [None], 'b': [None], From 6f9b0786c879551ffc34af9ed55123ccd1315175 Mon Sep 17 00:00:00 2001 From: Emmanuelle Gouillart Date: Sun, 3 Mar 2019 22:32:16 +0100 Subject: [PATCH 29/33] Discrete colormap when only contours are plotted (no markers) --- plotly/figure_factory/_ternary_contour.py | 94 ++++++++++--------- .../test_figure_factory.py | 49 ++++++---- 2 files changed, 83 insertions(+), 60 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 5d256ea1f7b..c7b1845bec4 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -10,7 +10,7 @@ scipy_interp = optional_imports.get_module('scipy.interpolate') -# ----------- Layout and tooltip ------------------------------ +# -------------------------- Layout ------------------------------ def _ternary_layout(title='Ternary contour plot', width=550, height=525, @@ -325,6 +325,9 @@ def _add_outer_contour(all_contours, all_values, all_areas, all_colors, Then we add information for the outer contour for the different lists provided as arguments. + + A discrete colormap with all used colors is also returned (to be used + by colorscale trace). """ # The exact value of outer contour is not used when defining the trace outer_contour = 20 * np.array([[0, 0, 1], [0, 1, 0.5]]).T @@ -341,7 +344,17 @@ def _add_outer_contour(all_contours, all_values, all_areas, all_colors, all_colors = [colors[index]] + all_colors all_values = [values[index]] + all_values all_areas = [0] + all_areas - return all_contours, all_values, all_areas, all_colors + used_colors = [color for color in colors if color in all_colors] + # Define discrete colorscale + color_number = len(used_colors) + scale = np.linspace(0, 1, color_number + 1) + discrete_cm = [] + for i, color in enumerate(used_colors): + discrete_cm.append([scale[i], used_colors[i]]) + discrete_cm.append([scale[i + 1], used_colors[i]]) + discrete_cm.append([scale[color_number], used_colors[color_number - 1]]) + + return all_contours, all_values, all_areas, all_colors, discrete_cm def _contour_trace(x, y, z, ncontours=None, @@ -372,9 +385,6 @@ def _contour_trace(x, y, z, ncontours=None, 'cartesian', contours are determined in Cartesian space. coloring : None or 'lines' How to display contour. Filled contours if None, lines if ``lines``. - tooltip_mode : str, 'proportions' or 'percents' - Coordinates inside the ternary plot can be displayed either as - proportions (adding up to 1) or as percents (adding up to 100). vmin, vmax : float Bounds of interval of values used for the colorspace @@ -382,12 +392,16 @@ def _contour_trace(x, y, z, ncontours=None, ===== """ # Prepare colors - if colorscale is not None: - # We do not take extrema, for example for one single contour - # the color will be the middle point of the colormap - colors = _colors(ncontours + 2, colorscale) - color_min, color_max = colors[0], colors[-1] - colors = colors[1:-1] + # We do not take extrema, for example for one single contour + # the color will be the middle point of the colormap + colors = _colors(ncontours + 2, colorscale) + # Values used for contours, extrema are not used + # For example for a binary array [0, 1], the value of + # the contour for ncontours=1 is 0.5. + values = np.linspace(v_min, v_max, ncontours + 2) + color_min, color_max = colors[0], colors[-1] + colors = colors[1:-1] + values = values[1:-1] # Color of line contours if linecolor is None: @@ -395,12 +409,7 @@ def _contour_trace(x, y, z, ncontours=None, else: colors = [linecolor] * ncontours - # Values used for contours, extrema are not used - # For example for a binary array [0, 1], the value of - # the contour for ncontours=1 is 0.5. - values = np.linspace(v_min, v_max, ncontours + 2)[1:-1] - - # Retrieve all contours + # Retrieve all contours all_contours, all_values, all_areas, all_colors = _extract_contours( z, values, colors) @@ -408,13 +417,14 @@ def _contour_trace(x, y, z, ncontours=None, order = np.argsort(all_areas)[::-1] # Add outer contour - all_contours, all_values, all_areas, all_colors = _add_outer_contour( + all_contours, all_values, all_areas, all_colors, discrete_cm = \ + _add_outer_contour( all_contours, all_values, all_areas, all_colors, values, all_values[order[0]], v_min, v_max, colors, color_min, color_max) order = np.concatenate(([0], order + 1)) - # Compute traces, in the order of decreasing area + # Compute traces, in the order of decreasing area traces = [] M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size @@ -453,13 +463,13 @@ def _contour_trace(x, y, z, ncontours=None, trace['fill'] = None traces.append(trace) - return traces + return traces, discrete_cm # -------------------- Figure Factory for ternary contour ------------- def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], - tooltip_mode='proportions', width=500, height=500, + width=500, height=500, ncontours=None, showscale=False, coloring=None, colorscale='Bluered', @@ -481,9 +491,6 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], Data points of field to be represented as contours. pole_labels : str, default ['a', 'b', 'c'] Names of the three poles of the triangle. - tooltip_mode : str, 'proportions' or 'percents' - Coordinates inside the ternary plot can be displayed either as - proportions (adding up to 1) or as percents (adding up to 100). width : int Figure width. height : int @@ -494,9 +501,6 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], If True, a colorbar showing the color scale is displayed. coloring : None or 'lines' How to display contour. Filled contours if None, lines if ``lines``. - showlabels : bool, default False - For line contours (coloring='lines'), the value of the contour is - displayed if showlabels is True. colorscale : None or str (Plotly colormap) colorscale of the contours. linecolor : None or rgb color @@ -571,37 +575,41 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], layout = _ternary_layout(pole_labels=pole_labels, width=width, height=height, title=title) - contour_trace = _contour_trace(gr_x, gr_y, grid_z, - ncontours=ncontours, - colorscale=colorscale, - linecolor=linecolor, - interp_mode=interp_mode, - coloring=coloring, - v_min=v_min, - v_max=v_max) + contour_trace, discrete_cm = _contour_trace(gr_x, gr_y, grid_z, + ncontours=ncontours, + colorscale=colorscale, + linecolor=linecolor, + interp_mode=interp_mode, + coloring=coloring, + v_min=v_min, + v_max=v_max) fig = go.Figure(data=contour_trace, layout=layout) + opacity = 1 if showmarkers else 0 a, b, c = coordinates hovertemplate = (pole_labels[0] + ": %{a:.3f}
" + pole_labels[1] + ": %{b:.3f}
" + pole_labels[2] + ": %{c:.3f}
" "z: %{marker.color:.3f}") + fig.add_scatterternary(a=a, b=b, c=c, - mode='markers', - marker={'color': values, - 'colorscale': colorscale, - 'line':{'color':'rgb(120, 120, 120)', - 'width':1, - }, - }, + mode='markers', + marker={'color': values, + 'colorscale': colorscale, + 'line':{'color':'rgb(120, 120, 120)', + 'width':1,}, + }, opacity=opacity, hovertemplate=hovertemplate) if showscale: + if not showmarkers: + colorscale = discrete_cm colorbar = dict({'type': 'scatterternary', 'a': [None], 'b': [None], 'c': [None], - 'marker': {'cmin': values.min(), + 'marker': { + 'cmin': values.min(), 'cmax': values.max(), 'colorscale': colorscale, 'showscale': True}, diff --git a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py index df3a98959ab..c9b69ef209f 100644 --- a/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py +++ b/plotly/tests/test_optional/test_figure_factory/test_figure_factory.py @@ -2928,35 +2928,51 @@ def test_wrong_coordinates(self): _ = ff.create_ternary_contour(np.stack((a, b, 2 - a - b)), z) - def test_tooltip(self): + def test_simple_ternary_contour(self): a, b = np.mgrid[0:1:20j, 0:1:20j] - mask = a + b <= 1. + mask = a + b < 1. a = a[mask].ravel() b = b[mask].ravel() c = 1 - a - b z = a * b * c - fig = ff.create_ternary_contour(np.stack((a, b, c)), z, - tooltip_mode='percents') - fig = ff.create_ternary_contour(np.stack((a, b, c)), z, - tooltip_mode='percent') + fig = ff.create_ternary_contour(np.stack((a, b, c)), z) + fig2 = ff.create_ternary_contour(np.stack((a, b)), z) + np.testing.assert_array_almost_equal(fig2['data'][0]['a'], + fig['data'][0]['a'], + decimal=3) - with self.assertRaises(ValueError): - fig = ff.create_ternary_contour(np.stack((a, b, c)), z, - tooltip_mode='wrong_mode') + + def test_colorscale(self): + a, b = np.mgrid[0:1:20j, 0:1:20j] + mask = a + b < 1. + a = a[mask].ravel() + b = b[mask].ravel() + c = 1 - a - b + z = a * b * c + z /= z.max() + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, + showscale=True) + fig2 = ff.create_ternary_contour(np.stack((a, b, c)), z, + showscale=True, showmarkers=True) + assert isinstance(fig.data[-1]['marker']['colorscale'], tuple) + assert isinstance(fig2.data[-1]['marker']['colorscale'], str) + assert fig.data[-1]['marker']['cmax'] == 1 + assert fig2.data[-1]['marker']['cmax'] == 1 - def test_simple_ternary_contour(self): + def check_pole_labels(self): a, b = np.mgrid[0:1:20j, 0:1:20j] mask = a + b < 1. a = a[mask].ravel() b = b[mask].ravel() c = 1 - a - b z = a * b * c - fig = ff.create_ternary_contour(np.stack((a, b, c)), z) - fig2 = ff.create_ternary_contour(np.stack((a, b)), z) - np.testing.assert_array_almost_equal(fig2['data'][0]['a'], - fig['data'][0]['a'], - decimal=3) + pole_labels = ['A', 'B', 'C'] + fig = ff.create_ternary_contour(np.stack((a, b, c)), z, + pole_labels=pole_labels) + assert fig.layout.ternary.aaxis.title.text == pole_labels[0] + assert fig.data[-1].hovertemplate[0] == pole_labels[0] + def test_optional_arguments(self): a, b = np.mgrid[0:1:20j, 0:1:20j] @@ -2978,6 +2994,5 @@ def test_optional_arguments(self): **arg_set) # This test does not work for ilr interpolation print(len(fig.data)) - assert (len(fig.data) == ncontours + 1 + arg_set['showmarkers'] + - arg_set['showscale']) + assert (len(fig.data) == ncontours + 2 + arg_set['showscale']) From a38fc545375705105f54644788d103fca287aded Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 4 Mar 2019 07:59:19 -0500 Subject: [PATCH 30/33] Hide trace name from hovertemplate --- plotly/figure_factory/_ternary_contour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index c7b1845bec4..e6dde03fa24 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -591,7 +591,7 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], hovertemplate = (pole_labels[0] + ": %{a:.3f}
" + pole_labels[1] + ": %{b:.3f}
" + pole_labels[2] + ": %{c:.3f}
" - "z: %{marker.color:.3f}") + "z: %{marker.color:.3f}") fig.add_scatterternary(a=a, b=b, c=c, mode='markers', From b43fff4fa1c6f0d509f540bb4fadb2956144e157 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 4 Mar 2019 08:06:19 -0500 Subject: [PATCH 31/33] Skip tooltip on contour lines in `lines` mode. This gives consistent behavior between fill and lines modes --- plotly/figure_factory/_ternary_contour.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index e6dde03fa24..463e97c3a96 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -457,6 +457,7 @@ def _contour_trace(x, y, z, ncontours=None, line=dict(color=_col, shape='spline', width=1), fill='toself', fillcolor=all_colors[index], showlegend=True, + hoverinfo='skip', name='%.3f' % val ) if coloring == 'lines': From 10e6eb441fd544795f3f273121fd101c7b844eee Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 4 Mar 2019 08:11:57 -0500 Subject: [PATCH 32/33] Only display marker outline for filled contours --- plotly/figure_factory/_ternary_contour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 463e97c3a96..5d4b1269de5 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -598,8 +598,8 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], mode='markers', marker={'color': values, 'colorscale': colorscale, - 'line':{'color':'rgb(120, 120, 120)', - 'width':1,}, + 'line':{'color': 'rgb(120, 120, 120)', + 'width': int(coloring != 'lines')}, }, opacity=opacity, hovertemplate=hovertemplate) From 004f6ef53fcce547771483cc6d0a7a89d6a53e99 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 4 Mar 2019 08:16:38 -0500 Subject: [PATCH 33/33] whitespace --- plotly/figure_factory/_ternary_contour.py | 48 ++++++++++++----------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index 5d4b1269de5..1cca22a7c28 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -123,11 +123,11 @@ def _ilr_inverse(x): Intl Assoc for Math Geology, 2003, pp 31-30. """ x = np.array(x) - matrix = np.array([[0.5, 1, 1.], - [-0.5, 1, 1.], - [0., 0., 1.]]) - s = np.sqrt(2)/2 - t = np.sqrt(3/2) + matrix = np.array([[0.5, 1, 1.], + [-0.5, 1, 1.], + [0., 0., 1.]]) + s = np.sqrt(2) / 2 + t = np.sqrt(3 / 2) Sk = np.einsum('ik, kj -> ij', np.array([[s, t], [-s, t]]), x) Z = -np.log(1 + np.exp(Sk).sum(axis=0)) log_barycentric = np.einsum('ik, kj -> ij', @@ -160,8 +160,8 @@ def _prepare_barycentric_coord(b_coords): raise ValueError('A point should have 2 (a, b) or 3 (a, b, c)' 'barycentric coordinates') if ((len(b_coords) == 3) and - not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01) and - not np.allclose(b_coords.sum(axis=0), 100, rtol=0.01)): + not np.allclose(b_coords.sum(axis=0), 1, rtol=0.01) and + not np.allclose(b_coords.sum(axis=0), 100, rtol=0.01)): msg = "The sum of coordinates should be 1 or 100 for all data points" raise ValueError(msg) @@ -213,16 +213,17 @@ def _compute_grid(coordinates, values, interp_mode='ilr'): grid_z_other = scipy_interp.griddata(coord_points[:2].T, values, (grid_x, grid_y), method='nearest') - #mask_nan = np.isnan(grid_z) - #grid_z[mask_nan] = grid_z_other[mask_nan] + # mask_nan = np.isnan(grid_z) + # grid_z[mask_nan] = grid_z_other[mask_nan] return grid_z, gr_x, gr_y + # ----------------------- Contour traces ---------------------- def _polygon_area(x, y): return (0.5 * np.abs(np.dot(x, np.roll(y, 1)) - - np.dot(y, np.roll(x, 1)))) + - np.dot(y, np.roll(x, 1)))) def _colors(ncontours, colormap=None): @@ -235,12 +236,12 @@ def _colors(ncontours, colormap=None): raise exceptions.PlotlyError( "Colorscale must be a valid Plotly Colorscale." "The available colorscale names are {}".format( - clrs.PLOTLY_SCALES.keys())) + clrs.PLOTLY_SCALES.keys())) values = np.linspace(0, 1, ncontours) vals_cmap = np.array([pair[0] for pair in cmap]) cols = np.array([pair[1] for pair in cmap]) inds = np.searchsorted(vals_cmap, values) - if '#'in cols[0]: # for Viridis + if '#' in cols[0]: # for Viridis cols = [clrs.label_rgb(clrs.hex_to_rgb(col)) for col in cols] colors = [cols[0]] @@ -312,8 +313,8 @@ def _extract_contours(im, values, colors): def _add_outer_contour(all_contours, all_values, all_areas, all_colors, - values, val_outer, v_min, v_max, - colors, color_min, color_max): + values, val_outer, v_min, v_max, + colors, color_min, color_max): """ Utility function for _contour_trace @@ -337,7 +338,7 @@ def _add_outer_contour(all_contours, all_values, all_areas, all_colors, [values[-1] + delta_values])) colors = np.concatenate(([color_min], colors, [color_max])) index = np.nonzero(values == val_outer)[0][0] - if index < len(values)/2: + if index < len(values) / 2: index -= 1 else: index += 1 @@ -424,7 +425,7 @@ def _contour_trace(x, y, z, ncontours=None, colors, color_min, color_max) order = np.concatenate(([0], order + 1)) - # Compute traces, in the order of decreasing area + # Compute traces, in the order of decreasing area traces = [] M, invM = _transform_barycentric_cartesian() dx = (x.max() - x.min()) / x.size @@ -435,8 +436,8 @@ def _contour_trace(x, y, z, ncontours=None, if interp_mode == 'cartesian': bar_coords = np.dot(invM, np.stack((dx * x_contour, - dy * y_contour, - np.ones(x_contour.shape)))) + dy * y_contour, + np.ones(x_contour.shape)))) elif interp_mode == 'ilr': bar_coords = _ilr_inverse(np.stack((dx * x_contour + x.min(), dy * y_contour + @@ -466,6 +467,7 @@ def _contour_trace(x, y, z, ncontours=None, return traces, discrete_cm + # -------------------- Figure Factory for ternary contour ------------- @@ -598,11 +600,11 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], mode='markers', marker={'color': values, 'colorscale': colorscale, - 'line':{'color': 'rgb(120, 120, 120)', - 'width': int(coloring != 'lines')}, - }, - opacity=opacity, - hovertemplate=hovertemplate) + 'line': {'color': 'rgb(120, 120, 120)', + 'width': int(coloring != 'lines')}, + }, + opacity=opacity, + hovertemplate=hovertemplate) if showscale: if not showmarkers: colorscale = discrete_cm