Subplot

This example show how to used MatplotFigureSubplot widget when you have multiple axes or any kind of other matplotlib figure with no lines only.

MatplotFigureSubplot example

example folder: 'example_4_differents_subplot'

from kivy.utils import platform
from kivy.config import Config

#avoid conflict between mouse provider and touch (very important with touch device)
#no need for android platform
if platform != 'android':
    Config.set('input', 'mouse', 'mouse,disable_on_activity')
else:
    #for android, we remove mouse input to not get extra touch 
    Config.remove_option('input', 'mouse')

from kivy.lang import Builder
from kivy.app import App

import matplotlib.pyplot as plt

from kivy.metrics import dp
import numpy as np

from kivy_matplotlib_widget.uix.hover_widget import add_hover,BaseHoverFloatLayout
from matplotlib.ticker import FormatStrFormatter
from kivy.properties import ColorProperty,NumericProperty,StringProperty
from matplotlib import gridspec
from numpy.random import rand
from matplotlib.ticker import NullFormatter, MaxNLocator
from numpy import linspace


KV = '''
BoxLayout:

    orientation:'vertical'
    BoxLayout:
        size_hint_y:0.2
        Button:
            text:"home"
            on_release:app.home()

        ToggleButton:
            group:'touch_mode'
            state:'down'
            text:"pan" 
            on_release:
                app.set_touch_mode('pan')
                self.state='down'    
        ToggleButton:
            group:'touch_mode'
            text: 'ZoomBox'
            on_press: 
                app.set_touch_mode('zoombox')
                self.state='down'

    BoxLayout: 
        ScreenManager:
            id:sm
            Screen1:
            Screen2:
            Screen3:
            Screen4:

    BoxLayout:
        size_hint_y:0.1
        Button:
            text:"previous screen"
            on_release:app.previous_screen()
        Button:
            text:"next screen"
            on_release:app.next_screen()

<Screen1@Screen>
    name:'screen1'  
    figure_wgt:figure_wgt                  
    MatplotFigureSubplot:
        id:figure_wgt
        fast_draw:True
        interactive_axis:True

<Screen2@Screen> 
    name:'screen2'  
    figure_wgt:figure_wgt                  
    MatplotFigureSubplot:
        id:figure_wgt
        fast_draw:True
        interactive_axis:True
        draw_all_axes:True

<Screen3@Screen> 
    name:'screen3'  
    figure_wgt:figure_wgt                  
    MatplotFigureSubplot:
        id:figure_wgt
        fast_draw:True
        interactive_axis:True
        draw_all_axes:True

<Screen4@Screen> 
    name:'screen4'  
    figure_wgt:figure_wgt                  
    MatplotFigureSubplot:
        id:figure_wgt
        max_hover_rate:5/60
        fast_draw:True
        interactive_axis:True
        draw_all_axes:True

<PlotlyHover2>
    custom_color: [0,0,0,1]
    BoxLayout:
        id:main_box
        x:
            root.x_hover_pos + dp(4)
        y:
            root.y_hover_pos - root.hover_height/2
        size_hint: None, None
        height: label.texture_size[1]+ dp(4)
        width: 
            self.minimum_width + dp(12) if root.show_cursor \
            else dp(0.0001)            
        orientation:'vertical'
        padding: 0,-dp(1),0,0

        canvas:            
            Color:
                rgba: root.custom_color if root.custom_color else [0,0,0,1]
            Rectangle:
                pos: self.pos
                size: self.size
            Triangle:
                points:
                    [ \
                    root.x_hover_pos, root.y_hover_pos, \
                    main_box.x, root.y_hover_pos+ dp(4), \
                    main_box.x, root.y_hover_pos- dp(4)  \
                    ]
            SmoothLine:
                width:dp(1)
                points:
                    [ \
                    root.x_hover_pos, root.y_hover_pos, \
                    main_box.x, root.y_hover_pos \
                    ]                           

        BoxLayout:
            size_hint_x:None
            width:label.texture_size[0]
            padding: dp(12),0,0,0
            Label:
                id:label
                text: 
                    '(' + root.label_x_value  +','+ root.label_y_value +')'
                font_size:root.text_size
                color:
                    [0,0,0,1] if (root.custom_color[0]*0.299 + \
                    root.custom_color[1]*0.587 + root.custom_color[2]*0.114) > 186/255 \
                    else [1,1,1,1]
                font_name : root.text_font

                font_name : root.text_font

        FloatLayout:
            size_hint: None,None
            width: dp(0.01) 
            height: dp(0.01) 
            BoxLayout:
                size_hint:None,None
                x:main_box.x + main_box.width + dp(4)
                y:main_box.y + main_box.height/2 - label3.texture_size[1]/2
                width:label3.texture_size[0]
                height:label3.texture_size[1]
                Label:
                    id:label3
                    text: 
                        root.custom_label if root.custom_label and not '_child' in root.custom_label else ''  
                    font_size:root.text_size
                    color: root.text_color
                    font_name : root.text_font      

<InfoHover2>
    custom_color: [0,0,0,1]
    BoxLayout:
        id:main_box
        x:
            root.x_hover_pos + dp(4) if root.x_hover_pos + dp(4) < root.figwidth - label.texture_size[0] - self.padding[0] * 2 \
            else root.x_hover_pos - dp(4) - max(label.texture_size[0],label2.texture_size[0]) - self.padding[0] * 2
        y:
            root.y_hover_pos + dp(4)
        size_hint: None, None
        height: root.hover_height
        width: 
            max(label.texture_size[0],label2.texture_size[0]) + dp(12) if root.show_cursor \
            else dp(0.0001)            
        orientation:'vertical'
        padding: 0,dp(4),0,dp(4)

        canvas:            
            Color:
                rgba: root.custom_color if root.custom_color else [0,0,0,1]
            Rectangle:
                pos: self.pos
                size: self.size
            Color:
                rgba: 0,0,0,1

            Line:
                width: 1    
                rounded_rectangle:
                    (self.x, self.y, self.width, self.height,\
                    dp(4), dp(4), dp(4), dp(4),\
                    self.height)                 


        canvas.after:            
            Color:
                rgba: 0,0,0,1
            Rectangle:
                size: (dp(8),dp(8))
                pos: 
                    (root.x_hover_pos-dp(8/2), \
                     root.y_hover_pos-dp(8/2))

        BoxLayout:
            size_hint_x:None
            width:label.texture_size[0]
            padding: dp(12),0,0,0
            Label:
                id:label
                text: 
                    root.label_x + ': ' + root.label_x_value  
                font_size:root.text_size
                color:
                    [0,0,0,1] if (root.custom_color[0]*0.299 + \
                    root.custom_color[1]*0.587 + root.custom_color[2]*0.114) > 186/255 \
                    else [1,1,1,1]
                font_name : root.text_font

        BoxLayout:
            size_hint_x:None
            width:label2.texture_size[0]   
            padding: dp(12),0,0,0
            Label:
                id:label2
                text:
                    root.label_y + ': ' + root.label_y_value    
                font_size:root.text_size
                color:
                    [0,0,0,1] if (root.custom_color[0]*0.299 + \
                    root.custom_color[1]*0.587 + root.custom_color[2]*0.114) > 186/255 \
                    else [1,1,1,1]
                font_name : root.text_font
        FloatLayout:
            size_hint: None,None
            width: dp(0.01) 
            height: dp(0.01) 
            BoxLayout:
                size_hint:None,None
                x:main_box.x + main_box.width + dp(4)
                y:main_box.y + main_box.height - label3.texture_size[1]
                width:label3.texture_size[0]
                height:label3.texture_size[1]
                Label:
                    id:label3
                    text: 
                        root.custom_label if root.custom_label and not '_child' in root.custom_label else ''  
                    font_size:root.text_size
                    color: root.text_color
                    font_name : root.text_font  

'''

class PlotlyHover2(BaseHoverFloatLayout):
    """ PlotlyHover adapt the background and the font color with the line or scatter color""" 
    text_color=ColorProperty([0,0,0,1])
    text_font=StringProperty("Roboto")
    text_size = NumericProperty(dp(14))
    hover_height = NumericProperty(dp(24))


    def __init__(self, **kwargs):
        """ init class """
        super().__init__(**kwargs)  

class InfoHover2(BaseHoverFloatLayout):
    """ InfoHover adapt the background and the font color with the line or scatter color""" 
    text_color=ColorProperty([0,0,0,1])
    text_font=StringProperty("Roboto")
    text_size = NumericProperty(dp(14))
    hover_height = NumericProperty(dp(48))

    def __init__(self, **kwargs):
        """ init class """
        super().__init__(**kwargs)     

class Test(App):
    lines = []

    def build(self):
        self.graph_app = Builder.load_string(KV)
        return self.graph_app


    def on_start(self, *args):

# =============================================================================
#         figure 1 - screen1
# =============================================================================
        x = np.linspace(0, 2 * np.pi, 400)
        y = np.sin(x ** 2)

        fig, axs = plt.subplots(2, 2)
        axs[0, 0].plot(x, y)
        axs[0, 0].set_title('Axis [0, 0]')
        axs[0, 1].plot(x, y, 'tab:orange')
        axs[0, 1].set_title('Axis [0, 1]')
        axs[1, 0].plot(x, -y, 'tab:green')
        axs[1, 0].set_title('Axis [1, 0]')
        axs[1, 1].plot(x, -y, 'tab:red')
        axs[1, 1].set_title('Axis [1, 1]')

        axs[1, 1].set_xlim(2,10)

        for ax in axs.flat:
            ax.set(xlabel='x-label', ylabel='y-label')

        # Hide x labels and tick labels for top plots and y ticks for right plots.
        for ax in axs.flat:
            ax.label_outer()

        screen1=self.graph_app.ids.sm.get_screen('screen1')
        screen1.figure_wgt.figure = fig
        screen1.figure_wgt.cursor_xaxis_formatter = FormatStrFormatter('%.1f') 
        screen1.figure_wgt.cursor_yaxis_formatter = FormatStrFormatter('%.1f') 

        screen1.figure_wgt.register_cursor()

        add_hover(screen1.figure_wgt,mode='desktop',hover_widget=PlotlyHover2())

# =============================================================================
#         figure 2 - screen2
# =============================================================================
        x = np.linspace(0, 2 * np.pi, 400)
        y = np.sin(x ** 2)

        fig2 = plt.figure()
        # set height ratios for subplots
        gs = gridspec.GridSpec(2, 1, height_ratios=[2, 1]) 

        # the first subplot
        ax0 = plt.subplot(gs[0])
        # log scale for axis Y of the first subplot
        ax0.set_yscale("log")
        line0, = ax0.plot(x, y, color='r',label='red line')

        # the second subplot
        # shared axis X
        ax1 = plt.subplot(gs[1], sharex = ax0)
        line1, = ax1.plot(x, y, color='b', linestyle='--',label='red line')
        plt.setp(ax0.get_xticklabels(), visible=False)
        # remove last tick label for the second subplot
        yticks = ax1.yaxis.get_major_ticks()
        yticks[-1].label1.set_visible(False)

        # put legend on first subplot
        ax0.legend((line0, line1), ('red line', 'blue line'), loc='lower left')

        # remove vertical gap between subplots
        plt.subplots_adjust(hspace=.0)

        screen2=self.graph_app.ids.sm.get_screen('screen2')
        screen2.figure_wgt.figure = fig2
        screen2.figure_wgt.cursor_xaxis_formatter = FormatStrFormatter('%.1f') 
        screen2.figure_wgt.cursor_yaxis_formatter = FormatStrFormatter('%.1f') 

        screen2.figure_wgt.register_cursor()

        add_hover(screen2.figure_wgt,mode='desktop',hover_widget=PlotlyHover2())

# =============================================================================
#         figure 3 - screen3
# =============================================================================

        # create all axes we need
        ax0 = plt.subplot(211)
        ax1 = ax0.twinx()
        ax2 = plt.subplot(212)
        ax3 = ax2.twinx()

        # share the secondary axes
        if hasattr(ax1,'sharey'):
            ax1.sharey(ax3)
        else:
            ax1.get_shared_y_axes().join(ax1, ax3)


        ax0.plot(rand(1) * rand(10),'r')
        ax1.plot(10*rand(1) * rand(10),'b')
        ax2.plot(3*rand(1) * rand(10),'g')
        ax3.plot(10*rand(1) * rand(10),'y')
        fig3=plt.gcf()

        screen3=self.graph_app.ids.sm.get_screen('screen3')
        screen3.figure_wgt.figure = fig3
        screen3.figure_wgt.cursor_xaxis_formatter = FormatStrFormatter('%.1f') 
        screen3.figure_wgt.cursor_yaxis_formatter = FormatStrFormatter('%.1f') 

        screen3.figure_wgt.register_cursor()

        add_hover(screen3.figure_wgt,mode='desktop',hover_widget=PlotlyHover2())

# =============================================================================
#         figure 4 - screen4
# =============================================================================

        # Define a function to make the ellipses
        def ellipse(ra,rb,ang,x0,y0,Nb=100):
            xpos,ypos=x0,y0
            radm,radn=ra,rb
            an=ang
            co,si=np.cos(an),np.sin(an)
            the=linspace(0,2*np.pi,Nb)
            X=radm*np.cos(the)*co-si*radn*np.sin(the)+xpos
            Y=radm*np.cos(the)*si+co*radn*np.sin(the)+ypos
            return X,Y

        # Define the x and y data 
        # For example just using random numbers
        x = np.random.randn(10000)
        y = np.random.randn(10000)

        # Set up default x and y limits
        xlims = [min(x),max(x)]
        ylims = [min(y),max(y)]

        # Set up your x and y labels
        xlabel = '$\mathrm{Your\\ X\\ Label}$'
        ylabel = '$\mathrm{Your\\ Y\\ Label}$'

        # Define the locations for the axes
        left, width = 0.12, 0.55
        bottom, height = 0.12, 0.55
        bottom_h = left_h = left+width+0.02

        # Set up the geometry of the three plots
        rect_temperature = [left, bottom, width, height] # dimensions of temp plot
        rect_histx = [left, bottom_h, width, 0.25] # dimensions of x-histogram
        rect_histy = [left_h, bottom, 0.25, height] # dimensions of y-histogram

        # Set up the size of the figure
        fig4 = plt.figure(1, figsize=(9.5,9))

        # Make the three plots
        axTemperature = plt.axes(rect_temperature) # temperature plot
        axHistx = plt.axes(rect_histx, sharex = axTemperature) # x histogram
        axHisty = plt.axes(rect_histy, sharey = axTemperature) # y histogram


        # Remove the inner axes numbers of the histograms
        nullfmt = NullFormatter()
        axHistx.xaxis.set_major_formatter(nullfmt)
        axHisty.yaxis.set_major_formatter(nullfmt)

        # Find the min/max of the data
        xmin = min(xlims)
        xmax = max(xlims)
        ymin = min(ylims)
        ymax = max(y)

        # Make the 'main' temperature plot
        # Define the number of bins
        nxbins = 50
        nybins = 50
        nbins = 100

        xbins = linspace(start = xmin, stop = xmax, num = nxbins)
        ybins = linspace(start = ymin, stop = ymax, num = nybins)
        xcenter = (xbins[0:-1]+xbins[1:])/2.0
        ycenter = (ybins[0:-1]+ybins[1:])/2.0

        H, xedges,yedges = np.histogram2d(y,x,bins=(ybins,xbins))
        X = xcenter
        Y = ycenter
        Z = H

        # Plot the temperature data
        cax = (axTemperature.imshow(H, extent=[xmin,xmax,ymin,ymax],
               interpolation='nearest', origin='lower',aspect='auto'))

        # Plot the temperature plot contours
        contourcolor = 'w'
        xcenter = np.mean(x)
        ycenter = np.mean(y)
        ra = np.std(x)
        rb = np.std(y)
        ang = 0

        X,Y=ellipse(ra,rb,ang,xcenter,ycenter)
        axTemperature.plot(X,Y,":",color = contourcolor,ms=1,linewidth=2.0)
        axTemperature.annotate('$1\\sigma$', xy=(X[15], Y[15]), xycoords='data',xytext=(10, 10),
                               textcoords='offset points', horizontalalignment='right',
                               verticalalignment='bottom',fontsize=25)

        X,Y=ellipse(2*ra,2*rb,ang,xcenter,ycenter)
        axTemperature.plot(X,Y,":",color = contourcolor,ms=1,linewidth=2.0)
        axTemperature.annotate('$2\\sigma$', xy=(X[15], Y[15]), xycoords='data',xytext=(10, 10),
                               textcoords='offset points',horizontalalignment='right',
                               verticalalignment='bottom',fontsize=25, color = contourcolor)

        X,Y=ellipse(3*ra,3*rb,ang,xcenter,ycenter)
        axTemperature.plot(X,Y,":",color = contourcolor, ms=1,linewidth=2.0)
        axTemperature.annotate('$3\\sigma$', xy=(X[15], Y[15]), xycoords='data',xytext=(10, 10),
                               textcoords='offset points',horizontalalignment='right',
                               verticalalignment='bottom',fontsize=25, color = contourcolor)

        #Plot the axes labels
        axTemperature.set_xlabel(xlabel,fontsize=25)
        axTemperature.set_ylabel(ylabel,fontsize=25)      

        #Make the tickmarks pretty
        ticklabels = axTemperature.get_xticklabels()
        for label in ticklabels:
            label.set_fontsize(18)
            label.set_family('serif')

        ticklabels = axTemperature.get_yticklabels()
        for label in ticklabels:
            label.set_fontsize(18)
            label.set_family('serif')

        #Set up the plot limits
        axTemperature.set_xlim(xlims)
        axTemperature.set_ylim(ylims)

        #Set up the histogram bins
        xbins = np.arange(xmin, xmax, (xmax-xmin)/nbins)
        ybins = np.arange(ymin, ymax, (ymax-ymin)/nbins)

        #Plot the histograms
        axHistx.hist(x, bins=xbins, color = 'blue')
        axHisty.hist(y, bins=ybins, orientation='horizontal', color = 'red')

        #Set up the histogram limits
        axHistx.set_xlim( min(x), max(x) )
        axHisty.set_ylim( min(y), max(y) )

        #Make the tickmarks pretty
        ticklabels = axHistx.get_yticklabels()
        for label in ticklabels:
            label.set_fontsize(12)
            label.set_family('serif')

        #Make the tickmarks pretty
        ticklabels = axHisty.get_xticklabels()
        for label in ticklabels:
            label.set_fontsize(12)
            label.set_family('serif')

        #Cool trick that changes the number of tickmarks for the histogram axes
        axHisty.xaxis.set_major_locator(MaxNLocator(4))
        axHistx.yaxis.set_major_locator(MaxNLocator(4))
        fig4=plt.gcf()

        screen4=self.graph_app.ids.sm.get_screen('screen4')
        screen4.figure_wgt.figure = fig4
        screen4.figure_wgt.cursor_xaxis_formatter = FormatStrFormatter('%.1f') 
        screen4.figure_wgt.cursor_yaxis_formatter = FormatStrFormatter('%.1f') 

        screen4.figure_wgt.register_cursor()

        add_hover(screen4.figure_wgt,mode='desktop',hover_widget=InfoHover2())


    def set_touch_mode(self,mode):
        for screen in self.graph_app.ids.sm.screens:
            if hasattr(screen,'figure_wgt'):
                screen.figure_wgt.touch_mode=mode

    def home(self):
        screen=self.graph_app.ids.sm.current_screen
        screen.figure_wgt.main_home()

    def previous_screen(self):
        screen_name=self.graph_app.ids.sm.current
        screen_number = int(screen_name[-1])
        if screen_number<=1:
            screen_number=4
        else:
            screen_number-=1

        self.graph_app.ids.sm.current = 'screen' + str(screen_number)        

    def next_screen(self):
        screen_name=self.graph_app.ids.sm.current
        screen_number = int(screen_name[-1])
        if screen_number>=4:
            screen_number=1
        else:
            screen_number+=1

        self.graph_app.ids.sm.current = 'screen' + str(screen_number)


Test().run()