View  Edit  Attributes  History  Attach  Print  Search

PYQTLAB

PyQt Lab' : Graphiques Math : Pyqtgraph : Créer un graphique Pyqtgraph avec une "timeline" interactive.

Par X. HINAULT - Juin 2013

Ce que l'on va faire ici

  • Je montre dans ce code comment créer un graphique pyqtgraph avec une "timeline" interactive permettant de zoomer, parcourir, etc... une série de valeurs.

Pré-requis

  • python 2.7
  • pyqt4.x
  • pyqtgraph

Le fichier d'interface *.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>495</width>
    <height>472</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>PyQt + pyqtgraph : Affichage courbe, point courant et timeline</string>
  </property>
  <widget class="PlotWidget" name="graph1">
   <property name="geometry">
    <rect>
     <x>5</x>
     <y>10</y>
     <width>480</width>
     <height>240</height>
    </rect>
   </property>
  </widget>
  <widget class="QLabel" name="labelX1">
   <property name="geometry">
    <rect>
     <x>5</x>
     <y>255</y>
     <width>71</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>X=</string>
   </property>
  </widget>
  <widget class="QLabel" name="labelY1">
   <property name="geometry">
    <rect>
     <x>80</x>
     <y>255</y>
     <width>231</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Y=</string>
   </property>
  </widget>
  <widget class="PlotWidget" name="graph2">
   <property name="geometry">
    <rect>
     <x>5</x>
     <y>275</y>
     <width>480</width>
     <height>160</height>
    </rect>
   </property>
  </widget>
  <widget class="QLabel" name="labelY2">
   <property name="geometry">
    <rect>
     <x>80</x>
     <y>440</y>
     <width>231</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>Y=</string>
   </property>
  </widget>
  <widget class="QLabel" name="labelX2">
   <property name="geometry">
    <rect>
     <x>5</x>
     <y>440</y>
     <width>71</width>
     <height>16</height>
    </rect>
   </property>
   <property name="text">
    <string>X=</string>
   </property>
  </widget>
 </widget>
 <customwidgets>
  <customwidget>
   <class>PlotWidget</class>
   <extends>QGraphicsView</extends>
   <header>pyqtgraph</header>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

 

Le fichier d'interface *.py

  • Fichier obtenu automatiquement avec l'utilitaire pyuic4 à partir du fichier *.ui créé avec QtDesigner :

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file
#
# Created: Mon May 27 15:38:31 2013
#      by: PyQt4 UI code generator 4.9.1
#
# WARNING! All changes made in this file will be lost!

from PyQt4 import QtCore, QtGui

try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    _fromUtf8 = lambda s: s

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName(_fromUtf8("Form"))
        Form.resize(495, 472)
        self.graph1 = PlotWidget(Form)
        self.graph1.setGeometry(QtCore.QRect(5, 10, 480, 240))
        self.graph1.setObjectName(_fromUtf8("graph1"))
        self.labelX1 = QtGui.QLabel(Form)
        self.labelX1.setGeometry(QtCore.QRect(5, 255, 71, 16))
        self.labelX1.setObjectName(_fromUtf8("labelX1"))
        self.labelY1 = QtGui.QLabel(Form)
        self.labelY1.setGeometry(QtCore.QRect(80, 255, 231, 16))
        self.labelY1.setObjectName(_fromUtf8("labelY1"))
        self.graph2 = PlotWidget(Form)
        self.graph2.setGeometry(QtCore.QRect(5, 275, 480, 160))
        self.graph2.setObjectName(_fromUtf8("graph2"))
        self.labelY2 = QtGui.QLabel(Form)
        self.labelY2.setGeometry(QtCore.QRect(80, 440, 231, 16))
        self.labelY2.setObjectName(_fromUtf8("labelY2"))
        self.labelX2 = QtGui.QLabel(Form)
        self.labelX2.setGeometry(QtCore.QRect(5, 440, 71, 16))
        self.labelX2.setObjectName(_fromUtf8("labelX2"))

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        Form.setWindowTitle(QtGui.QApplication.translate("Form", "PyQt + pyqtgraph : Affichage courbe, point courant et timeline", None, QtGui.QApplication.UnicodeUTF8))
        self.labelX1.setText(QtGui.QApplication.translate("Form", "X=", None, QtGui.QApplication.UnicodeUTF8))
        self.labelY1.setText(QtGui.QApplication.translate("Form", "Y=", None, QtGui.QApplication.UnicodeUTF8))
        self.labelY2.setText(QtGui.QApplication.translate("Form", "Y=", None, QtGui.QApplication.UnicodeUTF8))
        self.labelX2.setText(QtGui.QApplication.translate("Form", "X=", None, QtGui.QApplication.UnicodeUTF8))

from pyqtgraph import PlotWidget

if __name__ == "__main__":
    import sys
    app = QtGui.QApplication(sys.argv)
    Form = QtGui.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())



 

Le fichier d'application *Main.py


#!/usr/bin/python
# -*- coding: utf-8 -*-

# Par X. HINAULT - Tous droits réservés - GPLv3
# Mai 2013 - www.mon-club-elec.fr

# --- importation des modules utiles ---
from PyQt4.QtGui import *
from PyQt4.QtCore import * # inclut Qtimer..

import os,sys

import pyqtgraph as pg # pour accès à certaines constantes pyqtgraph, widget, etc...
# pas indispensable sinon car pyqtgraph est inclut par la déclaration dans QtDesigner de PlotWidget

import numpy as np # math et tableaux
import scipy.ndimage as ndi # fonctions utiles

# --- importation du fichier de description GUI ---
from tuto_pyqt_pyqtgraph_courbe_timeline import *

#-- variables globales
resolution=1.0 # intervalle entre 2 valeur x
nombreValeurs=5000 # nombre de valeurs à utiliser

# classe principale contenant le code actif
class myApp(QWidget, Ui_Form): # la classe reçoit le Qwidget principal ET la classe définie dans test.py obtenu avec pyuic4

        # Note : ici self représente la classe

        def __init__(self, parent=None):
                QWidget.__init__(self) # initialise le Qwidget principal
                self.setupUi(parent) # Obligatoire

                #-- variables utiles --

                #-- connexion des signaux des widgets --

                # ------ code actif initial ------             

                #-- initialise le graphique 1 = la section zoomée --
                # l'objet self.graph1 correspond au plotWidget créé dans QtDesigner

                # aspect fond /axes
                #self.graph.hideAxis('left') # masque axes - ‘left’, ‘bottom’, ‘right’, or ‘top’               
                self.graph1.setBackgroundBrush(QBrush(QColor(Qt.white))) # la classe PlotWidget est un GraphicsWidget qui est un QGraphics View
                self.graph1.showGrid(x=True, y=True)  # affiche la grille

                # adaptation échelle axes
                # autoscale par défaut
                #self.graph1.enableAutoRange(axis=pg.ViewBox.YAxis, enable=False) # fonction plotItem : désactive autoscale Y
                #self.graph1.setYRange(-9,9) # fonction plotItem : fixe échelle des Y

                # interactivité
                #self.graph.setInteractive(False) # fonction QGraphics View : pour inactiver interaction souris
                self.graph1.getViewBox().setMouseMode(pg.ViewBox.RectMode)  # fonction ViewBox pas accessible depuis PlotWidget : fixe selection par zone
                self.graph1.setMouseEnabled(x=False, y=True) # désactive interactivité axe X

                #-- initialise le graphique 2 = la timeline des données

                # paramétrage de la zone de sélection linéaire
                self.region = pg.LinearRegionItem() # crée un objet de sélection linéaire pour courbe
                self.region.setZValue(10) # place l'élément graphique au premier plan - fonction QGraphicsItem
                self.region.setRegion([1000, 2000]) # fixe la position actuelle de la zone de sélection
                self.region.setBounds([0,nombreValeurs]) # fixe les limites min/max utilisables pour la zone de sélection

                self.graph2.addItem(self.region) # ajoute la zone de sélection linéaire au graphique

                # interactivité du graph 2
                self.graph2.setMouseEnabled(x=False, y=True) # désactive interactivité axe X

                # interactivité région de sélection
                self.region.sigRegionChanged.connect(self.regionChanged) # Connexion signal Region Changed avec fonction voulue

                #-- initialise données --
                #self.x = np.arange(0.0, 361.0, 1.0/resolution) # crée un vecteur de n valeurs à intervalle régulier pour les x
                #print(self.x) # debug - affiche les valeurs x

                #-- calcul des y : courbe y=sin(x)
                #self.y=10*np.sin(np.radians(self.x))# crée un tableau de valeur y basé sur x
                #print(self.y) # debug - affiche les valeurs y

                # crée une série de valeurs aléatoires
                self.x = np.arange(0.0, nombreValeurs, 1) # crée un vecteur de n valeurs à intervalle régulier pour les x
                self.y=10000 + 15000 * ndi.gaussian_filter(np.random.random(size=nombreValeurs), 10) + 3000 * np.random.random(size=nombreValeurs)

                #-- affichage de la courbe --
                self.courbe=self.graph1.plot(self.x,self.y, pen=(0,0,255)) # avec couleur
                self.graph1.setXRange(1000, 2000, padding=0) # met à jour l'axe X du graphique d'affichage zone sélectionnée  - fonction ViewBox        
                # initial = idem region        

                self.courbe=self.graph2.plot(self.x,self.y, pen=(0,0,255)) # avec couleur

                # -- ajout d'une légende
                #self.label = pg.LabelItem()
                #self.graph.addItem(self.label)

                #-- point de sélection du graph 1 --
                self.pointSelect=np.array([[0,0]]) # tableau de 1 point
                self.courbe2=self.graph1.plot(self.pointSelect[:,0],self.pointSelect[:,1],pen=(0,0,255),symbolBrush=(255,0,0),symbolPen='r') # avac paramétrage symboles (parmi o, s, t, d, +) etc..
                # NB : les x : pointSelect[:,0], les y : pointSelect[:,1]

                #-- lignes sélection du graph 1 --
                self.vLine = pg.InfiniteLine(angle=90, movable=False) # crée une ligne inifinie
                self.vLine.setPen(pg.mkPen(0,0,0)) # couleur de la ligne
                self.graph1.addItem(self.vLine, ignoreBounds=True) # ajoute la ligne au graphique - fonction ViewBox accessible depuis PlotItem

                self.hLine = pg.InfiniteLine(angle=0, movable=False) # crée une ligne inifinie
                self.hLine.setPen(pg.mkPen(0,0,0)) # couleur de la ligne
                self.graph1.addItem(self.hLine, ignoreBounds=True) # ajoute la ligne au graphique - fonction ViewBox accessible depuis PlotItem

                self.vb=self.graph1.getViewBox() # récupère l'objet viewbox du graphique pour accès aux fonctions utiles
                print self.vb

                # connexion signal mouvement de la souris  - graph 1
                #proxy = pg.SignalProxy(self.graph.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved) # proxy implémente objet commun de gestion des signaux
                self.graph1.scene().sigMouseMoved.connect(self.mouseMovedGraph1) # connecte le signal souris bouge à la fonction voulue

                self.graph1.scene().sigMouseClicked.connect(self.mouseClicked) # connecte le signal souris bouge à la fonction voulue

                self.graph1.sigRangeChanged.connect(self.rangeChanged) # connexion signal RangeChanged - si on veut modif graph zoom entraîne modif graph entier

        # fonction signal région sélectionnée modifiée
        def regionChanged(self):
                self.region.setZValue(10) # replace region au premier plan
                minX, maxX = self.region.getRegion() # récupère les bornes actuelles de la sélection
                self.graph1.setXRange(minX, maxX, padding=0) # met à jour l'axe X du graphique d'affichage zone sélectionnée  - fonction ViewBox        

        #-- fonction pour mise à jour région sélection sur modif graphique zoom
        def rangeChanged(self, window, viewRange):
                rgn = viewRange[0]
                self.region.setRegion(rgn)     

        # fonction de gestion des mouvements souris - fonction appelée à partir pg.SignalProxy
        #def mouseMoved(self, evt):
        def mouseMovedGraph1(self, pos): # si connexion directe du signa "mouseMoved" : la fonction reçoit le point courant
                print ("Mouse moved")
                #pos = evt[0]  ## using signal proxy turns original arguments into a tuple
                print pos

                #pos=evt.pos()
                if self.graph1.sceneBoundingRect().contains(pos):
                        mousePoint = self.vb.mapSceneToView(pos) # récupère le point souris à partir ViewBox
                        index = int(mousePoint.x()*resolution) # en fonction de l'intervalle arange x

                        #-- mise à jour position point courant courbe
                        if index > 0 and index < len(self.x)-1: # si on est entre 0 et taille de X
                                pointSelect=np.array([[mousePoint.x(),self.y[index+1]]]) # tableau de 1 point = le point courant
                                #pointSelect=np.array([[mousePoint.x(),mousePoint.y()]]) # tableau de 1 point = le point courant
                                self.courbe2.setData(pointSelect[:,0],pointSelect[:,1]) # met à jour le point courant
                                #self.label.setText("<span style='font-size: 10pt'>x=%0.1f,   <span style='color: red'>y1=%0.1f</span>" % (mousePoint.x(), self.y[index+1]))

                                self.labelX1.setText("X = "+str(int(mousePoint.x())))
                                self.labelY1.setText("Y = "+ str(self.y[index+1]))

                                #-- met à jour position des lignes --
                                self.vLine.setPos(mousePoint.x())
                                #self.hLine.setPos(mousePoint.y())
                                self.hLine.setPos(self.y[index+1]) # point sur la courbe

        def mouseClicked(self, evt): # si connexion directe du signa "mouseMoved" : la fonction reçoit le point courant
                print ("Mouse clicked")

                # ici gestion du clic souris
                print evt

        #--- les fonctions appelées, utilisées par les signaux

        #--- autres fonctions actives
        # ...


# fonction principale exécutant l'application Qt                       
def main(args):
        a=QApplication(args) # crée l'objet application
        f=QWidget() # crée le QWidget racine
        c=myApp(f) # appelle la classe contenant le code de l'application
        f.show() # affiche la fenêtre QWidget
        r=a.exec_() # lance l'exécution de l'application
        return r

# pour rendre le fichier *.py exécutable
if __name__=="__main__": # pour rendre le code exécutable
        main(sys.argv) # appelle la fonction main

 

Utilisation

  • Les 2 fichiers suivants sont à enregistrer dans un même répertoire, l'un en nom.py et l'autre en nomMain.py.
  • Puis lancer l'application depuis Geany ou équivalent, en exécutant le fichier nomMain.py
  • Dans le graphique du haut s'affiche la zone de sélection du graphique du bas (= la "timeline")
  • Il est possible de déplacer, modifier la zone de sélection : le graphique du haut e modifie en conséquence.