I. Introduction▲
Il existe de nombreuses bibliothèques graphiques 2D libres en Java basées sur AWT/Swing :
- JFreeChart ;
- OpenChart2 (JOpenChart) ;
- GRAL ;
- XChart ;
- ...
Les seules bibliothèques graphiques basées sur SWT que je connaisse sont SWTChart et swt-xy-graph (qui utilise Draw2D).
Toutes ces bibliothèques offrent de grandes fonctionnalités et de très bons rendus, mais pour avoir testé JFreeChart, une référence en matière de bibliothèque graphique 2D, et SWTChart, j'ai pu me rendre compte qu'elles ne possédaient pas de bonnes performances, lorsqu'utilisées pour un affichage dynamique de données ou de signaux.
Pour vous faire une idée sur l'affichage dynamique de signaux, vous pouvez suivre ce lien qui présente, sous la forme d'une courte vidéo, l'utilisation et le réglage d'un oscilloscope. Ces appareils sont capables d'afficher des signaux qui varient rapidement dans le temps. Ce sont des outils très utilisés pour afficher et analyser des signaux en temps réel.
Il est bien sûr difficile, voire impossible, d'obtenir ce type de performance sur un ordinateur, mais il est par contre possible d'observer le comportement de ces bibliothèques en affichage temps réel. Pour avoir fait des tests avec JFreeChart et SWTChart, force est de constater que ces bibliothèques excellent dans l'affichage d'un ensemble de valeurs préétablies ou changeant très lentement, mais qu'elles deviennent assez poussives dès que vous souhaitez ajouter de nouvelles informations et rafraichir le graphe à une cadence régulière et relativement élevée : plusieurs dizaines de points toutes les cinquante millisecondes par exemple.
Je ne suis pas le seul à faire cette constatation et une petite recherche sur Internet vous montrera que cet avis est partagé. On trouve d'ailleurs dans la FAQ de JFreeChart la réponse suivante à la question : « Does JFreeChart support real-time charting ? » En substance, cela signifie simplement que cette bibliothèque n'a pas été développée pour faire de l'affichage temps réel.
Ce premier article vous propose donc une méthode de mesure des performances de JFreeChart en affichage temps réel. Après avoir installé et configuré les outils, nous réaliserons le développement de l'interface graphique de présentation des courbes dynamiques avant de mettre au point la méthode de mesure. Nous nous servirons de ces résultats comme référence dans le prochain article.
II. Installation et configuration des outils▲
Nous allons installer et utiliser Eclipse et JFreeChart. Nous créerons un projet Java sous Eclipse et nous le configurerons afin d'utiliser les bibliothèques SWT et JFreeChart.
II-A. Eclipse▲
Si vous ne connaissez pas du tout ce logiciel, je vous invite à suivre ce tutoriel de Developpez.com qui vous guidera dans son installation et utilisation. Vous pouvez bien sûr télécharger une version d'Eclipse plus récente, les explications contenues dans l'article n'étant pas obsolètes.
Télécharger Eclipse en suivant ce lien. Vous pouvez utiliser Eclipse IDE for Java Developers par exemple. Je vous propose de lancer Eclipse dans un répertoire de travail nommé RealTimeCharting.
Maintenant qu'Eclipse est installé, lancé et que vous êtes en perspective de développement Java ; dans le menu File, cliquez sur New -> Java Project pour créer le projet nommé RealTimeCharting. Laissez les options par défaut et cliquez sur le bouton Next.
Sélectionnez alors l'onglet Libraries puis cliquez sur le bouton Add Variable...
La fenêtre ci-dessous apparait. Sélectionnez la variable ECLIPSE_HOME et cliquez sur le bouton Extend...
Dans la zone de texte de la nouvelle fenêtre, tapez *swt*, déroulez le répertoire plugins et sélectionnez la bibliothèque SWT. Dans mon cas, cette bibliothèque se nomme org.eclipse.swt.cocoa.macosx.x86_64_3.8.0.v3833.jar, mais tout dépend de votre système d'exploitation et de la version d'Eclipse que vous utilisez.
Cliquez sur les boutons OK puis Finish pour terminer la création du projet. Nous venons d'ajouter une dépendance à la bibliothèque SWT à notre projet.
II-B. JFreeChart▲
Téléchargez la version 1.0.19 de JFreeChart en suivant ce lien. Une fois le fichier décompressé, vous devriez obtenir, dans le répertoire jfreechart-1.0.19, l'arborescence présentée dans l'image ci-dessous.
Dans la suite de cet article, nous allons utiliser des bibliothèques et le code source de JFreeChart. Nous allons donc importer entièrement ce répertoire dans le projet. Pour ce faire, sélectionnez le projet RealTimeCharting dans la vue Package Explorer, puis dans le menu File cliquez sur Import.... La fenêtre ci-dessous apparait, sélectionnez l'élément File System, puis cliquez sur le bouton Next.
Cliquez sur le bouton Browse... pour sélectionner le répertoire de JFreeChart et cochez ce répertoire comme le montre la figure ci-dessous. Ajoutez JFreeChart au chemin par défaut pour tout importer dans ce répertoire et cliquez finalement sur le bouton Finish pour terminer l'importation.
Vous devriez obtenir le résultat présenté dans l'image de la vue Package Explorer ci-dessous.
III. Développement et tests qualitatifs▲
Nous allons, dans un premier temps, écrire le code de l'interface, c'est-à-dire la création du contenu de la fenêtre des graphes. Pour afficher des données en temps réel dans ces graphes, il nous faudra, dans un deuxième temps, créer une classe utilitaire chargée d'envoyer des nouvelles informations régulièrement à tous les graphes. Le but est de tester qualitativement la bibliothèque JFreeChart en affichage temps réel, nous ferons des mesures par la suite.
III-A. Création de la fenêtre des graphes▲
Cette création va se faire en deux étapes. La première consiste simplement à créer le Shell et la boucle évènementielle classique de SWT. Le code de création des graphes et des courbes de ces graphes sera traité dans la deuxième étape.
Si vous ne connaissez pas la bibliothèque SWT, je vous invite à suivre ce cours de Developpez.com.
PREMIÈRE ÉTAPE : création de la classe JFreeChartRTCTest
C'est parti pour la première étape. Sélectionnez le répertoire src du projet puis dans le menu File, cliquez sur New -> Class. Donnez com.developpez.realtimecharting.jfreechart comme nom de package et JFreeChartRTCTest comme nom de classe. N'oubliez pas de cocher la boîte pour ajouter la méthode main à cette nouvelle classe avant de cliquer sur le bouton Finish.
Récupérez le code présenté dans le listing ci-dessous.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
package
com.developpez.realtimecharting.jfreechart;
import
org.eclipse.swt.widgets.Display;
import
org.eclipse.swt.widgets.Shell;
public
class
JFreeChartRTCTest {
/**
* The application shell
*/
private
static
Shell shell;
/**
* The entry point method for the test.
*
@param
args
: not used
*/
public
static
void
main
(
String[] args) {
/*
* Create display and shell.
* Assign application name, shell title and size.
*/
Display.setAppName
(
"JFreeChart RTC SWT"
);
Display display =
new
Display
(
);
shell =
new
Shell
(
display);
shell.setText
(
"JFreeChart Real Time Charting with SWT"
);
shell.setMaximized
(
true
);
/* Open the shell maximized */
shell.open
(
);
/* SWT display loop */
while
(!
shell.isDisposed
(
)) {
if
(!
display.readAndDispatch
(
)) display.sleep
(
);
}
display.dispose
(
);
}
}
Vous pouvez lancer ce code en cliquant sur Run As -> Java Application dans le menu contextuel de l'éditeur de la classe JFreeChartRTCTest. Ce code ne crée pour l'instant qu'une simple fenêtre avec le titre JFreeChart Real Time Charting with SWT.
DEUXIÈME ÉTAPE : création des graphes et courbes
Nous allons maintenant ajouter les trois bibliothèques suivantes pour construire les graphes dans cette fenêtre :
- jfreechart-1.0.19.jar : bibliothèque de référence de JFreeChart ;
- jfreechart-1.0.19-swt.jar : bibliothèque permettant d'utiliser JFreeChart dans SWT ;
- jcommon-1.0.23.jar : bibliothèque utilitaire pour JFreeChart.
Pour ce faire, sélectionnez ces trois bibliothèques comme le montre l'image de la vue Package Explorer ci-dessous. Puis dans le menu contextuel, cliquez sur Build Path -> Add to Build Path. Ces nouvelles bibliothèques devraient maintenant être visibles dans la liste des bibliothèques référencées.
Modifiez maintenant cette classe pour ajouter la création des graphes et séries. Vous pouvez directement copier le code présenté dans le listing ci-dessous. Les deux paramètres nbCharts et nbSeriesPerChart permettent de régler le nombre de graphes et le nombre de courbes dans chaque graphe. Si par exemple vous modifiez nbCharts à 4, vous aurez le résultat présenté dans l'image d'après. La méthode createChartsArea() crée l'ensemble des graphes présentés dans une disposition de type grille. Elle appelle la méthode createSeries() pour créer le nombre de courbes spécifiées dans chaque graphe.
III-B. Création de la classe de génération de signaux▲
À ce niveau du développement, rien n'est dynamique. Il nous faut maintenant ajouter un mécanisme qui permet de générer des données automatiquement et régulièrement. Ce sera le rôle de la classe GenerateData que nous allons créer dès maintenant.
La classe GenerateData
Créez une classe privée, statique et interne à la classe JFreeChartRTCTest nommée GenerateData. Cette classe étend TimerTask. Elle sera utilisée par une instance de la classe Timer afin de fournir un ensemble de valeurs à afficher à toutes les séries de tous les graphes présents dans la fenêtre de l'application. Cette mise à jour de l'affichage sera faite à une certaine cadence, spécifiée dans la méthode schedule(TimerTask task, long delay, long period) de la classe Timer. La méthode run(), responsable de cet affichage, de la classe GenerateData sera appelée à une cadence approximative de 10 Hz (c.-à-d. toutes les 100 ms). Par défaut, la génération comporte cent échantillons d'une sinusoïde, simulant ainsi une acquisition à 1000 Hz. Au bout de dix secondes, la génération de données s'arrête et l'application se ferme.
Attention !
Si L'affichage prend trop de temps, c.-à-d. plus de 100 ms, il serait en pratique impossible d'afficher tous les points acquis. Dans le cas de cette simulation, tous les évènements successifs seront décalés dans le temps comme il est spécifié dans la documentation de la classe Timer.
Voici le code de la classe GenerateData :
Attention !
Ce code ne comprend pas les « includes ». Si vous copiez ce code tel quel, vous aurez des erreurs de compilation que vous pourrez résoudre en ajoutant les includes correspondants.
Utilisation de GenerateData
Il nous faut maintenant utiliser cette classe. Quelques modifications dans la méthode main() permettent de lancer le Timer à la cadence voulue et de l'arrêter lorsque l'application se termine.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
public
static
void
main
(
String[] args) {
/*
* Create display and shell.
* Assign application name, shell title, size and layout.
*/
Display.setAppName
(
"JFreeChart SWT"
);
Display display =
new
Display (
);
shell =
new
Shell (
display);
shell.setText
(
"Real Time JFreeChart SWT"
);
shell.setMaximized
(
true
);
/* Create charts area */
createChartsArea
(
);
/* Open the shell to display charts */
shell.open
(
);
/* Launch Data timer generation : generates data each 100ms */
Timer timer =
new
Timer
(
);
GenerateData generateData =
new
GenerateData
(
);
timer.schedule
(
generateData, 1000
, 100
);
/* SWT display loop */
while
(!
shell.isDisposed
(
)) {
if
(!
display.readAndDispatch
(
)) display.sleep
(
);
}
display.dispose
(
);
/* Stop data timer */
timer.cancel
(
);
}
Vous pouvez tester avec un seul graphe et une seule série dans ce graphe, l'affichage semble assez correct. Vous pouvez jouer avec les paramètres nbCharts et nbSeriesPerChart : passez par exemple à six graphes avec deux courbes par graphe. Vous constaterez que ce n'est plus du tout la même chose. Tout dépend bien évidemment de votre hardware et nous sommes ici en évaluation qualitative, mais on peut déjà constater que les performances ne sont pas au rendez-vous dès qu'on augmente le nombre de graphes et de courbes. En acquisition de données, il n'est pas rare d'avoir plusieurs graphes avec éventuellement plusieurs courbes par graphe. Dans la dernière partie de cet article, nous allons modifier le code afin de faire des mesures sur les temps d'affichage.
IV. Mesures▲
Avant de mettre en place une automatisation des mesures, quelques configurations sont nécessaires.
IV-A. Configurations préalables▲
Pour réaliser des mesures de temps d'affichage de la bibliothèque JFreeChart, il nous faut modifier le code source de cette bibliothèque. Nous allons donc, dans un premier temps, créer un projet, nommé jfreechart, dans notre répertoire de travail d'Eclipse. Ce projet, une fois configuré, contiendra le code source opérationnel de JFreeChart, légèrement modifié. Nous supprimerons ensuite le lien vers la bibliothèque jfreechart-1.0.19.jar pour en ajouter un vers notre nouveau projet jfreechart avant de rajouter le code de mesure des temps d'affichage.
Création du projet Java jfreechart
- Créez un nouveau projet Java nommé jfreechart.
- Copiez le contenu du répertoire source de JFreeChart dans le répertoire src du projet jfreechart nouvellement créé.
Vous aurez des erreurs de compilation, comme le montre l'image ci-dessous, qui seront résolues un peu plus tard.
Continuons dans la création de ce projet jfreechart :
- créez un répertoire lib dans jfreechart ;
- copiez la bibliothèque jcommon-1.0.23.jar dans ce répertoire à partir du répertoire lib de RealTimeCharting ;
- copiez également la bibliothèque servlet.jar dans ce répertoire à partir du répertoire lib de RealTimeCharting ;
- ajoutez ces deux bibliothèques au Build Path de jfreechart.
L'image suivante vous montre le résultat : le répertoire lib et les références aux deux bibliothèques.
Vous aurez maintenant moins d'erreurs de compilation, mais des packages utilisant JavaFX posent encore problème. Dans notre cas, il suffit de supprimer ces trois packages : clic droit sur les trois packages sélectionnés, puis cliquez sur Delete et le tour est joué !
À ce stade, vous ne devriez plus avoir d'erreur de compilation. Pour utiliser ce nouveau projet Java jfreechart, il nous faut maintenant enlever la référence à la bibliothèque jfreechart-1.0.19.jar dans le projet RealTimeCharting (menu contextuel sur jfreechart-1.0.19.jar dans Referenced Libraries, puis Build Path et Remove From Build Path), pour lui rajouter une référence au projet jfreechart, comme le montre l'image ci-dessous :
Vérifiez que l'application fonctionne toujours.
Modification du code pour la mesure des temps d'affichage
Dans le package org.jfree.chart.plot du projet jfreechart, ouvrez la classe XYPlot. Localisez la méthode draw() pour ajouter en tout début et fin quelques instructions de mesure temporelle, comme le montre le code suivant. Il faudra également ajouter la déclaration du tableau drawTimes et son accesseur.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
private
ArrayList<
Long>
drawTimes =
new
ArrayList<
Long>(
0
);
/**
* Return draw times : elapsed times between start and end of chart drawing
*
*
@return
an array of double containing elapsed times, in ms
*/
public
double
[] getDrawTimes
(
) {
double
[] values =
new
double
[drawTimes.size
(
)];
for
(
int
i =
0
; i <
values.length; i++
) {
values[i] =
drawTimes.get
(
i);
}
return
values;
}
/**
...
*/
@Override
public
void
draw
(
Graphics2D g2, Rectangle2D area, Point2D anchor,
PlotState parentState, PlotRenderingInfo info) {
long
time =
System.nanoTime
(
);
...
...
drawTimes.add
(
System.nanoTime
(
) -
time);
}
Nous avons maintenant la possibilité de récupérer tous les temps d'affichage de chacun des graphes pour effectuer des calculs statistiques de type moyenne, écart type, etc. Pour ce faire, modifier le code de la méthode run() de la classe GenerateData afin qu'elle effectue ces calculs avant la fermeture définitive du programme. Ce code récupère tous les tableaux drawTimes de tous les graphes et les compile dans un HasMap nommé drawTimesMap avant d'appeler la méthode statique compute() de la classe com.developpez.realtimecharting.Statistics avec, entre autres, ce HasMap en paramètre. Le listing suivant présente les modifications apportées à la méthode run().
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
...
//Stop when 10s have been acquired
if
(
initialTime >
10
) {
cancel
(
);
//Retrieve all draw times of each chart, saving them in an HashMap
HashMap<
Integer, double
[]>
drawTimesMap =
new
HashMap<
Integer, double
[]>(
);
for
(
int
numChart =
0
; numChart <
charts.length; numChart++
) {
final
ChartComposite chart =
charts[numChart];
double
[] drawTimes =
chart.getChart
(
).getXYPlot
(
).getDrawTimes
(
);
drawTimesMap.put
(
numChart, drawTimes);
}
//Compute some statistics on these values
Statistics.compute
(
drawTimesMap, nbCharts, nbSeriesPerChart, "JF"
);
//Close application
shell.getDisplay
(
).asyncExec
(
new
Runnable
(
) {
@Override
public
void
run
(
) {
shell.close
(
);
}
}
);
}
...
Il faut maintenant créer la classe Statistics dans le package com.developpez.realtimechart avec la méthode statique compute(), comme le montre le code suivant :
Pour effectuer les calculs dans la méthode statique compute(), j'ai utilisé la bibliothèque Apache Commons Math que vous pouvez télécharger ici. Créez ensuite un répertoire lib dans le projet RealTimeCharting pour y copier le fichier commons-math3-3.4.1.jar. Ajoutez cette bibliothèque au Build Path du projet RealTimeCharting.
Vous pouvez maintenant lancer des mesures avec différentes conditions de graphes et de courbes :
- un graphe et une courbe : C1S1JF (JF pour JFreeChart) ;
- un graphe et deux courbes : C1S2JF ;
- deux graphes et une courbe : C2S1JF ;
- quatre graphes et deux courbes : C4S2JF ;
- ...
en changeant les valeurs de nbCharts et de nbSeriesPerChart. Des résultats dans la condition C1S1 sont présentés dans l'image ci-dessous : JfreeChart met en moyenne 120 ms pour afficher les données avec un écart type de 20 ms.
Ces valeurs peuvent évidemment changer suivant la machine que vous avez, l'essentiel étant de comparer les résultats sur la même machine. Dans le cas de l'exemple précédent, c'est un MacBook Pro, OS10.7.5, CPU 2.3GHz Intel Core i5, RAM 4Go 1.333GHz DDR3, GPU Intel HD Graphics 3000 384MB.
L'inconvénient à ce niveau de développement vient du fait qu'il est nécessaire de changer les paramètres à la main pour faire des mesures dans différentes conditions. L'étape suivante consiste à automatiser ces changements et à enregistrer les mesures dans un fichier texte.
IV-B. Automatisation des mesures▲
Pour automatiser les mesures, je vous propose de passer en paramètre de la méthode main() de la classe JFreeChartRTCTest :
- le nombre de graphes ;
- le nombre de courbes ;
- le nom du fichier de sauvegarde des mesures ;
- un booléen permettant de savoir si on ajoute les mesures dans un fichier existant ou si on crée ce fichier.
Ajoutons le traitement de ces paramètres à la méthode main() de JFreeChartRTCTest :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
/*
* Arguments (if not null) : nb chart, nb series per chart,
* file name and create or append to results file
*/
public
static
void
main
(
String[] args) {
/* Arguments (if not null) : nb chart, nb series per chart,
file name and create or append to results file */
if
(
args !=
null
&&
args.length ==
4
) {
nbCharts =
Integer.parseInt
(
args[0
]);
nbSeriesPerChart =
Integer.parseInt
(
args[1
]);
resultFileName =
args[2
];
createResultFile =
Boolean.parseBoolean
(
args[3
]);
}
...
N'oubliez pas de rajouter les champs statiques privés resultFileName et createResultFile dans la classe JFreeChartRTCTest, en tant que chaine de caractères et booléen respectivement. Modifions maintenant la méthode compute() de la classe Statistics afin de prendre en charge l'enregistrement des mesures :
Pour terminer, créons une classe de tests AllTests dans le package com.developpez.realtimecharting qui regroupera tous les appels aux tests élémentaires :
Vous devriez obtenir le type de fichier suivant :
Nous sommes maintenant armés pour analyser les résultats en conclusion de ce premier article.
V. Conclusion▲
Les résultats qui suivent ont été obtenus sur un iMac, OS X.9.5, CPU 3.1GHz Intel Core i7, RAM 16Go 1.6GHz DDR3, GPU NVIDIA GeForce GT 750M 1024 Mo.
La figure ci-dessous vous présente le diagramme en boîte issu de ces mesures. On constate évidemment que les temps d'affichage augmentent avec la complexité du graphique. La valeur médiane de ce temps, dans la condition C1S1JF, c.-à-d. la condition la plus simple, est d'environ 70 ms. Elle est déjà proche de la cadence à laquelle on demande un renouvellement de l'affichage. Toujours dans les cas où l'on ne considère qu'un seul graphe, la valeur médiane du temps d'affichage augmente avec le nombre de courbes : elle dépasse les 150 ms pour C1S4JF ! C'est-à-dire que JFreeChart met souvent plus de 150 ms à afficher les quatre courbes dans le graphe ! Cette analyse peut être renouvelée dans les conditions comportant plus d'un graphe : C2, C3 et C4. La valeur médiane du temps d'affichage pouvant approcher la seconde.
On peut aussi constater que la variabilité augmente également avec la complexité. Les premier et troisième quartiles s'écartent de la valeur médiane et il en est de même pour les valeurs maximales. Toujours sur le diagramme en boîte, il semble que les valeurs minimales ne varient pas, mais c'est un effet d'échelle : elles restent comprises entre 20 ms et 65 ms, comme le montre l'histogramme des minima.
Le dernier histogramme nous montre les valeurs moyennes en fonction des conditions. L'allure générale est identique au diagramme en boîte et les valeurs moyennes sont proches des médianes pour chaque condition. Par exemple, dans le cas C4S1JF, quatre graphes avec une seule courbe par graphe, cette valeur moyenne est d'environ 180 ms ! Pourtant, afficher quatre signaux, ça ne devrait pas prendre autant de temps ! JFreeChart n'est effectivement pas une solution pour faire de l'affichage temps réel.
Voici les codes R qui ont été utilisés pour obtenir ces figures :
VI. Remerciements▲
Un grand merci à Mickael BARON et Alexandre LAURENT pour leur aide ainsi qu'à Claude LELOUP pour la relecture orthographique.