Search code examples
pythonmatplotlibskfuzzy

Change the colormap of existing Matplotlib pyplot (after the plot has been created)?


I am working on a fuzzy control example, which uses the skfuzzy library. Its visualization module is built on top of Matplotlib.

Usually, after constructing a fuzzy variable, with the necessary membership function (either using an auto constructor, giving it the number of linguistic variables to be used - 3, 5 or 7,or supplying a custom list of linguistic variables), the .view() method can be called, which returns a plot of the membership function. That plot uses whatever is the default cmap.

This is usually fine, but in my case I'm building an example with temperature control, and the it would have been really nice, if for the membership functions' plot I could use a colour gradient from blue (cold) to red (warm) to represent the various temperature (qualitative) variables. Thus, I want to change the cmap to 'bwr'.

So I need to somehow address the figure or axes of the plot and give it a new cmap. After some digging in the skfuzzy library I found the FuzzyVariableVisualizer class which contains the .view() method and so instead of directly using the .view() on the skfuzzy.control.antecedent_consequent.Antecedent object I have created (which uses a .view().show() and thus does not give access to the underlying figure and axis), I first passed the .Antecedent to the FuzzyVariableVisualizer() and then used its .view() method, which does return the figure and the axes.

But now, I have no idea how to set new cmap for it. Unfortunately google searches yielded only one similar result (this), but it wasn't useful to me. And Matplotlib is a bit too complex for me to dig around (and it will just take too much time).

Here is some code to reproduce the state I'm at. Does anyone have an elegant way to address this?

import numpy as np
import matplotlib.pyplot as plt
import skfuzzy as fuzz
from skfuzzy import control as ctrl
from skfuzzy.control import visualization

room_temp = ctrl.Antecedent(np.arange(0, 11, 1), 'room temperature')
clothing = ctrl.Antecedent(np.arange(0, 11, 1), 'amount of clothing')
temp_control = ctrl.Consequent(np.arange(0, 11, 1), 'temperature control')

temp_qualitative_values = ['absolute zero',
                           'freezing',
                           'extremely cold',
                           'cold',
                           'chilly',
                           'OK',
                           'warm',
                           'hot',
                           'very hot',
                           'hot as hell',
                           'melting']

clothing.automf(3)
clothing.view()

temp_control.automf(5)
temp_control.view()

# I want to chage the cmap of the following figure
room_temp.automf(10, names=temp_qualitative_values)
room_temp_viz = visualization.FuzzyVariableVisualizer(room_temp)
fig, ax = room_temp_viz.view()

plt.show()

I've tried (and did not work):

plt.set_cmap('bwr')
plt.show()

Solution

  • Here is how I finally approached a solution (Thanks JohanC for pointing me in the right direction):

    As JohanC noted in his comment, the visualization module of skfuzzy doesn't use a colour map when calling the plot function for the individual membership functions. Rather, it uses matplotlib's colour cycler. So, by calling the cycler and giving it a custom list to use turns out a simple enough solution.

    First I had to import a cycler: from cycler import cycler. Then using JohanC's suggestion, one can use linspace() to sample the desired colormap (in this case 'RdYlBu') for as many colours as necessary (in this case 11), and pass them to the cycler: plt.rc('axes', prop_cycle=cycler(color=plt.get_cmap('RdYlBu')(np.linspace(0, 1, 11))))

    Edit: In previous iterations I tried using the .color method on the colormap (as JohanC suggested in his first comment), but it returned an error, since 'RdYlBu' is a LinerSegmentedColormap (and so are 'bwr' and 'coolwarm'), and in matplotlib those don't have the .color method (as they dynamically generate samples). But of course, linspace solves that. One might be able to use ListedColormap (which do have the .color method), but I couldn't find anything in the matplotlib colormap reference documentation, that tells which colormap is of which class.

    Or, instead of sampling colours from a colormap, you can also define your own list of colours, as I did in the final version. Just supply the necessary amount of colours as a list (with the RGBA notation matplotlib uses). For my example, I wrote a function to generate a gradient (as many samples from it as needed) given a starting and an ending colour.

    Here is the final code:

    import numpy as np
    import matplotlib.pyplot as plt
    import skfuzzy as fuzz
    from cycler import cycler
    from skfuzzy import control as ctrl
    from skfuzzy.control import visualization
    
    def gradient_colour_list(start, end, steps):
        '''
        The function generates 'steps' amount of solid colours,
        sampled from a gradient between the 'start' and 'end' colours.
        'start' and 'end' are defined in RGBA tuples.
        '''
        r_step = (end[0] - start[0]) / (steps - 1)
        g_step = (end[1] - start[1]) / (steps - 1)
        b_step = (end[2] - start[2]) / (steps - 1)
        a_step = (end[3] - start[3]) / (steps - 1)
    
        gradient = []
        for i in range(steps):
            r = start[0] + (r_step * i)
            g = start[1] + (g_step * i)
            b = start[2] + (b_step * i)
            a = start[3] + (a_step * i)
            gradient.append((r, g, b, a))
    
        return gradient
    
    
    room_temp = ctrl.Antecedent(np.arange(0, 11, 1), 'room temperature')
    clothing = ctrl.Antecedent(np.arange(0, 11, 1), 'amount of clothing')
    temp_control = ctrl.Consequent(np.arange(0, 11, 1), 'temperature control')
    
    temp_qualitative_values = ['absolute zero',
                               'freezing',
                               'extremely cold',
                               'cold',
                               'chilly',
                               'OK',
                               'warm',
                               'hot',
                               'very hot',
                               'hot as hell',
                               'melting']
    
    # Create a list of colours, for as many steps
    # as the amount of membership functions, sampled
    # from a gradient between blue and red, at 100% opacity.
    colours = gradient_colour_list((0.0, 0.0, 1.0, 1.0),
                                   (1.0, 0.4, 0.0, 1.0),
                                   len(temp_qualitative_values))
    
    clothing.automf(3)
    clothing.view()
    
    temp_control.automf(5)
    temp_control.view()
    
    # Tell the cycler to use the custom list of colours
    plt.rc('axes', prop_cycle=cycler(color=colours))
    
    # Or use a predefined matplotlib cmap (in which case
    # the list of custom colours and the function are redundant):
    # plt.rc('axes', prop_cycle=cycler(color=plt.get_cmap('RdYlBu')(np.linspace(0, 1, 11))))
    
    # No more need to pass through the FuzzyVariableVisualizer separately 
    room_temp.view()
    
    plt.show()