Outils pour utilisateurs

Outils du site


issue96:python

Ceci est une ancienne révision du document !


Last time, we discussed reading and using data from an Excel file directly. If you remember, my boss (from my “day” job) had a massive spreadsheet that if one calculation failed, it caused the entire process to abort. Well, I created a database from that spreadsheet that was easy to get a report from. However, the original spreadsheet created pretty charts and graphs that his bosses liked to see. So I undertook the task to create charts so everyone would be happy. After spending about two days digging into the existing charting/graphing packages already available for Python, most free, most of them output directly to a file, like a pdf file or some sort of a graphics (jpg, png, svg) file. What I was looking for was one that would go directly to a wxPython frame or panel so it can be displayed inside a GUI program. I found one solution, but it required so many interdependent libraries that the possibility of just giving the application on a flash drive quickly became nil. So, being the pig-headed, tenacious, never-say-die kinda guy that I am, I decided to write one on my own. The original goal was that it was to do (at least) bar-charts and maybe in the future line charts and/or other types. It also should eventually be able to do colours, but just plain black bars would suffice for the time being. It should be standalone in general so that it could be called as a library. It wasn’t supposed to be so generic that it gets complicated, just dates along the horizontal (bottom) axis, values along the vertical axis and bars that represent the daily sales for that period. In order to keep the chart somewhat neat, the dates should be angled so that they don’t overwrite each other. So, what I came up with will be presented here. Left, is a sample output of the code. Again, not fancy, not terribly pretty, but it does the job. If it needs to be prettier later on, then I can work on it down the road.

La dernière fois, nous avons présenté la lecture et l'utilisation de données directement depuis un fichier Excel. Si vous vous souvenez, mon patron (celui de mon travail « de jour »)avait une monstrueuse feuille de calcul dans laquelle un calcul plantait, ce qui mettait fin à tout le processus. Bien, j'ai créé une base de données à partir de cette feuille de calcul à partir duquel l'extraction du rapport était facile. Cependant, la feuille de calcul originale créait des jolis tableaux et graphiques que son patron aimait voir. Aussi, j'ai complété le travail pour créer des tableaux afin que chacun soit content.

Après deux jours à fouiller dans les paquets de tableaux et graphiques déjà existants pour Python, la plupart gratuits, la plupart directement avec sortie en fichier, comme en pdf ou en format graphique (jpg, png, svg). Ce que je cherchais, c'en était un qui sortirait en panneau ou en frame wxPython, de façon à pouvoir l'afficher dans un programme d'interface graphique utilisateur (GUI). J'ai trouvé une solution mais elle demandait tellement de bibliothèques interdépendantes que la possibilité de la mettre simplement et rapidement sur une clé USB devenait nulle.

Comme je suis le genre de type à tête de cochon, tenace, qui ne s'avoue jamais vaincu, j'ai décidé d'écrire mon propre programme. Le but original était de faire (au moins) des graphiques à barres et peut-être dans le futur des graphiques linéaires et/ou d'autres types.Ça devrait aussi être possible de mettre de la couleur,mais de simples barres noires devrait suffire pour le moment. Il devrait être utilisé sur les postes individuels donc il pourrait être appelé comme une bibliothèque. Il n'est pas prévu d'être si générique qu'il deviendrait très compliqué, simplement des dates sur l'axe horizontal (en bas), des valeurs sur l'axe vertical et des barres qui représentent les ventes journalières de la période. De façon à garder le tableau impeccable, les dates seront penchées pour éviter d'être écrites les unes sur les autres. Ainsi, ce à quoi je suis arrivé est présenté ici. A gauche, c'est l'impression d'un échantillon du code.

Je me répète, rien d'affriolant, pas vraiment sexy, mais il remplit son office. S'il doit devenir plus joli par la suite, dans ce cas je je le ferai au fur et à mesure.

The first thing I had to do was pull out my wxPython documentation to remember the graphic commands. In able to draw graphics, we use the a “dc” or Device Context. It’s sort of like a blank canvas that we can draw points, lines, and text to. wxPython offers 9 different types of dc objects and I chose the wx.PaintDC which works from the OnPaintEvent. We will use some very basic commands to do our drawing and painting. These are: dc.DrawLine dc.SetPen dc.SetFont dc.DrawText dc.DrawRectangle dc.DrawRotatedText dc.GetFullTextExtent Those are the only wxPython routines we will use, though there are many others that would make our program much prettier. We will combine these commands into our own “logical” routines like, DrawBars, DrawAxis, DrawValues, and so on. While I could have done it in one or two large routines, I wanted to break them out into routines that make sense for the teaching moment. So let’s get started looking into the code. Create a file called mygraph.py. I couldn’t come up with anything pithy, since PyChart, PyGraph and the like are all taken. Maybe if I had a bit more time, I’d come up with something, but that’s not important. Let’s get started. First, we’ll do the imports as we always do. #!/usr/bin/python # mygraph.py import wx from datetime import date, datetime, time import time import math

La première chose que j'ai eu à faire, c'est de récupérer ma documentation sur wxPython pour me rappeler des commandes des graphes. Pour être en mesure de dessiner des graphes, nous utilisons le « dc » ou Device Content (contenu de l'élément). C'est une espèce de canevas vierge dans lequel nous pouvons dessiner des lignes, des points et du texte. wxPython offre 9 types différents d'objets dc et j'ai choisi wx.PaintDC qui fonctionne à partir de OnPaintEvent. Nous utiliserons quelques commandes très simples pour dessiner et peindre. Ce sont :

dc.DrawLine dc.SetPen dc.SetFont dc.DrawText dc.DrawRectangle dc.DrawRotatedText dc.GetFullTextExtent

Ce sont les seules routines wxPython que nous utiliserons, bien qu'il y en ait plein d'autres qui rendraient notre programme beaucoup plus joli. Nous combinerons ces commandes dans nos propres routines « logiques » comme Drawbars, DrawAxis, DrawValues et ainsi de suite. Bien que j'aurai pu faire une ou deux grosses routines, je voulais les les découper en routines qui aient un sens pour la formation. Ainsi, commençons à regarder dans le code. Créeez un fichier nommé mygraph.py. Je ne pouvais pas arriver avec quelque chose de concis, car PyChart, PyGraph et équivalents sont déjà tous pris. Peut-être que si j'avais eu un peu plus de temps, j'aurai trouvé quelque chose, mais ce n'est pas important. Démarrons. D'abord faisons les imports comme nous le faisons toujours.

#!/usr/bin/python

# mygraph.py

import wx

from datetime import date, datetime, time

import time

import math

Obviously, we need to import he wxPython library and the math library will help us with some of the calculations. The datetime and time libraries are used to do the date calculations for the horizontal axis labels. Something to keep in mind as we go from here…When you think about drawing on a context, the upper left corner of the container window (our dc) is X=0, Y=0. X is the horizontal axis and Y is the vertical axis. The closer we get the lower right corner, both numbers go higher. In our program, we will actually start by drawing a box that defines our charting area which starts at upper left X=10, Y=10 and end with lower right at X=800, Y=700. However, before we get to that part, we have to define a class to handle the routines and the init routine. Hopefully you remember these from earlier sessions. Top right is the class definition and the init routine. Our class is called Line and we will be creating a wxFrame to do our drawing. This could also be a panel within a frame or any number of other options. My choice was to have a Frame pop up with our chart data on it. When the class is first instantiated, the init routine is called with the name of the parent object, the id of that object, the title of the frame (in the title bar), the data that we want to chart and finally the title of the chart itself. Next we create the wx.Frame object that is 1024×768 pixels in size. Next we bind the paint event (which is called everything the frame is created, moved, covered, uncovered, etc.) to our event routine OnPaint. Remember, since this is inside of a class we use the “self.” to say the routine belongs to the class not somewhere else. We set some variables (BoxWidth, BoxHeight, ChartTitle, data) for use later. After we set self.data to an empty list, we call a routine called SetData to find our data scale, which we will discuss further down. Finally, we set the frame to be centered in the screen and call the Show routine. This will automatically call the OnPaint routine since we are creating the Frame.

Évidemment, nous avons besoin d'importer la bibliothèque wxPython et celle des maths nous aidera pour certains calculs. Les bibliothèques de date et de temps sont utilisées pour les étiquettes de l'axe horizontal.

Quelque chose à garder en mémoire à partir de maintenant … Quand vous pensez à un dessin dans un contexte, le coin en haut à gauche de la fenêtre conteneur (notre dc) est x=0, y=0. X est l'axe horizontal et Y l'axe vertical. Plus nous sommes près de l'angle en bas à droite, plus les nombres deviennent grands. Dans notre programme, nous commencerons par dessiner une boîte qui définit la zone de notre graphe, qui commence en haut à gauche à x=10, Y=10 et finit à X=800, y=700. Cependant, avant de passer à cela, nous devons définir une classe pour manipuler les routines la routine init. Heureusement, nous nous souvenons des sessions précédentes.

En haut à droite, vous trouvez la définition de classe et la routine init.

Notre classe s'appelle Line et nous créerons une frame wxFrame pour faire notre dessin. Ce pourrait être un panneau dans une frame ou toute autre option. Mon choix a été d'avoir un pop-up Frame dans le graphe avec les données dedans. Si la classe est instanciée en premier, la routine init est appelée par le nom de l'objet parent, l'identifiant de cet objet, le titre de la frame (dans la barre de titre) les données que l'on veut mettre en graphe et finalement le titre du graphe lui-même. Ensuite, nous créons l'objet wx.frame qui a une taille de 1024*768 pixels. Puis nous relions l'événement paint (qui est appelé quand la frame est créée, déplacée, couverte, découverte, …) à notre routine d'événement OnPaint. Souvenez-vous, comme c'est à l'intérieur d'une classe, nous utilisons « self. » pour dire que la routine appartient à la classe et pas ailleurs. Nous déclarons les variables (BoxWidth, BoxHeight, ChartTitle, data) pour les utiliser plus tard. Ensuite, nous définissons self.data comme une liste vide, nous appelons une routine appelée SetData pour trouver notre échelle de données, dont nous parlerons plus tard. Enfin, nous déclarons que la frame est centrée sur l'écran et nous appelons la routine Show. La routine OnPaint est appelée automatiquement parce que nous créons Frame.

Next (above) we will write a routine that will create a box that shows the area that we want to constrict our graph to. This is not a clipping or constraining box, it is simply to draw the eye to what we want the user to look at. Not really difficult. We will be using the DrawLine function several times throughout the program. Next we will create a routine that will draw the X (horizontal) and Y (vertical) axis lines on the screen. We again pass the dc of the frame into the routine. Since we just discussed the DrawLine method, there’s nothing very out of the ordinary here. We are drawing a line 580 pixels down the Frame that starts at X=60 and ends at X=700. Then we draw a line that starts at X=60 Y=580 and goes up to X=60 Y=80. This one is drawn from the bottom up, but we could have drawn it from the top down. Next we will deal with the DrawTitle routine. Once again, we pass the dc of the frame as well as the text we want to draw. During this process, think of drawing text rather than printing text. It’s a very minor thing, but it will help. This routine is longer than most of the others, but part of that is the comments I put in. The first two lines set the font and the pen style that we will be using. In the first line (SetFont), we define the font to be the “default” font, 20 points, not italic and bold. Next we set the colour of the pen to black and the width to be 20. Now we need to figure out the width of the text that we will be drawing so we know how to center it in the box. We get this information by calling the GetFullTextExtent with the text that we will be drawing using the font, font size, pen width and so on that we just defined. The tuple that is returned contains Width, Height, Decent (how far down letters like “g” and “y” will go below the base line) and any leading space. For our purposes, all we are concerned with the width. If you remember, we defined the width of the box back in the init function as 790. To find the center of our text within our box we take the box width minus the width of the text and then divide it by 2. That will be the X value we use to draw our text. Finally, we reset the pen size and colour. Rather than use some default values we pick out of nowhere, we could have called the dc.GetPen function before we started, but when I started the project, I didn’t think about it.

Ensuite (ici au-dessus), nous écrirons une routine qui créera une boîte qui montre la zone dans laquelle le graphe sera confiné. Ce n'est pas une boîte découpée ou contraignante, c'est simplement c'est pour attirer l’œil de l'utilisateur où nous voulons qu'il regarde.

Pas vraiment difficile. Nous utiliserons la fonction Drawline plusieurs fois au long de ce programme. Ensuite, nous créerons une routine qui tracera les lignes d'axes X (horizontal) et Y (vertical) à l'écran. Nous passons à nouveau le dc de la frame dans la routine.

Pour ce qui est de la méthode DrawLine dont nous venons de parler, il n' y a rien d'extraordinaire. Nous dessinons une ligne de 580 pixels qui descend le long du Frame, commençant à x=60 et terminant à X=700. Ensuite nous traçons une ligne qui par à X=60, Y=580 et va jusqu'à X=60, Y=80. Cette ligne est tirée de bas en haut , mais vous pourriez la tracer de haut en bas.

Ensuite, nous nous occuperons de la routine DrawTitle. Une fois encore, nous passons le dc de la frame ainsi que le texte que nous voulons dessiner. Durant le processus, pensez à du texte dessiné plutôt qu'à du texte imprimé. Ce n'est pas grand chose mais ça aide.

Cette routine est plus longue que la plupart des autres, mais c'est du pour partie aux commentaires que j'ai mis. Les deux premières lignes initialisent la police et le style d'écriture que nous utiliserons. Dans la première ligne (SetFont), nous définissons la police qui sera celle par défaut, 20 points, pas italique et grasse. Ensuite, nous déclarons noire la couleur du crayon et la largeur à 20. Maintenant nous devons estimer la largeur du texte pour en positionner le centre dans la boîte. Nous obtenons cette information en appelant GetFullTextExtent avec le texte que nous voulons dessiner, en donnant la police et sa taille, la largeur du trait and tout ce que nous venons de définir. Le tuple qui est retourné contient Width, Height, Decent (largeur, hauteur, décalage - les lettres comme « g » ou « y » passeront sous la ligne de base) et toute espace initiale. Pour nos besoins, seule la largeur nous importe. Si vous vous souvenez, nous avons défini une largeur de boîte de 790 dans la fonction init. Pour trouver le centre de notre texte dans la boîte, nous prenons la largeur de la boîte moins la largeur du texte et nous divisons par 2. Ce sera la valeur X à utiliser pour tracer le texte. Enfin, nous remettons à zéro la taille du crayon et la couleur. Plutôt que d'utiliser des valeurs par défaut prise son ne sait où, nous aurions pu appeler la fonction de.GetPen avant de commencer, mais quand j'ai commencé le projet, je n'y ai pas pensé.

Our next routine will draw the tic lines along the horizontal axis at the bottom of the chart. We want them to be equidistant along the line. We pass (as usual) the dc and a value I called dcount which is the number of dates we want to show. Since the number of days in any given month can range from 28 to 31, I wanted to be a bit dynamic. We simply use a for loop to count the number of lines to draw, which one to draw and where. If you have been carefully paying attention, we will start the lines at position 85 and it will be 20 pixels high and they will be 20 pixels apart. When we get around to drawing the dates into the chart, we want to draw the text on an angle. That way, the text doesn’t draw over itself and, well let’s admit it, looks cool. For this we will use the DrawRotatedText function. The function takes the text that we want to have drawn, the X and Y location as a starting point and the angle we want the text to be drawn. In this case, we want the text to be rotated anti-clockwise by 45 degrees which we enter as “-45”. We will set the font and pen parameters each time the text is drawn. We’ll deal with the actual draw date function in a little bit. We will also want to draw the values along the vertical axis showing tics along the way. If we had the same range of data each time, it would be very easy to do. However, reality shows that the data range of our chart could vary from run to run. One time, the highest value could be 300. The next time it could be 3000. How could we create a generic routine that would account for this? I will try to explain my mindset here. You might have wondered why I chose the value of 500 for the vertical axis when we drew the line from 80 to 580 (or actually 580 to 80). I chose to use a 500 pixel “view port” to contain our values. That way, we can create a scaling value based on an offset of 500.

Notre prochaine routine dessinera les traits d'échelle le long de l'axe horizontal en bas du graphe. Nous les voulons équidistants tout le long de la ligne. nous passons (comme d'habitude) dc et une valeur que j'ai appelé dcount qui est le nombre de dates que nous voulons afficher. Comme le nombre de jours d'un mois varie entre 28 et 31, j'ai voulu que ce soit un dynamique. Nous utilisons simplement une boucle for pour compter le nombre de lignes à tracer, lesquelles tracer et où. Si nous avons été très attentifs, nous démarrerons les lignes à la position 85, elles auront 20 pixels de haut et seront espacées de 20 pixels.

Quand nous passons au tracé des dates sur le graphe, nous voulons les dessiner en biais. De cette manière, les textes ne se chevaucheront pas et,admettons-le, ça sera plus chouette. Pour cela, nous utiliserons la fonction DrawRotateText. La fonction prend le texte que nous voulons voir dessiné, la position en X et Y du point de départ et l'angle que nous choisissons pour le tracé. Dans le cas présent, nous vouons un texte tourné de 45 degrés en rotation anti-horaire, ce qui s'écrit « -45 ». Nous réglerons les paramètres de la police et du crayon à chaque tracé du texte. Nous nous arrangerons une petit peu avec la véritable fonction de date de fin.

Nous voudrons aussi tracer les valeurs le long de l'axe vertical, avec des traits d'échelle tout le long. Si nous avions chaque fois la même étendue des données, ce serait facile à faire. Cependant, la rélaité montre que la plage des données de notre graphe peut varier d'un mois sur l'autre. Une fois, la valeur la plus haute peut être 300. La fois suivante, ce pourrait être 3000. Comment créer une routine générique qui en tient compte ? Je vais essayer ici de vous expliquer mon raisonnement.

Vous pourriez vous demander pourquoi j'ai choisi une valeur de 500 pour l'axe vertical si je trace une ligne de 80 à 580 (en réalité de 580 à 80). J'ai choisi d'utiliser une « profondeur visuelle » de 500 pixel pour contenir les valeurs. De cette façon vous pouvez créer un facteur d'échelle basé sur un module de 500.

Let’s say that for a given run that our highest value is 395. We can simply draw a bar that is 395 pixels high to represent that value. The next run, our highest value is 2,345. If we try to draw the bar to its full height, it would disappear off the top of the chart. In order to show the value, we can round the value to the nearest 500, which would be 2500 and then set that as the top value of our axis. We then can scale the value by dividing 2500 by 500 which gives us a “scaling factor” of 4. Now if we take our data values and divide each one by our scaling factor, we can then plot the values that they will fit within our graph. So (shown top right) we need to find the highest value within our data and round that up to the nearest 500. So 375 would be 500, 3750 would be 5000 and so on. Next, we need to decide what kind of data we are going to use. You will see further down the program that I provide two different types of data in lists. One assumes that the date range we will use, along the X axis, is data for October, but you can easily follow that code (shown in a few moments) and change it to whatever month you wish. The second data list, is more generic and provides both a date and a value as a list of tuples. This allows for data to be passed for any time period. The date is a string and the value is either an integer or a float. The SetData function will look at the first value within the data list and to determine if it is a tuple. If it is, we assume that the data structure of the list is the second option, if not, it is the first. If it is a tuple, we create two lists, one for the dates and one for the values. We then walk the list splitting the data between the two lists. Once we have that done, we then find the highest value (max(self.ValList)) and send it the roundup function (shown above) so we can get our scaling value. If the data isn’t in tuples, then we clear BOTH lists and do the same steps as above.

Disons que pour un calcul donné notre valeur maximum sera 395. Nous pourrions simplement tracé une barre de 395 pixels de haut pour représenter la valeur. le calcul suivant, ce maximum est de 2345. Si nous essayons de tracer la barre à sa pleine hauteur, ça dépassera le haut du graphe. De façon à montrer cette valeur, je dois l'arrondir au 500 le plus près au-dessus, c'est-à-dire 2500, à prendre comme valeur la plus haute du graphe. Nous pouvons alors mettre à l'échelle en divisant 2500 par 500 soit un facteur d'échelle de 4. Maintenant, si nous prenons nos données que nous divisons chacune par le facteur d'échelle, nous pouvons tracer les valeurs, qui tiendront dans le graphe.

Aussi (montré en haut à droite), nous avons besoin de trouver la valeur la plus haute dans nos données et de l'arrondir au multiple de 500 supérieur le plus proche. Ainsi, pour 375, ce sera 500 ; pour 3750, ce sera 4000 et ainsi de suite.

Ensuite, nous devons décider quel type de données nous allons utiliser. Nous verrons plus loin dans le programme que je fournis deux différents types de données dans les listes. L'un assure que les plages de dates que nous utiliserons, le long de l'axe des X, sont les données pour Octobre, mais vous pouvez facilement suivre le code (montré dans un petit moment) et changer pour le mois que vous voulez. La seconde liste de données est plus générique et fournit à la fois une date et une valeur comme une liste de tuples.

Now that we have our scale value we can draw our tics and the values that will represent our vertical axis. We again use a for loop, this time from 580 to 30 with a step of -50 to work our way up the line and draw a 10 pixel line. Next we set the font (just in case it gets changed somehow) and draw the value of each of our values. Now we get into the routines that will create the date tics along the X axis if we choose to have a simple list of data without including the dates. We have two support routines, one called DateToStamp and the other Timestamp2Date (Yes, I got lazy when I wrote this one). Rather than going through a bunch of complicated DateTime routines to determine the number of days in any given month, I’m going to use a start date and an end date, convert both of those to Unix timestamps to get the proper day of month within the sequence. I’ve shown you the DateToStamp routine before and the Timestamp2Date simply reverses the process. The next routine takes the start date and end date, as we discussed a moment ago, converts them to Unix timestamps, then adds 86400 (the number of ticks in a 24 hour period) to make sure we get the last date in the sequence, then uses another for loop to draw the rotated text where we want it. We are now at the OnPaint event handler that calls all the helper routines we dealt with so far. Remember, by using the PaintDC, every time the frame is moved, re-sized, covered or uncovered, the OnPaint event handler is called, thereby assuring our graph will be persistent.

First (shown on the next page, top left) we get an instance of our dc, and then we call the DrawBox, DrawAxis, DrawTitle, and the DrawDateTicks routines. We then determine if the DateList list (created in the SetData routine called from init routine) is empty or if it has dates for us to draw. If so, we call the DrawDates routine with the proper values. We then call the DrawValues routine and finally the DrawBars routine. Now you should understand why I broke everything down into little bitty chunks. The last thing we have to look at is the runtime routine. You probably remember that the ’if name == “main”’ runs if we are calling the program as a standalone rather than as a library. The next two lines are the dummy data that I used to test the program. You could comment out the first one and run it with the second data line which is the one that uses the tuple. The last three lines will instantiate the wxPython routines, then the Line class and finally call the app.MainLoop wxPython routine to get the frame to run. So there it is. Our own graphing/charting program and library. I’ve put the full code up on pastebin at http://pastebin.com/m2feeh5P. Until next time, have fun coding.

issue96/python.1432297244.txt.gz · Dernière modification : 2015/05/22 14:20 de d52fr