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]