Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Added axis keyword to dendrogram function #3201

Closed
wants to merge 4 commits into from

3 participants

@jamestwebber

This fixes issue #1325 (Trac 798). It allows scipy dendrograms to easily be plotted on custom axes, e.g. as part of a larger figure. It does not import pylab at all if an axis is provided, which is useful for those who are using the matplotlib API.

@jamestwebber jamestwebber Added axis keyword to dendrogram function
This fixes issue 1325 (Trac #798). It allows scipy dendrograms to easily be plotted on custom axes, e.g. as part of a larger figure. It does not import pylab at all if an axis is provided, which is useful for those who are using the matplotlib API.
0483766
@rgommers
Owner

Overall this looks like a good idea. A couple of comments:

The axis argument should be named ax, because that's how this argument is typically called. axis normally means the axis of the input array over which to apply a certain operation (np.mean(x, axis=1)).

A test should be added for this keyword. Matplotlib is an optional dependency, so the test should only run if MPL is installed. You can look at the stats.probplot test as an example for how to add such a test:
https://github.com/scipy/scipy/blob/master/scipy/stats/tests/test_morestats.py#L17
https://github.com/scipy/scipy/blob/master/scipy/stats/tests/test_morestats.py#L436

@rgommers
Owner

TravisCI failure is unrelated, can be ignored.

@jamestwebber

Changing from axis to ax is easy enough to do. 'axis' was used throughout the existing code so I went with it.

I wasn't sure how to test a plot, it looks like (in the example you pointed me to) there's no test for the correct output, just to make sure it doesn't fail? I can add that to the existing cluster tests.

@jamestwebber jamestwebber Changed 'axis' to 'ax' throughout dendrogram code
Also changed "axis_gca" flag to be more descriptive "trigger_redraw".
7f3f8b9
@rgommers
Owner

Indeed, checking that it doesn't fail. Comparing graphics output is done by the MPL test suite, but is hard and overkill here.

@jamestwebber jamestwebber Added test for dendrogram plotting
Tests plotting to pylab.gca() and to an axis passed in as an argument.
d229aa3
@jamestwebber

The interface for dendrogram is slightly different than the one for probplot (and it seems out of scope to try to change that). I didn't compare the output of the two different calls because there isn't the option to pass in pyplot instead of an axis.

@coveralls

Coverage Status

Changes Unknown when pulling d229aa3 on jamestwebber:patch-1 into ** on scipy:master**.

@coveralls

Coverage Status

Changes Unknown when pulling d229aa3 on jamestwebber:patch-1 into ** on scipy:master**.

@coveralls

Coverage Status

Changes Unknown when pulling bf2ec22 on jamestwebber:patch-1 into ** on scipy:master**.

@rgommers rgommers referenced this pull request from a commit
@rgommers rgommers Merge branch 'pr/3201' into master.
Reviewed as #3201
dc7555b
@rgommers
Owner

Test and doc addition look good. PR merged in dc7555b. Thanks @jamestwebber

@rgommers rgommers closed this
@rgommers
Owner

Note that I did edit your commit messages to prepend the standard abbreviations listed at http://docs.scipy.org/doc/numpy-dev/dev/gitwash/development_workflow.html#writing-the-commit-message.

@jamestwebber jamestwebber deleted the jamestwebber:patch-1 branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 10, 2014
  1. @jamestwebber

    Added axis keyword to dendrogram function

    jamestwebber authored
    This fixes issue 1325 (Trac #798). It allows scipy dendrograms to easily be plotted on custom axes, e.g. as part of a larger figure. It does not import pylab at all if an axis is provided, which is useful for those who are using the matplotlib API.
Commits on Jan 12, 2014
  1. @jamestwebber

    Changed 'axis' to 'ax' throughout dendrogram code

    jamestwebber authored
    Also changed "axis_gca" flag to be more descriptive "trigger_redraw".
  2. @jamestwebber

    Added test for dendrogram plotting

    jamestwebber authored
    Tests plotting to pylab.gca() and to an axis passed in as an argument.
Commits on Jan 13, 2014
  1. @jamestwebber
This page is out of date. Refresh to see the latest.
Showing with 112 additions and 63 deletions.
  1. +85 −62 scipy/cluster/hierarchy.py
  2. +27 −1 scipy/cluster/tests/test_hierarchy.py
View
147 scipy/cluster/hierarchy.py
@@ -1686,116 +1686,132 @@ def _get_tick_rotation(p):
def _plot_dendrogram(icoords, dcoords, ivl, p, n, mh, orientation,
no_labels, color_list, leaf_font_size=None,
- leaf_rotation=None, contraction_marks=None):
+ leaf_rotation=None, contraction_marks=None,
+ ax=None):
# Import matplotlib here so that it's not imported unless dendrograms
# are plotted. Raise an informative error if importing fails.
try:
- import matplotlib.pylab
+ # if an axis is provided, don't use pylab at all
+ if ax is None:
+ import matplotlib.pylab
import matplotlib.patches
import matplotlib.collections
except ImportError:
raise ImportError("You must install the matplotlib library to plot the dendrogram. Use no_plot=True to calculate the dendrogram without plotting.")
- axis = matplotlib.pylab.gca()
+ if ax is None:
+ ax = matplotlib.pylab.gca()
+ # if we're using pylab, we want to trigger a draw at the end
+ trigger_redraw = True
+ else:
+ trigger_redraw = False
+
# Independent variable plot width
ivw = len(ivl) * 10
# Depenendent variable plot height
dvw = mh + mh * 0.05
ivticks = np.arange(5, len(ivl) * 10 + 5, 10)
if orientation == 'top':
- axis.set_ylim([0, dvw])
- axis.set_xlim([0, ivw])
+ ax.set_ylim([0, dvw])
+ ax.set_xlim([0, ivw])
xlines = icoords
ylines = dcoords
if no_labels:
- axis.set_xticks([])
- axis.set_xticklabels([])
+ ax.set_xticks([])
+ ax.set_xticklabels([])
else:
- axis.set_xticks(ivticks)
- axis.set_xticklabels(ivl)
- axis.xaxis.set_ticks_position('bottom')
- lbls = axis.get_xticklabels()
+ ax.set_xticks(ivticks)
+ ax.set_xticklabels(ivl)
+ ax.xaxis.set_ticks_position('bottom')
+
+ lbls = ax.get_xticklabels()
if leaf_rotation:
- matplotlib.pylab.setp(lbls, 'rotation', leaf_rotation)
+ map(lambda lbl: lbl.set_rotation(leaf_rotation), lbls)
else:
- matplotlib.pylab.setp(lbls, 'rotation',
- float(_get_tick_rotation(len(ivl))))
+ leaf_rot = float(_get_tick_rotation(len(ivl)))
+ map(lambda lbl: lbl.set_rotation(leaf_rot), lbls)
if leaf_font_size:
- matplotlib.pylab.setp(lbls, 'size', leaf_font_size)
+ map(lambda lbl: lbl.set_size(leaf_font_size), lbls)
else:
- matplotlib.pylab.setp(lbls, 'size',
- float(_get_tick_text_size(len(ivl))))
+ leaf_fs = float(_get_tick_text_size(len(ivl)))
+ map(lambda lbl: lbl.set_rotation(leaf_fs), lbls)
# Make the tick marks invisible because they cover up the links
- for line in axis.get_xticklines():
+ for line in ax.get_xticklines():
line.set_visible(False)
elif orientation == 'bottom':
- axis.set_ylim([dvw, 0])
- axis.set_xlim([0, ivw])
+ ax.set_ylim([dvw, 0])
+ ax.set_xlim([0, ivw])
xlines = icoords
ylines = dcoords
if no_labels:
- axis.set_xticks([])
- axis.set_xticklabels([])
+ ax.set_xticks([])
+ ax.set_xticklabels([])
else:
- axis.set_xticks(ivticks)
- axis.set_xticklabels(ivl)
- lbls = axis.get_xticklabels()
+ ax.set_xticks(ivticks)
+ ax.set_xticklabels(ivl)
+
+ lbls = ax.get_xticklabels()
if leaf_rotation:
- matplotlib.pylab.setp(lbls, 'rotation', leaf_rotation)
+ map(lambda lbl: lbl.set_rotation(leaf_rotation), lbls)
else:
- matplotlib.pylab.setp(lbls, 'rotation',
- float(_get_tick_rotation(p)))
+ leaf_rot = float(_get_tick_rotation(p))
+ map(lambda lbl: lbl.set_rotation(leaf_rot), lbls)
+
if leaf_font_size:
- matplotlib.pylab.setp(lbls, 'size', leaf_font_size)
+ map(lambda lbl: lbl.set_size(leaf_font_size), lbls)
else:
- matplotlib.pylab.setp(lbls, 'size',
- float(_get_tick_text_size(p)))
- axis.xaxis.set_ticks_position('top')
+ leaf_fs = float(_get_tick_text_size(p))
+ map(lambda lbl: lbl.set_rotation(leaf_fs), lbls)
+
+ ax.xaxis.set_ticks_position('top')
# Make the tick marks invisible because they cover up the links
- for line in axis.get_xticklines():
+ for line in ax.get_xticklines():
line.set_visible(False)
elif orientation == 'left':
- axis.set_xlim([0, dvw])
- axis.set_ylim([0, ivw])
+ ax.set_xlim([0, dvw])
+ ax.set_ylim([0, ivw])
xlines = dcoords
ylines = icoords
if no_labels:
- axis.set_yticks([])
- axis.set_yticklabels([])
+ ax.set_yticks([])
+ ax.set_yticklabels([])
else:
- axis.set_yticks(ivticks)
- axis.set_yticklabels(ivl)
+ ax.set_yticks(ivticks)
+ ax.set_yticklabels(ivl)
- lbls = axis.get_yticklabels()
+ lbls = ax.get_yticklabels()
if leaf_rotation:
- matplotlib.pylab.setp(lbls, 'rotation', leaf_rotation)
+ map(lambda lbl: lbl.set_rotation(leaf_rotation), lbls)
if leaf_font_size:
- matplotlib.pylab.setp(lbls, 'size', leaf_font_size)
- axis.yaxis.set_ticks_position('left')
+ map(lambda lbl: lbl.set_size(leaf_font_size), lbls)
+
+ ax.yaxis.set_ticks_position('left')
# Make the tick marks invisible because they cover up the
# links
- for line in axis.get_yticklines():
+ for line in ax.get_yticklines():
line.set_visible(False)
elif orientation == 'right':
- axis.set_xlim([dvw, 0])
- axis.set_ylim([0, ivw])
+ ax.set_xlim([dvw, 0])
+ ax.set_ylim([0, ivw])
xlines = dcoords
ylines = icoords
if no_labels:
- axis.set_yticks([])
- axis.set_yticklabels([])
+ ax.set_yticks([])
+ ax.set_yticklabels([])
else:
- axis.set_yticks(ivticks)
- axis.set_yticklabels(ivl)
- lbls = axis.get_yticklabels()
+ ax.set_yticks(ivticks)
+ ax.set_yticklabels(ivl)
+
+ lbls = ax.get_yticklabels()
if leaf_rotation:
- matplotlib.pylab.setp(lbls, 'rotation', leaf_rotation)
+ map(lambda lbl: lbl.set_rotation(leaf_rotation), lbls)
if leaf_font_size:
- matplotlib.pylab.setp(lbls, 'size', leaf_font_size)
- axis.yaxis.set_ticks_position('right')
+ map(lambda lbl: lbl.set_size(leaf_font_size), lbls)
+
+ ax.yaxis.set_ticks_position('right')
# Make the tick marks invisible because they cover up the links
- for line in axis.get_yticklines():
+ for line in ax.get_yticklines():
line.set_visible(False)
# Let's use collections instead. This way there is a separate legend
@@ -1820,18 +1836,18 @@ def _plot_dendrogram(icoords, dcoords, ivl, p, n, mh, orientation,
for color in colors_used:
if color != 'b':
- axis.add_collection(colors_to_collections[color])
+ ax.add_collection(colors_to_collections[color])
# If there is a blue grouping (i.e., links above the color threshold),
# it should go last.
if 'b' in colors_to_collections:
- axis.add_collection(colors_to_collections['b'])
+ ax.add_collection(colors_to_collections['b'])
if contraction_marks is not None:
if orientation in ('left', 'right'):
for (x, y) in contraction_marks:
e = matplotlib.patches.Ellipse((y, x),
width=dvw / 100, height=1.0)
- axis.add_artist(e)
+ ax.add_artist(e)
e.set_clip_box(axis.bbox)
e.set_alpha(0.5)
e.set_facecolor('k')
@@ -1839,12 +1855,13 @@ def _plot_dendrogram(icoords, dcoords, ivl, p, n, mh, orientation,
for (x, y) in contraction_marks:
e = matplotlib.patches.Ellipse((x, y),
width=1.0, height=dvw / 100)
- axis.add_artist(e)
+ ax.add_artist(e)
e.set_clip_box(axis.bbox)
e.set_alpha(0.5)
e.set_facecolor('k')
- matplotlib.pylab.draw_if_interactive()
+ if trigger_redraw:
+ matplotlib.pylab.draw_if_interactive()
_link_line_colors = ['g', 'r', 'c', 'm', 'y', 'k']
@@ -1880,7 +1897,7 @@ def dendrogram(Z, p=30, truncate_mode=None, color_threshold=None,
no_plot=False, no_labels=False, color_list=None,
leaf_font_size=None, leaf_rotation=None, leaf_label_func=None,
no_leaves=False, show_contracted=False,
- link_color_func=None):
+ link_color_func=None, ax=None):
"""
Plots the hierarchical clustering as a dendrogram.
@@ -2048,6 +2065,11 @@ def dendrogram(Z, p=30, truncate_mode=None, color_threshold=None,
colors the direct links below each untruncated non-singleton node
``k`` using ``colors[k]``.
+ ax : matplotlib Axes instance, optional
+ If None and no_plot is not True, the dendrogram will be plotted
+ on the current axes. Otherwise if no_plot is not True the
+ dendrogram will be plotted on the given Axes. This can be useful
+ if the dendrogram is part of a more complex figure.
Returns
-------
@@ -2152,7 +2174,8 @@ def dendrogram(Z, p=30, truncate_mode=None, color_threshold=None,
_plot_dendrogram(icoord_list, dcoord_list, ivl, p, n, mh, orientation,
no_labels, color_list, leaf_font_size=leaf_font_size,
leaf_rotation=leaf_rotation,
- contraction_marks=contraction_marks)
+ contraction_marks=contraction_marks,
+ ax=ax)
return R
View
28 scipy/cluster/tests/test_hierarchy.py
@@ -37,7 +37,7 @@
import os.path
import numpy as np
-from numpy.testing import TestCase, run_module_suite
+from numpy.testing import TestCase, run_module_suite, dec
from scipy.lib.six import xrange
from scipy.lib.six import u
@@ -49,6 +49,16 @@
is_valid_linkage, is_valid_im, to_tree, leaves_list, dendrogram
from scipy.spatial.distance import squareform, pdist
+
+# Matplotlib is not a scipy dependency but is optionally used in dendrogram, so
+# check if it's available
+try:
+ import matplotlib.pyplot as plt
+ have_matplotlib = True
+except:
+ have_matplotlib = False
+
+
_tdist = np.array([[0, 662, 877, 255, 412, 996],
[662, 0, 295, 468, 268, 400],
[877, 295, 0, 754, 564, 138],
@@ -1374,6 +1384,22 @@ def test_dendrogram_single_linkage_tdist(self):
R = dendrogram(Z, no_plot=True)
leaves = R["leaves"]
self.assertEqual(leaves, [2, 5, 1, 0, 3, 4])
+
+ @dec.skipif(not have_matplotlib)
+ def test_dendrogram_plot(self):
+ "Tests dendrogram plotting."
+ Z = linkage(_ytdist, 'single')
+
+ fig = plt.figure()
+ ax = fig.add_subplot(111)
+
+ # test that dendrogram accepts ax keyword
+ R1 = dendrogram(Z, ax=ax)
+ plt.close()
+
+ # test plotting to gca (will import pylab)
+ R2 = dendrogram(Z)
+ plt.close()
def calculate_maximum_distances(Z):
Something went wrong with that request. Please try again.