diff --git a/.gitignore b/.gitignore
index 0d20b64..2c08ef6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,10 @@
*.pyc
+
+# general things to ignore
+build/
+dist/
+*.egg-info/
+*.egg
+*.py[cod]
+__pycache__/
+*.ipynb_checkpoints/
diff --git a/README.md b/README.md
index 4db5e77..3de6c7d 100644
--- a/README.md
+++ b/README.md
@@ -23,7 +23,8 @@ The library provides functions for plotting projected lines, curves (trajectorie
-
+
+
@@ -394,6 +395,58 @@ do so automatically when you pass `clockwise=True` to `tax.ticks()`.
There is a [more detailed discussion](https://github.com/marcharper/python-ternary/issues/18)
on issue #18 (closed).
+# Custom Axis Data Limits
+
+By default, the axes limits are [0, scale] i.e. simplex coordinates but it is possible to set
+custom data limits to the axes instead. This is done by passing a dict into set_axis_limits.
+The keys are b, l and r for the three axes and the values are a list of the min and max in
+data coords for that axis. max-min for each axis is the same as the scale i.e. 9 in this case.
+
+```python
+tax.set_axis_limits({'b': [60, 75], 'l': [15, 30], 'r': [10, 25]})
+```
+
+This can be used to zoom in on a particular region, for example. Please see
+the [custom axis scaling example](examples/custom_axis_scaling.py) for further
+details.
+
+
+ +
+ +# Truncated Simplex + +One or more corners can be removed from the simplex by setting a truncation. +This may be useful for saving whitespace if the data are grouped in one area +of the plot. A truncation is specified in data coordinates by passing a dict +into tax.set_truncation. The keys are two letters specifying the start and end +axes of the truncation line and the values give the maximum of the first axis +specified in data coordinates. For the example on the left below, we write: + +```python +tax.set_truncation({'rl' : 8}) +``` + +The result is that the truncation line has been drawn from the right axis at +data coordinate 8 to the left axis, cutting off the top corner. As the figure is no +longer a triangle, we need to get and set custom axis ticks and tick labels. +There are convenience functions for this in the case of a truncation and/or +custom axis data limits. Again for the left image shown below: + +```python +tax.get_ticks_from_axis_limits(multiple=2) +offset=0.013 +tax.set_custom_ticks(fontsize=8, offset=offset, + tick_formats="%.1f", linewidth=0.25) +``` + ++ + +
+ +Please see the [truncated simplex example](examples/truncated_simplex_example.py) +for further details. # RGBA colors @@ -463,6 +516,7 @@ contribute. - Bryan Weinstein [btweinstein](https://github.com/btweinstein): Hexagonal heatmaps, colored trajectory plots - [chebee7i](https://github.com/chebee7i): Docs and figures, triangular heatmapping - [Cory Simon](https://github.com/CorySimon): Axis Colors, colored heatmap example +- [tgwoodcock](https://github.com/tgwoodcock): Custom axis data limits, truncated simplex # Known-Issues diff --git a/examples/custom_axis_scaling.py b/examples/custom_axis_scaling.py index 9559c4f..6199b7c 100644 --- a/examples/custom_axis_scaling.py +++ b/examples/custom_axis_scaling.py @@ -1,12 +1,24 @@ -import ternary +""" +This script gives two examples of setting custom axis data limits instead of +having the axis limits being from 0 to scale as is the default case. + +In the first example we simply set some axis data limits, get and set the +ticks for the axes and then scatter some data. This example is then repeated +but with the additional feature of showing custom axis tick formatting. + +The second example shows how to use custom axis scaling to achieve a zoom +effect. We draw the full plot on the left and then zoom into a specific +region and plot that on the right. The basic principle is the same as the +first example. +""" -# Simple example: -## Boundary and Gridlines -scale = 9 -figure, tax = ternary.figure(scale=scale) +import ternary +## Simple example: +figure, tax = ternary.figure(scale=9) +figure.set_size_inches((4.8,4.8)) +figure.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95) tax.ax.axis("off") -figure.set_facecolor('w') # Draw Boundary and Gridlines tax.boundary(linewidth=1.0) @@ -19,10 +31,10 @@ tax.bottom_axis_label("Hogs", fontsize=fontsize, offset=0.06) -# Set custom axis limits by passing a dict into set_limits. -# The keys are b, l and r for the three axes and the vals are a list +# Set custom axis DATA limits by passing a dict into set_axis_limits. +# The keys are b, l and r for the three axes and the values are a list # of the min and max in data coords for that axis. max-min for each -# axis must be the same as the scale i.e. 9 in this case. +# axis is the same as the scale i.e. 9 in this case. tax.set_axis_limits({'b': [67, 76], 'l': [24, 33], 'r': [0, 9]}) # get and set the custom ticks: tax.get_ticks_from_axis_limits() @@ -35,15 +47,14 @@ tax.ax.set_aspect('equal', adjustable='box') tax._redraw_labels() +figure.canvas.draw() ## Simple example with axis tick formatting: -## Boundary and Gridlines -scale = 9 -figure, tax = ternary.figure(scale=scale) - +figure, tax = ternary.figure(scale=9) +figure.set_size_inches((4.8,4.8)) +figure.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95) tax.ax.axis("off") -figure.set_facecolor('w') # Draw Boundary and Gridlines tax.boundary(linewidth=1.0) @@ -55,33 +66,34 @@ tax.right_axis_label("Dogs", fontsize=fontsize, offset=0.12) tax.bottom_axis_label("Hogs", fontsize=fontsize, offset=0.06) - -# Set custom axis limits by passing a dict into set_limits. -# The keys are b, l and r for the three axes and the vals are a list +# Set custom axis DATA limits by passing a dict into set_axis_limits. +# The keys are b, l and r for the three axes and the values are a list # of the min and max in data coords for that axis. max-min for each -# axis must be the same as the scale i.e. 9 in this case. +# axis is the same as the scale i.e. 9 in this case. tax.set_axis_limits({'b': [67, 76], 'l': [24, 33], 'r': [0, 9]}) + # get and set the custom ticks: # custom tick formats: # tick_formats can either be a dict, like below or a single format string # e.g. "%.3e" (valid for all 3 axes) or None, in which case, ints are # plotted for all 3 axes. tick_formats = {'b': "%.2f", 'r': "%d", 'l': "%.1f"} - tax.get_ticks_from_axis_limits() tax.set_custom_ticks(fontsize=10, offset=0.02, tick_formats=tick_formats) # data can be plotted by entering data coords (rather than simplex coords): points = [(70, 3, 27), (73, 2, 25), (68, 6, 26)] -points_c = tax.convert_coordinates(points,axisorder='brl') +points_c = tax.convert_coordinates(points, axisorder='brl') tax.scatter(points_c, marker='o', s=25, c='r') tax.ax.set_aspect('equal', adjustable='box') tax._redraw_labels() +figure.canvas.draw() + ## Zoom example: -## Draw a plot with the full range on the left and a second plot which -## shows a zoomed region of the left plot. +# Draw a plot with the full range on the left and a second plot which +# shows a zoomed region of the left plot. fig = ternary.plt.figure(figsize=(11, 6)) ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2) @@ -114,8 +126,8 @@ tax2.set_axis_limits({'b': [60, 75], 'l': [15, 30], 'r': [10, 25]}) tax2.get_ticks_from_axis_limits(multiple=5) tick_formats = "%.1f" -tax2.set_custom_ticks(fontsize=10, offset=0.025, multiple=5, - axes_colors=axes_colors, tick_formats=tick_formats) +tax2.set_custom_ticks(fontsize=10, offset=0.025, axes_colors=axes_colors, + tick_formats=tick_formats) # plot some data points = [(62, 12, 26), (63.5, 13.5, 23), (65, 14, 21), (61, 15, 24), @@ -132,11 +144,10 @@ tax1.line((60, 10, 30), (60, 25, 15), color='r', lw=2.0) tax1.line((75, 10, 15), (60, 25, 15), color='r', lw=2.0) -fig.set_facecolor("w") - tax1.ax.set_position([0.01, 0.05, 0.46, 0.8]) tax2.ax.set_position([0.50, 0.05, 0.46, 0.8]) tax1.resize_drawing_canvas() tax2.resize_drawing_canvas() +figure.canvas.draw() ternary.plt.show() diff --git a/examples/truncated_simplex_example.py b/examples/truncated_simplex_example.py new file mode 100644 index 0000000..e008deb --- /dev/null +++ b/examples/truncated_simplex_example.py @@ -0,0 +1,139 @@ +""" +This script gives two examples of truncation i.e. cutting one +or more corners off the simplex to save whitespace in a figure. + +The first example is simple: only the top corner is truncated and the data +coordainates are the same as the simplex coordinates. + +The second example is more complex: all three corners have been truncated +and we set axis data limits which are not the same as the simplex coords. +""" + +import ternary + +## Simple example with the top corner truncated +scale = 10 +figure, tax = ternary.figure(scale=scale) +figure.subplots_adjust(left=-0.1, right=1.1, bottom=0.1, top=1.1) +tax.set_truncation({'rl' : 8}) +tax.ax.axis("off") + + +# Draw Boundary and Gridlines +tax.boundary(linewidth=0.25) +tax.gridlines(color="black", multiple=1, linewidth=0.25, ls='-') + + +# Set Axis labels +tax.left_axis_label(r"$\longleftarrow$ Hogs", fontsize=10) +tax.right_axis_label(r"$\longleftarrow$ Dogs", fontsize=10) +tax.bottom_axis_label(r"Logs $\longrightarrow$", fontsize=10, offset=0.08) + + +# As we have set a truncation, we need to get and set custom ticks +tax.get_ticks_from_axis_limits(multiple=2) +offset=0.013 +tax.set_custom_ticks(fontsize=8, offset=offset, + tick_formats="%.1f", linewidth=0.25) +# here we have used a tick formatting string to label all the axes with +# floats to one decimal place. + +tax.ax.axis("scaled") +tax._redraw_labels() +figure.canvas.draw() + + + + + + + +## More complex example with truncations on all 3 corners of the simplex +## and axes with data coordinates which are different to simplex coordinates. +scale = 14 +figure, tax = ternary.figure(scale=scale) +w_i,h_i = 3.15, 2.36 +figure.set_size_inches((w_i,h_i)) +figure.set_dpi(150) +tax.ax.axis("off") +figure.subplots_adjust(left=0, right=1, bottom=0, top=1) + +# here we set the data limits of the axes (of the complete simplex) +tax.set_axis_limits({'b' : [52, 59], 'r' : [0, 7], 'l' : [41, 48]}) + +# now we set the truncation of all 3 corners in data coords. The truncation +# point refers to the first axis in the key e.g. "b" will be cut off at 57.5 +# parallel to the left axis until we reach the "r" axis for "br" : 57.5 +tax.set_truncation({'br' : 57.5, 'rl' : 4, 'lb' : 46.5}) + +# Draw Boundary and Gridlines +tax.boundary(linewidth=0.25) +tax.gridlines(color="black", multiple=1, linewidth=0.25, ls='-') + +# As the truncated figure is no longer a triangle, we need to set custom +# positions for the axis labels. One way is to enter positions as 2-tuples +# in xy data coords (e.g. obtained interactively by the hovering the mouse +# over the matplotlib figure axes) and convert them to simplex coords +# (3-tuples) for plotting: +qs = {"l" : (1.94, 5.80), + "b" : (6.67, -1.5), + "r" : (12.34, 5.58) + } + +pos = {i : ternary.helpers.planar_to_coordinates(j, tax._scale).tolist() + for i, j in qs.items() + } + +fontsize = 10 +tax.left_axis_label(r"$\longleftarrow$ Hogs", position=pos["l"], + fontsize=fontsize) +tax.right_axis_label(r"$\longleftarrow$ Dogs", position=pos["r"], + fontsize=fontsize) +tax.bottom_axis_label(r"Logs $\longrightarrow$", position=pos["b"], + fontsize=fontsize) + + +# We also need to set custom ticks for this example. +# We could use tax.get_ticks_from_axis_limits(multiple=1) and this gives +# us all the ticks on the remaining visible parts of the simplex boundary. +# Instead we show here how to plot specific ticks. +# define ticks in data coords along each axis: +tax._ticks = {'b' : [54,55,56,57], + 'r' : [2,3,4], + 'l' : [44,45,46]} +# define tick locations along each axis in simplex coords: +tax._ticklocs = {'b' : [4,6,8,10], + 'r' : [4,6,8], + 'l' : [4,6,8]} + +offset=0.013 +tax.set_custom_ticks(fontsize=8, offset=offset, tick_formats="%i", + linewidth=0.25) + +# As we have applied a truncation, it can be helpful to plot extra +# ticks which are not located on the simplex boundary. We specify +# which axis the tick belongs to so that the tick can be drawn with +# the correct orientation: horizontal, right parallel or left parallel. +# Then give the position of the tick in simplex coordinates and specify +# the tick label as a string in data coordinates: +tax.add_extra_tick('r', (11,2,1), offset, '1', fontsize=8, color='k', + linewidth=0.25) +tax.add_extra_tick('r', (11,0,3), offset, '0', fontsize=8, color='k', + linewidth=0.25) + +tax.add_extra_tick('l', (2,8,4), offset, '43', fontsize=8, color='k', + linewidth=0.25) +tax.add_extra_tick('l', (4,8,2), offset, '42', fontsize=8, color='k', + linewidth=0.25) +tax.add_extra_tick('l', (6,8,0), offset, '41', fontsize=8, color='k', + linewidth=0.25) + +tax.add_extra_tick('b', (2,1,11), offset, '53', fontsize=8, color='k', + linewidth=0.25) + + +tax.ax.axis("scaled") +tax.ax.axis([0, 14, -2, 9]) +tax._redraw_labels() + +ternary.plt.show() diff --git a/readme_images/truncation_all_corners.png b/readme_images/truncation_all_corners.png new file mode 100644 index 0000000..1d509b8 Binary files /dev/null and b/readme_images/truncation_all_corners.png differ diff --git a/readme_images/truncation_top_corner.png b/readme_images/truncation_top_corner.png new file mode 100644 index 0000000..841e215 Binary files /dev/null and b/readme_images/truncation_top_corner.png differ diff --git a/readme_images/zoom_example.png b/readme_images/zoom_example.png new file mode 100644 index 0000000..089e3cb Binary files /dev/null and b/readme_images/zoom_example.png differ diff --git a/ternary/colormapping.py b/ternary/colormapping.py index c264463..6c1fe6f 100644 --- a/ternary/colormapping.py +++ b/ternary/colormapping.py @@ -91,7 +91,7 @@ def colorbar_hack(ax, vmin, vmax, cmap, scientific=False, cbarlabel=None, norm=N if norm is None: norm = plt.Normalize(vmin=vmin, vmax=vmax) sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) - sm._A = [] + sm.set_array(None) cb = plt.colorbar(sm, ax=ax, **kwargs) if cbarlabel is not None: cb.set_label(cbarlabel) diff --git a/ternary/heatmapping.py b/ternary/heatmapping.py index 7487f69..63d051d 100644 --- a/ternary/heatmapping.py +++ b/ternary/heatmapping.py @@ -318,7 +318,7 @@ def heatmapf(func, scale=10, boundary=True, cmap=None, ax=None, # Pass everything to the heatmapper ax = heatmap(data, scale, cmap=cmap, ax=ax, style=style, scientific=scientific, colorbar=colorbar, - permutation=permutation, vmin=vmin, vmax=vmax, + permutation=permutation, vmin=vmin, vmax=vmax, cbarlabel=cbarlabel, cb_kwargs=cb_kwargs) return ax @@ -411,9 +411,20 @@ def svg_heatmap(data, scale, filename, vmax=None, vmin=None, style='h', output_file.write('\n') -def background_color(ax, color, scale, zorder=-1000, alpha=None): +def background_color(ax, color, scale, axis_min_max, zorder=-1000, alpha=None): """Draws a triangle behind the plot to serve as the background color.""" - vertices = [(scale, 0, 0), (0, scale, 0), (0, 0, scale)] + vertices = [(axis_min_max["b"][1], 0, scale - axis_min_max["b"][1]), + (axis_min_max["b"][1], axis_min_max["r"][0], 0), + (scale - axis_min_max["r"][1], axis_min_max["r"][1], 0), + (0, axis_min_max["r"][1], axis_min_max["l"][0]), + (0, scale - axis_min_max["l"][1], axis_min_max["l"][1]), + (axis_min_max["b"][0], 0, axis_min_max["l"][1]) + ] + + _, idx = np.unique(vertices, return_index=True, axis=0) + idx.sort() + vertices = [vertices[i] for i in idx] + vertices = map(project_point, vertices) xs, ys = unzip(vertices) poly = ax.fill(xs, ys, facecolor=color, edgecolor=color, zorder=zorder, alpha=alpha) diff --git a/ternary/helpers.py b/ternary/helpers.py index 660c046..f6afd54 100644 --- a/ternary/helpers.py +++ b/ternary/helpers.py @@ -112,7 +112,7 @@ def planar_to_coordinates(p, scale): ---------- p: 2-tuple The planar simplex (x, y) point to be transformed to maps (x,y,z) coordinates - + scale: Int The normalized scale of the simplex, i.e. N such that points (x,y,z) satisfy x + y + z == N @@ -122,8 +122,8 @@ def planar_to_coordinates(p, scale): x = p[0] - y / 2. z = scale - x - y return np.array([x, y, z]) - - + + def project_sequence(s, permutation=None): """ Projects a point or sequence of points using `project_point` to lists xs, ys @@ -136,7 +136,7 @@ def project_sequence(s, permutation=None): Returns ------- - xs, ys: The sequence of projected points in coordinates as two lists + xs, ys: The sequence of projected points in coordinates as two lists """ xs, ys = unzip([project_point(p, permutation=permutation) for p in s]) @@ -147,7 +147,7 @@ def project_sequence(s, permutation=None): def convert_coordinates(q, conversion, axisorder): """ - Convert a 3-tuple in data coordinates into to simplex data + Convert a 3-tuple in data coordinates to simplex coordinates for plotting. Parameters @@ -188,7 +188,7 @@ def get_conversion(scale, limits): "r": lambda x: (x - limits['r'][0]) * fr} return conversion - + def convert_coordinates_sequence(qs, scale, limits, axisorder): """ @@ -216,5 +216,5 @@ def convert_coordinates_sequence(qs, scale, limits, axisorder): the points converted to simplex coordinates """ conversion = get_conversion(scale, limits) - + return [convert_coordinates(q, conversion, axisorder) for q in qs] diff --git a/ternary/lines.py b/ternary/lines.py index 436798c..e495538 100644 --- a/ternary/lines.py +++ b/ternary/lines.py @@ -31,7 +31,7 @@ def line(ax, p1, p2, permutation=None, **kwargs): ax.add_line(Line2D((pp1[0], pp2[0]), (pp1[1], pp2[1]), **kwargs)) -def horizontal_line(ax, scale, i, **kwargs): +def horizontal_line(ax, scale, i, axis_min_max, **kwargs): """ Draws the i-th horizontal line parallel to the lower axis. @@ -43,16 +43,32 @@ def horizontal_line(ax, scale, i, **kwargs): Simplex scale size. i: float The index of the line to draw + axis_min_max: dict + The min and max values of the axes in simplex coordinates. + These may not be equal to (0, scale) if a truncation has been + applied. kwargs: Dictionary Any kwargs to pass through to Matplotlib. """ + if i <= axis_min_max['r'][1]: + if i < scale-axis_min_max['l'][1]: + p1 = (axis_min_max['b'][0] - i, + i, + axis_min_max['l'][1]) + else: + p1 = (0, i, scale-i) - p1 = (0, i, scale - i) - p2 = (scale - i, i, 0) - line(ax, p1, p2, **kwargs) + if i < axis_min_max['r'][0]: + p2 = (axis_min_max['b'][1], + i, + scale - axis_min_max['b'][1] - i) + else: + p2 = (scale - i, i, 0) + line(ax, p1, p2, **kwargs) -def left_parallel_line(ax, scale, i, **kwargs): + +def left_parallel_line(ax, scale, i, axis_min_max, **kwargs): """ Draws the i-th line parallel to the left axis. @@ -64,16 +80,32 @@ def left_parallel_line(ax, scale, i, **kwargs): Simplex scale size. i: float The index of the line to draw + axis_min_max: dict + The min and max values of the axes in simplex coordinates. + These may not be equal to (0, scale) if a truncation has been + applied. kwargs: Dictionary Any kwargs to pass through to Matplotlib. """ + if i <= axis_min_max['b'][1]: + if i < scale-axis_min_max['r'][1]: + p1 = (i, + axis_min_max['r'][1], + axis_min_max['l'][0] - i) + else: + p1 = (i, scale - i, 0) + + if i < axis_min_max['b'][0]: + p2 = (i, + scale - axis_min_max['l'][1] - i, + axis_min_max['l'][1]) + else: + p2 = (i, 0, scale - i) - p1 = (i, scale - i, 0) - p2 = (i, 0, scale - i) - line(ax, p1, p2, **kwargs) + line(ax, p1, p2, **kwargs) -def right_parallel_line(ax, scale, i, **kwargs): +def right_parallel_line(ax, scale, i, axis_min_max=None, **kwargs): """ Draws the i-th line parallel to the right axis. @@ -85,18 +117,34 @@ def right_parallel_line(ax, scale, i, **kwargs): Simplex scale size. i: float The index of the line to draw + axis_min_max: dict + The min and max values of the axes in simplex coordinates. + These may not be equal to (0, scale) if a truncation has been + applied. kwargs: Dictionary Any kwargs to pass through to Matplotlib. """ + if i <= axis_min_max['l'][1]: + if i < axis_min_max['l'][0]: + p1 = (scale - axis_min_max['r'][1] - i, + axis_min_max['r'][1], + i) + else: + p1 = (0, scale - i, i) + + if i < scale - axis_min_max['b'][1]: + p2 = (axis_min_max['b'][1], + scale - axis_min_max['b'][1] - i, + i) + else: + p2 = (scale - i, 0, i) - p1 = (0, scale - i, i) - p2 = (scale - i, 0, i) - line(ax, p1, p2, **kwargs) + line(ax, p1, p2, **kwargs) ## Boundary, Gridlines ## -def boundary(ax, scale, axes_colors=None, **kwargs): +def boundary(ax, scale, axis_min_max, axes_colors=None, **kwargs): """ Plots the boundary of the simplex. Creates and returns matplotlib axis if none given. @@ -107,23 +155,45 @@ def boundary(ax, scale, axes_colors=None, **kwargs): The subplot to draw on. scale: float Simplex scale size. - kwargs: - Any kwargs to pass through to matplotlib. + axis_min_max: dict + The min and max values of the axes in simplex coordinates. + These may not be equal to (0, scale) if a truncation has been + applied. axes_colors: dict Option for coloring boundaries different colors. e.g. {'l': 'g'} for coloring the left axis boundary green + kwargs: + Any kwargs to pass through to matplotlib. """ - # Set default color as black. + # set default color as black if axes_colors is None: axes_colors = dict() for _axis in ['l', 'r', 'b']: if _axis not in axes_colors.keys(): axes_colors[_axis] = 'black' - horizontal_line(ax, scale, 0, color=axes_colors['b'], **kwargs) - left_parallel_line(ax, scale, 0, color=axes_colors['l'], **kwargs) - right_parallel_line(ax, scale, 0, color=axes_colors['r'], **kwargs) + horizontal_line(ax, scale, 0, axis_min_max, + color=axes_colors['b'], **kwargs) + left_parallel_line(ax, scale, 0, axis_min_max, + color=axes_colors['l'], **kwargs) + right_parallel_line(ax, scale, 0, axis_min_max, + color=axes_colors['r'], **kwargs) + + if axis_min_max['r'][1] < scale: + horizontal_line(ax, scale, axis_min_max['r'][1], + axis_min_max=axis_min_max, + color=axes_colors['r'], **kwargs) + if axis_min_max['b'][1] < scale: + left_parallel_line(ax, scale, axis_min_max['b'][1], + axis_min_max=axis_min_max, + color=axes_colors['b'], **kwargs) + if axis_min_max['l'][1] < scale: + right_parallel_line(ax, scale, axis_min_max['l'][1], + axis_min_max=axis_min_max, + color=axes_colors['l'], **kwargs) + + return ax @@ -147,8 +217,9 @@ def merge_dicts(base, updates): return z -def gridlines(ax, scale, multiple=None, horizontal_kwargs=None, - left_kwargs=None, right_kwargs=None, **kwargs): +def gridlines(ax, scale, axis_min_max, multiple=None, + horizontal_kwargs=None, left_kwargs=None, right_kwargs=None, + **kwargs): """ Plots grid lines excluding boundary. @@ -158,8 +229,12 @@ def gridlines(ax, scale, multiple=None, horizontal_kwargs=None, The subplot to draw on. scale: float Simplex scale size. + axis_min_max: dict + The min and max values of the axes in simplex coordinates. + These may not be equal to (0, scale) if a truncation has been + applied. multiple: float, None - Specifies which inner gridelines to draw. For example, if scale=30 and + Specifies which inner gridlines to draw. For example, if scale=30 and multiple=6, only 5 inner gridlines will be drawn. horizontal_kwargs: dict, None Any kwargs to pass through to matplotlib for horizontal gridlines @@ -184,11 +259,12 @@ def gridlines(ax, scale, multiple=None, horizontal_kwargs=None, ## Draw grid-lines # Parallel to horizontal axis for i in arange(0, scale, multiple): - horizontal_line(ax, scale, i, **horizontal_kwargs) + horizontal_line(ax, scale, i, axis_min_max, **horizontal_kwargs) # Parallel to left and right axes for i in arange(0, scale + multiple, multiple): - left_parallel_line(ax, scale, i, **left_kwargs) - right_parallel_line(ax, scale, i, **right_kwargs) + left_parallel_line(ax, scale, i, axis_min_max, **left_kwargs) + right_parallel_line(ax, scale, i, axis_min_max, **right_kwargs) + return ax @@ -222,7 +298,7 @@ def ticks(ax, scale, ticks=None, locations=None, multiple=1, axis='b', locations: list of points, None The locations of the ticks multiple: float, None - Specifies which ticks gridelines to draw. For example, if scale=30 and + Specifies which ticks to draw. For example, if scale=30 and multiple=6, only 5 ticks will be drawn. axis: str, 'b' The axis or axes to draw the ticks for. `axis` must be a substring of @@ -337,3 +413,59 @@ def ticks(ax, scale, ticks=None, locations=None, multiple=1, axis='b', s = tick_formats['b'] % tick ax.text(x, y, s, horizontalalignment="center", color=axes_colors['b'], fontsize=fontsize) + + +def add_extra_tick(ax, axis, loc1, offset, scale, tick, fontsize, **kwargs): + """ + Add an extra tick on an axis but not necessarily on + the boundary of the simplex. This may be useful if a truncation is applied. + + Parameters + ---------- + ax : matplotlib.Axes + The matplotlib Axes object containing the plot. + axis : STR + A string giving the axis on which the extra tick should be drawn. + One of 'l', 'b' or 'r'. + loc1 : 3-tuple + A 3-tuple giving the location of the extra tick in simplex coords. + offset : FLOAT + Defines an offset of the tick label and the length of the tick + scale : INT + The self._scale attibute of the simplex + tick : STR + A string giving the text for the tick label + fontsize : INT + Describing the font size of the tick label + **kwargs : DICT + Kwargs to pass through to matplotlib Line2D. + + Returns + ------- + None. + + """ + toff = offset * scale + + if axis == 'r': + loc2 = (loc1[0] + toff, loc1[1], loc1[2]-toff) + text_location = (loc1[0] + 2.6 * toff, + loc1[1] - 0.5 * toff, + loc1[2] - 2.6 * toff) + + elif axis == 'l': + loc2 = (loc1[0] - toff, loc1[1] + toff, loc1[2]) + text_location = (loc1[0] - 2 * toff, + loc1[1] + 1.5 * toff, + loc1[2] + 2 * toff) + + elif axis == 'b': + loc2 = (loc1[0], loc1[1] - toff, loc1[2] + toff) + text_location = (loc1[0] - 0.5 * toff, + loc1[1] - 3.5 * toff, + loc1[2] + 2 * toff) + + + line(ax, loc1, loc2, **kwargs) + x, y = project_point(text_location) + ax.text(x,y,tick,horizontalalignment='center',fontsize=fontsize) diff --git a/ternary/ternary_axes_subplot.py b/ternary/ternary_axes_subplot.py index dd56f9b..488ea30 100644 --- a/ternary/ternary_axes_subplot.py +++ b/ternary/ternary_axes_subplot.py @@ -71,6 +71,18 @@ def __init__(self, ax=None, scale=None, permutation=None): self._labels = dict() self._corner_labels = dict() self._ticks = dict() + self._ticklocs = dict() + # Container for data limits for the axes. Custom limits can + # be set by the user + self.set_axis_limits({"b" : [0, self._scale], + "r" : [0, self._scale], + "l" : [0, self._scale]}) + + # Container for parameters describing a possible truncation + self._truncation = dict() + self._axis_min_max = {"b" : [0, self._scale], + "r" : [0, self._scale], + "l" : [0, self._scale]} # Container for the redrawing of labels self._to_remove = [] self._connect_callbacks() @@ -119,8 +131,80 @@ def set_axis_limits(self, axis_limits=None): self._axis_limits = axis_limits def get_axis_limits(self): + """Get the data limits for each axis""" return self._axis_limits + def set_axis_min_max(self, truncation): + """ + Set the min and max values of the axes in SIMPLEX coords + (rather than data coords) given various + truncation points. + + !! Assumes the truncation lines do NOT cross each other!! + + truncation: dict + keys are 'br', 'rl' and/or 'lb' + values are a value in SIMPLEX coords giving the maximum of the + first axis mentioned in the key + """ + for k in truncation.keys(): + self._axis_min_max[k[0]][1] = truncation[k] + self._axis_min_max[k[1]][0] = self._scale - truncation[k] + + + def get_axis_min_max(self): + """Get the simplex limits for each axis""" + return self._axis_min_max + + + def set_truncation(self, truncation_data): + """ + Set one or more truncation lines which will be used to truncate + the simplex i.e. cut one or more corners off to remove whitespace. + + The self.axis_limits (data limits) and self.axis_min_max (simplex + limits) are set by this function. + + Truncation lines may not cross each other! + + Parameters + ---------- + truncation_data : dict + keys are 'br', 'rl' and/or 'lb' + values are a value in DATA coords giving the maximum of the + first axis mentioned in the key. These are then transformed + into SIMPLEX coords and stored internally. + + Returns + ------- + None. + + """ + steps = {i : (j[1] - j[0]) / float(self._scale) for i, j in + self._axis_limits.items()} + + axlim = {i : j[:] for i, j in self._axis_limits.items()} + + for k in truncation_data: + + self._truncation[k] = int((truncation_data[k]- + axlim[k[0]][0])/steps[k[0]]) + + self._axis_limits[k[0]][1] = truncation_data[k] + + self._axis_limits[k[1]][0] = axlim[k[1]][0] + steps[k[1]] *\ + (self._scale - self._truncation[k]) + + self.set_axis_min_max(self._truncation) + self._draw_background() + + + def get_truncation(self): + """ + This returns the truncation in SIMPLEX coords + """ + return self._truncation + # Title and Axis Labels def set_title(self, title, **kwargs): @@ -128,8 +212,8 @@ def set_title(self, title, **kwargs): ax = self.get_axes() ax.set_title(title, **kwargs) - def left_axis_label(self, label, position=None, rotation=60, offset=0.08, - **kwargs): + def left_axis_label(self, label, position=None, rotation=60, offset=0.08, + transform_type="transData", **kwargs): """ Sets the label on the left axis. @@ -149,10 +233,12 @@ def left_axis_label(self, label, position=None, rotation=60, offset=0.08, if not position: position = (-offset, 3./5, 2./5) - self._labels["left"] = (label, position, rotation, kwargs) + transform_type = "transAxes" + self._labels["left"] = (label, position, rotation, + transform_type, kwargs) def right_axis_label(self, label, position=None, rotation=-60, offset=0.08, - **kwargs): + transform_type="transData", **kwargs): """ Sets the label on the right axis. @@ -173,10 +259,12 @@ def right_axis_label(self, label, position=None, rotation=-60, offset=0.08, if not position: position = (2. / 5 + offset, 3. / 5, 0) - self._labels["right"] = (label, position, rotation, kwargs) + transform_type = "transAxes" + self._labels["right"] = (label, position, rotation, + transform_type, kwargs) def bottom_axis_label(self, label, position=None, rotation=0, offset=0.02, - **kwargs): + transform_type="transData", **kwargs): """ Sets the label on the bottom axis. @@ -196,10 +284,12 @@ def bottom_axis_label(self, label, position=None, rotation=0, offset=0.02, if not position: position = (0.5, -offset / 2., 0.5) - self._labels["bottom"] = (label, position, rotation, kwargs) + transform_type = "transAxes" + self._labels["bottom"] = (label, position, rotation, + transform_type, kwargs) def right_corner_label(self, label, position=None, rotation=0, offset=0.08, - **kwargs): + transform_type="transData", **kwargs): """ Sets the label on the right corner (complements left axis). @@ -219,10 +309,12 @@ def right_corner_label(self, label, position=None, rotation=0, offset=0.08, if not position: position = (1, offset / 2, 0) - self._corner_labels["right"] = (label, position, rotation, kwargs) + transform_type = "transAxes" + self._corner_labels["right"] = (label, position, rotation, + transform_type, kwargs) def left_corner_label(self, label, position=None, rotation=0, offset=0.08, - **kwargs): + transform_type="transData", **kwargs): """ Sets the label on the left corner (complements right axis.) @@ -242,10 +334,12 @@ def left_corner_label(self, label, position=None, rotation=0, offset=0.08, if not position: position = (-offset / 2, offset / 2, 0) - self._corner_labels["left"] = (label, position, rotation, kwargs) + transform_type="transAxes" + self._corner_labels["left"] = (label, position, rotation, + transform_type, kwargs) def top_corner_label(self, label, position=None, rotation=0, offset=0.2, - **kwargs): + transform_type="transData", **kwargs): """ Sets the label on the bottom axis. @@ -265,7 +359,9 @@ def top_corner_label(self, label, position=None, rotation=0, offset=0.2, if not position: position = (-offset / 2, 1 + offset, 0) - self._corner_labels["top"] = (label, position, rotation, kwargs) + transform_type="transAxes" + self._corner_labels["top"] = (label, position, rotation, + transform_type, kwargs) def annotate(self, text, position, **kwargs): ax = self.get_axes() @@ -275,27 +371,80 @@ def annotate(self, text, position, **kwargs): # Boundary and Gridlines def boundary(self, scale=None, axes_colors=None, **kwargs): + """ + Draw a boundary around the simplex. + + Parameters + ---------- + scale : INT, optional + An int describing the scale of the boundary to be drawn. + Sometimes you may want to draw a bigger boundary than + specified in the initialisation of the tax. The default is None. + axes_colors: dict + Option for coloring boundaries different colors. + e.g. {'l': 'g'} for coloring the left axis boundary green + **kwargs : dict + Any kwargs to pass through to matplotlib.. + + Returns + ------- + None. + + """ # Sometimes you want to draw a bigger boundary if not scale: - scale = self._boundary_scale # defaults to self._scale + scale = self._boundary_scale # defaults to self._scale ax = self.get_axes() self.resize_drawing_canvas(scale) - lines.boundary(scale=scale, ax=ax, axes_colors=axes_colors, **kwargs) - def gridlines(self, multiple=None, horizontal_kwargs=None, left_kwargs=None, - right_kwargs=None, **kwargs): + lines.boundary(ax, scale, self._axis_min_max, + axes_colors=axes_colors, **kwargs) + + + def gridlines(self, multiple=None, horizontal_kwargs=None, + left_kwargs=None, right_kwargs=None, **kwargs): + """ + Draw gridlines on the simplex (excluding the boundary). + + Parameters + ---------- + multiple: int, optional + Specifies which inner gridelines to draw. For example, + if scale=30 and multiple=6, only 5 inner gridlines will be drawn. + The default is None. + horizontal_kwargs: dict, optional + Any kwargs to pass through to matplotlib for horizontal gridlines + The default is None. + left_kwargs: dict, optional + Any kwargs to pass through to matplotlib for left parallel gridlines + right_kwargs: dict, optional + Any kwargs to pass through to matplotlib for right parallel gridlines + The default is None. + kwargs: + Any kwargs to pass through to matplotlib, if not using + horizontal_kwargs, left_kwargs, or right_kwargs + + Returns + ------- + None. + + """ ax = self.get_axes() scale = self.get_scale() - lines.gridlines(scale=scale, multiple=multiple, - ax=ax, horizontal_kwargs=horizontal_kwargs, - left_kwargs=left_kwargs, right_kwargs=right_kwargs, + + lines.gridlines(ax, scale, self._axis_min_max, + multiple=multiple, + horizontal_kwargs=horizontal_kwargs, + left_kwargs=left_kwargs, + right_kwargs=right_kwargs, **kwargs) # Various Lines def line(self, p1, p2, **kwargs): ax = self.get_axes() - lines.line(ax, p1, p2, **kwargs) + permutation = self._permutation + lines.line(ax, p1, p2, permutation=permutation, **kwargs) def horizontal_line(self, i, **kwargs): ax = self.get_axes() @@ -340,33 +489,129 @@ def clear_matplotlib_ticks(self, axis="both"): ax = self.get_axes() plotting.clear_matplotlib_ticks(ax=ax, axis=axis) + def get_ticks_from_axis_limits(self, multiple=1): """ - Taking self._axis_limits and self._boundary_scale get the scaled + Taking self._axis_limits, self.axis_min_max and self._scale get the ticks for all three axes and store them in self._ticks under the - keys 'b' for bottom, 'l' for left and 'r' for right axes. + keys 'b' for bottom, 'l' for left and 'r' for right axes. Get the + locations of the tickes and store them under self._ticklocs with the + same keys. + + NB. the tick locations for the left axis have to be shifted if there + is a truncation of that axis, otherwise they are projected in the + wrong place by lines.line(), which calls helpers.project_point(). + This is handled in the last 3 lines of this function. """ - for k in ['b', 'l', 'r']: - self._ticks[k] = np.linspace( - self._axis_limits[k][0], - self._axis_limits[k][1], - int(self._boundary_scale / float(multiple) + 1) - ).tolist() + for k in ['b','l','r']: + gg = self._axis_min_max[k][1] - self._axis_min_max[k][0] + ff = self._axis_limits[k][1] - self._axis_limits[k][0] + step = ff/gg - def set_custom_ticks(self, locations=None, clockwise=False, multiple=1, - axes_colors=None, tick_formats=None, **kwargs): + self._ticklocs[k] = np.arange(self._axis_min_max[k][0], + self._axis_min_max[k][1] + step, + multiple).astype("int").tolist() + + self._ticks[k] = np.arange(self._axis_limits[k][0], + self._axis_limits[k][1] + step, + step*multiple).tolist() + + + self._ticklocs['l'] = [i - self._axis_min_max["l"][0] + + (self._scale - self._axis_min_max["l"][1]) + for i in self._ticklocs['l']] + + + + def set_custom_ticks(self, clockwise=False, axes_colors=None, + tick_formats=None, **kwargs): """ - Having called get_ticks_from_axis_limits, set the custom ticks on the - plot. + Having called get_ticks_from_axis_limits(), draw the custom ticks on + the plot. We call self.ticks() for each axis in turn with the ticks + and ticklocs already defined using get_ticks_from_axis_limits(). + + Parameters + ---------- + clockwise : BOOL, optional + Whether the axes of the simplex run clockwise or not. + The default is False. + axes_colors: Dict, optional + Option to color ticks differently for each axis, 'l', 'r', 'b' + e.g. {'l': 'g', 'r':'b', 'b': 'y'} + The default is None. + tick_formats: None, Dict, Str, optional + If None, all axes will be labelled with ints. + If Dict, the keys are 'b', 'l' and 'r' and the values are + format strings e.g. "%.3f" for a float with 3 decimal places + or "%.3e" for scientific format with 3 decimal places or + "%d" for ints. + If tick_formats is a string, it is assumed that this is a + format string to be applied to all axes. + The default is None + kwargs: + Any kwargs to pass through to matplotlib. + + Returns + ------- + None. + """ - for k in ['b', 'l', 'r']: - self.ticks(ticks=self._ticks[k], locations=locations, - axis=k, clockwise=clockwise, multiple=multiple, - axes_colors=axes_colors, tick_formats=tick_formats, - **kwargs) + for k in ['b','l','r']: + self.ticks(ticks=self._ticks[k], locations=self._ticklocs[k], + axis=k, clockwise=clockwise, axes_colors=axes_colors, + tick_formats=tick_formats, **kwargs) + def ticks(self, ticks=None, locations=None, multiple=1, axis='blr', clockwise=False, axes_colors=None, tick_formats=None, **kwargs): + """ + Convenience function passthrough to lines.ticks. + + Calling this function with all the default arguments results in + each axis of the simplex being labelled on every gridline with + an int. This is the simplest case where the axis_limits (data) + have not been set and are therefore the same as the axis_min_max + (simplex) limits. + + If set_axis_limits() or set_truncation() has been called, this + function should not be used. Instead call get_ticks_from_axis_limits() + and then set_custom_ticks(). + + + Parameters + ---------- + ticks: list of strings, None + The tick labels + locations: list of points, None + The locations of the ticks + multiple: float, None + Specifies which ticks to draw. For example, + if scale=30 and multiple=6, only 5 ticks will be drawn. + axis: str, 'b' + The axis or axes to draw the ticks for. `axis` must be a + substring of 'lrb' (as sets) + offset: float, 0.01 + controls the length of the ticks + clockwise: bool, False + Draw ticks marks clockwise or counterclockwise + axes_colors: Dict, None + Option to color ticks differently for each axis, 'l', 'r', 'b' + e.g. {'l': 'g', 'r':'b', 'b': 'y'} + tick_formats: None, Dict, Str + If None, all axes will be labelled with ints. If Dict, the keys + are 'b', 'l' and 'r' and the values are format strings + e.g. "%.3f" for a float with 3 decimal places or "%.3e" for + scientific format with 3 decimal places or "%d" for ints. + If tick_formats is a string, it + is assumed that this is a format string to be applied to all axes. + kwargs: + Any kwargs to pass through to matplotlib. + + Returns + ------- + None. + + """ ax = self.get_axes() scale = self.get_scale() lines.ticks(ax, scale, ticks=ticks, locations=locations, @@ -374,6 +619,40 @@ def ticks(self, ticks=None, locations=None, multiple=1, axis='blr', axes_colors=axes_colors, tick_formats=tick_formats, **kwargs) + + def add_extra_tick(self, axis, loc1, offset, tick, fontsize, **kwargs): + """ + Convenience function passthrough to lines.add_extra_tick. + + Add an extra tick on an axis but not necessarily on + the boundary of the simplex. This may be useful if a + truncation is applied. + + Parameters + ---------- + axis : STR + A string giving the axis on which the extra tick should be drawn. + One of 'l', 'b' or 'r'. + loc1 : 3-tuple + A 3-tuple giving the location of the extra tick in simplex coords. + offset : FLOAT + Defines an offset of the tick label and the length of the tick + tick : STR + A string giving the text for the tick label + fontsize : INT + Describing the font size of the tick label + **kwargs : DICT + Kwargs to pass through to matplotlib Line2D. + + Returns + ------- + None. + + """ + lines.add_extra_tick(self.get_axes(), axis, loc1, offset, + self.get_scale(), tick, fontsize, **kwargs) + + # Redrawing and resizing def resize_drawing_canvas(self, scale=None): @@ -382,8 +661,11 @@ def resize_drawing_canvas(self, scale=None): scale = self.get_scale() plotting.resize_drawing_canvas(ax, scale=scale) + def _redraw_labels(self): - """Redraw axis labels, typically after draw or resize events.""" + """ + Redraw axis labels, typically after draw or resize events. + """ ax = self.get_axes() # Remove any previous labels for mpl_object in self._to_remove: @@ -392,25 +674,30 @@ def _redraw_labels(self): # Redraw the labels with the appropriate angles label_data = list(self._labels.values()) label_data.extend(self._corner_labels.values()) - for (label, position, rotation, kwargs) in label_data: - transform = ax.transAxes + for (label, position, rotation, transform_type, kwargs) in label_data: + if transform_type == "transAxes": + transform = ax.transAxes + elif transform_type == "transData": + transform = ax.transData + x, y = project_point(position) # Calculate the new angle. position = np.array([x, y]) - new_rotation = ax.transData.transform_angles( - np.array((rotation,)), position.reshape((1, 2)))[0] + new_rotation = ax.transData.transform_angles(np.array((rotation,)), + position.reshape((1, 2)))[0] text = ax.text(x, y, label, rotation=new_rotation, transform=transform, horizontalalignment="center", **kwargs) text.set_rotation_mode("anchor") self._to_remove.append(text) + def convert_coordinates(self, points, axisorder='blr'): """ Convert data coordinates to simplex coordinates for plotting in the case that axis limits have been applied. """ - return convert_coordinates_sequence(points,self._boundary_scale, + return convert_coordinates_sequence(points, self._boundary_scale, self._axis_limits, axisorder) # Various Plots @@ -422,18 +709,21 @@ def scatter(self, points, **kwargs): **kwargs) return plot_ + def plot(self, points, **kwargs): ax = self.get_axes() permutation = self._permutation plotting.plot(points, ax=ax, permutation=permutation, **kwargs) + def plot_colored_trajectory(self, points, cmap=None, **kwargs): ax = self.get_axes() permutation = self._permutation plotting.plot_colored_trajectory(points, cmap=cmap, ax=ax, permutation=permutation, **kwargs) + def heatmap(self, data, scale=None, cmap=None, scientific=False, style='triangular', colorbar=True, use_rgba=False, vmin=None, vmax=None, cbarlabel=None, cb_kwargs=None): @@ -449,6 +739,7 @@ def heatmap(self, data, scale=None, cmap=None, scientific=False, vmin=vmin, vmax=vmax, cbarlabel=cbarlabel, cb_kwargs=cb_kwargs) + def heatmapf(self, func, scale=None, cmap=None, boundary=True, style='triangular', colorbar=True, scientific=False, vmin=None, vmax=None, cbarlabel=None, cb_kwargs=None): @@ -464,18 +755,24 @@ def heatmapf(self, func, scale=None, cmap=None, boundary=True, vmin=vmin, vmax=vmax, cbarlabel=cbarlabel, cb_kwargs=cb_kwargs) + def set_background_color(self, color="whitesmoke", zorder=-1000, alpha=0.75): self._background_parameters = BackgroundParameters(color=color, alpha=alpha, zorder=zorder) self._draw_background() + def _draw_background(self): color, alpha, zorder = self._background_parameters scale = self.get_scale() ax = self.get_axes() + axis_min_max = self.get_axis_min_max() # Remove any existing background if self._background_triangle: self._background_triangle.remove() # Draw the background - self._background_triangle = heatmapping.background_color(ax, color, scale, alpha=alpha, zorder=zorder)[0] + self._background_triangle = heatmapping.background_color(ax, color, scale, + axis_min_max, + alpha=alpha, + zorder=zorder)[0]