Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3908788

Browse files
emmanuellejonmmease
authored andcommittedFeb 1, 2019
Ternary contour plot (#1413)
Prototype of Ternary contour figure factory
1 parent 15aff13 commit 3908788

File tree

4 files changed

+599
-0
lines changed

4 files changed

+599
-0
lines changed
 

‎plotly/figure_factory/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ It is often not a good idea to put all your code into your `create_foo()` functi
142142

143143
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.
144144

145+
6. Tests
146+
147+
Add unit tests in
148+
`plotly/tests/test_optional/test_figure_factory/test_figure_factory.py`.
145149

146150
## Create a Pull Request
147151

‎plotly/figure_factory/__init__.py

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

‎plotly/tests/test_optional/test_figure_factory/test_figure_factory.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2858,6 +2858,7 @@ def test_full_choropleth(self):
28582858

28592859
self.assertEqual(fig['data'][2]['x'][:50], exp_fig_head)
28602860

2861+
28612862
class TestQuiver(TestCase):
28622863

28632864
def test_scaleratio_param(self):
@@ -2897,3 +2898,75 @@ def test_scaleratio_param(self):
28972898
self.assertEqual(fig_head, exp_fig_head)
28982899

28992900

2901+
class TestTernarycontour(NumpyTestUtilsMixin, TestCase):
2902+
2903+
def test_wrong_coordinates(self):
2904+
a, b = np.mgrid[0:1:20j, 0:1:20j]
2905+
a = a.ravel()
2906+
b = b.ravel()
2907+
z = a * b
2908+
with self.assertRaises(ValueError,
2909+
msg='Barycentric coordinates should be positive.'):
2910+
_ = ff.create_ternary_contour(np.stack((a, b)), z)
2911+
mask = a + b < 1.
2912+
a = a[mask]
2913+
b = b[mask]
2914+
with self.assertRaises(ValueError):
2915+
_ = ff.create_ternary_contour(np.stack((a, b, a, b)), z)
2916+
with self.assertRaises(ValueError,
2917+
msg='different number of values and points'):
2918+
_ = ff.create_ternary_contour(np.stack((a, b, 1 - a - b)),
2919+
np.concatenate((z, [1])))
2920+
# Different sums for different points
2921+
c = a
2922+
with self.assertRaises(ValueError):
2923+
_ = ff.create_ternary_contour(np.stack((a, b, c)), z)
2924+
# Sum of coordinates is different from one but is equal
2925+
# for all points.
2926+
with self.assertRaises(ValueError):
2927+
_ = ff.create_ternary_contour(np.stack((a, b, 2 - a - b)), z)
2928+
2929+
2930+
def test_tooltip(self):
2931+
a, b = np.mgrid[0:1:20j, 0:1:20j]
2932+
mask = a + b < 1.
2933+
a = a[mask].ravel()
2934+
b = b[mask].ravel()
2935+
c = 1 - a - b
2936+
z = a * b * c
2937+
fig = ff.create_ternary_contour(np.stack((a, b, c)), z,
2938+
tooltip_mode='percents')
2939+
fig = ff.create_ternary_contour(np.stack((a, b, c)), z,
2940+
tooltip_mode='percent')
2941+
2942+
with self.assertRaises(ValueError):
2943+
fig = ff.create_ternary_contour(np.stack((a, b, c)), z,
2944+
tooltip_mode='wrong_mode')
2945+
2946+
2947+
def test_simple_ternary_contour(self):
2948+
a, b = np.mgrid[0:1:20j, 0:1:20j]
2949+
mask = a + b < 1.
2950+
a = a[mask].ravel()
2951+
b = b[mask].ravel()
2952+
c = 1 - a - b
2953+
z = a * b * c
2954+
fig = ff.create_ternary_contour(np.stack((a, b, c)), z)
2955+
fig2 = ff.create_ternary_contour(np.stack((a, b)), z)
2956+
np.testing.assert_array_equal(fig2['data'][0]['z'],
2957+
fig['data'][0]['z'])
2958+
2959+
2960+
def test_contour_attributes(self):
2961+
a, b = np.mgrid[0:1:20j, 0:1:20j]
2962+
mask = a + b < 1.
2963+
a = a[mask].ravel()
2964+
b = b[mask].ravel()
2965+
c = 1 - a - b
2966+
z = a * b * c
2967+
contour_dict = {'ncontours': 10,
2968+
'showscale': True}
2969+
2970+
fig = ff.create_ternary_contour(np.stack((a, b, c)), z, **contour_dict)
2971+
for key, value in contour_dict.items():
2972+
assert fig['data'][0][key] == value

0 commit comments

Comments
 (0)
Please sign in to comment.