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/plotly/figure_factory/_ternary_contour.py b/plotly/figure_factory/_ternary_contour.py index d74254f2854..1cca22a7c28 100644 --- a/plotly/figure_factory/_ternary_contour.py +++ b/plotly/figure_factory/_ternary_contour.py @@ -1,372 +1,151 @@ from __future__ import absolute_import +import plotly.colors as clrs +from plotly.graph_objs import graph_objs as go +from plotly import exceptions, optional_imports from plotly import optional_imports from plotly.graph_objs import graph_objs as go -import numpy as np -interpolate = 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)']] +np = optional_imports.get_module('numpy') +sk_measure = optional_imports.get_module('skimage.measure') +scipy_interp = optional_imports.get_module('scipy.interpolate') -def _transform_barycentric_cartesian(): - """ - Returns 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) +# -------------------------- Layout ------------------------------ -def _contour_trace(x, y, z, tooltip, ncontours=None, colorscale='Viridis', - showscale=False, linewidth=0.5, - linecolor='rgb(150,150,150)', - coloring=None, fontcolor='blue', - fontsize=12): +def _ternary_layout(title='Ternary contour plot', width=550, height=525, + pole_labels=['a', 'b', 'c']): """ - Contour trace in Cartesian coordinates. + Layout of ternary contour plot, to be passed to ``go.FigureWidget`` + object. 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. - ncontours : int or None - Number of contours to display (determined automatically if None). - colorscale : str o array, optional - Colorscale to use for contours - showscale : bool - If True, a colorbar showing the color scale is displayed. - linewidth : int - Line width of contours - linecolor : color string - Color on contours - coloring : None or 'lines' - How to display contour. Filled contours if None, lines if ``lines``. - colorscale : None or array-like - Colorscale of the contours. - fontcolor : color str - Color of contour labels. - fontsize : int - Font size of contour labels. + title : str or None + Title of ternary plot + width : int + Figure width. + height : int + Figure height. + pole_labels : str, default ['a', 'b', 'c'] + Names of the three poles of the triangle. """ + return dict(title=title, + width=width, height=height, + ternary=dict(sum=1, + aaxis=dict(title=dict(text=pole_labels[0]), + min=0.01, linewidth=2, + ticks='outside'), + baxis=dict(title=dict(text=pole_labels[1]), + min=0.01, linewidth=2, + ticks='outside'), + caxis=dict(title=dict(text=pole_labels[2]), + min=0.01, linewidth=2, + ticks='outside')), + showlegend=False, + ) - c_dict = dict(type='contour', - x=x, y=y, z=z, - text=tooltip, - hoverinfo='text', - ncontours=ncontours, - colorscale=colorscale, - showscale=showscale, - line=dict(width=linewidth, color=linecolor), - colorbar=dict(thickness=20, ticklen=4) - ) - if coloring == 'lines': - contours = dict(coloring=coloring) - c_dict.update(contours=contours) - return go.Contour(c_dict) - - -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') +# ------------- Transformations of coordinates ------------------- -def _side_coord_ticks(side, t=0.01): +def _replace_zero_coords(ternary_data, delta=0.0005): """ - Cartesian coordinates of ticks loactions for one side (0, 1, 2) - of ternary diagram. + Replaces zero ternary coordinates with delta and normalize the new + triplets (a, b, c). 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 + ternary_data : ndarray of shape (N, 3) + delta : float + Small float to regularize logarithm. -def _cart_coord_ticks(t=0.01): + 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. """ - Cartesian coordinates of ticks loactions. + zero_mask = (ternary_data == 0) + is_any_coord_zero = np.any(zero_mask, axis=0) - Parameters - ========== + 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 - 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). +def _ilr_transform(barycentric): """ - 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 + Perform Isometric Log-Ratio on barycentric (compositional) data. + Parameters + ---------- + barycentric: ndarray of shape (3, N) + Barycentric coordinates. -def _set_ticklabels(annotations, posx, posy, proportions=True): + 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) + 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 - 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%... +def _ilr_inverse(x): """ - 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. + Perform inverse Isometric Log-Ratio (ILR) transform to retrieve + barycentric (compositional) data. Parameters - ========== + ---------- + x : array of shape (2, N) + Coordinates in ILR space. - x_ticks : array_like, 1D - x Cartesian coordinate of ticks - y_ticks : array_like, 1D - y Cartesian coordinate of ticks + 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. """ - 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') + 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) + 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 - 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)', - 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=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 _transform_barycentric_cartesian(): """ - 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). + Returns the transformation matrix from barycentric to Cartesian + coordinates and conversely. """ - 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)] - 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)] - else: - raise ValueError("""tooltip mode must be either "proportions" or - "percents".""") - return tooltip + # 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 _prepare_barycentric_coord(b_coords): @@ -374,25 +153,32 @@ 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') + 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') + 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 + return np.stack((A, B, C)) -def _compute_grid(coordinates, values, tooltip_mode): +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 ========== @@ -401,43 +187,299 @@ def _compute_grid(coordinates, values, tooltip_mode): 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) - 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, 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(100, int(np.sqrt(len(values)))) - n_interp = 20 + 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 = interpolate.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)] = 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] = 0 # None - tooltip = _tooltip(n_interp, bar_coords, grid_z, xy1, tooltip_mode) - return grid_z, gr_x, gr_y, tooltip + # 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 _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): + """ + Return a list of ``ncontours`` colors from the ``colormap`` colorscale. + """ + if colormap in clrs.PLOTLY_SCALES.keys(): + 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())) + 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 + 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:]): + 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') + colors.append(col) + return colors + + +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 + np.all(np.abs(y - y[0]) < 2)) + return too_small + + +def _extract_contours(im, values, colors): + """ + 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(), + 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 + + 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. + + 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 + all_contours = [outer_contour] + all_contours + 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: + index -= 1 + else: + index += 1 + all_colors = [colors[index]] + all_colors + all_values = [values[index]] + all_values + all_areas = [0] + all_areas + 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, + colorscale='Electric', + linecolor='rgb(150,150,150)', interp_mode='llr', + coloring=None, + v_min=0, v_max=1): + """ + Contour trace in Cartesian coordinates. + + Parameters + ========== + + x, y : array-like + 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``. + vmin, vmax : float + Bounds of interval of values used for the colorspace + + Notes + ===== + """ + # Prepare colors + # 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: + linecolor = 'rgb(150, 150, 150)' + else: + colors = [linecolor] * ncontours + + # 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] + + # 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 + 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] + 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()))) + 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): + continue + + _col = all_colors[index] if coloring == 'lines' else linecolor + trace = dict( + type='scatterternary', + a=a, b=b, c=c, mode='lines', + line=dict(color=_col, shape='spline', width=1), + fill='toself', fillcolor=all_colors[index], + showlegend=True, + hoverinfo='skip', + name='%.3f' % val + ) + if coloring == 'lines': + trace['fill'] = None + traces.append(trace) + + 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=None, - plot_bgcolor='rgb(240,240,240)', - title=None): + showscale=False, coloring=None, + colorscale='Bluered', + linecolor=None, + title=None, + interp_mode='ilr', + showmarkers=False): """ Ternary contour plot. @@ -452,9 +494,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 @@ -465,12 +504,20 @@ 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``. - colorscale : None or array-like + colorscale : None or str (Plotly colormap) colorscale of the contours. - plot_bgcolor : - color of figure background + linecolor : None or rgb color + Color used for lines. ``colorscale`` has to be set to None, otherwise + line colors are determined from ``colorscale``. title : str or None Title of ternary plot + 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. Examples ======== @@ -485,42 +532,91 @@ def create_ternary_contour(coordinates, values, pole_labels=['a', 'b', 'c'], c = 1 - a - b # Values to be displayed as contours z = a * b * c - fig = ff.create_ternary_contour(np.stack((a, b, c)), z) + 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_ternary_contour(np.stack((a, b)), z) + fig = ff.create_ternarycontour(np.stack((a, b)), z) + plotly.iplot(fig) Example 2: ternary contour plot with line contours - fig = ff.create_ternary_contour(np.stack((a, b)), z, coloring='lines') - """ - if interpolate is None: - raise ImportError("""\ -The create_ternary_contour figure factory requires the scipy package""") + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, coloring='lines') - grid_z, gr_x, gr_y, tooltip = _compute_grid(coordinates, values, - tooltip_mode) + Example 3: customize number of contours - x_ticks, y_ticks, posx, posy = _cart_coord_ticks(t=0.01) + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, ncontours=8) - layout = _ternary_layout(pole_labels=pole_labels, - width=width, height=height, title=title, - plot_bgcolor=plot_bgcolor) + Example 4: superimpose contour plot and original data as markers + + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, coloring='lines', + showmarkers=True) + + Example 5: customize title and pole labels - annotations = _set_ticklabels(layout['annotations'], posx, posy, - proportions=True) + fig = ff.create_ternarycontour(np.stack((a, b, c)), z, + title='Ternary plot', + pole_labels=['clay', 'quartz', 'fledspar']) + """ + 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: - colorscale = _pl_deep() - - contour_trace = _contour_trace(gr_x, gr_y, grid_z, tooltip, - ncontours=ncontours, - showscale=showscale, - colorscale=colorscale, - coloring=coloring) - 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 + 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, + interp_mode=interp_mode) + + layout = _ternary_layout(pole_labels=pole_labels, + width=width, height=height, title=title) + + 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': int(coloring != 'lines')}, + }, + 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(), + 'cmax': values.max(), + 'colorscale': colorscale, + 'showscale': True}, + 'mode': 'markers'}) + fig.add_trace(colorbar) + 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 b360fbc72e9..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 @@ -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): @@ -2908,7 +2909,7 @@ def test_wrong_coordinates(self): with self.assertRaises(ValueError, msg='Barycentric coordinates should be positive.'): _ = ff.create_ternary_contour(np.stack((a, b)), z) - mask = a + b < 1. + mask = a + b <= 1. a = a[mask] b = b[mask] with self.assertRaises(ValueError): @@ -2916,7 +2917,7 @@ def test_wrong_coordinates(self): with self.assertRaises(ValueError, msg='different number of values and points'): _ = ff.create_ternary_contour(np.stack((a, b, 1 - a - b)), - np.concatenate((z, [1]))) + np.concatenate((z, [1]))) # Different sums for different points c = a with self.assertRaises(ValueError): @@ -2927,46 +2928,71 @@ 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. 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') - - with self.assertRaises(ValueError): - fig = ff.create_ternary_contour(np.stack((a, b, c)), z, - tooltip_mode='wrong_mode') + 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_simple_ternary_contour(self): + 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 - 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_equal(fig2['data'][0]['z'], - fig['data'][0]['z']) + 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_contour_attributes(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 - contour_dict = {'ncontours': 10, - 'showscale': True} + 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] + 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 + print(len(fig.data)) + assert (len(fig.data) == ncontours + 2 + arg_set['showscale']) - fig = ff.create_ternary_contour(np.stack((a, b, c)), z, **contour_dict) - for key, value in contour_dict.items(): - assert fig['data'][0][key] == value diff --git a/tox.ini b/tox.ini index 511f1973ff3..ae79862938a 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.13.1 ; CORE ENVIRONMENTS [testenv:py27-core]