Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Flex Charts : canvas et backgroundElements pour avoir un contrôle total des gridLines

Compatible Flex 3. Cliquer pour en savoir plus sur les compatibilités.Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par sebastien.portebois (Sébastien Portebois), le 06 juillet 2009

Introduction

Bonjour,
Les composants mx.charts de Flex Builder sont très puissants, mais étonnement on trouve relativement peu d'information quand il s'agit de faire plus que des modifications de style simple.
Dans ce tutorial nous allons voir comment recréer nous même les gridLines en utilisant un canvas pour permettre de personnaliser beaucoup plus notre graphique. Dans l'exemple que nous déroulerons, nous nous contenterons d'ajouter des un quadrillage intermédiaire entre les ticks de l'axe Y, et d'y dessiner certaines lignes de la grille en pointillés. Ce point de départ vous donnera toutes les clefs pour compléter à souhait votre graphique avec n'importe quelle information visuelle que vous souhaitez placer en fonction des données, vous affranchissant totalement de ce qui est initialement prévu dans les paramètres de style des composants mx.charts.

Voici ce que nous allons réaliser (swf) :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Appréhender la structure des charts

Les graphiques mx.charts sont composés des axes et de leurs labels et ticks, et de la zone centrale d'affichage. C'est cette zone qui va nous intéresser dans ce tutorial, nous allons y tracer des compléments graphiques en fonction des échelles calculées par le composant.

La liste d'affichage de ce composant, pour la partie centrale, est essentiellement composée de 3 éléments :
* les backgroundElements,
* les series,
* les annotationElements.
Les series sont sans doute les plus évidentes, puisqu'il va s'agir des courbes/histogrammes … que nous retrouverons donc à chaque utilisation des graphs.
Les backgroundElements et annotationElements servent à ajouter des éléments en dessous (background) et au dessus (annotation) du graph, tout en fournissant le même système de coordonnées. Les annotations permettront par exemple d'ajouter des labels personnalisés, des liaisons entre des courbes, …
Les backgroundElements serviront à afficher un arrière plan statique… ou à dessiner des informations enrichissant les series. C'est précisément ce que nous allons faire.

Ce qui va nous intéresser : placer un canvas dans les backgroundElements pour y dessiner tout ce qu'on veut.

Ce dont on aura besoin pour la démo : une source de données pour avoir des courbes

    [Bindable]
    public var expenses:ArrayCollection = new ArrayCollection([
        {Month:"Jan", Profit:2000, Expenses:1500, Amount:450},
        {Month:"Feb", Profit:1000, Expenses:200, Amount:600},
        {Month:"Mar", Profit:1500, Expenses:500, Amount:300}
    ]);

et d'un graphique

	<mx:LineChart id="myChart" 
		x="100" y="100" width="400" height="400"
		gutterLeft="100" gutterRight="0" paddingLeft="0" gutterBottom="10"
		dataProvider="{expenses}" 
		showDataTips="true"  >
 
		<!--On supprime les ombres portées du graph-->
		<mx:seriesFilters>
			<mx:Array/>
		</mx:seriesFilters>
 
		<mx:verticalAxis>
			<mx:LinearAxis id="axisY" />
		</mx:verticalAxis>
		<mx:verticalAxisRenderers>
			<mx:AxisRenderer axis="{axisY}" 
				showLine="false" tickPlacement="outside">
			</mx:AxisRenderer>
		</mx:verticalAxisRenderers>
 
 
		<mx:horizontalAxis>
			<mx:CategoryAxis 
				dataProvider="{expenses}" 
				categoryField="Month"
			/>
		</mx:horizontalAxis>
 
		<mx:series>
			<mx:LineSeries displayName="Profit"
				yField="Profit" />
			<mx:LineSeries displayName="Expenses"
				yField="Expenses"/>
		</mx:series>
 
	</mx:LineChart>

Manipuler cette structure

Maintenant que nous avons nos données et un chart classique, nous allons pouvoir commencer à les manipuler. Tout d'abord nous allons devoir créer un canvas dans lequel nous afficherons nos résultats, aussi nous définissons ce canvas dans les backgroundElements du LineChart :

<mx:backgroundElements>
	<mx:Canvas id="rawCanvas"/>
</mx:backgroundElements>

Enfin, nous allons devoir savoir quand traiter les données pour mettre à jour l'affichage. Nous cherchons ici à afficher des graduations, qui sont donc liées à l'axe Y calculé et mis à jour par le composant charts. Nous allons donc faire dépendre la mise à jour de notre canvas sur la mise à jour de l'axe Y que nous avons déclaré avec l'ID axisY en définissant un écouteur pour l'événement updateComplete :

<mx:verticalAxisRenderers>
	<mx:AxisRenderer axis="{axisY}" 
		showLine="false" tickPlacement="outside" 
		updateComplete="renderVerticalGridLines(event)" >
	</mx:AxisRenderer>
</mx:verticalAxisRenderers>

Il nous reste donc à définir une méthode renderVerticalGridLines qui va devoir :
- connaître les valeurs des différents ticks calculées par le composant charts pour l'axe Y,
- connaître la taille d'affichage exploitée pour cet axe, c'est à dire la taille du composant chart moins ses gutters,
- calculer un espacement homogène entre ces ticks pour qu'on y crée des vraies fausses gridLines intermédiaires,
- dessiner sur le canvas en utilisant le contexte Graphics.

Pour récupérer ces valeurs, une fois la documentation explorée nous trouvons ce que nous cherchons :

private function renderVerticalGridLines(event:Event = null):void
{
	// pour ne pas avoir d'id du graph en hardocdé, on le cherche à partir de l'event
	var chart:CartesianChart = event.currentTarget.owner as CartesianChart; 
	var ticksValues:Array /* of Number  */ = event.currentTarget.ticks as Array;
// ...

On pourrait très bien utiliser l'id myGraph de notre graph, mais autant préparer la suite et rendre cette routine de dessin indépendante, aussi il est plus intéressant de récupérer event.currentTarger.owner pour avoir une référence à notre graph.
Notre exemple se base sur un enrichissement des gridLines, basés sur les ticks calculés par le graph sur l'axe Y. Ce code devrait donc être compatible avec les axes linéaires, aussi au lieu de transtyper event.currentTarget.owner en LinearChart, nous nous contentons d'un CartesianChart, ce qui permet d'utiliser exactement le même code pour des histogrammes.

Le reste du code de cette routine est assez basique, nous avons eu toutes les informations nécessaires. Il nous reste deux étapes : calculer les valeurs y des graduations intermédiaires entre les ticks (que je vais stocker dans l'array minorTicks, puis dessiner sur le canvas en 3 étapes:
* faire un clear()
* dessiner les lignes des ticks, correspondant aux gridLines classiques,
* dessiner les lignes des ticks interpolés

// On calcule un espacement homogène des graduations intermédiaires
var nSubTicksByInterval:int = 2; // on indique ici le nombre de graduations intermédiaires entre 2 ticks
var minorTicks:Array = new Array();
var minorTicksCnt:Number = 1+ (nSubTicksByInterval+1)*(n-1);
var minorTickInterval:Number = (ticksValues[n-1] - ticksValues[0]) / ((nSubTicksByInterval+1)*(n-1));
var currentSubTick:Number = ticksValues[0];
minorTicks.push(currentSubTick);
for (var i:int = 0; i <minorTicksCnt; i++) {
	currentSubTick+= minorTickInterval;
	minorTicks.push(currentSubTick);
}
 
 
var realY:Number;
var g:Graphics = rawCanvas.graphics; 
g.clear();
g.lineStyle(2, 0xFFFFFF, 0.5);
// La taille réelle de l'espace 'données' du graph est sa hauteur moins ses gutters
var heightFactor:Number = chart.explicitHeight - chart.computedGutters.bottom - chart.computedGutters.top;
// On dessine les graduations principales correspondants aux ticks de l'axe
for (i = 0; i < ticksValues.length; i++) {
	realY = ticksValues[i] * heightFactor;
	g.moveTo(0,realY);
	g.lineTo(chart.explicitWidth, realY);
}
// On dessine les graduations intermédiaires en plus fin
g.lineStyle(1, 0xFFFFFF, 0.2);
for (i = 0; i < minorTicks.length; i++) {
	realY = minorTicks[i] * heightFactor;
	g.moveTo(0,realY);
	g.lineTo(chart.explicitWidth, realY);
}

Tout y est, il ne reste plus qu'à tester : vous pouvez le compiler chez vous, et devriez obtenir le résultat suivant :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

(Les sources sont disponibles à la fin du tutorial).

Rendre plus génériques ces manipulations

Les bases sont là, il nous reste à améliorer ce code pour séparer ce qui sera spécifique à l'implémentation de ce qui sera générique à tous les CartesianChart (ici nous utilisons l'id du canvas directement, les paramètres d'apparence de la grille sont directement dans le code comme son épaisseur, sa couleur, … ).

Nous allons donc dans notre mxml modifier la méthode renderHorizontalGridLines pour n'y contenir les informations spécifiques à l'implémentation, et déporter toute la logique générique dans une classe dédiée.:

private function renderHorizontalGridLines(event:Event = null):void
{
	var chart:CartesianChart = event.currentTarget.owner as CartesianChart; // pour ne pas avoir d'id du graph en hardocdé
	var ticksValues:Array /* of Number  */ = event.currentTarget.ticks as Array;
	var renderArea:Canvas = chart.backgroundElements[0] as Canvas; // gfxBgCanvas;
	var nSubTicksByInterval:int = 2; // on indique ici le nombre de graduations intermédiaires entre 2 ticks
 
	var params:ChartHGridLinesParams = new ChartHGridLinesParams(chart, ticksValues, renderArea);
	params.subLinesByInterval 	= 2; // On demande 2 graduations intermédiaires entre les ticks;
	params.linesThickness 		= 2;
	params.linesAlpha 		= 1;
	params.linesColor 		= 0xFFFFFF;
	params.sublinesThickness = 1;
	params.sublinesAlpha 	 = 0.5;
	params.sublinesColor 	 = 0xFFFFFF;
	params.subLinesDotStrokeLength = 4; // On défini des pointillés
	ChartUtil.drawHGridLines(params);
 
}

Qu'avons nous fait ? On remarque un appel à deux nouvelles classes : ChartHGridLinesParams et ChartUtil. ChartHGrifLinesParams n'est qu'une classe conteneur des paramètres de notre affichage. Nous pourrions très bien utiliser un simple Object, mais l'utilisation d'une classe spécifique nous permet de vérifier le typage des paramètres et leur attribuer des valeurs par défaut. Une fois ces paramètres stockés dans une instance de ChartHGridLinesParams, on la passe en argument à la méthode statique drawHGridLines de ChartUtil. Comme on le verra cette méthode a tout ce dont elle a besoin : une référence vers le graph, vers le canvas dans lequel elle devra faire son affichage, et toutes les infos pour interpoler et dessiner les axes.

Commençons par la classe ChartHGridLinesParams, la plus simple, pour constater qu'effectivement nous ne faisons que contenir les arguments :

package exemple
{
    //--------------------------------------------------------------------------
    //
    //  Cette classe sert de formatteur d'argument pour ChartUtil.drawHGridLines
    //  Il ne s'agit que d'un simple conteneur de variable pour s'assurer du 
    //  typage des arguments
    //
    //  Pour dessiner des graduations intermédiaires en pointillés, il faut 
    //  Définir une valeur à la propriété subLinesDotStrokeLength. 
    //  Si Null on dessiner des lignes pleines 
    //--------------------------------------------------------------------------
 
	import mx.charts.chartClasses.CartesianChart;
	import mx.containers.Canvas;
 
	public dynamic class ChartHGridLinesParams {
 
		public var chart:CartesianChart;
		public var ticks:Array;
		public var renderCanvas:Canvas;
 
		public var linesThickness:Number = 2;
		public var linesAlpha:Number 	 = 1;
		public var linesColor:uint 	 = 0xFFFFFF;
 
		public var subLinesByInterval:int = 2;
 
		public var sublinesThickness:Number = 1;
		public var sublinesAlpha:Number     = 1;
		public var sublinesColor:uint       = 0xFFFFFF;
 
		public var subLinesDotStrokeLength:Number;
 
		public function ChartHGridLinesParams(chart:CartesianChart=null, ticks:Array = null, renderCanvas:Canvas = null)
		{
			this.chart = chart;
			this.ticks = ticks;
			this.renderCanvas = renderCanvas;
		}
 
	}
}

Enfin nous allons regarder la méthode statique drawHGridLines de la classe ChartUtil : nous y retrouvons une version un peu plus complète de notre routine de dessin de notre premier exemple. Nous commençons par lire les arguments passés, à calculer de la même manière que précédemment les valeurs des ticks intermédiaires, par contre nous complexifions un peu l'affichage : nous gérons désormais les lignes continues et les pointillés.

	var gu:GraphicsUtil = new GraphicsUtil(renderArea);
 
	gu.clear().lineStyle(params.linesThickness, params.linesColor, params.linesAlpha);
	// La taille réelle de l'espace 'données' du graph est sa hauteur moins ses gutters
	var heightFactor:Number = chart.explicitHeight - chart.computedGutters.bottom - chart.computedGutters.top;
	// On dessine les graduations principales correspondants aux ticks de l'axe
	for (i = 0; i < ticksValues.length; i++) {
		realY = ticksValues[i] * heightFactor;
		gu.moveTo(0, realY).lineTo(chart.explicitWidth, realY);
	}
 
	// On dessine les graduations intermédiaires en pointillés
	gu.lineStyle(params.sublinesThickness, params.sublinesColor, params.sublinesAlpha);
	var bDottedLine:Boolean = (params.subLinesDotStrokeLength > 0);
	var size:Number = params.subLinesDotStrokeLength;
 
	for (i = 0; i < minorTicks.length; i++) {
		realY = minorTicks[i] * heightFactor;
		if (bDottedLine) {
			gu.moveTo(0, realY).dottedLineTo(chart.explicitWidth, realY, size, size);
		} else {
			gu.moveTo(0, realY).lineTo(chart.explicitWidth, realY);
		}
	}

Ce code ressemble très fort à notre version précédente, à la différence près qu'on ne manipule plus directement le canvas.graphics, mais on le fait vie l'intermédiaire d'une nouvelle classe, GraphicsUtil. Cette classe permet d'enchainer les commandes sur la même ligne, en retournant son instance après chaque méthode. Mais surtout elle apporte en plus la méthode dottedLineTo(..) qui permet de tracer une ligne en pointillés. Les pointillés sont un exemple simple, mais qui montre qu'on s'est totalement affranchi de l'affichage par défaut des composants mx.charts de Flex pour dessiner ce qu'on veut sur un contexte graphics.

Cette classe GraphicsUtil est ici réduite à sa plus simple expression : enrober un contexte graphics pour le manipuler plus facilement et lui ajouter de nouvelles fonctionnalités ; mais on pourra l'étendre à souhait selon les opérations désirées :

package exemple
{
 
	import flash.geom.Point;
	import mx.containers.Canvas;
 
	public class GraphicsUtil
	{
 
		private var _context:Canvas;
		private var lineThickness:Number = 1;
		private var lineColor:uint = 0;
		private var lineAlpha:Number = 1;
 
		private var turttleLoc:Point;
 
		public function GraphicsUtil(canvas:Canvas=null)
		{
			this._context = canvas;
			this.turttleLoc = new Point();
		}
 
 
		public function set context(canvas:Canvas):void
		{
			this._context = canvas;
		}
 
		/**
		 * Wrapper du graphics.clear 
		 * @return L'instance de cet objet pour pouvoir enchainer les appels 
		 */		
		public function clear():GraphicsUtil
		{
			if (_context != null)
				_context.graphics.clear();
			return this;
		}
 
		/**
		 * 
		 * @param thickness
		 * @param color
		 * @param alpha
		 * @return	L'instance de cet objet pour pouvoir enchainer les appels  
		 */
		public function lineStyle(thickness:Number=1, color:uint=0, alpha:Number=1):GraphicsUtil 
		{
			this.lineThickness = thickness;
			this.lineColor = color;
			this.lineAlpha = alpha;
			_context.graphics.lineStyle(thickness, color, alpha);
			return this;
		}
 
		/**
		 * Wrapper du graphics.moveTo
		 * @param x
		 * @param y
		 * @return L'instance de cet objet pour pouvoir enchainer les appels 
		 */		
		public function moveTo(x:Number, y:Number):GraphicsUtil
		{
			if (_context != null)
				_context.graphics.moveTo(x, y);
			turttleLoc = new Point(x,y);
			return this;
		}
 
		/**
		 * Wrapper du graphics.lineTo
		 * @param x
		 * @param y
		 * @return L'instance de cet objet pour pouvoir enchainer les appels 
		 */		
		public function lineTo(x:Number, y:Number):GraphicsUtil 
		{
			if (_context != null)
				_context.graphics.lineTo(x, y);
			turttleLoc = new Point(x,y);
			return this;
		}
 
		/**
		 * Equivalent du lineTo qui alterne lineTo et moveTo pour dessiner une ligne en pointillés
		 * @param x
		 * @param y
		 * @param strokeLength
		 * @param emptyLength
		 * @return L'instance de cet objet pour pouvoir enchainer les appels 
		 */		
		public function dottedLineTo(x:Number, y:Number, strokeLength:Number=5, emptyLength:Number=5):GraphicsUtil
		{
			if (_context == null)
				return this;
 
			// On défini les vecteurs pour dessiner un trait ou un vide
			var directionStroke:Point = new Point(x-turttleLoc.x, y-turttleLoc.y);
			var directionEmpty:Point  = new Point(x-turttleLoc.x, y-turttleLoc.y);
			directionStroke.normalize(strokeLength);
			directionEmpty.normalize(emptyLength);
 
			var totalDistance:Number = Point.distance(turttleLoc, new Point(x,y));
			var sumDistance:Number = 0;
			var bMod:Boolean = true;
			while (sumDistance < totalDistance)
			{
				if (bMod) 
				{
					lineTo(turttleLoc.x + directionStroke.x, turttleLoc.y + directionStroke.y);
					sumDistance += strokeLength;
				}
				else 
				{
					moveTo(turttleLoc.x + directionEmpty.x, turttleLoc.y + directionEmpty.y);
					sumDistance += emptyLength;
				}
				// On prépare la prochaine itération
				bMod = !bMod;
			}
			return this;
		}
	}
}

En savoir plus

Vous pouvez télécharger ici les sources Flex 3
Pour continuer à travailler avec les mx.charts, la documentation officielle des charting components regorge d'informations et reste d'une grande aide : http://livedocs.adobe.com/flex/3/html/Part1_charting_1.html#72388

Aller plus loin

La même technique est tout à fait utilisable pour faire des rollovers très particuliers, en dessinant dans les annotationElements. Toute la logique reste la même concernant les manipulations de données, on pourra placer un canvas dans les annotationsElements et y dessiner ce qu'on veut.

Ici nous avons interagit avec les données de manière relativement simple. Dans la suite nous verrons comment manipuler les données des series de nos graphs pour dessiner des formes de remplissage beaucoup plus complètes que les area charts standard, pour dessiner par exemple des zones mettant en avant de manière différente quand une courbe A a ses valeurs supérieures ou inférieurs à une courbe B :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Lire la suite...