Edit in JSFiddle

//=============================================================================
// Sphinx-Chart2.js
//=============================================================================

/*:
 * @plugindesc Graphique sous forme de sprite
 * @author Sphinx
 * @help
 * //==========================================================================
 * // Plugin : Sphinx-Chart2
 * // Date   : 29 décembre 2019
 * // Auteur : Sphinx
 * //==========================================================================
 * Ce plugin ne contient aucune commande de plugin. Il met à disposition des
 * scripteurs une nouvelle classe de sprite représentant des graphiques.
 * Créer un graphique :
 * variable = new SphinxChart2(
 *     type: TYPE_GRAPHIQUE,
 *     title: TITRE_GRAPHIQUE,
 *     categoriesLabels: LABELS_DE_CATEGORIES,
 *     xAxisLabels: LABELS_DES_ABSCISSES,
 *     datas: DONNEES,
 *     colors: COULEURS,
 *     options: OPTIONS
 * );
 * où :
 *      TYPE_GRAPHIQUE est l'un des types suivants :
 *          - line
 *          - bar
 *          - radar
 *          - pie
 *          - doughnut
 *      TITRE_GRAPHIQUE est une chaine de caractères qui sera affichée au
 *          dessus du graphique
 *      LABELS_DE_CATEGORIES est un tableau contenant la liste des étiquettes
 *          des légendes à afficher
 *      LABELS_DES_ABSCISSES est un tableau contenant la liste des étiquettes
 *          de l'axe des abscisses à afficher
 *      DONNEES est un tableau à 2 dimensions contenant les données à afficher
 *          Pour les graphiques pie et doughnut, utiliser un tableau à 1
 *          dimension
 *      COULEURS Liste des couleurs des données
 *      OPTIONS est un objet de configuration du graphique contenant les clés
 *          suivantes :
 *          - easing : méthode d'animation de la classe Math.easing
 *          - duration : durée de l'animation à l'ouverture du graphique
 *          - forceMinToZero: true - le graphique commencera forcément à 0
 *                            false - configuration par défaut
 *              Inutilisé par les graphiques pie et doughnut
 *          - background : couleur(s) d'arrière plan. Chaine de caractères pour
 *              une couleur unie, tableau de chaines de caractères pour un
 *              dégradé, un objet Bitmap pour une image de fond
 *          - position : objet placant le graphique contenant les clés
 *              suivantes :
 *              - x : coordonnée X
 *              - y : coordonnée Y
 *              - width : largeur
 *              - height : hauteur
 *          - padding : objet définissant les marges de chaque côté contenant
 *              les clés suivantes :
 *              - top : marge en haut du graphique
 *              - left : marge à gauche du graphique
 *              - right : marge à droite du graphique
 *              - bottom : marge en bas du graphique
 * 
 * Ensuite, il suffit de l'ajouter à la scène ou à une window comme un sprite
 * ordinaire.
 * 
 * Exemple de graphique :
 * chart = new SphinxChart2({
 *     type: "line",
 *     title: "Test",
 *     categoriesLabels: [
 *         "Category 1",
 *         "Category 2",
 *         "Category 3"
 *     ],
 *     xAxisLabels: [
 *         "1", "2", "3", "4", "5"
 *     ],
 *     datas: [
 *         [ 5, 2, 1, 4, 3 ],
 *         [ 50, 21, 18, 34, 16 ],
 *         [ 8, 5, 3, 7, 9 ],
 *     ],
 *     colors: [
 *         "#0099FF",
 *         "#FF3300",
 *         "#00CC00",
 *         "#CC3399",
 *         "#FF9933",
 *         "#99CC00",
 *         "#009999",
 *         "#CC99FF"
 *     ],
 *     options: {
 *         easing: "easeOutQuart",
 *         duration: 1,
 *         background: "transparent",
 *         forceMinToZero: true,
 *         position: {
 *             x: 50,
 *             y: 50,
 *             width: 640,
 *             height: 480,
 *         },
 *         padding: {
 *             top: 0,
 *             left: 25,
 *             right: 25,
 *             bottom: 25,
 *         }
 *     }
 * });
 * 
 *     - Dépendances : Sphinx-Polyfill
 *                     Sphinx-MathEasing
 */
DEFAULT_COLORS = [
    "DeepPink", "DodgerBlue", "Gold", "OrangeRed", "WhiteSmoke", "DarkViolet"
];

function SphinxChart2() {
    this.initialize.apply(this, arguments);
};

SphinxChart2.prototype = Object.create(Sprite_Button.prototype);
SphinxChart2.prototype.constructor = SphinxChart2;

SphinxChart2.prototype.initialize = function(config) {
    Sprite_Button.prototype.initialize.call(this);
    
    // Configuration par défaut
    this.config = config || {};
    this.config.type = config.type || "line";
    this.config.title = config.title || "";
    this.config.categoriesLabels = config.categoriesLabels || [];
    this.config.xAxisLabels = config.xAxisLabels || [];
    this.config.datas = config.datas || [];
    this.config.colors = config.colors || DEFAULT_COLORS;
    this.config.options = config.options || {};
    this.config.options.easing = config.options.easing || "linearTween";
    this.config.options.duration = config.options.duration || 1;
    this.config.options.background = config.options.background || "transparent";
    this.config.options.forceMinToZero = config.options.forceMinToZero || false;
    this.config.options.position = config.options.position || {};
    this.config.options.position.x = config.options.position.x || 0;
    this.config.options.position.y = config.options.position.y || 0;
    this.config.options.position.width = config.options.position.width || 0;
    this.config.options.position.height = config.options.position.height || 0;
    this.config.options.padding = config.options.padding || {};
    this.config.options.padding.top = config.options.padding.top || 0;
    this.config.options.padding.left = config.options.padding.left || 0;
    this.config.options.padding.right = config.options.padding.right || 0;
    this.config.options.padding.bottom = config.options.padding.bottom || 0;
    
    // Taille et position du sprite
    this.x = this.config.options.position.x;
    this.y = this.config.options.position.y;
    this.width = this.config.options.position.width;
    this.height = this.config.options.position.height;
    
    // Récupération du contexte de dessin et initialisation du compteur de frames
    this.frame = 0;
    this.bitmap = new Bitmap(this.width, this.height);
    this.context = this.bitmap._context;
    
    // Gestion du clic
    this.setClickHandler(SphinxChart2.prototype.onTouch.bind(this));
};

SphinxChart2.prototype.update = function() {
    Sprite_Button.prototype.update.call(this);
    
    // Mise à jour du graphisme
    this.drawFrame();
    
    // Mise à jour du compteur de frames
    ++this.frame;
};

SphinxChart2.prototype.onTouch = function() {
    // Mise à jour du compteur de frames
    this.frame = 0;
};

SphinxChart2.prototype.drawFrame = function() {
    // Si l'animation est finie, on sort
    if(this.frame > this.config.options.duration * 60) return;
    
    // Nettoyage du cache
    this.context.clearRect(0, 0, this.width, this.height);
    
    // Dessin de l'arrière plan
    if(this.config.options.background instanceof Bitmap) {
        backgroundResize = {};
        if(this.config.options.background.width / this.config.options.background.height > this.width / this.height) {
            backgroundResize.width = this.width;
            backgroundResize.height = this.width / (this.config.options.background.width / this.config.options.background.height);
            backgroundResize.top = (this.height - backgroundResize.height) / 2;
            backgroundResize.left = 0;
        } else {
            backgroundResize.width = this.height * this.config.options.background.width / this.config.options.background.height;
            backgroundResize.height = this.height;
            backgroundResize.top = 0;
            backgroundResize.left = (this.width - backgroundResize.width) / 2;
        }
        this.bitmap.blt(this.config.options.background, 0, 0, this.config.options.background.width, this.config.options.background.height, backgroundResize.left, backgroundResize.top, backgroundResize.width, backgroundResize.height);
    } else if(this.config.options.background instanceof Array && this.config.options.background.length > 1) {
        gradient = this.context.createLinearGradient(0, 0, 0, this.height);
        gradient.addColorStop(0, this.config.options.background[0]);
        for(i = 1; i < this.config.options.background.length; ++i) {
            gradient.addColorStop(i / (this.config.options.background.length - 1), this.config.options.background[i]);
        }
        this.context.fillStyle = gradient;
        this.context.fillRect(0, 0, this.width, this.height);
    } else if(this.config.options.background instanceof Array) {
        this.context.fillStyle = this.config.options.background[0];
        this.context.fillRect(0, 0, this.width, this.height);
    } else {
        this.context.fillStyle = this.config.options.background;
        this.context.fillRect(0, 0, this.width, this.height);
    }
    
    // Calcul de la hauteur et écriture du titre
    if(this.config.title.length > 0) {
        titleHeight = this.bitmap.measureTextHeight(this.config.title);
        this.bitmap.fontSize = 28;
        this.context.font = this.bitmap._makeFontNameText();
        this.context.textAlign = "center";
        this.context.fillStyle = "black";
        this.context.fillText(this.config.title, this.width / 2, titleHeight + this.config.options.padding.top, this.width);
    }
    
    // Math.easing[this.config.options.easing](this.frame, 50, 285, this.config.options.duration * 60);
    legendPosition = "bottom";
    switch(this.config.type) {
        case "line":
            this.drawLineFrame();
            break;
        case "bar":
            this.drawBarFrame();
            break;
        case "radar":
            this.drawRadarFrame();
            break;
        case "pie":
            legendPosition = "right";
            this.drawPieFrame();
            break;
        case "doughnut":
            legendPosition = "right";
            this.drawDoughnutFrame();
            break;
    }
    
    // Calcul de la largeur des légendes
    legendsWidth = 0;
    for(category of this.config.categoriesLabels) {
        legendsWidth += 60 + this.bitmap.measureTextWidth(category);
    }
    
    // Ecriture des légendes
    legendsLeft = (this.width - legendsWidth) / 2
    for(i = 0; i < this.config.categoriesLabels.length; ++i) {
        // Nom de la catégorie
        category = this.config.categoriesLabels[i];
        
        // Ecriture de la couleur (rectangle)
        this.context.save();
        this.context.globalAlpha = 0.59765625;
        this.context.fillStyle = this.config.colors[i % this.config.colors.length];
        this.context.fillRect(legendsLeft, this.height - categoriesHeight, 30, categoriesHeight - 5);
        this.context.restore();
        this.context.lineWidth = 2;
        this.context.strokeStyle = this.config.colors[i % this.config.colors.length];
        this.context.strokeRect(legendsLeft, this.height - categoriesHeight, 30, categoriesHeight - 5);
        legendsLeft += 40;
        
        // Ecriture du nom de la catégorie
        this.context.textAlign = "left";
        this.context.fillStyle = "black";
        this.context.fillText(category, legendsLeft, this.height - 8);
        legendsLeft += this.bitmap.measureTextWidth(category) + 20;
    }
    
    // Réécriture du dessin
    this.bitmap._setDirty();
};

//-----------------------------------------------------------------------------
// Line chart
SphinxChart2.prototype.drawLineFrame = function() {
    // Récupération de la hauteur du titre
    titleHeight = 0;
    if(this.config.title.length > 0) {
        titleHeight = this.bitmap.measureTextHeight(this.config.title);
    }
    
    // Taille de la police des axes
    this.bitmap.fontSize = 18;
    
    // Récupération de la police
    this.context.font = this.bitmap._makeFontNameText();
    
    // Récupération des bornes des données
    datasMin = null;
    if(this.config.options.forceMinToZero) datasMin = 0;
    datasMax = null;
    datasCountMax = 0;
    for(datasCategory of this.config.datas) {
        if(datasMin == null) datasMin = Math.min(...datasCategory);
        else datasMin = Math.min(datasMin, ...datasCategory);
        if(datasMax == null) datasMax = Math.max(...datasCategory);
        else datasMax = Math.max(datasMax, ...datasCategory);
        datasCountMax = Math.max(datasCountMax, datasCategory.length);
    }
    yAxisLimitMin = Math.floor(datasMin / 5) * 5;
    yAxisLimitMax = Math.ceil(datasMax / 5) * 5;
    
    // Calcul de la hauteur des étiquettes de l'axe des abscisses
    xAxisHeight = 0;
    for(label of this.config.xAxisLabels) {
        height = this.bitmap.measureTextHeight(label);
        xAxisHeight = Math.max(xAxisHeight, height);
    }
    
    // Calcul de la hauteur des catégories
    categoriesHeight = 0;
    for(category of this.config.categoriesLabels) {
        height = this.bitmap.measureTextHeight(category);
        categoriesHeight = Math.max(categoriesHeight, height);
    }
    
    // Coordonnées de l'axe des abscisses
    xAxis = {};
    xAxis.width = this.width - Math.max(this.bitmap.measureTextWidth(yAxisLimitMin), this.bitmap.measureTextWidth(yAxisLimitMax)) - this.config.options.padding.left - this.config.options.padding.right;
    
    // Coordonnées de l'axe des ordonnées
    yAxis = {};
    yAxis.left = this.config.options.padding.left + Math.max(this.bitmap.measureTextWidth(yAxisLimitMin), this.bitmap.measureTextWidth(yAxisLimitMax));
    yAxis.top = titleHeight + this.config.options.padding.top + 10;
    yAxis.height = this.height - titleHeight - xAxisHeight - categoriesHeight - this.config.options.padding.top - this.config.options.padding.bottom;
    
    // Dessin des axes des abscisses et des ordonnées
    this.context.beginPath();
    this.context.moveTo(yAxis.left, yAxis.top);
    this.context.lineTo(yAxis.left, yAxis.top + yAxis.height);
    this.context.lineTo(yAxis.left + xAxis.width, yAxis.top + yAxis.height);
    this.context.lineWidth = 2;
    this.context.strokeStyle = "#999999";
    this.context.stroke();
    
    // Ajout de labels manquants sur l'axe des abscisses
    if(this.config.xAxisLabels.length < datasCountMax) {
        for(i = this.config.xAxisLabels.length + 1; i <= datasCountMax; ++i) {
            this.config.xAxisLabels.push(i);
        }
    }
    
    // Ajout de labels manquants sur l'axe des ordonnées
    if(this.config.categoriesLabels.length < this.config.datas.length) {
        for(i = this.config.categoriesLabels.length + 1; i <= this.config.datas.length; ++i) {
            this.config.categoriesLabels.push("Category " + i);
        }
    } else if(this.config.categoriesLabels.length > this.config.datas.length) {
        this.config.categoriesLabels = this.config.categoriesLabels.slice(0, this.config.datas.length);
    }
    
    // Ecriture des étiquettes des abscisses
    xAxisGapWidth = xAxis.width / (datasCountMax - 1);
    for(i = 0; i < datasCountMax; ++i) {
        // Repère abscisse
        this.context.beginPath();
        this.context.moveTo(yAxis.left + xAxisGapWidth * i, yAxis.top + yAxis.height - 10);
        this.context.lineTo(yAxis.left + xAxisGapWidth * i, yAxis.top + yAxis.height + 10);
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#999999";
        this.context.stroke();
        
        // Prolongement repère abscisse
        if(i > 0) {
            this.context.beginPath();
            this.context.moveTo(yAxis.left + xAxisGapWidth * i, yAxis.top + yAxis.height - 10);
            this.context.lineTo(yAxis.left + xAxisGapWidth * i, yAxis.top);
            this.context.lineWidth = 2;
            this.context.strokeStyle = "#CCCCCC";
            this.context.stroke();
        }
        
        // Etiquette abscisse
        this.context.textAlign = "center";
        this.context.fillStyle = "black";
        labelHeight = this.bitmap.measureTextHeight(this.config.xAxisLabels[i]);
        this.context.fillText(this.config.xAxisLabels[i], yAxis.left + xAxisGapWidth * i, yAxis.top + yAxis.height + labelHeight + 5, xAxisGapWidth);
    }

    // Ecriture des étiquettes des ordonnées
    i = 5;
    yAxisTotalGap = Math.ceil((yAxisLimitMax - yAxisLimitMin) / 5) * 5;
    yAxisGap = Math.ceil(yAxisTotalGap / i / 5) * 5;
    yAxisLimitMax = yAxisLimitMin + (yAxisGap * 5);
    yAxisGapHeight = yAxis.height / (yAxisGap * 5 / yAxisGap);
    for(i = 5; i >= 0; --i) {
        // Label abscisse
        yAxisLabel = yAxisLimitMax - yAxisGap * i;
        
        // Repère ordonnée
        this.context.beginPath();
        this.context.moveTo(yAxis.left - 10, yAxis.top + yAxisGapHeight * i);
        this.context.lineTo(yAxis.left + 10, yAxis.top + yAxisGapHeight * i);
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#999999";
        this.context.stroke();
        
        // Prolongement repère ordonnée
        if(i < 5) {
            this.context.beginPath();
            this.context.moveTo(yAxis.left + 10, yAxis.top + yAxisGapHeight * i);
            this.context.lineTo(yAxis.left + xAxis.width, yAxis.top + yAxisGapHeight * i);
            this.context.lineWidth = 2;
            this.context.strokeStyle = "#CCCCCC";
            this.context.stroke();
        }
        
        // Etiquette ordonnée
        this.context.textAlign = "right";
        this.context.fillStyle = "black";
        labelHeight = this.bitmap.measureTextHeight(yAxisLabel);
        this.context.fillText(yAxisLabel, yAxis.left - 15, yAxis.top + yAxisGapHeight * i + labelHeight / 4);
    }
    
    // Tracé des données du graphique
    easing = Math.easing[this.config.options.easing];
    for(i = 0; i < this.config.datas.length; ++i) {
        // Données de la catégorie
        datasCategory = this.config.datas[i];
        
        // Graphique
        this.context.beginPath();
        for(j = 0; j < datasCategory.length; ++j) {
            data = datasCategory[j];
            y = easing(this.frame, yAxis.top + yAxis.height, -yAxisGapHeight * (data - yAxisLimitMin) / yAxisGap, this.config.options.duration * 60);
            if(j == 0) {
                this.context.moveTo(yAxis.left, y);
            } else {
                this.context.lineTo(yAxis.left + xAxisGapWidth * j, y);
            }
        }
        this.context.lineWidth = 2;
        this.context.strokeStyle = this.config.colors[i % this.config.colors.length];
        this.context.stroke();
    }
};

//-----------------------------------------------------------------------------
// Bar chart
SphinxChart2.prototype.drawBarFrame = function() {
    // Récupération de la hauteur du titre
    titleHeight = 0;
    if(this.config.title.length > 0) {
        titleHeight = this.bitmap.measureTextHeight(this.config.title);
    }
    
    // Taille de la police des axes
    this.bitmap.fontSize = 18;
    
    // Récupération de la police
    this.context.font = this.bitmap._makeFontNameText();
    
    // Récupération des bornes des données
    datasMin = null;
    if(this.config.options.forceMinToZero) datasMin = 0;
    datasMax = null;
    datasCountMax = 0;
    for(datasCategory of this.config.datas) {
        if(datasMin == null) datasMin = Math.min(...datasCategory);
        else datasMin = Math.min(datasMin, ...datasCategory);
        if(datasMax == null) datasMax = Math.max(...datasCategory);
        else datasMax = Math.max(datasMax, ...datasCategory);
        datasCountMax = Math.max(datasCountMax, datasCategory.length);
    }
    yAxisLimitMin = Math.floor(datasMin / 5) * 5;
    yAxisLimitMax = Math.ceil(datasMax / 5) * 5;
    
    // Calcul de la hauteur des étiquettes de l'axe des abscisses
    xAxisHeight = 0;
    for(label of this.config.xAxisLabels) {
        height = this.bitmap.measureTextHeight(label);
        xAxisHeight = Math.max(xAxisHeight, height);
    }
    
    // Calcul de la hauteur des catégories
    categoriesHeight = 0;
    for(category of this.config.categoriesLabels) {
        height = this.bitmap.measureTextHeight(category);
        categoriesHeight = Math.max(categoriesHeight, height);
    }
    
    // Ajout de labels manquants sur l'axe des abscisses
    if(this.config.xAxisLabels.length < datasCountMax) {
        for(i = this.config.xAxisLabels.length + 1; i <= datasCountMax; ++i) {
            this.config.xAxisLabels.push(i);
        }
    }
    
    // Ajout de labels manquants sur l'axe des ordonnées
    if(this.config.categoriesLabels.length < this.config.datas.length) {
        for(i = this.config.categoriesLabels.length + 1; i <= this.config.datas.length; ++i) {
            this.config.categoriesLabels.push("Category " + i);
        }
    } else if(this.config.categoriesLabels.length > this.config.datas.length) {
        this.config.categoriesLabels = this.config.categoriesLabels.slice(0, this.config.datas.length);
    }
    
    // Coordonnées de l'axe des abscisses
    xAxis = {};
    xAxis.width = this.width - Math.max(this.bitmap.measureTextWidth(yAxisLimitMin), this.bitmap.measureTextWidth(yAxisLimitMax)) - this.config.options.padding.left - this.config.options.padding.right;
    
    // Coordonnées de l'axe des ordonnées
    yAxis = {};
    yAxis.left = this.config.options.padding.left + Math.max(this.bitmap.measureTextWidth(yAxisLimitMin), this.bitmap.measureTextWidth(yAxisLimitMax));
    yAxis.top = titleHeight + this.config.options.padding.top + 10;
    yAxis.height = this.height - titleHeight - xAxisHeight - categoriesHeight - this.config.options.padding.top - this.config.options.padding.bottom;
    
    // Dessin des axes des abscisses et des ordonnées
    this.context.beginPath();
    this.context.moveTo(yAxis.left, yAxis.top);
    this.context.lineTo(yAxis.left, yAxis.top + yAxis.height);
    this.context.lineTo(yAxis.left + xAxis.width, yAxis.top + yAxis.height);
    this.context.lineWidth = 2;
    this.context.strokeStyle = "#999999";
    this.context.stroke();
    
    // Ecriture des étiquettes des abscisses
    xAxisGapWidth = xAxis.width / datasCountMax;
    this.context.beginPath();
    this.context.moveTo(yAxis.left, yAxis.top + yAxis.height - 10);
    this.context.lineTo(yAxis.left, yAxis.top + yAxis.height + 10);
    this.context.lineWidth = 2;
    this.context.strokeStyle = "#999999";
    this.context.stroke();
    for(i = 0; i < datasCountMax; ++i) {
        // Repère abscisse
        this.context.beginPath();
        this.context.moveTo(yAxis.left + xAxisGapWidth * (i + 1), yAxis.top + yAxis.height - 10);
        this.context.lineTo(yAxis.left + xAxisGapWidth * (i + 1), yAxis.top + yAxis.height + 10);
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#999999";
        this.context.stroke();
        
        // Prolongement repère abscisse
        this.context.beginPath();
        this.context.moveTo(yAxis.left + xAxisGapWidth * (i + 1), yAxis.top + yAxis.height - 10);
        this.context.lineTo(yAxis.left + xAxisGapWidth * (i + 1), yAxis.top);
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#CCCCCC";
        this.context.stroke();
        
        // Etiquette abscisse
        this.context.textAlign = "center";
        this.context.fillStyle = "black";
        labelHeight = this.bitmap.measureTextHeight(this.config.xAxisLabels[i]);
        this.context.fillText(this.config.xAxisLabels[i], yAxis.left + xAxisGapWidth * i + xAxisGapWidth / 2, yAxis.top + yAxis.height + labelHeight + 5, xAxisGapWidth);
    }

    // Ecriture des étiquettes des ordonnées
    i = 5;
    yAxisTotalGap = Math.ceil((yAxisLimitMax - yAxisLimitMin) / 5) * 5;
    yAxisGap = Math.ceil(yAxisTotalGap / i / 5) * 5;
    yAxisLimitMax = yAxisLimitMin + (yAxisGap * 5);
    yAxisGapHeight = yAxis.height / (yAxisGap * 5 / yAxisGap);
    for(i = 5; i >= 0; --i) {
        // Label abscisse
        yAxisLabel = yAxisLimitMax - yAxisGap * i;
        
        // Repère ordonnée
        this.context.beginPath();
        this.context.moveTo(yAxis.left - 10, yAxis.top + yAxisGapHeight * i);
        this.context.lineTo(yAxis.left + 10, yAxis.top + yAxisGapHeight * i);
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#999999";
        this.context.stroke();
        
        // Prolongement repère ordonnée
        if(i < 5) {
            this.context.beginPath();
            this.context.moveTo(yAxis.left + 10, yAxis.top + yAxisGapHeight * i);
            this.context.lineTo(yAxis.left + xAxis.width, yAxis.top + yAxisGapHeight * i);
            this.context.lineWidth = 2;
            this.context.strokeStyle = "#CCCCCC";
            this.context.stroke();
        }
        
        // Etiquette ordonnée
        this.context.textAlign = "right";
        this.context.fillStyle = "black";
        labelHeight = this.bitmap.measureTextHeight(yAxisLabel);
        this.context.fillText(yAxisLabel, yAxis.left - 15, yAxis.top + yAxisGapHeight * i + labelHeight / 4);
    }
    
    // Tracé des données du graphique
    easing = Math.easing[this.config.options.easing];
    for(i = 0; i < this.config.datas.length; ++i) {
        // Données de la catégorie
        datasCategory = this.config.datas[i];
        
        // Graphique
        widthWithSpace = xAxisGapWidth / this.config.datas.length;
        widthWithoutSpace = xAxisGapWidth / (this.config.datas.length + 1);
        widthSpace = widthWithSpace - widthWithoutSpace;
        for(j = 0; j < datasCategory.length; ++j) {
            data = datasCategory[j];
            width = widthWithoutSpace;
            height = easing(this.frame, 0, (data - yAxisLimitMin) * yAxisGapHeight / yAxisGap, this.config.options.duration * 60);
            x = yAxis.left + xAxisGapWidth * j + widthWithSpace * i + widthSpace / 2;
            y = yAxis.top + yAxis.height - height;
            this.context.lineWidth = 2;
            this.context.strokeStyle = this.config.colors[i % this.config.colors.length];
            this.context.strokeRect(x, y, width, height);
            this.context.save();
            this.context.globalAlpha = 0.59765625;
            this.context.fillStyle = this.config.colors[i % this.config.colors.length];
            this.context.fillRect(x, y, width, height);
            this.context.restore();
        }
    }
};

//-----------------------------------------------------------------------------
// Radar chart
SphinxChart2.prototype.drawRadarFrame = function() {
    // Récupération de la hauteur du titre
    titleHeight = 0;
    if(this.config.title.length > 0) {
        titleHeight = this.bitmap.measureTextHeight(this.config.title);
    }
    
    // Taille de la police des axes
    this.bitmap.fontSize = 18;
    
    // Récupération de la police
    this.context.font = this.bitmap._makeFontNameText();
    
    // Récupération des bornes des données
    datasMin = null;
    if(this.config.options.forceMinToZero) datasMin = 0;
    datasMax = null;
    datasCountMax = 0;
    for(datasCategory of this.config.datas) {
        if(datasMin == null) datasMin = Math.min(...datasCategory);
        else datasMin = Math.min(datasMin, ...datasCategory);
        if(datasMax == null) datasMax = Math.max(...datasCategory);
        else datasMax = Math.max(datasMax, ...datasCategory);
        datasCountMax = Math.max(datasCountMax, datasCategory.length);
    }
    yAxisLimitMin = Math.floor(datasMin / 5) * 5;
    yAxisLimitMax = Math.ceil(datasMax / 5) * 5;
    
    // Calcul de la hauteur des étiquettes de l'axe des abscisses
    xAxisHeight = 0;
    for(label of this.config.xAxisLabels) {
        height = this.bitmap.measureTextHeight(label);
        xAxisHeight = Math.max(xAxisHeight, height);
    }
    
    // Calcul de la hauteur des catégories
    categoriesHeight = 0;
    for(category of this.config.categoriesLabels) {
        height = this.bitmap.measureTextHeight(category);
        categoriesHeight = Math.max(categoriesHeight, height);
    }
    
    // Ajout de labels manquants sur l'axe des abscisses
    if(this.config.xAxisLabels.length < datasCountMax) {
        for(i = this.config.xAxisLabels.length + 1; i <= datasCountMax; ++i) {
            this.config.xAxisLabels.push(i);
        }
    }
    
    // Ajout de labels manquants sur l'axe des ordonnées
    if(this.config.categoriesLabels.length < this.config.datas.length) {
        for(i = this.config.categoriesLabels.length + 1; i <= this.config.datas.length; ++i) {
            this.config.categoriesLabels.push("Category " + i);
        }
    } else if(this.config.categoriesLabels.length > this.config.datas.length) {
        this.config.categoriesLabels = this.config.categoriesLabels.slice(0, this.config.datas.length);
    }
    
    // Angle, rayon et centre du radar
    degreesAngle = 360 / datasCountMax;
    ray = Math.min(this.width - 40, this.height - titleHeight - categoriesHeight - 40) / 2;
    centerX = this.width / 2;
    centerY = titleHeight + ray + 20;
    
    // Dessin des rayons
    for(i = 0; i < datasCountMax; ++i) {
        this.context.beginPath();
        this.context.moveTo(centerX, centerY);
        this.context.lineTo(centerX + ray * Math.cos(((270 + degreesAngle * i) % 360) * Math.PI / 180), centerY + ray * Math.sin(((270 + degreesAngle * i) % 360) * Math.PI / 180));
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#999999";
        this.context.stroke();
    }
    
    // Ecriture des étiquettes des abscisses
    for(i = 0; i < this.config.xAxisLabels.length; ++i) {
        label = this.config.xAxisLabels[i];
        this.context.textAlign = "center";
        this.context.fillStyle = "black";
        this.context.fillText(label, centerX + (ray + 10) * Math.cos(((270 + degreesAngle * i) % 360) * Math.PI / 180), centerY + (ray + 10) * Math.sin(((270 + degreesAngle * i) % 360) * Math.PI / 180));
    }
    
    // Ecriture des étiquettes des ordonnées
    i = 5;
    yAxisTotalGap = Math.ceil((yAxisLimitMax - yAxisLimitMin) / 5) * 5;
    yAxisGap = Math.ceil(yAxisTotalGap / i / 5) * 5;
    yAxisLimitMax = yAxisLimitMin + (yAxisGap * 5);
    yAxisGapHeight = ray / (yAxisGap * 5 / yAxisGap);
    for(i = 0; i <= 5; ++i) {
        // Label abscisse
        yAxisLabel = yAxisLimitMin + yAxisGap * i;
        y = centerY - (ray / 5 * i);
        
        // Repère ordonnée
        this.context.beginPath();
        this.context.moveTo(centerX + ray / 5 * i * Math.cos(270 * Math.PI / 180), centerY + ray / 5 * i * Math.sin(270 * Math.PI / 180));
        for(j = 0; j <= datasCountMax; ++j) {
            this.context.lineTo(centerX + ray / 5 * i * Math.cos(((270 + degreesAngle * j) % 360) * Math.PI / 180), centerY + ray / 5 * i * Math.sin(((270 + degreesAngle * j) % 360) * Math.PI / 180));
        }
        this.context.lineWidth = 2;
        this.context.strokeStyle = "#CCCCCC";
        this.context.stroke();
        
        // Etiquette ordonnée
        this.context.textAlign = "right";
        this.context.fillStyle = "black";
        labelHeight = this.bitmap.measureTextHeight(yAxisLabel);
        this.context.fillText(yAxisLabel, centerX - 10, centerY - ray / 5 * i + 10);
    }
    
    // Tracé des données du graphique
    easing = Math.easing[this.config.options.easing];
    for(i = 0; i < this.config.datas.length; ++i) {
        // Données de la catégorie
        datasCategory = this.config.datas[i];
        
        // Graphique
        dataRay = easing(this.frame, 0, ray * (datasCategory[0] - yAxisLimitMin) / (yAxisLimitMax - yAxisLimitMin), this.config.options.duration * 60);
        this.context.beginPath();
        this.context.moveTo(centerX + dataRay * Math.cos(270 * Math.PI / 180), centerY + dataRay * Math.sin(270 * Math.PI / 180));
        for(j = 0; j < datasCategory.length; ++j) {
            data = datasCategory[j];
            dataRay = easing(this.frame, 0, ray * (data - yAxisLimitMin) / (yAxisLimitMax - yAxisLimitMin), this.config.options.duration * 60);
            this.context.lineTo(centerX + dataRay * Math.cos(((270 + degreesAngle * j) % 360) * Math.PI / 180), centerY + dataRay * Math.sin(((270 + degreesAngle * j) % 360) * Math.PI / 180));
        }
        this.context.lineWidth = 2;
        this.context.strokeStyle = this.config.colors[i % this.config.colors.length];
        this.context.stroke();
        this.context.save();
        this.context.globalAlpha = 0.59765625;
        this.context.fillStyle = this.config.colors[i % this.config.colors.length];
        this.context.fill();
        this.context.restore();
    }
};

//-----------------------------------------------------------------------------
// Pie chart
SphinxChart2.prototype.drawPieFrame = function() {
    // Récupération de la hauteur du titre
    titleHeight = 0;
    if(this.config.title.length > 0) {
        titleHeight = this.bitmap.measureTextHeight(this.config.title);
    }
    
    // Taille de la police des axes
    this.bitmap.fontSize = 18;
    
    // Récupération de la police
    this.context.font = this.bitmap._makeFontNameText();
    
    // Calcul de la hauteur des catégories
    categoriesHeight = 0;
    for(category of this.config.categoriesLabels) {
        height = this.bitmap.measureTextHeight(category);
        categoriesHeight = Math.max(categoriesHeight, height);
    }
    
    // Ajout de labels manquants sur l'axe des ordonnées
    if(this.config.categoriesLabels.length < this.config.datas.length) {
        for(i = this.config.categoriesLabels.length + 1; i <= this.config.datas.length; ++i) {
            this.config.categoriesLabels.push("Category " + i);
        }
    } else if(this.config.categoriesLabels.length > this.config.datas.length) {
        this.config.categoriesLabels = this.config.categoriesLabels.slice(0, this.config.datas.length);
    }
    
    // Rayon et centre du camembert
    ray = Math.min(this.width - 40, this.height - titleHeight - categoriesHeight - 40) / 2;
    centerX = this.width / 2;
    centerY = titleHeight + ray + 20;
    
    // Tracé des données du graphique
    easing = Math.easing[this.config.options.easing];
    
    // Calcul du total des données
    sumDatas = 0;
    for(data of this.config.datas) {
        sumDatas += data;
    }
    
    // Dessin du camembert
    lastDegreesAngle = 270;
    for(i = 0; i < this.config.datas.length; ++i) {
        // Données de la catégorie
        data = this.config.datas[i];
        nextDegreesAngle = (lastDegreesAngle + easing(this.frame, 0, 360 * data / sumDatas, this.config.options.duration * 60)) % 360;
        
        // Graphique
        this.context.beginPath();
        this.context.moveTo(centerX, centerY);
        this.context.lineTo(centerX + ray * Math.cos(lastDegreesAngle * Math.PI / 180), centerY + ray * Math.sin(lastDegreesAngle * Math.PI / 180));
        this.context.arc(centerX, centerY, ray, lastDegreesAngle * Math.PI / 180, nextDegreesAngle * Math.PI / 180);
        this.context.lineWidth = 2;
        this.context.strokeStyle = this.config.colors[i % this.config.colors.length];
        this.context.stroke();
        this.context.save();
        this.context.globalAlpha = 0.59765625;
        this.context.fillStyle = this.config.colors[i % this.config.colors.length];
        this.context.fill();
        this.context.restore();
        
        // Décallage
        lastDegreesAngle = nextDegreesAngle;
    }
};

//-----------------------------------------------------------------------------
// Doughnut chart
SphinxChart2.prototype.drawDoughnutFrame = function() {
    // Récupération de la hauteur du titre
    titleHeight = 0;
    if(this.config.title.length > 0) {
        titleHeight = this.bitmap.measureTextHeight(this.config.title);
    }
    
    // Taille de la police des axes
    this.bitmap.fontSize = 18;
    
    // Récupération de la police
    this.context.font = this.bitmap._makeFontNameText();
    
    // Calcul de la hauteur des catégories
    categoriesHeight = 0;
    for(category of this.config.categoriesLabels) {
        height = this.bitmap.measureTextHeight(category);
        categoriesHeight = Math.max(categoriesHeight, height);
    }
    
    // Ajout de labels manquants sur l'axe des ordonnées
    if(this.config.categoriesLabels.length < this.config.datas.length) {
        for(i = this.config.categoriesLabels.length + 1; i <= this.config.datas.length; ++i) {
            this.config.categoriesLabels.push("Category " + i);
        }
    } else if(this.config.categoriesLabels.length > this.config.datas.length) {
        this.config.categoriesLabels = this.config.categoriesLabels.slice(0, this.config.datas.length);
    }
    
    // Rayon et centre du camembert
    ray = Math.min(this.width - 40, this.height - titleHeight - categoriesHeight - 40) / 2;
    centerX = this.width / 2;
    centerY = titleHeight + ray + 20;
    
    // Tracé des données du graphique
    easing = Math.easing[this.config.options.easing];
    
    // Calcul du total des données
    sumDatas = 0;
    for(data of this.config.datas) {
        sumDatas += data;
    }
    
    // Dessin du camembert
    lastDegreesAngle = 270;
    for(i = 0; i < this.config.datas.length; ++i) {
        // Données de la catégorie
        data = this.config.datas[i];
        nextDegreesAngle = (lastDegreesAngle + easing(this.frame, 0, 360 * data / sumDatas, this.config.options.duration * 60)) % 360;
        
        // Graphique
        this.context.beginPath();
        this.context.moveTo(centerX + ray / 2 * Math.cos(lastDegreesAngle * Math.PI / 180), centerY + ray / 2 * Math.sin(lastDegreesAngle * Math.PI / 180));
        this.context.lineTo(centerX + ray * Math.cos(lastDegreesAngle * Math.PI / 180), centerY + ray * Math.sin(lastDegreesAngle * Math.PI / 180));
        this.context.arc(centerX, centerY, ray, lastDegreesAngle * Math.PI / 180, nextDegreesAngle * Math.PI / 180);
        this.context.lineTo(centerX + ray / 2 * Math.cos(nextDegreesAngle * Math.PI / 180), centerY + ray / 2 * Math.sin(nextDegreesAngle * Math.PI / 180));
        this.context.arc(centerX, centerY, ray / 2, nextDegreesAngle * Math.PI / 180, lastDegreesAngle * Math.PI / 180, true);
        this.context.lineWidth = 2;
        this.context.strokeStyle = this.config.colors[i % this.config.colors.length];
        this.context.stroke();
        this.context.save();
        this.context.globalAlpha = 0.59765625;
        this.context.fillStyle = this.config.colors[i % this.config.colors.length];
        this.context.fill();
        this.context.restore();
        
        // Décallage
        lastDegreesAngle = nextDegreesAngle;
    }
};