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