1 /*
  2     Copyright 2008-2013
  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 
249                         if (yp >= 0) {
250                             // Static offset for label
251                             yp += fs * 0.5 / board.unitX;
252                         } else {
253                             // Static offset for label
254                             yp -= fs * strwidth / board.unitX;
255                         }
256                         xp1 -= fs * 0.2 / board.unitY;
257                         text = board.create('text', [yp, xp1, attributes.labels[i]].toString(), attr);
258                     }
259                 } else { // vertical bars
260                     p[0] = board.create('point', [xp0, 0], hiddenPoint);
261                     p[1] = board.create('point', [xp0, yp], hiddenPoint);
262                     p[2] = board.create('point', [xp2, yp], hiddenPoint);
263                     p[3] = board.create('point', [xp2, 0], hiddenPoint);
264 
265                     if (Type.exists(attributes.labels) && Type.exists(attributes.labels[i])) {
266                         strwidth = attributes.labels[i].toString().length;
267                         strwidth = 0.6 * strwidth * fs / board.unitX;
268 
269                         if (yp >= 0) {
270                             // Static offset for label
271                             yp += fs * 0.5 / board.unitY;
272                         } else {
273                             // Static offset for label
274                             yp -= fs / board.unitY;
275                         }
276                         text = board.create('text', [xp1 - strwidth * 0.5, yp, attributes.labels[i].toString()], attr);
277                     }
278                 }
279 
280                 attributes.withlines = false;
281 
282                 if (Type.isArray(attributes.colors)) {
283                     colors = attributes.colors;
284                     attributes.fillcolor = colors[i % colors.length];
285                 }
286 
287                 pols[i] = board.create('polygon', p, attributes);
288 
289                 if (Type.exists(attributes.labels) && Type.exists(attributes.labels[i])) {
290                     pols[i].text = text;
291                 }
292             }
293 
294             return pols;
295         },
296 
297         drawPoints: function (board, x, y, attributes) {
298             var i,
299                 points = [],
300                 infoboxArray = attributes.infoboxarray;
301 
302             attributes.fixed = true;
303             attributes.name = '';
304 
305             for (i = 0; i < x.length; i++) {
306                 attributes.infoboxtext = infoboxArray ? infoboxArray[i % infoboxArray.length] : false;
307                 points[i] = board.create('point', [x[i], y[i]], attributes);
308             }
309 
310             return points;
311         },
312 
313         drawPie: function (board, y, attributes) {
314             var i, center,
315                 p = [],
316                 sector = [],
317                 s = Statistics.sum(y),
318                 colorArray = attributes.colors,
319                 highlightColorArray = attributes.highlightcolors,
320                 labelArray = attributes.labels,
321                 r = attributes.radius || 4,
322                 radius = r,
323                 cent = attributes.center || [0, 0],
324                 xc = cent[0],
325                 yc = cent[1],
326 
327                 makeRadPointFun = function (j, fun, xc) {
328                     return function () {
329                         var s, t = 0, i, rad;
330 
331                         for (i = 0; i <= j; i++) {
332                             t += parseFloat(Type.evaluate(y[i]));
333                         }
334 
335                         s = t;
336                         for (i = j + 1; i < y.length; i++) {
337                             s += parseFloat(Type.evaluate(y[i]));
338                         }
339                         rad = (s !== 0) ? (2 * Math.PI * t / s) : 0;
340 
341                         return radius() * Math[fun](rad) + xc;
342                     };
343                 },
344 
345                 highlightHandleLabel = function (f, s) {
346                     var dx = -this.point1.coords.usrCoords[1] + this.point2.coords.usrCoords[1],
347                         dy = -this.point1.coords.usrCoords[2] + this.point2.coords.usrCoords[2];
348 
349                     if (Type.exists(this.label)) {
350                         this.label.rendNode.style.fontSize = (s * this.label.visProp.fontsize) + 'px';
351                         this.label.prepareUpdate().update().updateRenderer();
352                     }
353 
354                     this.point2.coords = new Coords(Const.COORDS_BY_USER, [
355                         this.point1.coords.usrCoords[1] + dx * f,
356                         this.point1.coords.usrCoords[2] + dy * f
357                     ], this.board);
358                     this.prepareUpdate().update().updateRenderer();
359                 },
360 
361                 highlightFun = function () {
362                     if (!this.highlighted) {
363                         this.highlighted = true;
364                         this.board.highlightedObjects[this.id] = this;
365                         this.board.renderer.highlight(this);
366 
367                         highlightHandleLabel.call(this, 1.1, 2);
368                     }
369                 },
370 
371                 noHighlightFun = function () {
372                     if (this.highlighted) {
373                         this.highlighted = false;
374                         this.board.renderer.noHighlight(this);
375 
376                         highlightHandleLabel.call(this, 0.90909090, 1);
377                     }
378                 },
379 
380                 hiddenPoint = {
381                     fixed: true,
382                     withLabel: false,
383                     visible: false,
384                     name: ''
385                 };
386 
387             if (!Type.isArray(labelArray)) {
388                 labelArray = [];
389                 for (i = 0; i < y.length; i++) {
390                     labelArray[i] = '';
391                 }
392             }
393 
394             if (!Type.isFunction(r)) {
395                 radius = function () {
396                     return r;
397                 };
398             }
399 
400             attributes.highlightonsector = attributes.highlightonsector || false;
401             attributes.straightfirst = false;
402             attributes.straightlast = false;
403 
404             center = board.create('point', [xc, yc], hiddenPoint);
405             p[0] = board.create('point', [
406                 function () {
407                     return radius() + xc;
408                 },
409                 function () {
410                     return yc;
411                 }
412             ], hiddenPoint);
413 
414             for (i = 0; i < y.length; i++) {
415                 p[i + 1] = board.create('point', [makeRadPointFun(i, 'cos', xc), makeRadPointFun(i, 'sin', yc)], hiddenPoint);
416 
417                 attributes.name = labelArray[i];
418                 attributes.withlabel = attributes.name !== '';
419                 attributes.fillcolor = colorArray && colorArray[i % colorArray.length];
420                 attributes.labelcolor = colorArray && colorArray[i % colorArray.length];
421                 attributes.highlightfillcolor = highlightColorArray && highlightColorArray[i % highlightColorArray.length];
422 
423                 sector[i] = board.create('sector', [center, p[i], p[i + 1]], attributes);
424 
425                 if (attributes.highlightonsector) {
426                     // overwrite hasPoint so that the whole sector is used for highlighting
427                     sector[i].hasPoint = sector[i].hasPointSector;
428                 }
429                 if (attributes.highlightbysize) {
430                     sector[i].highlight = highlightFun;
431 
432                     sector[i].noHighlight = noHighlightFun;
433                 }
434 
435             }
436 
437             // Not enough! We need points, but this gives an error in setAttribute.
438             return {sectors: sector, points: p, midpoint: center};
439         },
440 
441         /*
442          * labelArray=[ row1, row2, row3 ]
443          * paramArray=[ paramx, paramy, paramz ]
444          * parents=[[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]]
445          */
446         drawRadar: function (board, parents, attributes) {
447             var i, j, paramArray, numofparams, maxes, mins,
448                 la, pdata, ssa, esa, ssratio, esratio,
449                 sshifts, eshifts, starts, ends,
450                 labelArray, colorArray, highlightColorArray, radius, myAtts,
451                 cent, xc, yc, center, start_angle, rad, p, line, t,
452                 xcoord, ycoord, polygons, legend_position, circles, lxoff, lyoff,
453                 cla, clabelArray, ncircles, pcircles, angle, dr, sw, data,
454                 len = parents.length,
455 
456                 get_anchor = function () {
457                     var x1, x2, y1, y2,
458                         relCoords = this.visProp.label.offset.slice(0);
459 
460                     x1 = this.point1.X();
461                     x2 = this.point2.X();
462                     y1 = this.point1.Y();
463                     y2 = this.point2.Y();
464                     if (x2 < x1) {
465                         relCoords[0] = -relCoords[0];
466                     }
467 
468                     if (y2 < y1) {
469                         relCoords[1] = -relCoords[1];
470                     }
471 
472                     this.setLabelRelativeCoords(relCoords);
473 
474                     return new Coords(Const.COORDS_BY_USER, [this.point2.X(), this.point2.Y()], this.board);
475                 },
476 
477                 get_transform = function (angle, i) {
478                     var t, tscale, trot;
479 
480                     t = board.create('transform', [-(starts[i] - sshifts[i]), 0], {type: 'translate'});
481                     tscale = board.create('transform', [radius / ((ends[i] + eshifts[i]) - (starts[i] - sshifts[i])), 1], {type: 'scale'});
482                     t.melt(tscale);
483                     trot = board.create('transform', [angle], {type: 'rotate'});
484                     t.melt(trot);
485 
486                     return t;
487                 };
488 
489             if (len <= 0) {
490                 JXG.debug("No data");
491                 return;
492             }
493             // labels for axes
494             paramArray = attributes.paramarray;
495             if (!Type.exists(paramArray)) {
496                 JXG.debug("Need paramArray attribute");
497                 return;
498             }
499             numofparams = paramArray.length;
500             if (numofparams <= 1) {
501                 JXG.debug("Need more than 1 param");
502                 return;
503             }
504 
505             for (i = 0; i < len; i++) {
506                 if (numofparams !== parents[i].length) {
507                     JXG.debug("Use data length equal to number of params (" + parents[i].length + " != " + numofparams + ")");
508                     return;
509                 }
510             }
511 
512             maxes = [];
513             mins = [];
514 
515             for (j = 0; j < numofparams; j++) {
516                 maxes[j] = parents[0][j];
517                 mins[j] = maxes[j];
518             }
519 
520             for (i = 1; i < len; i++) {
521                 for (j = 0; j < numofparams; j++) {
522                     if (parents[i][j] > maxes[j]) {
523                         maxes[j] = parents[i][j];
524                     }
525 
526                     if (parents[i][j] < mins[j]) {
527                         mins[j] = parents[i][j];
528                     }
529                 }
530             }
531 
532             la = [];
533             pdata = [];
534 
535             for (i = 0; i < len; i++) {
536                 la[i] = '';
537                 pdata[i] = [];
538             }
539 
540             ssa = [];
541             esa = [];
542 
543             // 0 <= Offset from chart center <=1
544             ssratio = attributes.startshiftratio || 0;
545             // 0 <= Offset from chart radius <=1
546             esratio = attributes.endshiftratio || 0;
547 
548             for (i = 0; i < numofparams; i++) {
549                 ssa[i] = (maxes[i] - mins[i]) * ssratio;
550                 esa[i] = (maxes[i] - mins[i]) * esratio;
551             }
552 
553             // Adjust offsets per each axis
554             sshifts = attributes.startshiftarray || ssa;
555             eshifts = attributes.endshiftarray || esa;
556             // Values for inner circle, minimums by default
557             starts = attributes.startarray || mins;
558 
559             if (Type.exists(attributes.start)) {
560                 for (i = 0; i < numofparams; i++) {
561                     starts[i] = attributes.start;
562                 }
563             }
564 
565             // Values for outer circle, maximums by default
566             ends = attributes.endarray || maxes;
567             if (Type.exists(attributes.end)) {
568                 for (i = 0; i < numofparams; i++) {
569                     ends[i] = attributes.end;
570                 }
571             }
572 
573             if (sshifts.length !== numofparams) {
574                 JXG.debug("Start shifts length is not equal to number of parameters");
575                 return;
576             }
577 
578             if (eshifts.length !== numofparams) {
579                 JXG.debug("End shifts length is not equal to number of parameters");
580                 return;
581             }
582 
583             if (starts.length !== numofparams) {
584                 JXG.debug("Starts length is not equal to number of parameters");
585                 return;
586             }
587 
588             if (ends.length !== numofparams) {
589                 JXG.debug("Ends length is not equal to number of parameters");
590                 return;
591             }
592 
593             // labels for legend
594             labelArray = attributes.labelarray || la;
595             colorArray = attributes.colors;
596             highlightColorArray = attributes.highlightcolors;
597             radius = attributes.radius || 10;
598             sw = attributes.strokewidth || 1;
599 
600             if (!Type.exists(attributes.highlightonsector)) {
601                 attributes.highlightonsector = false;
602             }
603 
604             myAtts = {
605                 name: attributes.name,
606                 id: attributes.id,
607                 strokewidth: sw,
608                 polystrokewidth: attributes.polystrokewidth || sw,
609                 strokecolor: attributes.strokecolor || 'black',
610                 straightfirst: false,
611                 straightlast: false,
612                 fillcolor: attributes.fillColor || '#FFFF88',
613                 fillopacity: attributes.fillOpacity || 0.4,
614                 highlightfillcolor: attributes.highlightFillColor || '#FF7400',
615                 highlightstrokecolor: attributes.highlightStrokeColor || 'black',
616                 gradient: attributes.gradient || 'none'
617             };
618 
619             cent = attributes.center || [0, 0];
620             xc = cent[0];
621             yc = cent[1];
622             center = board.create('point', [xc, yc], {name: '', fixed: true, withlabel: false, visible: false});
623             start_angle = Math.PI / 2 - Math.PI / numofparams;
624             start_angle = attributes.startangle || 0;
625             rad = start_angle;
626             p = [];
627             line = [];
628 
629             for (i = 0; i < numofparams; i++) {
630                 rad += 2 * Math.PI / numofparams;
631                 xcoord = radius * Math.cos(rad) + xc;
632                 ycoord = radius * Math.sin(rad) + yc;
633 
634                 p[i] = board.create('point', [xcoord, ycoord], {name: '', fixed: true, withlabel: false, visible: false});
635                 line[i] = board.create('line', [center, p[i]], {
636                     name: paramArray[i],
637                     strokeColor: myAtts.strokecolor,
638                     strokeWidth: myAtts.strokewidth,
639                     strokeOpacity: 1.0,
640                     straightFirst: false,
641                     straightLast: false,
642                     withLabel: true,
643                     highlightStrokeColor: myAtts.highlightstrokecolor
644                 });
645                 line[i].getLabelAnchor = get_anchor;
646                 t = get_transform(rad, i);
647 
648                 for (j = 0; j < parents.length; j++) {
649                     data = parents[j][i];
650                     pdata[j][i] = board.create('point', [data, 0], {name: '', fixed: true, withlabel: false, visible: false});
651                     pdata[j][i].addTransform(pdata[j][i], t);
652                 }
653             }
654 
655             polygons = [];
656             for (i = 0; i < len; i++) {
657                 myAtts.labelcolor = colorArray && colorArray[i % colorArray.length];
658                 myAtts.strokecolor = colorArray && colorArray[i % colorArray.length];
659                 myAtts.fillcolor = colorArray && colorArray[i % colorArray.length];
660                 polygons[i] = board.create('polygon', pdata[i], {
661                     withLines: true,
662                     withLabel: false,
663                     fillColor: myAtts.fillcolor,
664                     fillOpacity: myAtts.fillopacity,
665                     highlightFillColor: myAtts.highlightfillcolor
666                 });
667 
668                 for (j = 0; j < numofparams; j++) {
669                     polygons[i].borders[j].setAttribute('strokecolor:' + colorArray[i % colorArray.length]);
670                     polygons[i].borders[j].setAttribute('strokewidth:' + myAtts.polystrokewidth);
671                 }
672             }
673 
674             legend_position = attributes.legendposition || 'none';
675             switch (legend_position) {
676             case 'right':
677                 lxoff = attributes.legendleftoffset || 2;
678                 lyoff = attributes.legendtopoffset || 1;
679 
680                 this.legend = board.create('legend', [xc + radius + lxoff, yc + radius - lyoff], {
681                     labels: labelArray,
682                     colors: colorArray
683                 });
684                 break;
685             case 'none':
686                 break;
687             default:
688                 JXG.debug('Unknown legend position');
689             }
690 
691             circles = [];
692             if (attributes.showcircles) {
693                 cla = [];
694                 for (i = 0; i < 6; i++) {
695                     cla[i] = 20 * i;
696                 }
697                 cla[0] = "0";
698                 clabelArray = attributes.circlelabelarray || cla;
699                 ncircles = clabelArray.length;
700 
701                 if (ncircles < 2) {
702                     JXG.debug("Too less circles");
703                     return;
704                 }
705 
706                 pcircles = [];
707                 angle = start_angle + Math.PI / numofparams;
708                 t = get_transform(angle, 0);
709 
710                 myAtts.fillcolor = 'none';
711                 myAtts.highlightfillcolor = 'none';
712                 myAtts.strokecolor = attributes.strokecolor || 'black';
713                 myAtts.strokewidth = attributes.circlestrokewidth || 0.5;
714                 myAtts.layer = 0;
715 
716                 // we have ncircles-1 intervals between ncircles circles
717                 dr = (ends[0] - starts[0]) / (ncircles - 1);
718 
719                 for (i = 0; i < ncircles; i++) {
720                     pcircles[i] = board.create('point', [starts[0] + i * dr, 0], {
721                         name: clabelArray[i],
722                         size: 0,
723                         fixed: true,
724                         withLabel: true,
725                         visible: true
726                     });
727                     pcircles[i].addTransform(pcircles[i], t);
728                     circles[i] = board.create('circle', [center, pcircles[i]], myAtts);
729                 }
730 
731             }
732             this.rendNode = polygons[0].rendNode;
733             return {
734                 circles: circles,
735                 lines: line,
736                 points: pdata,
737                 midpoint: center,
738                 polygons: polygons
739             };
740         },
741 
742         /**
743          * Then, the update function of the renderer
744          * is called.  Since a chart is only an abstract element,
745          * containing other elements, this function is empty.
746          */
747         updateRenderer: function () {
748             return this;
749         },
750 
751         /**
752          * Update of the defining points
753          */
754         update: function () {
755             if (this.needsUpdate) {
756                 this.updateDataArray();
757             }
758 
759             return this;
760         },
761 
762         /**
763          * For dynamic charts update
764          * can be used to compute new entries
765          * for the arrays this.dataX and
766          * this.dataY. It is used in @see update.
767          * Default is an empty method, can be overwritten
768          * by the user.
769          */
770         updateDataArray: function () {}
771     });
772 
773     JXG.createChart = function (board, parents, attributes) {
774         var data, row, i, j, col, charts = [], w, x, showRows, attr,
775             originalWidth, name, strokeColor, fillColor, hStrokeColor, hFillColor, len,
776             table = Env.isBrowser ? document.getElementById(parents[0]) : null;
777 
778         if ((parents.length === 1) && (typeof parents[0] === 'string')) {
779             if (Type.exists(table)) {
780                 // extract the data
781                 attr = Type.copyAttributes(attributes, board.options, 'chart');
782 
783                 table = (new DataSource()).loadFromTable(parents[0], attr.withheaders, attr.withheaders);
784                 data = table.data;
785                 col = table.columnHeaders;
786                 row = table.rowHeaders;
787 
788                 originalWidth = attr.width;
789                 name = attr.name;
790                 strokeColor = attr.strokecolor;
791                 fillColor = attr.fillcolor;
792                 hStrokeColor = attr.highlightstrokecolor;
793                 hFillColor = attr.highlightfillcolor;
794 
795                 board.suspendUpdate();
796 
797                 len = data.length;
798                 showRows = [];
799                 if (attr.rows && Type.isArray(attr.rows)) {
800                     for (i = 0; i < len; i++) {
801                         for (j = 0; j < attr.rows.length; j++) {
802                             if ((attr.rows[j] === i) || (attr.withheaders && attr.rows[j] === row[i])) {
803                                 showRows.push(data[i]);
804                                 break;
805                             }
806                         }
807                     }
808                 } else {
809                     showRows = data;
810                 }
811 
812                 len = showRows.length;
813 
814                 for (i = 0; i < len; i++) {
815 
816                     x = [];
817                     if (attr.chartstyle && attr.chartstyle.indexOf('bar') !== -1) {
818                         if (originalWidth) {
819                             w = originalWidth;
820                         } else {
821                             w = 0.8;
822                         }
823 
824                         x.push(1 - w / 2 + (i + 0.5) * w / len);
825 
826                         for (j = 1; j < showRows[i].length; j++) {
827                             x.push(x[j - 1] + 1);
828                         }
829 
830                         attr.width = w / len;
831                     }
832 
833                     if (name && name.length === len) {
834                         attr.name = name[i];
835                     } else if (attr.withheaders) {
836                         attr.name = col[i];
837                     }
838 
839                     if (strokeColor && strokeColor.length === len) {
840                         attr.strokecolor = strokeColor[i];
841                     } else {
842                         attr.strokecolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 0.6);
843                     }
844 
845                     if (fillColor && fillColor.length === len) {
846                         attr.fillcolor = fillColor[i];
847                     } else {
848                         attr.fillcolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 1.0);
849                     }
850 
851                     if (hStrokeColor && hStrokeColor.length === len) {
852                         attr.highlightstrokecolor = hStrokeColor[i];
853                     } else {
854                         attr.highlightstrokecolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 1.0);
855                     }
856 
857                     if (hFillColor && hFillColor.length === len) {
858                         attr.highlightfillcolor = hFillColor[i];
859                     } else {
860                         attr.highlightfillcolor = Color.hsv2rgb(((i + 1) / len) * 360, 0.9, 0.6);
861                     }
862 
863                     if (attr.chartstyle && attr.chartstyle.indexOf('bar') !== -1) {
864                         charts.push(new JXG.Chart(board, [x, showRows[i]], attr));
865                     } else {
866                         charts.push(new JXG.Chart(board, [showRows[i]], attr));
867                     }
868                 }
869 
870                 board.unsuspendUpdate();
871 
872             }
873             return charts;
874         }
875 
876         attr = Type.copyAttributes(attributes, board.options, 'chart');
877         return new JXG.Chart(board, parents, attr);
878     };
879 
880     JXG.registerElement('chart', JXG.createChart);
881 
882     /**
883      * Legend for chart
884      *
885      **/
886     JXG.Legend = function (board, coords, attributes) {
887         var attr;
888 
889         /* Call the constructor of GeometryElement */
890         this.constructor();
891 
892         attr = Type.copyAttributes(attributes, board.options, 'legend');
893 
894         this.board = board;
895         this.coords = new Coords(Const.COORDS_BY_USER, coords, this.board);
896         this.myAtts = {};
897         this.label_array = attr.labelarray || attr.labels;
898         this.color_array = attr.colorarray || attr.colors;
899         this.lines = [];
900         this.myAtts.strokewidth = attr.strokewidth || 5;
901         this.myAtts.straightfirst = false;
902         this.myAtts.straightlast = false;
903         this.myAtts.withlabel = true;
904         this.myAtts.fixed = true;
905         this.style = attr.legendstyle || attr.style;
906 
907         if (this.style === 'vertical') {
908             this.drawVerticalLegend(board, attr);
909         } else {
910             throw new Error('JSXGraph: Unknown legend style: ' + this.style);
911         }
912     };
913     JXG.Legend.prototype = new GeometryElement();
914 
915     JXG.Legend.prototype.drawVerticalLegend = function (board, attributes) {
916         var i,
917             line_length = attributes.linelength || 1,
918             offy = (attributes.rowheight || 20) / this.board.unitY,
919 
920             getLabelAnchor = function () {
921                 this.setLabelRelativeCoords(this.visProp.label.offset);
922                 return new Coords(Const.COORDS_BY_USER, [this.point2.X(), this.point2.Y()], this.board);
923             };
924 
925         for (i = 0; i < this.label_array.length; i++) {
926             this.myAtts.strokecolor = this.color_array[i];
927             this.myAtts.highlightstrokecolor = this.color_array[i];
928             this.myAtts.name = this.label_array[i];
929             this.myAtts.label = {
930                 offset: [10, 0],
931                 strokeColor: this.color_array[i],
932                 strokeWidth: this.myAtts.strokewidth
933             };
934 
935             this.lines[i] = board.create('line', [
936                 [this.coords.usrCoords[1], this.coords.usrCoords[2] - i * offy],
937                 [this.coords.usrCoords[1] + line_length, this.coords.usrCoords[2] - i * offy]],
938                 this.myAtts);
939 
940             this.lines[i].getLabelAnchor = getLabelAnchor;
941 
942         }
943     };
944 
945     JXG.createLegend = function (board, parents, attributes) {
946         //parents are coords of left top point of the legend
947         var start_from = [0, 0];
948 
949         if (Type.exists(parents)) {
950             if (parents.length === 2) {
951                 start_from = parents;
952             }
953         }
954 
955         return new JXG.Legend(board, start_from, attributes);
956     };
957     JXG.registerElement('legend', JXG.createLegend);
958 
959     return {
960         Chart: JXG.Chart,
961         Legend: JXG.Legend,
962         createChart: JXG.createChart,
963         createLegend: JXG.createLegend
964     };
965 });
966