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