This is a plotting library for use with matplotlib to make ternary plots plots in the two dimensional simplex projected onto a two dimensional plane.
The library provides functions for plotting projected lines, curves (trajectories), scatter plots, and heatmaps. There are several examples and a short tutorial below.
Last image from: Genetic Drift and Selection in Many-Allele Range Expansions.
See the citations below for more example images.
Have you used python-ternary in a publication? Open a PR or issue to include your citations or example plots!
See the partial list of citations and instructions on how to cite.
You can install python-ternary with conda:
conda config --add channels conda-forge
conda install python-ternary
See here for more information.
You can install the current release (1.0.6) with pip:
pip install python-ternary
Alternatively you can clone the repository and run setup.py
in the usual
manner:
git clone git@github.com:marcharper/python-ternary.git
cd python-ternary
python setup.py install
You can explore some of these examples with this Jupyter notebook.
The easiest way to use python-ternary is with the wrapper class
TernaryAxesSubplot
, which mimics Matplotlib's AxesSubplot. Start with:
fig, tax = ternary.figure()
With a ternary axes object tax
you can use many of the usual matplotlib
axes object functions:
tax.set_title("Scatter Plot", fontsize=20)
tax.scatter(points, marker='s', color='red', label="Red Squares")
tax.legend()
Most drawing functions can take standard matplotlib keyword arguments such as linestyle and linewidth. You can use LaTeX in titles and labels.
If you need to act directly on the underlying matplotlib axes, you can access them easily:
ax = tax.get_axes()
You can also wrap an existing Matplotlib AxesSubplot object:
figure, ax = pyplot.subplots()
tax = ternary.TernaryAxesSubplot(ax=ax)
This is useful if you want to use ternary as a part of another figure, such as
from matplotlib import pyplot, gridspec
pyplot.figure()
gs = gridspec.GridSpec(2, 2)
ax = pyplot.subplot(gs[0, 0])
figure, tax = ternary.figure(ax=ax)
...
Some ternary functions expect the simplex to be partitioned into some number
of steps, determined by the scale
parameter. A few functions will do this
partitioning automatically for you, but when working with real data or
simulation output, you may have partitioned already. If you are working with
probability distributions, just use scale=1
(the default). Otherwise the scale
parameter effectively controls the resolution of many plot types
(e.g. heatmaps).
TernaryAxesSubplot
objects keep track of the scale, axes, and other
parameters, supplying them as needed to other functions.
The following code draws a boundary for the simplex and gridlines.
import ternary
## Boundary and Gridlines
scale = 40
figure, tax = ternary.figure(scale=scale)
# Draw Boundary and Gridlines
tax.boundary(linewidth=2.0)
tax.gridlines(color="black", multiple=5)
tax.gridlines(color="blue", multiple=1, linewidth=0.5)
# Set Axis labels and Title
fontsize = 20
tax.set_title("Simplex Boundary and Gridlines", fontsize=fontsize)
tax.left_axis_label("Left label $\\alpha^2$", fontsize=fontsize)
tax.right_axis_label("Right label $\\beta^2$", fontsize=fontsize)
tax.bottom_axis_label("Bottom label $\\Gamma - \\Omega$", fontsize=fontsize)
# Set ticks
tax.ticks(axis='lbr', linewidth=1)
# Remove default Matplotlib Axes
tax.clear_matplotlib_ticks()
ternary.plt.show()
Each position in the ternary plot can be represented by a three-tuple coordinate, the first element corresponds to the value of the ''right corner element'', the second to the ''top corner element'' and the third to the ''left corner element''. Plotting the function f(p) = (p[0] - p[1])(1 - p[2]) gives the resulting heatmap
You can draw individual lines between any two points with line
and lines
parallel to the axes with horizonal_line
, left_parallel_line
, and
right_parallel_line
:
import ternary
scale = 40
figure, tax = ternary.figure(scale=scale)
# Draw Boundary and Gridlines
tax.boundary(linewidth=2.0)
tax.gridlines(color="blue", multiple=5)
# Set Axis labels and Title
fontsize = 12
offset = 0.14
tax.set_title("Various Lines\n", fontsize=fontsize)
tax.right_corner_label("X", fontsize=fontsize)
tax.top_corner_label("Y", fontsize=fontsize)
tax.left_corner_label("Z", fontsize=fontsize)
tax.left_axis_label("Left label $\\alpha^2$", fontsize=fontsize, offset=offset)
tax.right_axis_label("Right label $\\beta^2$", fontsize=fontsize, offset=offset)
tax.bottom_axis_label("Bottom label $\\Gamma - \\Omega$", fontsize=fontsize, offset=offset)
# Draw lines parallel to the axes
tax.horizontal_line(16)
tax.left_parallel_line(10, linewidth=2., color='red', linestyle="--")
tax.right_parallel_line(20, linewidth=3., color='blue')
# Draw an arbitrary line, ternary will project the points for you
p1 = (22, 8, 10)
p2 = (2, 22, 16)
tax.line(p1, p2, linewidth=3., marker='s', color='green', linestyle=":")
tax.ticks(axis='lbr', multiple=5, linewidth=1, offset=0.025)
tax.get_axes().axis('off')
tax.clear_matplotlib_ticks()
tax.show()
The line drawing functions accept the matplotlib keyword arguments of Line2D.
Curves can be plotted by specifying the points of the curve, just like matplotlib's plot. Simply use:
ternary.plot(points)
Points is a list of tuples or numpy arrays, such as
[(0.5, 0.25, 0.25), (1./3, 1./3, 1./3)]
,
import ternary
## Sample trajectory plot
figure, tax = ternary.figure(scale=1.0)
tax.boundary()
tax.gridlines(multiple=0.2, color="black")
tax.set_title("Plotting of sample trajectory data", fontsize=20)
points = []
# Load some data, tuples (x,y,z)
with open("sample_data/curve.txt") as handle:
for line in handle:
points.append(list(map(float, line.split(' '))))
# Plot the data
tax.plot(points, linewidth=2.0, label="Curve")
tax.ticks(axis='lbr', multiple=0.2, linewidth=1, tick_formats="%.1f")
tax.legend()
tax.show()
There are many more examples in this paper.
Similarly, ternary can make scatter plots:
import ternary
### Scatter Plot
scale = 40
figure, tax = ternary.figure(scale=scale)
tax.set_title("Scatter Plot", fontsize=20)
tax.boundary(linewidth=2.0)
tax.gridlines(multiple=5, color="blue")
# Plot a few different styles with a legend
points = random_points(30, scale=scale)
tax.scatter(points, marker='s', color='red', label="Red Squares")
points = random_points(30, scale=scale)
tax.scatter(points, marker='D', color='green', label="Green Diamonds")
tax.legend()
tax.ticks(axis='lbr', linewidth=1, multiple=5)
tax.show()
Ternary can plot heatmaps in two ways and three styles. Given a function, ternary will evaluate the function at the specified number of steps (determined by the scale, expected to be an integer in this case). Additional arguments can be passed to the function as an iterable throught the keyword argument 'fargs'. The simplex can be split up into triangles or hexagons and colored according to one of three styles:
- Triangular --
triangular
(default): coloring triangles by summing the values on the vertices - Dual-triangular --
dual-triangular
: mapping (i,j,k) to the upright triangles △ and blending the neigboring triangles for the downward triangles ▽ - Hexagonal --
hexagonal
: which does not blend values at all, and divides the simplex up into hexagonal regions
The two triangular heatmap styles and the hexagonal heatmap style can be visualized as follows: left is triangular, right is dual triangular.
Thanks to chebee7i for the above images.
Let's define a function on the simplex for illustration, the Shannon entropy of a probability distribution:
def shannon_entropy(p):
"""Computes the Shannon Entropy at a distribution in the simplex."""
s = 0.
for i in range(len(p)):
try:
s += p[i] * math.log(p[i])
except ValueError:
continue
return -1.*s
We can get a heatmap of this function as follows:
import ternary
scale = 60
figure, tax = ternary.figure(scale=scale)
tax.heatmapf(shannon_entropy, boundary=True, style="triangular")
tax.boundary(linewidth=2.0)
tax.set_title("Shannon Entropy Heatmap")
tax.show()
In this case the keyword argument boundary indicates whether you wish to
evaluate points on the boundary of the partition (which is sometimes
undesirable). Specify style="hexagonal"
for hexagons. Large scalings can use
a lot of RAM since the number of polygons rendered is O(n^2).
You may specify a matplotlib colormap (an instance or the colormap name) in the cmap argument.
Ternary can also make heatmaps from data. In this case you need to supply a
dictionary mapping (i, j)
or (i, j, k)
for i + j + k = scale
to a float
as input for a heatmap. It is not necessary to include k
in the dictionary
keys since it can be determined from scale
, i
, and j
. This reduces the
memory requirements when the partition is very fine (significant when scale
is in the hundreds).
Make the heatmap as follows:
ternary.heatmap(data, scale, ax=None, cmap=None)
or on a TernaryAxesSubplot
object:
tax.heatmap(data, cmap=None)
This can produces images such as:
For a given ternary plot there are two valid ways to label the axes ticks
corresponding to the clockwise and counterclockwise orientations. However note
that the axes labels need to be adjusted accordingly, and ternary
does not
do so automatically when you pass clockwise=True
to tax.ticks()
.
There is a more detailed discussion on issue #18 (closed).
Setting custom values corresponding to the max and min values of the axis, rather than the scale
parameter can be done using a combination of tax.get_ticks_from_axis_limits()
and tax.set_custom_ticks()
.
import ternary
from matplotlib.cm import get_cmap
import matplotlib.pyplot as plt
cmap = get_cmap('bwr')
def well_behaved_func(x):
return (x[0] - x[1]) * (1 - x[2])
scale = 60
fig, tax = ternary.figure(scale=scale)
tax.set_axis_limits({'b' : [0,1], 'l' : [0, 1], 'r' : [0, 1]})
tax.heatmapf(well_behaved_func, boundary=True,
style='triangular', cmap=cmap,
vmin=-1, vmax=1)
offset = 0.14
tax.boundary(linewidth=1.0)
tax.top_corner_label('p[1]')
tax.right_axis_label('p[1]', offset=offset)
tax.right_corner_label('p[0]')
tax.bottom_axis_label('p[0]', offset=offset)
tax.left_corner_label('p[2]', offset=offset)
tax.left_axis_label('p[2]', offset=offset)
tax._redraw_labels()
# This part fixes the custom ticks
tax.get_ticks_from_axis_limits(multiple=11)
tax.set_custom_ticks(tick_formats='%.1f', offset=0.025)
tax.get_axes().axis('off')
tax.clear_matplotlib_ticks()
plt.savefig('meaningful_ticks')
You can alternatively specify colors as rgba tuples (r, g, b, a)
(all between zero and one). To use this feature, pass colormap=False
to
heatmap()
so that the library will not attempt to map the tuple to a value
with a matplotlib colormap. Note that this disables the inclusion of a colorbar.
Here is an example:
import math
from matplotlib import pyplot as plt
import ternary
def color_point(x, y, z, scale):
w = 255
x_color = x * w / float(scale)
y_color = y * w / float(scale)
z_color = z * w / float(scale)
r = math.fabs(w - y_color) / w
g = math.fabs(w - x_color) / w
b = math.fabs(w - z_color) / w
return (r, g, b, 1.)
def generate_heatmap_data(scale=5):
from ternary.helpers import simplex_iterator
d = dict()
for (i, j, k) in simplex_iterator(scale):
d[(i, j, k)] = color_point(i, j, k, scale)
return d
scale = 80
data = generate_heatmap_data(scale)
figure, tax = ternary.figure(scale=scale)
tax.heatmap(data, style="hexagonal", use_rgba=True)
tax.boundary()
tax.set_title("RGBA Heatmap")
plt.show()
This produces the following image:
You can run the test suite as follows:
python -m unittest discover tests
Contributions are welcome! Please share any nice example plots, contribute features, and add unit tests! Use the pull request and issue systems to contribute.
- Marc Harper marcharper: maintainer
- Bryan Weinstein btweinstein: Hexagonal heatmaps, colored trajectory plots
- chebee7i: Docs and figures, triangular heatmapping
- Cory Simon: Axis Colors, colored heatmap example
At one point there was an issue on macs that causes the axes labels not to render. The workaround is to manually call
tax._redraw_labels()
before showing or rendering the image.