Search code examples
pythonmatplotlibplotlegendparallel-coordinates

Adding legend to parallel coordinated plot using matplotlib proxy artist


I am having a hard time adding legend to matplotlib.pyplot, my goal is to make parallel coordinates plot similar to the one

Parallel Coordinates plot in Matplotlib

Since my use-case is similar, I used the solution provided, except that I have only 2 observations, 1 in each group and I added plt.legend(axes,style), so that legend is created, but when I run the code, I get the below warning and no legend.

:\Python27\lib\site-packages\matplotlib\legend.py:634: UserWarning: Legend does not support instances. A proxy artist may be used instead. See: http://matplotlib.org/users/legend_guide.html#using-proxy-artist "#using-proxy-artist".format(orig_handle)

I tried to go through documentation but could not find a solution.

I found another stackoverflow post listed below but still not quite clear on the usage of legends, especially on how to unpack subplots before passing to legend function. Could anyone please explain how it works.

Using a proxy artist inside a legend, matplotlib, Python

#!/usr/bin/python
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker

def parallel_coordinates(data_sets, style=None):

    dims = len(data_sets[0])
    x    = range(dims)
    fig, axes = plt.subplots(1, dims-1, sharey=False)

    if style is None:
        style = ['r-']*len(data_sets)

    # Calculate the limits on the data
    min_max_range = list()
    for m in zip(*data_sets):
        mn = min(m)
        mx = max(m)
        if mn == mx:
            mn -= 0.5
            mx = mn + 1.
        r  = float(mx - mn)
        min_max_range.append((mn, mx, r))

    # Normalize the data sets
    norm_data_sets = list()
    for ds in data_sets:
        nds = [(value - min_max_range[dimension][0]) / 
                min_max_range[dimension][2] 
                for dimension,value in enumerate(ds)]
        norm_data_sets.append(nds)
    data_sets = norm_data_sets

    # Plot the datasets on all the subplots
    for i, ax in enumerate(axes):
        for dsi, d in enumerate(data_sets):
            ax.plot(x, d, style[dsi])
        ax.set_xlim([x[i], x[i+1]])

    # Set the x axis ticks 
    for dimension, (axx,xx) in enumerate(zip(axes, x[:-1])):
        axx.xaxis.set_major_locator(ticker.FixedLocator([xx]))
        ticks = len(axx.get_yticklabels())
        labels = list()
        step = min_max_range[dimension][2] / (ticks - 1)
        mn   = min_max_range[dimension][0]
        for i in xrange(ticks):
            v = mn + i*step
            labels.append('%4.2f' % v)
        axx.set_yticklabels(labels)


    # Move the final axis' ticks to the right-hand side
    axx = plt.twinx(axes[-1])
    dimension += 1
    axx.xaxis.set_major_locator(ticker.FixedLocator([x[-2], x[-1]]))
    ticks = len(axx.get_yticklabels())
    step = min_max_range[dimension][2] / (ticks - 1)
    mn   = min_max_range[dimension][0]
    labels = ['%4.2f' % (mn + i*step) for i in xrange(ticks)]
    axx.set_yticklabels(labels)

    # Stack the subplots 

    plt.subplots_adjust(wspace=0)
    plt.legend(axes,style)
    return plt

if __name__ == '__main__':
    import random
    base  = [0,   0,  5,   5,  0]
    scale = [1.5, 2., 1.0, 2., 2.]
    data = [[base[x] + random.uniform(0., 1.)*scale[x]
            for x in xrange(5)] for y in xrange(1)]
    colors = ['r'] * 1

    base  = [3,   6,  0,   1,  3]
    scale = [1.5, 2., 2.5, 2., 2.]
    data.extend([[base[x] + random.uniform(0., 1.)*scale[x]
                 for x in xrange(5)] for y in xrange(1)])
    colors.extend(['b'] * 1)


    parallel_coordinates(data, style=colors).show()

Solution

  • Basically the error is because matplotlib has no support for procedurally generating the legend, and is telling you that you will have to do so manually. For example

    blue_line = mlines.Line2D([], [], color='blue', label='Blue')
    red_line = mlines.Line2D([], [], color='red', label='Red')
    plt.legend(handles=[blue_line, red_line])
    

    which relies on importing matplotlib.lines as mlines. In the code in question this will generate

    You can also use matplotlib.patches if you prefer a patch instead of a line, i.e.

    blue_patch = mpatches.Patch(color='blue', label='Blue')
    red_patch = mpatches.Patch(color='red', label='Red')
    plt.legend(handles=[blue_patch, red_patch])
    

    which relies on importing matplotlib.patches as mpatches. This generates

    You can add whatever custom handles you need to the legend - mixing patches, lines, and markers is acceptable.