      1 /*
      2     Copyright 2008-2015
      3         Matthias Ehmann,
      4         Michael Gerhaeuser,
      5         Carsten Miller,
      6         Bianca Valentin,
      7         Alfred Wassermann,
      8         Peter Wilfahrt
      9
     10     This file is part of JSXGraph.
     11
     12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
     13
     14     You can redistribute it and/or modify it under the terms of the
     15
     16       * GNU Lesser General Public License as published by
     17         the Free Software Foundation, either version 3 of the License, or
     18         (at your option) any later version
     19       OR
     20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
     21
     22     JSXGraph is distributed in the hope that it will be useful,
     23     but WITHOUT ANY WARRANTY; without even the implied warranty of
     24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     25     GNU Lesser General Public License for more details.
     26
     27     You should have received a copy of the GNU Lesser General Public License and
     28     the MIT License along with JSXGraph. If not, see <http://www.gnu.org/licenses/>
     29     and <http://opensource.org/licenses/MIT/>.
     30  */
     31
     32
     33 /*global JXG: true, define: true, document: true*/
     34 /*jslint nomen: true, plusplus: true*/
     35
     36 /* depends:
     37  jxg
     38  math/numerics
     39  math/statistics
     40  base/constants
     41  base/coords
     42  base/element
     43  parser/datasource
     44  utils/color
     45  utils/type
     46  utils/env
     47   elements:
     48    curve
     49    spline
     50    functiongraph
     51    point
     52    text
     53    polygon
     54    sector
     55    transform
     56    line
     57    legend
     58    circle
     59  */
     60
     61 define([
     62     'jxg', 'math/numerics', 'math/statistics', 'base/constants', 'base/coords', 'base/element', 'parser/datasource',
     63     'utils/color', 'utils/type', 'utils/env', 'base/curve', 'base/point', 'base/text', 'base/polygon', 'element/sector',
     64     'base/transformation', 'base/line', 'base/circle'
     65 ], function (JXG, Numerics, Statistics, Const, Coords, GeometryElement, DataSource, Color, Type, Env, Curve, Point, Text,
     66         Polygon, Sector, Transform, Line, Circle) {
     67
     68     "use strict";
     69
     70     /**
     71      * Chart plotting
     72      */
     73     JXG.Chart = function (board, parents, attributes) {
     74         this.constructor(board, attributes);
     75
     76         var x, y, i, c, style, len;
     77
     78         if (!Type.isArray(parents) || parents.length === 0) {
     79             throw new Error('JSXGraph: Can\'t create a chart without data');
     80         }
     81
     82         /**
     83          * Contains pointers to the various subelements of the chart.
     84          */
     85         this.elements = [];
     86
     87         if (Type.isNumber(parents[0])) {
     88             // parents looks like [a,b,c,..]
     89             // x has to be filled
     90
     91             y = parents;
     92             x = [];
     93             for (i = 0; i < y.length; i++) {
     94                 x[i] = i + 1;
     95             }
     96         } else if (parents.length === 1 && Type.isArray(parents[0])) {
     97             // parents looks like [[a,b,c,..]]
     98             // x has to be filled
     99
    100             y = parents[0];
    101             x = [];
    102
    103             len = Type.evaluate(y).length;
    104             for (i = 0; i < len; i++) {
    105                 x[i] = i + 1;
    106             }
    107         } else if (parents.length === 2) {
    108             // parents looks like [[x0,x1,x2,...],[y1,y2,y3,...]]
    109             len = Math.min(parents[0].length, parents[1].length);
    110             x = parents[0].slice(0, len);
    111             y = parents[1].slice(0, len);
    112         }
    113
    114         if (Type.isArray(y) && y.length === 0) {
    115             throw new Error('JSXGraph: Can\'t create charts without data.');
    116         }
    117
    118         // does this really need to be done here? this should be done in createChart and then
    119         // there should be an extra chart for each chartstyle
    120         style = attributes.chartstyle.replace(/ /g, '').split(',');
    121         for (i = 0; i < style.length; i++) {
    122             switch (style[i]) {
    123             case 'bar':
    124                 c = this.drawBar(board, x, y, attributes);
    125                 break;
    126             case 'line':
    127                 c = this.drawLine(board, x, y, attributes);
    128                 break;
    129             case 'fit':
    130                 c = this.drawFit(board, x, y, attributes);
    131                 break;
    132             case 'spline':
    133                 c = this.drawSpline(board, x, y, attributes);
    134                 break;
    135             case 'pie':
    136                 c = this.drawPie(board, y, attributes);
    137                 break;
    138             case 'point':
    139                 c = this.drawPoints(board, x, y, attributes);
    140                 break;
    141             case 'radar':
    142                 c = this.drawRadar(board, parents, attributes);
    143                 break;
    144             }
    145             this.elements.push(c);
    146         }
    147         this.id = this.board.setId(this, 'Chart');
    148
    149         return this.elements;
    150     };
    151     JXG.Chart.prototype = new GeometryElement();
    152
    153     JXG.extend(JXG.Chart.prototype, /** @lends JXG.Chart.prototype */ {
    154         drawLine: function (board, x, y, attributes) {
    155             // we don't want the line chart to be filled
    156             attributes.fillcolor = 'none';
    157             attributes.highlightfillcolor = 'none';
    158
    159             return board.create('curve', [x, y], attributes);
    160         },
    161
    162         drawSpline: function (board, x, y, attributes) {
    163             // we don't want the spline chart to be filled
    164             attributes.fillColor = 'none';
    165             attributes.highlightfillcolor = 'none';
    166
    167             return board.create('spline', [x, y], attributes);
    168         },
    169
    170         drawFit: function (board, x, y, attributes) {
    171             var deg = attributes.degree;
    172
    173             deg = Math.max(parseInt(deg, 10), 1) || 1;
    174
    175             // never fill
    176             attributes.fillcolor = 'none';
    177             attributes.highlightfillcolor = 'none';
    178
    179             return board.create('functiongraph', [Numerics.regressionPolynomial(deg, x, y)], attributes);
    180         },
    181
    182         drawBar: function (board, x, y, attributes) {
    183             var i, strwidth, fill, fs, text, w, xp0, xp1, xp2, yp, colors,
    184                 pols = [],
    185                 p = [],
    186                 attr,
    187
    188                 makeXpFun = function (i, f) {
    189                     return function () {
    190                         return x[i]() - f * w;
    191                     };
    192                 },
    193
    194                 hiddenPoint = {
    195                     fixed: true,
    196                     withLabel: false,
    197                     visible: false,
    198                     name: ''
    199                 };
    200
    201             if (!Type.exists(attributes.fillopacity)) {
    202                 attributes.fillopacity = 0.6;
    203             }
    204
    205             // Determine the width of the bars
    206             if (attributes && attributes.width) {  // width given
    207                 w = attributes.width;
    208             } else {
    209                 if (x.length <= 1) {
    210                     w = 1;
    211                 } else {
    212                     // Find minimum distance between to bars.
    213                     w = x[1] - x[0];
    214                     for (i = 1; i < x.length - 1; i++) {
    215                         w = (x[i + 1] - x[i] < w) ? x[i + 1] - x[i] : w;
    216                     }
    217                 }
    218                 w *= 0.8;
    219             }
    220
    221             fill = attributes.fillcolor;
    222
    223             attr = Type.copyAttributes(attributes, board.options, 'chart', 'label');
    224             fs = parseFloat(attr.fontsize);
    225
    226             for (i = 0; i < x.length; i++) {
    227                 if (Type.isFunction(x[i])) {
    228                     xp0 = makeXpFun(i, -0.5);
    229
    230                     xp1 = makeXpFun(i, 0);
    231
    232                     xp2 = makeXpFun(i, 0.5);
    233                 } else {
    234                     xp0 = x[i] - w * 0.5;
    235                     xp1 = x[i];
    236                     xp2 = x[i] + w * 0.5;
    237                 }
    238                 yp = y[i];
    239                 if (attributes.dir === 'horizontal') {  // horizontal bars
    240                     p[0] = board.create('point', [0, xp0], hiddenPoint);
    241                     p[1] = board.create('point', [yp, xp0], hiddenPoint);
    242                     p[2] = board.create('point', [yp, xp2], hiddenPoint);
    243                     p[3] = board.create('point', [0, xp2], hiddenPoint);
    244
    245                     if (Type.exists(attributes.labels) && Type.exists(attributes.labels[i])) {
    246                         strwidth = attributes.labels[i].toString().length;
    247                         strwidth = 2 * strwidth * fs / board.unitX;
    248                         if (yp >= 0) {
    249                             // Static offset for label
    250                             yp += fs * 0.5 / board.unitX;
    251                         } else {
    252                             // Static offset for label
    253                             yp -= fs * strwidth / board.unitX;
    254                         }
    255                         xp1 -= fs * 0.2 / board.unitY;
    256                         text = board.create('text', [yp, xp1, attributes.labels[i].toString()], attr);
    257                     }
    258                 } else { // vertical bars
    259                     p[0] = board.create('point', [xp0, 0], hiddenPoint);
    260                     p[1] = board.create('point', [xp0, yp], hiddenPoint);
    261                     p[2] = board.create('point', [xp2, yp], hiddenPoint);
    262                     p[3] = board.create('point', [xp2, 0], hiddenPoint);
    263
    264                     if (Type.exists(attributes.labels) && Type.exists(attributes.labels[i])) {
    265                         strwidth = attributes.labels[i].toString().length;
    266                         strwidth = 0.6 * strwidth * fs / board.unitX;
    267
    268                         if (yp >= 0) {
    269                             // Static offset for label
    270                             yp += fs * 0.5 / board.unitY;
    271                         } else {
    272                             // Static offset for label
    273                             yp -= fs / board.unitY;
    274                         }
    275                         text = board.create('text', [xp1 - strwidth * 0.5, yp, attributes.labels[i].toString()], attr);
    276                     }
    277                 }
    278
    279                 attributes.withlines = false;
    280
    281                 if (Type.isArray(attributes.colors)) {
    282                     colors = attributes.colors;
    283                     attributes.fillcolor = colors[i % colors.length];
    284                 }
    285
    286                 pols[i] = board.create('polygon', p, attributes);
    287                 if (Type.exists(attributes.labels) && Type.exists(attributes.labels[i])) {
    288                     pols[i].text = text;
    289                 }
    290             }
    291
    292             return pols;
    293         },
    294
    295         drawPoints: function (board, x, y, attributes) {
    296             var i,
    297                 points = [],
    298                 infoboxArray = attributes.infoboxarray;
    299
    300             attributes.fixed = true;
    301             attributes.name = '';
    302
    303             for (i = 0; i < x.length; i++) {
    304                 attributes.infoboxtext = infoboxArray ? infoboxArray[i % infoboxArray.length] : false;
    305                 points[i] = board.create('point', [x[i], y[i]], attributes);
    306             }
    307
    308             return points;
    309         },
    310
    311         drawPie: function (board, y, attributes) {
    312             var i, center,
    313                 p = [],
    314                 sector = [],
    315                 s = Statistics.sum(y),
    316                 colorArray = attributes.colors,
    317                 highlightColorArray = attributes.highlightcolors,
    318                 labelArray = attributes.labels,
    319                 r = attributes.radius || 4,
    320                 radius = r,
    321                 cent = attributes.center || [0, 0],
    322                 xc = cent[0],
    323                 yc = cent[1],
    324
    325                 makeRadPointFun = function (j, fun, xc) {
    326                     return function () {
    327                         var s, t = 0, i, rad;
    328
    329                         for (i = 0; i <= j; i++) {
    330                             t += parseFloat(Type.evaluate(y[i]));
    331                         }
    332
    333                         s = t;
    334                         for (i = j + 1; i < y.length; i++) {
    335                             s += parseFloat(Type.evaluate(y[i]));
    336                         }
    337                         rad = (s !== 0) ? (2 * Math.PI * t / s) : 0;
    338
    339                         return radius() * Math[fun](rad) + xc;
    340                     };
    341                 },
    342
    343                 highlightHandleLabel = function (f, s) {
    344                     var dx = -this.point1.coords.usrCoords[1] + this.point2.coords.usrCoords[1],
    345                         dy = -this.point1.coords.usrCoords[2] + this.point2.coords.usrCoords[2];
    346
    347                     if (Type.exists(this.label)) {
    348                         this.label.rendNode.style.fontSize = (s * this.label.visProp.fontsize) + 'px';
    349                         this.label.prepareUpdate().update().updateRenderer();
    350                     }
    351
    352                     this.point2.coords = new Coords(Const.COORDS_BY_USER, [
    353                         this.point1.coords.usrCoords[1] + dx * f,
    354                         this.point1.coords.usrCoords[2] + dy * f
    355                     ], this.board);
    356                     this.prepareUpdate().update().updateRenderer();
    357                 },
    358
    359                 highlightFun = function () {
    360                     if (!this.highlighted) {
    361                         this.highlighted = true;
    362                         this.board.highlightedObjects[this.id] = this;
    363                         this.board.renderer.highlight(this);
    364
    365                         highlightHandleLabel.call(this, 1.1, 2);
    366                     }
    367                 },
    368
    369                 noHighlightFun = function () {
    370                     if (this.highlighted) {
    371                         this.highlighted = false;
    372                         this.board.renderer.noHighlight(this);
    373
    374                         highlightHandleLabel.call(this, 0.90909090, 1);
    375                     }
    376                 },
    377
    378                 hiddenPoint = {
    379                     fixed: true,
    380                     withLabel: false,
    381                     visible: false,
    382                     name: ''
    383                 };
    384
    385             if (!Type.isArray(labelArray)) {
    386                 labelArray = [];
    387                 for (i = 0; i < y.length; i++) {
    388                     labelArray[i] = '';
    389                 }
    390             }
    391
    392             if (!Type.isFunction(r)) {
    393                 radius = function () {
    394                     return r;
    395                 };
    396             }
    397
    398             attributes.highlightonsector = attributes.highlightonsector || false;
    399             attributes.straightfirst = false;
    400             attributes.straightlast = false;
    401
    402             center = board.create('point', [xc, yc], hiddenPoint);
    403             p[0] = board.create('point', [
    404                 function () {
    405                     return radius() + xc;
    406                 },
    407                 function () {
    408                     return yc;
    409                 }
    410             ], hiddenPoint);
    411
    412             for (i = 0; i < y.length; i++) {
    413                 p[i + 1] = board.create('point', [makeRadPointFun(i, 'cos', xc), makeRadPointFun(i, 'sin', yc)], hiddenPoint);
    414
    415                 attributes.name = labelArray[i];
    416                 attributes.withlabel = attributes.name !== '';
    417                 attributes.fillcolor = colorArray && colorArray[i % colorArray.length];
    418                 attributes.labelcolor = colorArray && colorArray[i % colorArray.length];
    419                 attributes.highlightfillcolor = highlightColorArray && highlightColorArray[i % highlightColorArray.length];
    420
    421                 sector[i] = board.create('sector', [center, p[i], p[i + 1]], attributes);
    422
    423                 if (attributes.highlightonsector) {
    424                     // overwrite hasPoint so that the whole sector is used for highlighting
    425                     sector[i].hasPoint = sector[i].hasPointSector;
    426                 }
    427                 if (attributes.highlightbysize) {
    428                     sector[i].highlight = highlightFun;
    429
    430                     sector[i].noHighlight = noHighlightFun;
    431                 }
    432
    433             }
    434
    435             // Not enough! We need points, but this gives an error in setAttribute.
    436             return {sectors: sector, points: p, midpoint: center};
    437         },
    438
    439         /*
    440          * labelArray=[ row1, row2, row3 ]
    441          * paramArray=[ paramx, paramy, paramz ]
    442          * parents=[[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]]
    443          */
    444         drawRadar: function (board, parents, attributes) {
    445             var i, j, paramArray, numofparams, maxes, mins,
    446                 la, pdata, ssa, esa, ssratio, esratio,
    447                 sshifts, eshifts, starts, ends,
    448                 labelArray, colorArray, highlightColorArray, radius, myAtts,
    449                 cent, xc, yc, center, start_angle, rad, p, line, t,
    450                 xcoord, ycoord, polygons, legend_position, circles, lxoff, lyoff,
    451                 cla, clabelArray, ncircles, pcircles, angle, dr, sw, data,
    452                 len = parents.length,
    453
    454                 get_anchor = function () {
    455                     var x1, x2, y1, y2,
    456                         relCoords = this.visProp.label.offset.slice(0);
    457
    458                     x1 = this.point1.X();
    459                     x2 = this.point2.X();
    460                     y1 = this.point1.Y();
    461                     y2 = this.point2.Y();
    462                     if (x2 < x1) {
    463                         relCoords[0] = -relCoords[0];
    464                     }
    465
    466                     if (y2 < y1) {
    467                         relCoords[1] = -relCoords[1];
    468                     }
    469
    470                     this.setLabelRelativeCoords(relCoords);
    471
    472                     return new Coords(Const.COORDS_BY_USER, [this.point2.X(), this.point2.Y()], this.board);
    473                 },
    474
    475                 get_transform = function (angle, i) {
    476                     var t, tscale, trot;
    477
    478                     t = board.create('transform', [-(starts[i] - sshifts[i]), 0], {type: 'translate'});
    479                     tscale = board.create('transform', [radius / ((ends[i] + eshifts[i]) - (starts[i] - sshifts[i])), 1], {type: 'scale'});
    480                     t.melt(tscale);
    481                     trot = board.create('transform', [angle], {type: 'rotate'});
    482                     t.melt(trot);
    483
    484                     return t;
    485                 };
    486
    487             if (len <= 0) {
    488                 JXG.debug("No data");
    489                 return;
    490             }
    491             // labels for axes
    492             paramArray = attributes.paramarray;
    493             if (!Type.exists(paramArray)) {
    494                 JXG.debug("Need paramArray attribute");
    495                 return;
    496             }
    497             numofparams = paramArray.length;
    498             if (numofparams <= 1) {
    499                 JXG.debug("Need more than 1 param");
    500                 return;
    501             }
    502
    503             for (i = 0; i < len; i++) {
    504                 if (numofparams !== parents[i].length) {
    505                     JXG.debug("Use data length equal to number of params (" + parents[i].length + " != " + numofparams + ")");
    506                     return;
    507                 }
    508             }
    509
    510             maxes = [];
    511             mins = [];
    512
    513             for (j = 0; j < numofparams; j++) {
    514                 maxes[j] = parents[0][j];
    515                 mins[j] = maxes[j];
    516             }
    517
    518             for (i = 1; i < len; i++) {
    519                 for (j = 0; j < numofparams; j++) {
    520                     if (parents[i][j] > maxes[j]) {
    521                         maxes[j] = parents[i][j];
    522                     }
    523
    524                     if (parents[i][j] < mins[j]) {
    525                         mins[j] = parents[i][j];
    526                     }
    527                 }
    528             }
    529
    530             la = [];
    531             pdata = [];
    532
    533             for (i = 0; i < len; i++) {
    534                 la[i] = '';
    535                 pdata[i] = [];
    536             }
    537
    538             ssa = [];
    539             esa = [];
    540
    541             // 0 <= Offset from chart center <=1
    542             ssratio = attributes.startshiftratio || 0;
    543             // 0 <= Offset from chart radius <=1
    544             esratio = attributes.endshiftratio || 0;
    545
    546             for (i = 0; i < numofparams; i++) {
    547                 ssa[i] = (maxes[i] - mins[i]) * ssratio;
    548                 esa[i] = (maxes[i] - mins[i]) * esratio;
    549             }
    550
    551             // Adjust offsets per each axis
    552             sshifts = attributes.startshiftarray || ssa;
    553             eshifts = attributes.endshiftarray || esa;
    554             // Values for inner circle, minimums by default
    555             starts = attributes.startarray || mins;
    556
    557             if (Type.exists(attributes.start)) {
    558                 for (i = 0; i < numofparams; i++) {
    559                     starts[i] = attributes.start;
    560                 }
    561             }
    562
    563             // Values for outer circle, maximums by default
    564             ends = attributes.endarray || maxes;
    565             if (Type.exists(attributes.end)) {
    566                 for (i = 0; i < numofparams; i++) {
    567                     ends[i] = attributes.end;
    568                 }
    569             }
    570
    571             if (sshifts.length !== numofparams) {
    572                 JXG.debug("Start shifts length is not equal to number of parameters");
    573                 return;
    574             }
    575
    576             if (eshifts.length !== numofparams) {
    577                 JXG.debug("End shifts length is not equal to number of parameters");
    578                 return;
    579             }
    580
    581             if (starts.length !== numofparams) {
    582                 JXG.debug("Starts length is not equal to number of parameters");
    583                 return;
    584             }
    585
    586             if (ends.length !== numofparams) {
    587                 JXG.debug("Ends length is not equal to number of parameters");
    588                 return;
    589             }
    590
    591             // labels for legend
    592             labelArray = attributes.labelarray || la;
    593             colorArray = attributes.colors;
    594             highlightColorArray = attributes.highlightcolors;
    595             radius = attributes.radius || 10;
    596             sw = attributes.strokewidth || 1;
    597
    598             if (!Type.exists(attributes.highlightonsector)) {
    599                 attributes.highlightonsector = false;
    600             }
    601
    602             myAtts = {
    603                 name: attributes.name,
    604                 id: attributes.id,
    605                 strokewidth: sw,
    606                 polystrokewidth: attributes.polystrokewidth || sw,
    607                 strokecolor: attributes.strokecolor || 'black',
    608                 straightfirst: false,
    609                 straightlast: false,
    610                 fillcolor: attributes.fillColor || '#FFFF88',
    611                 fillopacity: attributes.fillOpacity || 0.4,
    612                 highlightfillcolor: attributes.highlightFillColor || '#FF7400',
    613                 highlightstrokecolor: attributes.highlightStrokeColor || 'black',
    614                 gradient: attributes.gradient || 'none'
    615             };
    616
    617             cent = attributes.center || [0, 0];
    618             xc = cent[0];
    619             yc = cent[1];
    620             center = board.create('point', [xc, yc], {name: '', fixed: true, withlabel: false, visible: false});
    621             start_angle = Math.PI / 2 - Math.PI / numofparams;
    622             start_angle = attributes.startangle || 0;
    623             rad = start_angle;
    624             p = [];
    625             line = [];
    626
    627             for (i = 0; i < numofparams; i++) {
    628                 rad += 2 * Math.PI / numofparams;
    629                 xcoord = radius * Math.cos(rad) + xc;
    630                 ycoord = radius * Math.sin(rad) + yc;
    631
    632                 p[i] = board.create('point', [xcoord, ycoord], {name: '', fixed: true, withlabel: false, visible: false});
    633                 line[i] = board.create('line', [center, p[i]], {
    634                     name: paramArray[i],
    635                     strokeColor: myAtts.strokecolor,
    636                     strokeWidth: myAtts.strokewidth,
    637                     strokeOpacity: 1.0,
    638                     straightFirst: false,
    639                     straightLast: false,
    640                     withLabel: true,
    641                     highlightStrokeColor: myAtts.highlightstrokecolor
    642                 });
    643                 line[i].getLabelAnchor = get_anchor;
    644                 t = get_transform(rad, i);
    645
    646                 for (j = 0; j < parents.length; j++) {
    647                     data = parents[j][i];
    648                     pdata[j][i] = board.create('point', [data, 0], {name: '', fixed: true, withlabel: false, visible: false});
    649                     pdata[j][i].addTransform(pdata[j][i], t);
    650                 }
    651             }
    652
    653             polygons = [];
    654             for (i = 0; i < len; i++) {
    655                 myAtts.labelcolor = colorArray && colorArray[i % colorArray.length];
    656                 myAtts.strokecolor = colorArray && colorArray[i % colorArray.length];
    657                 myAtts.fillcolor = colorArray && colorArray[i % colorArray.length];
    658                 polygons[i] = board.create('polygon', pdata[i], {
    659                     withLines: true,
    660                     withLabel: false,
    661                     fillColor: myAtts.fillcolor,
    662                     fillOpacity: myAtts.fillopacity,
    663                     highlightFillColor: myAtts.highlightfillcolor
    664                 });
    665
    666                 for (j = 0; j < numofparams; j++) {
    667                     polygons[i].borders[j].setAttribute('strokecolor:' + colorArray[i % colorArray.length]);
    668                     polygons[i].borders[j].setAttribute('strokewidth:' + myAtts.polystrokewidth);
    669                 }
    670             }
    671
    672             legend_position = attributes.legendposition || 'none';
    673             switch (legend_position) {
    674             case 'right':
    675                 lxoff = attributes.legendleftoffset || 2;
    676                 lyoff = attributes.legendtopoffset || 1;
    677
    678                 this.legend = board.create('legend', [xc + radius + lxoff, yc + radius - lyoff], {
    679                     labels: labelArray,
    680                     colors: colorArray
    681                 });
    682                 break;
    683             case 'none':
    684                 break;
    685             default:
    686                 JXG.debug('Unknown legend position');
    687             }
    688
    689             circles = [];
    690             if (attributes.showcircles) {
    691                 cla = [];
    692                 for (i = 0; i < 6; i++) {
    693                     cla[i] = 20 * i;
    694                 }
    695                 cla[0] = "0";
    696                 clabelArray = attributes.circlelabelarray || cla;
    697                 ncircles = clabelArray.length;
    698
    699                 if (ncircles < 2) {
    700                     JXG.debug("Too less circles");
    701                     return;
    702                 }
    703
    704                 pcircles = [];
    705                 angle = start_angle + Math.PI / numofparams;
    706                 t = get_transform(angle, 0);
    707
    708                 myAtts.fillcolor = 'none';
    709                 myAtts.highlightfillcolor = 'none';
    710                 myAtts.strokecolor = attributes.strokecolor || 'black';
    711                 myAtts.strokewidth = attributes.circlestrokewidth || 0.5;
    712                 myAtts.layer = 0;
    713
    714                 // we have ncircles-1 intervals between ncircles circles
    715                 dr = (ends[0] - starts[0]) / (ncircles - 1);
    716
    717                 for (i = 0; i < ncircles; i++) {
    718                     pcircles[i] = board.create('point', [starts[0] + i * dr, 0], {
    719                         name: clabelArray[i],
    720                         size: 0,
    721                         fixed: true,
    722                         withLabel: true,
    723                         visible: true
    724                     });
    725                     pcircles[i].addTransform(pcircles[i], t);
    726                     circles[i] = board.create('circle', [center, pcircles[i]], myAtts);
    727                 }
    728
    729             }
    730             this.rendNode = polygons[0].rendNode;
    731             return {
    732                 circles: circles,
    733                 lines: line,
    734                 points: pdata,
    735                 midpoint: center,
    736                 polygons: polygons
    737             };
    738         },
    739
    740         /**
    741          * Then, the update function of the renderer
    742          * is called.  Since a chart is only an abstract element,
    743          * containing other elements, this function is empty.
    744          */
    745         updateRenderer: function () {
    746             return this;
    747         },
    748
    749         /**
    750          * Update of the defining points
    751          */
    752         update: function () {
    753             if (this.needsUpdate) {
    754                 this.updateDataArray();
    755             }
    756
    757             return this;
    758         },
    759
    760         /**
    761          * For dynamic charts update
    762          * can be used to compute new entries
    763          * for the arrays this.dataX and
    764          * this.dataY. It is used in @see update.
    765          * Default is an empty method, can be overwritten
    766          * by the user.
    767          */
    768         updateDataArray: function () {}
    769     });
    770
    771     JXG.createChart = function (board, parents, attributes) {
    772         var data, row, i, j, col, charts = [], w, x, showRows, attr,
    773             originalWidth, name, strokeColor, fillColor, hStrokeColor, hFillColor, len,
    774             table = Env.isBrowser ? board.document.getElementById(parents[0]) : null;
    775
    776         if ((parents.length === 1) && (typeof parents[0] === 'string')) {
    777             if (Type.exists(table)) {
    778                 // extract the data
    779                 attr = Type.copyAttributes(attributes, board.options, 'chart');
    780
    781                 table = (new DataSource()).loadFromTable(parents[0], attr.withheaders, attr.withheaders);
    782                 data = table.data;
    783                 col = table.columnHeaders;
    784                 row = table.rowHeaders;
    785
    786                 originalWidth = attr.width;
    787                 name = attr.name;
    788                 strokeColor = attr.strokecolor;
    789                 fillColor = attr.fillcolor;
    790                 hStrokeColor = attr.highlightstrokecolor;
    791                 hFillColor = attr.highlightfillcolor;
    792
    793                 board.suspendUpdate();
    794
    795                 len = data.length;
    796                 showRows = [];
    797                 if (attr.rows && Type.isArray(attr.rows)) {
    798                     for (i = 0; i < len; i++) {
    799                         for (j = 0; j < attr.rows.length; j++) {
    800                             if ((attr.rows[j] === i) || (attr.withheaders && attr.rows[j] === row[i])) {
    801                                 showRows.push(data[i]);
    802                                 break;
    803                             }
    804                         }
    805                     }
    806                 } else {
    807                     showRows = data;
    808                 }
    809
    810                 len = showRows.length;
    811
    812                 for (i = 0; i < len; i++) {
    813
    814                     x = [];
    815                     if (attr.chartstyle && attr.chartstyle.indexOf('bar') !== -1) {
    816                         if (originalWidth) {
    817                             w = originalWidth;
    818                         } else {
    819                             w = 0.8;
    820                         }
    821
    822                         x.push(1 - w / 2 + (i + 0.5) * w / len);
    823
    824                         for (j = 1; j < showRows[i].length; j++) {
    825                             x.push(x[j - 1] + 1);
    826                         }
    827
    828                         attr.width = w / len;
    829                     }
    830
    831                     if (name && name.length === len) {
    832                         attr.name = name[i];
    833                     } else if (attr.withheaders) {
    834                         attr.name = col[i];
    835                     }
    836
    837                     if (strokeColor && strokeColor.length === len) {
    838                         attr.strokecolor = strokeColor[i];
    839                     } else {
    840                         attr.strokecolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 0.6);
    841                     }
    842
    843                     if (fillColor && fillColor.length === len) {
    844                         attr.fillcolor = fillColor[i];
    845                     } else {
    846                         attr.fillcolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 1.0);
    847                     }
    848
    849                     if (hStrokeColor && hStrokeColor.length === len) {
    850                         attr.highlightstrokecolor = hStrokeColor[i];
    851                     } else {
    852                         attr.highlightstrokecolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 1.0);
    853                     }
    854
    855                     if (hFillColor && hFillColor.length === len) {
    856                         attr.highlightfillcolor = hFillColor[i];
    857                     } else {
    858                         attr.highlightfillcolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 0.6);
    859                     }
    860
    861                     if (attr.chartstyle && attr.chartstyle.indexOf('bar') !== -1) {
    862                         charts.push(new JXG.Chart(board, [x, showRows[i]], attr));
    863                     } else {
    864                         charts.push(new JXG.Chart(board, [showRows[i]], attr));
    865                     }
    866                 }
    867
    868                 board.unsuspendUpdate();
    869
    870             }
    871             return charts;
    872         }
    873
    874         attr = Type.copyAttributes(attributes, board.options, 'chart');
    875         return new JXG.Chart(board, parents, attr);
    876     };
    877
    878     JXG.registerElement('chart', JXG.createChart);
    879
    880     /**
    881      * Legend for chart
    882      *
    883      **/
    884     JXG.Legend = function (board, coords, attributes) {
    885         var attr;
    886
    887         /* Call the constructor of GeometryElement */
    888         this.constructor();
    889
    890         attr = Type.copyAttributes(attributes, board.options, 'legend');
    891
    892         this.board = board;
    893         this.coords = new Coords(Const.COORDS_BY_USER, coords, this.board);
    894         this.myAtts = {};
    895         this.label_array = attr.labelarray || attr.labels;
    896         this.color_array = attr.colorarray || attr.colors;
    897         this.lines = [];
    898         this.myAtts.strokewidth = attr.strokewidth || 5;
    899         this.myAtts.straightfirst = false;
    900         this.myAtts.straightlast = false;
    901         this.myAtts.withlabel = true;
    902         this.myAtts.fixed = true;
    903         this.style = attr.legendstyle || attr.style;
    904
    905         if (this.style === 'vertical') {
    906             this.drawVerticalLegend(board, attr);
    907         } else {
    908             throw new Error('JSXGraph: Unknown legend style: ' + this.style);
    909         }
    910     };
    911     JXG.Legend.prototype = new GeometryElement();
    912
    913     JXG.Legend.prototype.drawVerticalLegend = function (board, attributes) {
    914         var i,
    915             line_length = attributes.linelength || 1,
    916             offy = (attributes.rowheight || 20) / this.board.unitY,
    917
    918             getLabelAnchor = function () {
    919                 this.setLabelRelativeCoords(this.visProp.label.offset);
    920                 return new Coords(Const.COORDS_BY_USER, [this.point2.X(), this.point2.Y()], this.board);
    921             };
    922
    923         for (i = 0; i < this.label_array.length; i++) {
    924             this.myAtts.strokecolor = this.color_array[i];
    925             this.myAtts.highlightstrokecolor = this.color_array[i];
    926             this.myAtts.name = this.label_array[i];
    927             this.myAtts.label = {
    928                 offset: [10, 0],
    929                 strokeColor: this.color_array[i],
    930                 strokeWidth: this.myAtts.strokewidth
    931             };
    932
    933             this.lines[i] = board.create('line', [
    934                 [this.coords.usrCoords[1], this.coords.usrCoords[2] - i * offy],
    935                 [this.coords.usrCoords[1] + line_length, this.coords.usrCoords[2] - i * offy]],
    936                 this.myAtts);
    937
    938             this.lines[i].getLabelAnchor = getLabelAnchor;
    939
    940         }
    941     };
    942
    943     JXG.createLegend = function (board, parents, attributes) {
    944         //parents are coords of left top point of the legend
    945         var start_from = [0, 0];
    946
    947         if (Type.exists(parents)) {
    948             if (parents.length === 2) {
    949                 start_from = parents;
    950             }
    951         }
    952
    953         return new JXG.Legend(board, start_from, attributes);
    954     };
    955     JXG.registerElement('legend', JXG.createLegend);
    956
    957     return {
    958         Chart: JXG.Chart,
    959         Legend: JXG.Legend,
    960         createChart: JXG.createChart,
    961         createLegend: JXG.createLegend
    962     };
    963 });
    964