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