initial commit
[home-automation.git] / libraries / RGraph.line.js
1     /**
2     * o------------------------------------------------------------------------------o
3     * | This file is part of the RGraph package - you can learn more at:             |
4     * |                                                                              |
5     * |                          http://www.rgraph.net                               |
6     * |                                                                              |
7     * | This package is licensed under the RGraph license. For all kinds of business |
8     * | purposes there is a small one-time licensing fee to pay and for non          |
9     * | commercial  purposes it is free to use. You can read the full license here:  |
10     * |                                                                              |
11     * |                      http://www.rgraph.net/LICENSE.txt                       |
12     * o------------------------------------------------------------------------------o
13     */
14     
15     if (typeof(RGraph) == 'undefined') RGraph = {};
16
17     /**
18     * The line chart constructor
19     * 
20     * @param object canvas The cxanvas object
21     * @param array  data   The chart data
22     * @param array  ...    Other lines to plot
23     */
24     RGraph.Line = function (id)
25     {
26         // Get the canvas and context objects
27         this.id      = id;
28         this.canvas  = document.getElementById(id);
29         this.context = this.canvas.getContext ? this.canvas.getContext("2d") : null;
30         this.canvas.__object__ = this;
31         this.type              = 'line';
32         this.max               = 0;
33         this.coords            = [];
34         this.hasnegativevalues = false;
35         this.isRGraph          = true;
36
37
38
39         /**
40         * Compatibility with older browsers
41         */
42         RGraph.OldBrowserCompat(this.context);
43
44
45         // Various config type stuff
46         this.properties = {
47             'chart.background.barcolor1':   'rgba(0,0,0,0)',
48             'chart.background.barcolor2':   'rgba(0,0,0,0)',
49             'chart.background.grid':        1,
50             'chart.background.grid.width':  1,
51             'chart.background.grid.hsize':  25,
52             'chart.background.grid.vsize':  25,
53             'chart.background.grid.color':  '#ddd',
54             'chart.background.grid.vlines': true,
55             'chart.background.grid.hlines': true,
56             'chart.background.grid.border': true,
57             'chart.background.grid.autofit':false,
58             'chart.background.grid.autofit.numhlines': 7,
59             'chart.background.grid.autofit.numvlines': 20,
60             'chart.background.hbars':       null,
61             'chart.labels':                 null,
62             'chart.labels.ingraph':         null,
63             'chart.labels.above':           false,
64             'chart.labels.above.size':      8,
65             'chart.xtickgap':               20,
66             'chart.smallxticks':            3,
67             'chart.largexticks':            5,
68             'chart.ytickgap':               20,
69             'chart.smallyticks':            3,
70             'chart.largeyticks':            5,
71             'chart.linewidth':              1,
72             'chart.colors':                 ['red', '#0f0', '#00f', '#f0f', '#ff0', '#0ff'],
73             'chart.hmargin':                0,
74             'chart.tickmarks.dot.color':    'white',
75             'chart.tickmarks':              null,
76             'chart.ticksize':               3,
77             'chart.gutter':                 25,
78             'chart.tickdirection':          -1,
79             'chart.yaxispoints':            5,
80             'chart.fillstyle':              null,
81             'chart.xaxispos':               'bottom',
82             'chart.yaxispos':               'left',
83             'chart.xticks':                 null,
84             'chart.text.size':              10,
85             'chart.text.angle':             0,
86             'chart.text.color':             'black',
87             'chart.text.font':              'Verdana',
88             'chart.ymin':                   null,
89             'chart.ymax':                   null,
90             'chart.title':                  '',
91             'chart.title.background':       null,
92             'chart.title.hpos':             null,
93             'chart.title.vpos':             null,
94             'chart.title.xaxis':            '',
95             'chart.title.yaxis':            '',
96             'chart.title.xaxis.pos':        0.25,
97             'chart.title.yaxis.pos':        0.25,
98             'chart.shadow':                 false,
99             'chart.shadow.offsetx':         2,
100             'chart.shadow.offsety':         2,
101             'chart.shadow.blur':            3,
102             'chart.shadow.color':           'rgba(0,0,0,0.5)',
103             'chart.tooltips':               null,
104             'chart.tooltips.effect':         'fade',
105             'chart.tooltips.css.class':      'RGraph_tooltip',
106             'chart.tooltips.coords.adjust':  [0,0],
107             'chart.tooltips.highlight':     true,
108             'chart.stepped':                false,
109
110             'chart.key':                    [],
111             'chart.key.background':         'white',
112             'chart.key.position':           'graph',
113             'chart.key.shadow':             false,
114             'chart.key.shadow.color':       '#666',
115             'chart.key.shadow.blur':        3,
116             'chart.key.shadow.offsetx':     2,
117             'chart.key.shadow.offsety':     2,
118             'chart.key.position.gutter.boxed': true,
119             'chart.key.position.x':         null,
120             'chart.key.position.y':         null,
121             'chart.key.color.shape':        'square',
122             'chart.key.rounded':            true,
123
124             'chart.contextmenu':            null,
125
126             'chart.ylabels':                true,
127             'chart.ylabels.count':          5,
128             'chart.ylabels.inside':         false,
129             'chart.ylabels.invert':         false,
130             'chart.ylabels.specific':       null,
131
132             'chart.xlabels.inside':         false,
133             'chart.xlabels.inside.color':   'rgba(255,255,255,0.5)',
134
135             'chart.noaxes':                 false,
136             'chart.noyaxis':                false,
137             'chart.noxaxis':                false,
138
139             'chart.noendxtick':             false,
140             'chart.units.post':             '',
141             'chart.units.pre':              '',
142             'chart.scale.decimals':         null,
143             'chart.scale.point':            '.',
144             'chart.scale.thousand':         ',',
145             'chart.crosshairs':             false,
146             'chart.crosshairs.color':       '#333',
147             'chart.annotatable':            false,
148             'chart.annotate.color':         'black',
149             'chart.axesontop':              false,
150             'chart.filled.range':           false,
151             'chart.variant':                null,
152             'chart.axis.color':             'black',
153             'chart.zoom.factor':            1.5,
154             'chart.zoom.fade.in':           true,
155             'chart.zoom.fade.out':          true,
156             'chart.zoom.hdir':              'right',
157             'chart.zoom.vdir':              'down',
158             'chart.zoom.frames':            15,
159             'chart.zoom.delay':             33,
160             'chart.zoom.shadow':            true,
161             'chart.zoom.mode':              'canvas',
162             'chart.zoom.thumbnail.width':   75,
163             'chart.zoom.thumbnail.height':  75,
164             'chart.zoom.background':        true,
165             'chart.zoom.action':            'zoom',
166             'chart.backdrop':               false,
167             'chart.backdrop.size':          30,
168             'chart.backdrop.alpha':         0.2,
169             'chart.resizable':              false,
170             'chart.adjustable':             false,
171             'chart.noredraw':               false,
172             'chart.outofbounds':            false,
173             'chart.chromefix':              true
174         }
175
176         /**
177         * Change null arguments to empty arrays
178         */
179         for (var i=1; i<arguments.length; ++i) {
180             if (typeof(arguments[i]) == 'null' || !arguments[i]) {
181                 arguments[i] = [];
182             }
183         }
184
185         // Check the common library has been included
186         if (typeof(RGraph) == 'undefined') {
187             alert('[LINE] Fatal error: The common library does not appear to have been included');
188         }
189
190
191         /**
192         * Store the original data. Thiss aqlso allows for giving arguments as one big array.
193         */
194         this.original_data = [];
195
196         for (var i=1; i<arguments.length; ++i) {
197             if (arguments[1] && typeof(arguments[1]) == 'object' && arguments[1][0] && typeof(arguments[1][0]) == 'object' && typeof(arguments[1][0][0]) == 'number') {
198
199                 var tmp = [];
200
201                 for (var i=0; i<arguments[1].length; ++i) {
202                     tmp[i] = RGraph.array_clone(arguments[1][i]);
203                 }
204
205                 for (var j=0; j<tmp.length; ++j) {
206                     this.original_data[j] = RGraph.array_clone(tmp[j]);
207                 }
208
209             } else {
210                 this.original_data[i - 1] = RGraph.array_clone(arguments[i]);
211             }
212         }
213
214         // Check for support
215         if (!this.canvas) {
216             alert('[LINE] Fatal error: no canvas support');
217             return;
218         }
219         
220         /**
221         * Store the data here as one big array
222         */
223         this.data_arr = [];
224
225         for (var i=1; i<arguments.length; ++i) {
226             for (var j=0; j<arguments[i].length; ++j) {
227                 this.data_arr.push(arguments[i][j]);
228             }
229         }
230     }
231
232
233     /**
234     * An all encompassing accessor
235     * 
236     * @param string name The name of the property
237     * @param mixed value The value of the property
238     */
239     RGraph.Line.prototype.Set = function (name, value)
240     {
241         // Consolidate the tooltips
242         if (name == 'chart.tooltips') {
243         
244             var tooltips = [];
245
246             for (var i=1; i<arguments.length; i++) {
247                 if (typeof(arguments[i]) == 'object' && arguments[i][0]) {
248                     for (var j=0; j<arguments[i].length; j++) {
249                         tooltips.push(arguments[i][j]);
250                     }
251
252                 } else if (typeof(arguments[i]) == 'function') {
253                     tooltips = arguments[i];
254
255                 } else {
256                     tooltips.push(arguments[i]);
257                 }
258             }
259
260             // Because "value" is used further down at the end of this function, set it to the expanded array os tooltips
261             value = tooltips;
262         }
263
264         /**
265         * Reverse the tickmarks to make them correspond to the right line
266         */
267         if (name == 'chart.tickmarks' && typeof(value) == 'object' && value) {
268             value = RGraph.array_reverse(value);
269         }
270         
271         /**
272         * Inverted Y axis should show the bottom end of the scale
273         */
274         if (name == 'chart.ylabels.invert' && value && this.Get('chart.ymin') == null) {
275             this.Set('chart.ymin', 0);
276         }
277         
278         /**
279         * If (buggy) Chrome and the linewidth is 1, change it to 1.01
280         */
281         if (name == 'chart.linewidth' && navigator.userAgent.match(/Chrome/) && value == 1) {
282             value = 1.01;
283         }
284
285         this.properties[name] = value;
286     }
287
288
289     /**
290     * An all encompassing accessor
291     * 
292     * @param string name The name of the property
293     */
294     RGraph.Line.prototype.Get = function (name)
295     {
296         return this.properties[name];
297     }
298
299
300     /**
301     * The function you call to draw the line chart
302     */
303     RGraph.Line.prototype.Draw = function ()
304     {
305         /**
306         * Fire the onbeforedraw event
307         */
308         RGraph.FireCustomEvent(this, 'onbeforedraw');
309
310         /**
311         * Clear all of this canvases event handlers (the ones installed by RGraph)
312         */
313         RGraph.ClearEventListeners(this.id);
314
315
316         /**
317         * Check for Chrome 6 and shadow
318         * 
319         * TODO Remove once it's been fixed (for a while)
320         * SEARCH TAGS: CHROME FIX SHADOW BUG
321         */
322         if (   this.Get('chart.shadow')
323             && navigator.userAgent.match(/Chrome/)
324             && this.Get('chart.linewidth') <= 1
325             && this.Get('chart.chromefix')
326             && this.Get('chart.shadow.blur') > 0) {
327                 alert('[RGRAPH WARNING] Chrome 6 has a shadow bug, meaning you should increase the linewidth to at least 1.01');
328         }
329
330
331         // Cache the gutter as an object variable
332         this.gutter = this.Get('chart.gutter');
333
334         // Reset the data back to that which was initially supplied
335         this.data = RGraph.array_clone(this.original_data);
336
337
338         // Reset the max value
339         this.max = 0;
340
341         /**
342         * Reverse the datasets so that the data and the labels tally
343         */
344         this.data = RGraph.array_reverse(this.data);
345
346         if (this.Get('chart.filled') && !this.Get('chart.filled.range') && this.data.length > 1) {
347         
348             var accumulation = [];
349         
350             for (var set=0; set<this.data.length; ++set) {
351                 for (var point=0; point<this.data[set].length; ++point) {
352                     this.data[set][point] = Number(accumulation[point] ? accumulation[point] : 0) + this.data[set][point];
353                     accumulation[point] = this.data[set][point];
354                 }
355             }
356         }
357
358         /**
359         * Get the maximum Y scale value
360         */
361         if (this.Get('chart.ymax')) {
362             
363             this.max = this.Get('chart.ymax');
364             this.min = this.Get('chart.ymin') ? this.Get('chart.ymin') : 0;
365
366             this.scale = [
367                           ( ((this.max - this.min) * (1/5)) + this.min).toFixed(this.Get('chart.scale.decimals')),
368                           ( ((this.max - this.min) * (2/5)) + this.min).toFixed(this.Get('chart.scale.decimals')),
369                           ( ((this.max - this.min) * (3/5)) + this.min).toFixed(this.Get('chart.scale.decimals')),
370                           ( ((this.max - this.min) * (4/5)) + this.min).toFixed(this.Get('chart.scale.decimals')),
371                           this.max.toFixed(this.Get('chart.scale.decimals'))
372                          ];
373
374             // Check for negative values
375             if (!this.Get('chart.outofbounds')) {
376                 for (dataset=0; dataset<this.data.length; ++dataset) {
377                     for (var datapoint=0; datapoint<this.data[dataset].length; datapoint++) {
378             
379                         // Check for negative values
380                         this.hasnegativevalues = (this.data[dataset][datapoint] < 0) || this.hasnegativevalues;
381                     }
382                 }
383             }
384
385         } else {
386
387             this.min = this.Get('chart.ymin') ? this.Get('chart.ymin') : 0;
388
389             // Work out the max Y value
390             for (dataset=0; dataset<this.data.length; ++dataset) {
391                 for (var datapoint=0; datapoint<this.data[dataset].length; datapoint++) {
392     
393                     this.max = Math.max(this.max, this.data[dataset][datapoint] ? Math.abs(parseFloat(this.data[dataset][datapoint])) : 0);
394     
395                     // Check for negative values
396                     if (!this.Get('chart.outofbounds')) {
397                         this.hasnegativevalues = (this.data[dataset][datapoint] < 0) || this.hasnegativevalues;
398                     }
399                 }
400             }
401
402             // 20th April 2009 - moved out of the above loop
403             this.scale = RGraph.getScale(Math.abs(parseFloat(this.max)), this);
404             this.max   = this.scale[4] ? this.scale[4] : 0;
405
406             if (this.Get('chart.ymin')) {
407                 this.scale[0] = ((this.max - this.Get('chart.ymin')) * (1/5)) + this.Get('chart.ymin');
408                 this.scale[1] = ((this.max - this.Get('chart.ymin')) * (2/5)) + this.Get('chart.ymin');
409                 this.scale[2] = ((this.max - this.Get('chart.ymin')) * (3/5)) + this.Get('chart.ymin');
410                 this.scale[3] = ((this.max - this.Get('chart.ymin')) * (4/5)) + this.Get('chart.ymin');
411                 this.scale[4] = ((this.max - this.Get('chart.ymin')) * (5/5)) + this.Get('chart.ymin');
412             }
413
414             if (typeof(this.Get('chart.scale.decimals')) == 'number') {
415                 this.scale[0] = Number(this.scale[0]).toFixed(this.Get('chart.scale.decimals'));
416                 this.scale[1] = Number(this.scale[1]).toFixed(this.Get('chart.scale.decimals'));
417                 this.scale[2] = Number(this.scale[2]).toFixed(this.Get('chart.scale.decimals'));
418                 this.scale[3] = Number(this.scale[3]).toFixed(this.Get('chart.scale.decimals'));
419                 this.scale[4] = Number(this.scale[4]).toFixed(this.Get('chart.scale.decimals'));
420             }
421         }
422
423         /**
424         * Setup the context menu if required
425         */
426         if (this.Get('chart.contextmenu')) {
427             RGraph.ShowContext(this);
428         }
429
430         /**
431         * Reset the coords array otherwise it will keep growing
432         */
433         this.coords = [];
434
435         /**
436         * Work out a few things. They need to be here because they depend on things you can change before you
437         * call Draw() but after you instantiate the object
438         */
439         this.grapharea      = this.canvas.height - ( (2 * this.gutter));
440         this.halfgrapharea  = this.grapharea / 2;
441         this.halfTextHeight = this.Get('chart.text.size') / 2;
442
443         // Check the combination of the X axis position and if there any negative values
444         //
445         // 19th Dec 2010 - removed for Opera since it can be reported incorrectly whn there
446         // are multiple graphs on the page
447         if (this.Get('chart.xaxispos') == 'bottom' && this.hasnegativevalues && navigator.userAgent.indexOf('Opera') == -1) {
448             alert('[LINE] You have negative values and the X axis is at the bottom. This is not good...');
449         }
450
451         if (this.Get('chart.variant') == '3d') {
452             RGraph.Draw3DAxes(this);
453         }
454         
455         // Progressively Draw the chart
456         RGraph.background.Draw(this);
457
458         /**
459         * Draw any horizontal bars that have been defined
460         */
461         if (this.Get('chart.background.hbars') && this.Get('chart.background.hbars').length > 0) {
462             RGraph.DrawBars(this);
463         }
464
465         if (this.Get('chart.axesontop') == false) {
466             this.DrawAxes();
467         }
468
469         /**
470         * Handle the appropriate shadow color. This now facilitates an array of differing
471         * shadow colors
472         */
473         var shadowColor = this.Get('chart.shadow.color');
474         
475         if (typeof(shadowColor) == 'object') {
476             shadowColor = RGraph.array_reverse(RGraph.array_clone(this.Get('chart.shadow.color')));
477         }
478
479         for (var i=(this.data.length - 1), j=0; i>=0; i--, j++) {
480
481             this.context.beginPath();
482
483             /**
484             * Turn on the shadow if required
485             */
486             if (this.Get('chart.shadow') && !this.Get('chart.filled')) {
487
488                 /**
489                 * Accommodate an array of shadow colors as well as a single string
490                 */
491                 if (typeof(shadowColor) == 'object' && shadowColor[i - 1]) {
492                     this.context.shadowColor = shadowColor[i];
493                 } else if (typeof(shadowColor) == 'object') {
494                     this.context.shadowColor = shadowColor[0];
495                 } else if (typeof(shadowColor) == 'string') {
496                     this.context.shadowColor = shadowColor;
497                 }
498
499                 this.context.shadowBlur    = this.Get('chart.shadow.blur');
500                 this.context.shadowOffsetX = this.Get('chart.shadow.offsetx');
501                 this.context.shadowOffsetY = this.Get('chart.shadow.offsety');
502             
503             } else if (this.Get('chart.filled') && this.Get('chart.shadow')) {
504                 alert('[LINE] Shadows are not permitted when the line is filled');
505             }
506
507             /**
508             * Draw the line
509             */
510
511             if (this.Get('chart.fillstyle')) {
512                 if (typeof(this.Get('chart.fillstyle')) == 'object' && this.Get('chart.fillstyle')[j]) {
513                    var fill = this.Get('chart.fillstyle')[j];
514                 
515                 } else if (typeof(this.Get('chart.fillstyle')) == 'string') {
516                     var fill = this.Get('chart.fillstyle');
517     
518                 } else {
519                     alert('[LINE] Warning: chart.fillstyle must be either a string or an array with the same number of elements as you have sets of data');
520                 }
521             } else if (this.Get('chart.filled')) {
522                 var fill = this.Get('chart.colors')[j];
523
524             } else {
525                 var fill = null;
526             }
527
528             /**
529             * Figure out the tickmark to use
530             */
531             if (this.Get('chart.tickmarks') && typeof(this.Get('chart.tickmarks')) == 'object') {
532                 var tickmarks = this.Get('chart.tickmarks')[i];
533             } else if (this.Get('chart.tickmarks') && typeof(this.Get('chart.tickmarks')) == 'string') {
534                 var tickmarks = this.Get('chart.tickmarks');
535             } else if (this.Get('chart.tickmarks') && typeof(this.Get('chart.tickmarks')) == 'function') {
536                 var tickmarks = this.Get('chart.tickmarks');
537             } else {
538                 var tickmarks = null;
539             }
540
541
542             this.DrawLine(this.data[i],
543                           this.Get('chart.colors')[j],
544                           fill,
545                           this.GetLineWidth(j),
546                            tickmarks);
547
548             this.context.stroke();
549         }
550
551
552
553
554
555
556
557
558
559
560
561
562         /**
563         * If tooltips are defined, handle them
564         */
565         if (this.Get('chart.tooltips') && (this.Get('chart.tooltips').length || typeof(this.Get('chart.tooltips')) == 'function')) {
566
567             // Need to register this object for redrawing
568             if (this.Get('chart.tooltips.highlight')) {
569                 RGraph.Register(this);
570             }
571
572             canvas_onmousemove_func = function (e)
573             {
574                 e = RGraph.FixEventObject(e);
575
576                 var canvas  = e.target;
577                 var context = canvas.getContext('2d');
578                 var obj     = canvas.__object__;
579                 var point   = obj.getPoint(e);
580
581                 if (obj.Get('chart.tooltips.highlight')) {
582                     RGraph.Register(obj);
583                 }
584
585                 if (   point
586                     && typeof(point[0]) == 'object'
587                     && typeof(point[1]) == 'number'
588                     && typeof(point[2]) == 'number'
589                     && typeof(point[3]) == 'number'
590                    ) {
591
592                     // point[0] is the graph object
593                     var xCoord = point[1];
594                     var yCoord = point[2];
595                     var idx    = point[3];
596
597                     if ((obj.Get('chart.tooltips')[idx] || typeof(obj.Get('chart.tooltips')) == 'function')) {
598
599                         // Get the tooltip text
600                         if (typeof(obj.Get('chart.tooltips')) == 'function') {
601                             var text = obj.Get('chart.tooltips')(idx);
602                         
603                         } else if (typeof(obj.Get('chart.tooltips')) == 'object' && typeof(obj.Get('chart.tooltips')[idx]) == 'function') {
604                             var text = obj.Get('chart.tooltips')[idx](idx);
605                         
606                         } else if (typeof(obj.Get('chart.tooltips')) == 'object') {
607                             var text = String(obj.Get('chart.tooltips')[idx]);
608
609                         } else {
610                             var text = '';
611                         }
612
613                         // Chnage the pointer to a hand
614                         canvas.style.cursor = 'pointer';
615
616                         /**
617                         * If the tooltip is the same one as is currently visible (going by the array index), don't do squat and return.
618                         */
619                         if (RGraph.Registry.Get('chart.tooltip') && RGraph.Registry.Get('chart.tooltip').__index__ == idx && RGraph.Registry.Get('chart.tooltip').__canvas__.id == canvas.id) {
620                             return;
621                         }
622
623                         /**
624                         * Redraw the graph
625                         */
626                         if (obj.Get('chart.tooltips.highlight')) {
627                            // Redraw the graph
628                             RGraph.Redraw();
629                         }
630     
631                         // SHOW THE CORRECT TOOLTIP
632                         RGraph.Tooltip(canvas, text, e.pageX, e.pageY, idx);
633                         
634                         // Store the tooltip index on the tooltip object
635                         RGraph.Registry.Get('chart.tooltip').__index__ = Number(idx);
636
637                         /**
638                         * Highlight the graph
639                         */
640                         if (obj.Get('chart.tooltips.highlight')) {
641                             context.beginPath();
642                             context.moveTo(xCoord, yCoord);
643                             context.arc(xCoord, yCoord, 2, 0, 6.28, 0);
644                             context.strokeStyle = '#999';
645                             context.fillStyle = 'white';
646                             context.stroke();
647                             context.fill();
648                         }
649                         
650                         e.stopPropagation();
651                         return;
652                     }
653                 }
654                 
655                 /**
656                 * Not over a hotspot?
657                 */
658                 canvas.style.cursor = 'default';
659             }
660             
661             this.canvas.addEventListener('mousemove', canvas_onmousemove_func, false);
662             RGraph.AddEventListener(this.id, 'mousemove', canvas_onmousemove_func);
663         }
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678         /**
679         * If the axes have been requested to be on top, do that
680         */
681         if (this.Get('chart.axesontop')) {
682             this.DrawAxes();
683         }
684
685         /**
686         * Draw the labels
687         */
688         this.DrawLabels();
689         
690         /**
691         * Draw the range if necessary
692         */
693         this.DrawRange();
694         
695         // Draw a key if necessary
696         if (this.Get('chart.key').length) {
697             RGraph.DrawKey(this, this.Get('chart.key'), this.Get('chart.colors'));
698         }
699
700         /**
701         * Draw " above" labels if enabled
702         */
703         if (this.Get('chart.labels.above')) {
704             this.DrawAboveLabels();
705         }
706
707         /**
708         * Draw the "in graph" labels
709         */
710         RGraph.DrawInGraphLabels(this);
711
712         /**
713         * Draw crosschairs
714         */
715         RGraph.DrawCrosshairs(this);
716         
717         /**
718         * If the canvas is annotatable, do install the event handlers
719         */
720         if (this.Get('chart.annotatable')) {
721             RGraph.Annotate(this);
722         }
723
724         /**
725         * Redraw the lines if a filled range is on the cards
726         */
727         if (this.Get('chart.filled') && this.Get('chart.filled.range') && this.data.length == 2) {
728
729             this.context.beginPath();
730             var len = this.coords.length / 2;
731             this.context.lineWidth = this.Get('chart.linewidth');
732             this.context.strokeStyle = this.Get('chart.colors')[0];
733
734             for (var i=0; i<len; ++i) {
735                 if (i == 0) {
736                     this.context.moveTo(this.coords[i][0], this.coords[i][1]);
737                 } else {
738                     this.context.lineTo(this.coords[i][0], this.coords[i][1]);
739                 }
740             }
741             
742             this.context.stroke();
743
744
745             this.context.beginPath();
746             
747             if (this.Get('chart.colors')[1]) {
748                 this.context.strokeStyle = this.Get('chart.colors')[1];
749             }
750             
751             for (var i=this.coords.length - 1; i>=len; --i) {
752                 if (i == (this.coords.length - 1) ) {
753                     this.context.moveTo(this.coords[i][0], this.coords[i][1]);
754                 } else {
755                     this.context.lineTo(this.coords[i][0], this.coords[i][1]);
756                 }
757             }
758             
759             this.context.stroke();
760         } else if (this.Get('chart.filled') && this.Get('chart.filled.range')) {
761             alert('[LINE] You must have only two sets of data for a filled range chart');
762         }
763
764         /**
765         * This bit shows the mini zoom window if requested
766         */
767         if (this.Get('chart.zoom.mode') == 'thumbnail') {
768             RGraph.ShowZoomWindow(this);
769         }
770
771         /**
772         * This function enables the zoom in area mode
773         */
774         if (this.Get('chart.zoom.mode') == 'area') {
775             RGraph.ZoomArea(this);
776         }
777         
778         /**
779         * This function enables resizing
780         */
781         if (this.Get('chart.resizable')) {
782             RGraph.AllowResizing(this);
783         }
784         
785         /**
786         * This function enables adjustments
787         */
788         if (this.Get('chart.adjustable')) {
789             RGraph.AllowAdjusting(this);
790         }
791         
792         /**
793         * Fire the RGraph ondraw event
794         */
795         RGraph.FireCustomEvent(this, 'ondraw');
796     }
797
798     
799     /**
800     * Draws the axes
801     */
802     RGraph.Line.prototype.DrawAxes = function ()
803     {
804         var gutter = this.gutter;
805
806         // Don't draw the axes?
807         if (this.Get('chart.noaxes')) {
808             return;
809         }
810         
811         // Turn any shadow off
812         RGraph.NoShadow(this);
813
814         this.context.lineWidth   = 1;
815         this.context.strokeStyle = this.Get('chart.axis.color');
816         this.context.beginPath();
817
818         // Draw the X axis
819         if (this.Get('chart.noxaxis') == false) {
820             if (this.Get('chart.xaxispos') == 'center') {
821                 this.context.moveTo(gutter, this.grapharea / 2 + gutter);
822                 this.context.lineTo(this.canvas.width - gutter, this.grapharea / 2 + gutter);
823             } else {
824                 this.context.moveTo(gutter, this.canvas.height - gutter);
825                 this.context.lineTo(this.canvas.width - gutter, this.canvas.height - gutter);
826             }
827         }
828         
829         // Draw the Y axis
830         if (this.Get('chart.noyaxis') == false) {
831             if (this.Get('chart.yaxispos') == 'left') {
832                 this.context.moveTo(gutter, gutter);
833                 this.context.lineTo(gutter, this.canvas.height - (gutter) );
834             } else {
835                 this.context.moveTo(this.canvas.width - gutter, gutter);
836                 this.context.lineTo(this.canvas.width - gutter, this.canvas.height - gutter );
837             }
838         }
839
840         /**
841         * Draw the X tickmarks
842         */
843         if (this.Get('chart.noxaxis') == false) {
844             var xTickInterval = (this.canvas.width - (2 * gutter)) / (this.Get('chart.xticks') ? this.Get('chart.xticks') : this.data[0].length);
845     
846             for (x=gutter + (this.Get('chart.yaxispos') == 'left' ? xTickInterval : 0); x<=(this.canvas.width - gutter + 1 ); x+=xTickInterval) {
847     
848                 if (this.Get('chart.yaxispos') == 'right' && x >= (this.canvas.width - gutter - 1) ) {
849                     break;
850                 }
851                 
852                 // If the last tick is not desired...
853                 if (this.Get('chart.noendxtick')) {
854                     if (this.Get('chart.yaxispos') == 'left' && x >= (this.canvas.width - gutter)) {
855                         break;
856                     } else if (this.Get('chart.yaxispos') == 'right' && x == gutter) {
857                         continue;
858                     }
859                 }
860     
861                 var yStart = this.Get('chart.xaxispos') == 'center' ? (this.canvas.height / 2) - 3 : this.canvas.height - gutter;
862                 var yEnd = this.Get('chart.xaxispos') == 'center' ? yStart + 6 : this.canvas.height - gutter - (x % 60 == 0 ? this.Get('chart.largexticks') * this.Get('chart.tickdirection') : this.Get('chart.smallxticks') * this.Get('chart.tickdirection'));
863     
864                 this.context.moveTo(x, yStart);
865                 this.context.lineTo(x, yEnd);
866             }
867         
868         // Draw an extra tickmark if there is no X axis, but there IS a Y axis
869         } else if (this.Get('chart.noyaxis') == false) {
870
871             if (this.Get('chart.yaxispos') == 'left') {
872                 this.context.moveTo(this.Get('chart.gutter'), this.canvas.height - this.Get('chart.gutter'));
873                 this.context.lineTo(this.Get('chart.gutter') - this.Get('chart.smallyticks'), this.canvas.height - this.Get('chart.gutter'));
874             } else {
875                 this.context.moveTo(this.canvas.width - this.Get('chart.gutter'), this.canvas.height - this.Get('chart.gutter'));
876                 this.context.lineTo(this.canvas.width - this.Get('chart.gutter') + this.Get('chart.smallyticks'), this.canvas.height - this.Get('chart.gutter'));
877             }
878         }
879
880         /**
881         * Draw the Y tickmarks
882         */
883         if (this.Get('chart.noyaxis') == false) {
884             var counter    = 0;
885             var adjustment = 0;
886     
887             if (this.Get('chart.yaxispos') == 'right') {
888                 adjustment = (this.canvas.width - (2 * gutter));
889             }
890     
891             if (this.Get('chart.xaxispos') == 'center') {
892                 var interval = (this.grapharea / 10);
893                 var lineto = (this.Get('chart.yaxispos') == 'left' ? gutter : this.canvas.width - gutter + this.Get('chart.smallyticks'));
894     
895                 // Draw the upper halves Y tick marks
896                 for (y=gutter; y < (this.grapharea / 2) + gutter; y+=interval) {
897                     this.context.moveTo((this.Get('chart.yaxispos') == 'left' ? gutter - this.Get('chart.smallyticks') : this.canvas.width - gutter), y);
898                     this.context.lineTo(lineto, y);
899                 }
900                 
901                 // Draw the lower halves Y tick marks
902                 for (y=gutter + (this.halfgrapharea) + interval; y <= this.grapharea + gutter; y+=interval) {
903                     this.context.moveTo((this.Get('chart.yaxispos') == 'left' ? gutter - this.Get('chart.smallyticks') : this.canvas.width - gutter), y);
904                     this.context.lineTo(lineto, y);
905                 }
906     
907             } else {
908                 var lineto = (this.Get('chart.yaxispos') == 'left' ? gutter - this.Get('chart.smallyticks') : this.canvas.width - gutter + this.Get('chart.smallyticks'));
909     
910                 for (y=gutter; y < (this.canvas.height - gutter) && counter < 10; y+=( (this.canvas.height - (2 * gutter)) / 10) ) {
911     
912                     this.context.moveTo(gutter + adjustment, y);
913                     this.context.lineTo(lineto, y);
914                 
915                     var counter = counter +1;
916                 }
917             }
918         
919         // Draw an extra X tickmark
920         } else if (this.Get('chart.noxaxis') == false) {
921             if (this.Get('chart.yaxispos') == 'left') {
922                 this.context.moveTo(this.Get('chart.gutter'), this.canvas.height - this.Get('chart.gutter'));
923                 this.context.lineTo(this.Get('chart.gutter'), this.canvas.height - this.Get('chart.gutter') + this.Get('chart.smallxticks'));
924             } else {
925                 this.context.moveTo(this.canvas.width - this.Get('chart.gutter'), this.canvas.height - this.Get('chart.gutter'));
926                 this.context.lineTo(this.canvas.width - this.Get('chart.gutter'), this.canvas.height - this.Get('chart.gutter') + this.Get('chart.smallxticks'));
927             }
928         }
929
930         this.context.stroke();
931     }
932
933
934     /**
935     * Draw the text labels for the axes
936     */
937     RGraph.Line.prototype.DrawLabels = function ()
938     {
939         this.context.strokeStyle = 'black';
940         this.context.fillStyle   = this.Get('chart.text.color');
941         this.context.lineWidth   = 1;
942         
943         // Turn off any shadow
944         RGraph.NoShadow(this);
945
946         // This needs to be here
947         var font      = this.Get('chart.text.font');
948         var gutter    = this.Get('chart.gutter');
949         var text_size = this.Get('chart.text.size');
950         var context   = this.context;
951         var canvas    = this.canvas;
952
953         // Draw the Y axis labels
954         if (this.Get('chart.ylabels') && this.Get('chart.ylabels.specific') == null) {
955
956             var units_pre  = this.Get('chart.units.pre');
957             var units_post = this.Get('chart.units.post');
958             var xpos       = this.Get('chart.yaxispos') == 'left' ? gutter - 5 : this.canvas.width - gutter + 5;
959             var align      = this.Get('chart.yaxispos') == 'left' ? 'right' : 'left';
960             
961             var numYLabels = this.Get('chart.ylabels.count');
962             var bounding   = false;
963             var bgcolor    = this.Get('chart.ylabels.inside') ? this.Get('chart.ylabels.inside.color') : null;
964
965             
966             /**
967             * If the Y labels are inside the Y axis, invert the alignment
968             */
969             if (this.Get('chart.ylabels.inside') == true && align == 'left') {
970                 xpos -= 10;
971                 align = 'right';
972                 bounding = true;
973                 
974
975             } else if (this.Get('chart.ylabels.inside') == true && align == 'right') {
976                 xpos += 10;
977                 align = 'left';
978                 bounding = true;
979             }
980
981
982
983             if (this.Get('chart.xaxispos') == 'center') {
984                 var half = this.grapharea / 2;
985
986                 if (numYLabels == 1 || numYLabels == 3 || numYLabels == 5) {
987                     //  Draw the upper halves labels
988                     RGraph.Text(context, font, text_size, xpos, gutter + ( (0/5) * half ) + this.halfTextHeight, RGraph.number_format(this, this.scale[4], units_pre, units_post), null, align, bounding, null, bgcolor);
989     
990                     if (numYLabels == 5) {
991                         RGraph.Text(context, font, text_size, xpos, gutter + ( (1/5) * half ) + this.halfTextHeight, RGraph.number_format(this, this.scale[3], units_pre, units_post), null, align, bounding, null, bgcolor);
992                         RGraph.Text(context, font, text_size, xpos, gutter + ( (3/5) * half ) + this.halfTextHeight, RGraph.number_format(this, this.scale[1], units_pre, units_post), null, align, bounding, null, bgcolor);
993                     }
994     
995                     if (numYLabels >= 3) {
996                         RGraph.Text(context, font, text_size, xpos, gutter + ( (2/5) * half ) + this.halfTextHeight, RGraph.number_format(this, this.scale[2], units_pre, units_post), null, align, bounding, null, bgcolor);
997                         RGraph.Text(context, font, text_size, xpos, gutter + ( (4/5) * half ) + this.halfTextHeight, RGraph.number_format(this, this.scale[0], units_pre, units_post), null, align, bounding, null, bgcolor);
998                     }
999                     
1000                     //  Draw the lower halves labels
1001                     if (numYLabels >= 3) {
1002                         RGraph.Text(context, font, text_size, xpos, gutter + ( (6/5) * half ) + this.halfTextHeight, '-' + RGraph.number_format(this, this.scale[0], units_pre, units_post), null, align, bounding, null, bgcolor);
1003                         RGraph.Text(context, font, text_size, xpos, gutter + ( (8/5) * half ) + this.halfTextHeight, '-' + RGraph.number_format(this, this.scale[2], units_pre, units_post), null, align, bounding, null, bgcolor);
1004                     }
1005     
1006                     if (numYLabels == 5) {
1007                         RGraph.Text(context, font, text_size, xpos, gutter + ( (7/5) * half ) + this.halfTextHeight, '-' + RGraph.number_format(this, this.scale[1], units_pre, units_post), null, align, bounding, null, bgcolor);
1008                         RGraph.Text(context, font, text_size, xpos, gutter + ( (9/5) * half ) + this.halfTextHeight, '-' + RGraph.number_format(this, this.scale[3], units_pre, units_post), null, align, bounding, null, bgcolor);
1009                     }
1010     
1011                     RGraph.Text(context, font, text_size, xpos, gutter + ( (10/5) * half ) + this.halfTextHeight, '-' + RGraph.number_format(this, (this.scale[4] == '1.0' ? '1.0' : this.scale[4]), units_pre, units_post), null, align, bounding, null, bgcolor);
1012                 
1013                 } else if (numYLabels == 10) {
1014
1015                     // 10 Y labels
1016                     var interval = (this.grapharea / numYLabels) / 2;
1017                 
1018                     for (var i=0; i<numYLabels; ++i) {
1019                         // This draws the upper halves labels
1020                         RGraph.Text(context,font, text_size, xpos, gutter + this.halfTextHeight + ((i/20) * (this.grapharea) ), RGraph.number_format(this, ((this.scale[4] / numYLabels) * (numYLabels - i)).toFixed((this.Get('chart.scale.decimals'))),units_pre, units_post), null, align, bounding, null, bgcolor);
1021                         
1022                         // And this draws the lower halves labels
1023                         RGraph.Text(context, font, text_size, xpos,
1024                         
1025                         gutter + this.halfTextHeight + ((i/20) * this.grapharea) + (this.grapharea / 2) + (this.grapharea / 20),
1026                         
1027                         '-' + RGraph.number_format(this, (this.scale[4] - ((this.scale[4] / numYLabels) * (numYLabels - i - 1))).toFixed((this.Get('chart.scale.decimals'))),units_pre, units_post), null, align, bounding, null, bgcolor);
1028                     }
1029                     
1030                 } else {
1031                     alert('[LINE SCALE] The number of Y labels must be 1/3/5/10');
1032                 }
1033
1034                 // Draw the lower limit if chart.ymin is specified
1035                 if (typeof(this.Get('chart.ymin')) == 'number') {
1036                     RGraph.Text(context, font, text_size, xpos, this.canvas.height / 2, RGraph.number_format(this, this.Get('chart.ymin').toFixed(this.Get('chart.scale.decimals')), units_pre, units_post), 'center', align, bounding, null, bgcolor);
1037                 }
1038                 
1039                 // No X axis - so draw 0
1040                 if (this.Get('chart.noxaxis') == true) {
1041                     RGraph.Text(context,font,text_size,xpos,gutter + ( (5/5) * half ) + this.halfTextHeight,'0',null, align, bounding, null, bgcolor);
1042                 }
1043
1044             } else {
1045
1046                 /**
1047                 * Accommodate reversing the Y labels
1048                 */
1049                 if (this.Get('chart.ylabels.invert')) {
1050                     this.scale = RGraph.array_reverse(this.scale);
1051                     this.context.translate(0, this.grapharea * 0.2);
1052                     if (typeof(this.Get('chart.ymin')) == null) {
1053                         this.Set('chart.ymin', 0);
1054                     }
1055                 }
1056
1057                 if (numYLabels == 1 || numYLabels == 3 || numYLabels == 5) {
1058                     RGraph.Text(context, font, text_size, xpos, gutter + this.halfTextHeight + ((0/5) * (this.grapharea ) ), RGraph.number_format(this, this.scale[4], units_pre, units_post), null, align, bounding, null, bgcolor);
1059     
1060                     if (numYLabels == 5) {
1061                         RGraph.Text(context, font, text_size, xpos, gutter + this.halfTextHeight + ((3/5) * (this.grapharea) ), RGraph.number_format(this, this.scale[1], units_pre, units_post), null, align, bounding, null, bgcolor);
1062                         RGraph.Text(context, font, text_size, xpos, gutter + this.halfTextHeight + ((1/5) * (this.grapharea) ), RGraph.number_format(this, this.scale[3], units_pre, units_post), null, align, bounding, null, bgcolor);
1063                     }
1064     
1065                     if (numYLabels >= 3) {
1066                         RGraph.Text(context, font, text_size, xpos, gutter + this.halfTextHeight + ((2/5) * (this.grapharea ) ), RGraph.number_format(this, this.scale[2], units_pre, units_post), null, align, bounding, null, bgcolor);
1067                         RGraph.Text(context, font, text_size, xpos, gutter + this.halfTextHeight + ((4/5) * (this.grapharea) ), RGraph.number_format(this, this.scale[0], units_pre, units_post), null, align, bounding, null, bgcolor);
1068                     }
1069                 
1070                 } else if (numYLabels == 10) {
1071
1072                     // 10 Y labels
1073                     var interval = (this.grapharea / numYLabels) / 2;
1074                 
1075                     for (var i=0; i<numYLabels; ++i) {
1076                         RGraph.Text(context,font,text_size,xpos,gutter + this.halfTextHeight + ((i/10) * (this.grapharea) ),RGraph.number_format(this,((this.scale[4] / numYLabels) * (numYLabels - i)).toFixed((this.Get('chart.scale.decimals'))),units_pre,units_post),null,align,bounding,null,bgcolor);
1077                     }
1078
1079                 } else {
1080                     alert('[LINE SCALE] The number of Y labels must be 1/3/5/10');
1081                 }
1082
1083
1084                 /**
1085                 * Accommodate translating back after reversing the labels
1086                 */
1087                 if (this.Get('chart.ylabels.invert')) {
1088                     this.context.translate(0, 0 - (this.grapharea * 0.2));
1089                 }
1090
1091                 // Draw the lower limit if chart.ymin is specified
1092                 if (typeof(this.Get('chart.ymin')) == 'number') {
1093                     RGraph.Text(context,font,text_size,xpos,this.Get('chart.ylabels.invert') ? gutter : this.canvas.height - gutter,RGraph.number_format(this, this.Get('chart.ymin').toFixed(this.Get('chart.scale.decimals')), units_pre, units_post),'center',align,bounding,null,bgcolor);
1094                 }
1095             }
1096
1097             // No X axis - so draw 0
1098             if (   this.Get('chart.noxaxis') == true
1099                 && this.Get('chart.ymin') == null
1100                ) {
1101
1102                 RGraph.Text(context,font,text_size,xpos,this.canvas.height - gutter + this.halfTextHeight,'0',null, align, bounding, null, bgcolor);
1103             }
1104         
1105         } else if (this.Get('chart.ylabels') && typeof(this.Get('chart.ylabels.specific')) == 'object') {
1106             
1107             // A few things
1108             var gap      = this.grapharea / this.Get('chart.ylabels.specific').length;
1109             var halign   = this.Get('chart.yaxispos') == 'left' ? 'right' : 'left';
1110             var bounding = false;
1111             var bgcolor  = null;
1112             
1113             // Figure out the X coord based on the position of the axis
1114             if (this.Get('chart.yaxispos') == 'left') {
1115                 var x = gutter - 5;
1116                 
1117                 if (this.Get('chart.ylabels.inside')) {
1118                     x += 10;
1119                     halign   = 'left';
1120                     bounding = true;
1121                     bgcolor  = 'rgba(255,255,255,0.5)';
1122                 }
1123
1124             } else if (this.Get('chart.yaxispos') == 'right') {
1125                 var x = this.canvas.width - gutter + 5;
1126                 
1127                 if (this.Get('chart.ylabels.inside')) {
1128                     x -= 10;
1129                     halign = 'right';
1130                     bounding = true;
1131                     bgcolor  = 'rgba(255,255,255,0.5)';
1132                 }
1133             }
1134
1135
1136             // Draw the labels
1137             if (this.Get('chart.xaxispos') == 'center') {
1138             
1139                 // Draw the top halfs labels
1140                 for (var i=0; i<this.Get('chart.ylabels.specific').length; ++i) {
1141                     var y = gutter + ((this.grapharea / (this.Get('chart.ylabels.specific').length * 2) ) * i);
1142                     RGraph.Text(context, font, text_size,x,y,String(this.Get('chart.ylabels.specific')[i]), 'center', halign, bounding, 0, bgcolor);
1143                 }
1144                 
1145                 // Now reverse the labels and draw the bottom half
1146                 var reversed_labels = RGraph.array_reverse(this.Get('chart.ylabels.specific'));
1147             
1148                 // Draw the bottom halfs labels
1149                 for (var i=0; i<reversed_labels.length; ++i) {
1150                     var y = (this.grapharea / 2) + gutter + ((this.grapharea / (reversed_labels.length * 2) ) * (i + 1));
1151                     RGraph.Text(context, font, text_size,x,y,String(reversed_labels[i]), 'center', halign, bounding, 0, bgcolor);
1152                 }
1153
1154             } else {
1155                 for (var i=0; i<this.Get('chart.ylabels.specific').length; ++i) {
1156                     var y = gutter + ((this.grapharea / this.Get('chart.ylabels.specific').length) * i);
1157                     RGraph.Text(context, font, text_size,x,y,String(this.Get('chart.ylabels.specific')[i]), 'center', halign, bounding, 0, bgcolor);
1158                 }
1159             }
1160         }
1161
1162         // Draw the X axis labels
1163         if (this.Get('chart.labels') && this.Get('chart.labels').length > 0) {
1164
1165             var yOffset  = 13;
1166             var bordered = false;
1167             var bgcolor  = null;
1168
1169             if (this.Get('chart.xlabels.inside')) {
1170                 yOffset = -5;
1171                 bordered = true;
1172                 bgcolor  = this.Get('chart.xlabels.inside.color');
1173             }
1174
1175             /**
1176             * Text angle
1177             */
1178             var angle  = 0;
1179             var valign = null;
1180             var halign = 'center';
1181
1182             if (typeof(this.Get('chart.text.angle')) == 'number' && this.Get('chart.text.angle') > 0) {
1183                 angle   = -1 * this.Get('chart.text.angle');
1184                 valign  = 'center';
1185                 halign  = 'right';
1186                 yOffset = 5
1187             }
1188
1189             this.context.fillStyle = this.Get('chart.text.color');
1190             var numLabels = this.Get('chart.labels').length;
1191
1192             for (i=0; i<numLabels; ++i) {
1193
1194                 // Changed 8th Nov 2010 to be not reliant on the coords
1195                 //if (this.Get('chart.labels')[i] && this.coords && this.coords[i] && this.coords[i][0]) {
1196                 if (this.Get('chart.labels')[i]) {
1197
1198                     var labelX = ((this.canvas.width - (2 * this.Get('chart.gutter')) - (2 * this.Get('chart.hmargin'))) / (numLabels - 1) ) * i;
1199                         labelX += this.Get('chart.gutter') + this.Get('chart.hmargin');
1200
1201                     /**
1202                     * Account for an unrelated number of labels
1203                     */
1204                     if (this.Get('chart.labels').length != this.data[0].length) {
1205                         labelX = this.gutter + this.Get('chart.hmargin') + ((this.canvas.width - (2 * this.gutter) - (2 * this.Get('chart.hmargin'))) * (i / (this.Get('chart.labels').length - 1)));
1206                     }
1207                     
1208                     // This accounts for there only being one point on the chart
1209                     if (!labelX) {
1210                         labelX = this.gutter + this.Get('chart.hmargin');
1211                     }
1212
1213                     RGraph.Text(context, font,text_size,labelX,(this.canvas.height - gutter) + yOffset,String(this.Get('chart.labels')[i]),valign,halign,bordered,angle,bgcolor);
1214                 }
1215             }
1216
1217         }
1218
1219         this.context.stroke();
1220         this.context.fill();
1221
1222     }
1223
1224
1225     /**
1226     * Draws the line
1227     */
1228     RGraph.Line.prototype.DrawLine = function (lineData, color, fill, linewidth, tickmarks)
1229     {
1230         var penUp = false;
1231         var yPos  = 0;
1232         var xPos  = 0;
1233         this.context.lineWidth = 1;
1234         var lineCoords = [];
1235         var gutter     = this.Get('chart.gutter');
1236
1237         // Work out the X interval
1238         var xInterval = (this.canvas.width - (2 * this.Get('chart.hmargin')) - ( (2 * this.gutter)) ) / (lineData.length - 1);
1239
1240         // Loop thru each value given, plotting the line
1241         for (i=0; i<lineData.length; i++) {
1242
1243             yPos  = this.canvas.height - ( ( (lineData[i] - (lineData[i] > 0 ?  this.Get('chart.ymin') : (-1 * this.Get('chart.ymin')) ) ) / (this.max - this.min) ) * ((this.canvas.height - (2 * this.gutter)) ));
1244
1245             if (this.Get('chart.ylabels.invert')) {
1246                 yPos -= gutter;
1247                 yPos -= gutter;
1248                 yPos = this.canvas.height - yPos;
1249             }
1250
1251             // Make adjustments depending on the X axis position
1252             if (this.Get('chart.xaxispos') == 'center') {
1253                 yPos /= 2;
1254             } else if (this.Get('chart.xaxispos') == 'bottom') {
1255                 yPos -= this.gutter; // Without this the line is out of place due to the gutter
1256             }
1257             
1258             // Null data points
1259             if (lineData[i] == null) {
1260                 yPos = null;
1261             }
1262
1263             // Not always very noticeable, but it does have an effect
1264             // with thick lines
1265             this.context.lineCap  = 'round';
1266             this.context.lineJoin = 'round';
1267
1268             // Plot the line if we're at least on the second iteration
1269             if (i > 0) {
1270                 xPos = xPos + xInterval;
1271             } else {
1272                 xPos = this.Get('chart.hmargin') + gutter; // Might need to be this.gutter - 27th August 2010
1273             }
1274
1275             /**
1276             * Add the coords to an array
1277             */
1278             this.coords.push([xPos, yPos]);
1279             lineCoords.push([xPos, yPos]);
1280         }
1281
1282         this.context.stroke();
1283
1284         /**
1285         * For IE only: Draw the shadow ourselves as ExCanvas doesn't produce shadows
1286         */
1287         if (RGraph.isIE8() && this.Get('chart.shadow')) {
1288             this.DrawIEShadow(lineCoords, this.context.shadowColor);
1289         }
1290
1291         /**
1292         * Now draw the actual line [FORMERLY SECOND]
1293         */
1294         this.context.beginPath();
1295         this.context.strokeStyle = 'rgba(240,240,240,0.9)'; // Almost transparent - changed on 10th May 2010
1296         //this.context.strokeStyle = fill;
1297         if (fill) this.context.fillStyle   = fill;
1298
1299         var isStepped = this.Get('chart.stepped');
1300         var isFilled = this.Get('chart.filled');
1301
1302
1303         for (var i=0; i<lineCoords.length; ++i) {
1304
1305             xPos = lineCoords[i][0];
1306             yPos = lineCoords[i][1];
1307
1308             var prevY     = (lineCoords[i - 1] ? lineCoords[i - 1][1] : null);
1309             var isLast    = (i + 1) == lineCoords.length;
1310
1311             /**
1312             * This nullifys values which are out-of-range
1313             */
1314             if (prevY < this.Get('chart.gutter') || prevY > (this.canvas.height - this.Get('chart.gutter')) ) {
1315                 penUp = true;
1316             }
1317
1318             if (i == 0 || penUp || !yPos || !prevY || prevY < this.gutter) {
1319                 if (this.Get('chart.filled') && !this.Get('chart.filled.range')) {
1320                     this.context.moveTo(xPos + 1, this.canvas.height - this.gutter - (this.Get('chart.xaxispos') == 'center' ? (this.canvas.height - (2 * this.gutter)) / 2 : 0) -1);
1321                     this.context.lineTo(xPos + 1, yPos);
1322
1323                 } else {
1324                     this.context.moveTo(xPos, yPos);
1325                 }
1326                 
1327                 penUp = false;
1328
1329             } else {
1330
1331                 // Draw the stepped part of stepped lines
1332                 if (isStepped) {
1333                     this.context.lineTo(xPos, lineCoords[i - 1][1]);
1334                 }
1335
1336                 if ((yPos >= this.gutter && yPos <= (this.canvas.height - this.gutter)) || this.Get('chart.outofbounds')) {
1337
1338                     if (isLast && this.Get('chart.filled') && !this.Get('chart.filled.range') && this.Get('chart.yaxispos') == 'right') {
1339                         xPos -= 1;
1340                     }
1341
1342
1343                     // Added 8th September 2009
1344                     if (!isStepped || !isLast) {
1345                         this.context.lineTo(xPos, yPos);
1346                         
1347                         if (isFilled && lineCoords[i+1] && lineCoords[i+1][1] == null) {
1348                             this.context.lineTo(xPos, this.canvas.height - this.gutter);
1349                         }
1350                     
1351                     // Added August 2010
1352                     } else if (isStepped && isLast) {
1353                         this.context.lineTo(xPos,yPos);
1354                     }
1355
1356
1357                     penUp = false;
1358                 } else {
1359                     penUp = true;
1360                 }
1361             }
1362         }
1363
1364         if (this.Get('chart.filled') && !this.Get('chart.filled.range')) {
1365             var fillStyle = this.Get('chart.fillstyle');
1366
1367             this.context.lineTo(xPos, this.canvas.height - this.gutter - 1 -  + (this.Get('chart.xaxispos') == 'center' ? (this.canvas.height - (2 * this.gutter)) / 2 : 0));
1368             this.context.fillStyle = fill;
1369
1370             this.context.fill();
1371             this.context.beginPath();
1372         }
1373
1374         /**
1375         * FIXME this may need removing when Chrome is fixed
1376         * SEARCH TAGS: CHROME SHADOW BUG
1377         */
1378         if (navigator.userAgent.match(/Chrome/) && this.Get('chart.shadow') && this.Get('chart.chromefix') && this.Get('chart.shadow.blur') > 0) {
1379
1380             for (var i=lineCoords.length - 1; i>=0; --i) {
1381                 if (
1382                        typeof(lineCoords[i][1]) != 'number'
1383                     || (typeof(lineCoords[i+1]) == 'object' && typeof(lineCoords[i+1][1]) != 'number')
1384                    ) {
1385                     this.context.moveTo(lineCoords[i][0],lineCoords[i][1]);
1386                 } else {
1387                     this.context.lineTo(lineCoords[i][0],lineCoords[i][1]);
1388                 }
1389             }
1390         }
1391
1392         this.context.stroke();
1393
1394
1395         if (this.Get('chart.backdrop')) {
1396             this.DrawBackdrop(lineCoords, color);
1397         }
1398
1399         // Now redraw the lines with the correct line width
1400         this.RedrawLine(lineCoords, color, linewidth);
1401         
1402         this.context.stroke();
1403
1404         // Draw the tickmarks
1405         for (var i=0; i<lineCoords.length; ++i) {
1406
1407             i = Number(i);
1408
1409             if (isStepped && i == (lineCoords.length - 1)) {
1410                 this.context.beginPath();
1411                 //continue;
1412             }
1413
1414             if (
1415                 (
1416                     tickmarks != 'endcircle'
1417                  && tickmarks != 'endsquare'
1418                  && tickmarks != 'filledendsquare'
1419                  && tickmarks != 'endtick'
1420                  && tickmarks != 'arrow'
1421                  && tickmarks != 'filledarrow'
1422                 )
1423                 || (i == 0 && tickmarks != 'arrow' && tickmarks != 'filledarrow')
1424                 || i == (lineCoords.length - 1)
1425                ) {
1426
1427                 var prevX = (i <= 0 ? null : lineCoords[i - 1][0]);
1428                 var prevY = (i <= 0 ? null : lineCoords[i - 1][1]);
1429
1430                 this.DrawTick(lineData, lineCoords[i][0], lineCoords[i][1], color, false, prevX, prevY, tickmarks, i);
1431
1432                 // Draws tickmarks on the stepped bits of stepped charts. Takend out 14th July 2010
1433                 //
1434                 //if (this.Get('chart.stepped') && lineCoords[i + 1] && this.Get('chart.tickmarks') != 'endsquare' && this.Get('chart.tickmarks') != 'endcircle' && this.Get('chart.tickmarks') != 'endtick') {
1435                 //    this.DrawTick(lineCoords[i + 1][0], lineCoords[i][1], color);
1436                 //}
1437             }
1438         }
1439
1440         // Draw something off canvas to skirt an annoying bug
1441         this.context.beginPath();
1442         this.context.arc(this.canvas.width + 50, this.canvas.height + 50, 2, 0, 6.38, 1);
1443     }
1444     
1445     
1446     /**
1447     * This functions draws a tick mark on the line
1448     * 
1449     * @param xPos  int  The x position of the tickmark
1450     * @param yPos  int  The y position of the tickmark
1451     * @param color str  The color of the tickmark
1452     * @param       bool Whether the tick is a shadow. If it is, it gets offset by the shadow offset
1453     */
1454     RGraph.Line.prototype.DrawTick = function (lineData, xPos, yPos, color, isShadow, prevX, prevY, tickmarks, index)
1455     {
1456         var gutter = this.Get('chart.gutter');
1457
1458         // If the yPos is null - no tick
1459         if ((yPos == null || yPos > (this.canvas.height - gutter) || yPos < gutter) && !this.Get('chart.outofbounds')) {
1460             return;
1461         }
1462
1463         this.context.beginPath();
1464
1465         var offset   = 0;
1466
1467         // Reset the stroke and lineWidth back to the same as what they were when the line was drawm
1468         this.context.lineWidth   = this.Get('chart.linewidth');
1469         this.context.strokeStyle = isShadow ? this.Get('chart.shadow.color') : this.context.strokeStyle;
1470         this.context.fillStyle   = isShadow ? this.Get('chart.shadow.color') : this.context.strokeStyle;
1471
1472         // Cicular tick marks
1473         if (   tickmarks == 'circle'
1474             || tickmarks == 'filledcircle'
1475             || tickmarks == 'endcircle') {
1476
1477             if (tickmarks == 'circle'|| tickmarks == 'filledcircle' || (tickmarks == 'endcircle') ) {
1478                 this.context.beginPath();
1479                 this.context.arc(xPos + offset, yPos + offset, this.Get('chart.ticksize'), 0, 360 / (180 / Math.PI), false);
1480
1481                 if (tickmarks == 'filledcircle') {
1482                     this.context.fillStyle = isShadow ? this.Get('chart.shadow.color') : this.context.strokeStyle;
1483                 } else {
1484                     this.context.fillStyle = isShadow ? this.Get('chart.shadow.color') : 'white';
1485                 }
1486
1487                 this.context.fill();
1488                 this.context.stroke();
1489             }
1490
1491         // Halfheight "Line" style tick marks
1492         } else if (tickmarks == 'halftick') {
1493             this.context.beginPath();
1494             this.context.moveTo(xPos, yPos);
1495             this.context.lineTo(xPos, yPos + this.Get('chart.ticksize'));
1496
1497             this.context.stroke();
1498         
1499         // Tick style tickmarks
1500         } else if (tickmarks == 'tick') {
1501             this.context.beginPath();
1502             this.context.moveTo(xPos, yPos -  this.Get('chart.ticksize'));
1503             this.context.lineTo(xPos, yPos + this.Get('chart.ticksize'));
1504
1505             this.context.stroke();
1506         
1507         // Endtick style tickmarks
1508         } else if (tickmarks == 'endtick') {
1509             this.context.beginPath();
1510             this.context.moveTo(xPos, yPos -  this.Get('chart.ticksize'));
1511             this.context.lineTo(xPos, yPos + this.Get('chart.ticksize'));
1512
1513             this.context.stroke();
1514         
1515         // "Cross" style tick marks
1516         } else if (tickmarks == 'cross') {
1517             this.context.beginPath();
1518             this.context.moveTo(xPos - this.Get('chart.ticksize'), yPos - this.Get('chart.ticksize'));
1519             this.context.lineTo(xPos + this.Get('chart.ticksize'), yPos + this.Get('chart.ticksize'));
1520             this.context.moveTo(xPos + this.Get('chart.ticksize'), yPos - this.Get('chart.ticksize'));
1521             this.context.lineTo(xPos - this.Get('chart.ticksize'), yPos + this.Get('chart.ticksize'));
1522             
1523             this.context.stroke();
1524         
1525         // A white bordered circle
1526         } else if (tickmarks == 'borderedcircle' || tickmarks == 'dot') {
1527                 this.context.lineWidth   = 1;
1528                 this.context.strokeStyle = this.Get('chart.tickmarks.dot.color');
1529                 this.context.fillStyle   = this.Get('chart.tickmarks.dot.color');
1530
1531                 // The outer white circle
1532                 this.context.beginPath();
1533                 this.context.arc(xPos, yPos, this.Get('chart.ticksize'), 0, 360 / (180 / Math.PI), false);
1534                 this.context.closePath();
1535
1536
1537                 this.context.fill();
1538                 this.context.stroke();
1539                 
1540                 // Now do the inners
1541                 this.context.beginPath();
1542                 this.context.fillStyle   = color;
1543                 this.context.strokeStyle = color;
1544                 this.context.arc(xPos, yPos, this.Get('chart.ticksize') - 2, 0, 360 / (180 / Math.PI), false);
1545
1546                 this.context.closePath();
1547
1548                 this.context.fill();
1549                 this.context.stroke();
1550         
1551         } else if (   tickmarks == 'square'
1552                    || tickmarks == 'filledsquare'
1553                    || (tickmarks == 'endsquare')
1554                    || (tickmarks == 'filledendsquare') ) {
1555
1556             this.context.fillStyle   = 'white';
1557             this.context.strokeStyle = this.context.strokeStyle; // FIXME Is this correct?
1558
1559             this.context.beginPath();
1560             this.context.strokeRect(xPos - this.Get('chart.ticksize'), yPos - this.Get('chart.ticksize'), this.Get('chart.ticksize') * 2, this.Get('chart.ticksize') * 2);
1561
1562             // Fillrect
1563             if (tickmarks == 'filledsquare' || tickmarks == 'filledendsquare') {
1564                 this.context.fillStyle = isShadow ? this.Get('chart.shadow.color') : this.context.strokeStyle;
1565                 this.context.fillRect(xPos - this.Get('chart.ticksize'), yPos - this.Get('chart.ticksize'), this.Get('chart.ticksize') * 2, this.Get('chart.ticksize') * 2);
1566
1567             } else if (tickmarks == 'square' || tickmarks == 'endsquare') {
1568                 this.context.fillStyle = isShadow ? this.Get('chart.shadow.color') : 'white';
1569                 this.context.fillRect((xPos - this.Get('chart.ticksize')) + 1, (yPos - this.Get('chart.ticksize')) + 1, (this.Get('chart.ticksize') * 2) - 2, (this.Get('chart.ticksize') * 2) - 2);
1570             }
1571
1572             this.context.stroke();
1573             this.context.fill();
1574
1575         /**
1576         * FILLED arrowhead
1577         */
1578         } else if (tickmarks == 'filledarrow') {
1579         
1580             var x = Math.abs(xPos - prevX);
1581             var y = Math.abs(yPos - prevY);
1582
1583             if (yPos < prevY) {
1584                 var a = Math.atan(x / y) + 1.57;
1585             } else {
1586                 var a = Math.atan(y / x) + 3.14;
1587             }
1588
1589             this.context.beginPath();
1590                 this.context.moveTo(xPos, yPos);
1591                 this.context.arc(xPos, yPos, 7, a - 0.5, a + 0.5, false);
1592             this.context.closePath();
1593
1594             this.context.stroke();
1595             this.context.fill();
1596
1597         /**
1598         * Arrow head, NOT filled
1599         */
1600         } else if (tickmarks == 'arrow') {
1601
1602             var x = Math.abs(xPos - prevX);
1603             var y = Math.abs(yPos - prevY);
1604
1605             if (yPos < prevY) {
1606                 var a = Math.atan(x / y) + 1.57;
1607             } else {
1608                 var a = Math.atan(y / x) + 3.14;
1609             }
1610
1611             this.context.beginPath();
1612                 this.context.moveTo(xPos, yPos);
1613                 this.context.arc(xPos, yPos, 7, a - 0.5 - (document.all ? 0.1 : 0.01), a - 0.4, false);
1614
1615                 this.context.moveTo(xPos, yPos);
1616                 this.context.arc(xPos, yPos, 7, a + 0.5 + (document.all ? 0.1 : 0.01), a + 0.5, true);
1617
1618
1619             this.context.stroke();
1620         
1621         /**
1622         * Custom tick drawing function
1623         */
1624         } else if (typeof(tickmarks) == 'function') {
1625             tickmarks(this, lineData, lineData[index], index, xPos, yPos, color, prevX, prevY);
1626         }
1627     }
1628
1629
1630     /**
1631     * Draws a filled range if necessary
1632     */
1633     RGraph.Line.prototype.DrawRange = function ()
1634     {
1635         /**
1636         * Fill the range if necessary
1637         */
1638         if (this.Get('chart.filled.range') && this.Get('chart.filled')) {
1639             this.context.beginPath();
1640             this.context.fillStyle = this.Get('chart.fillstyle');
1641             this.context.strokeStyle = this.Get('chart.fillstyle');
1642             this.context.lineWidth = 1;
1643             var len = (this.coords.length / 2);
1644
1645             for (var i=0; i<len; ++i) {
1646                 if (i == 0) {
1647                     this.context.moveTo(this.coords[i][0], this.coords[i][1])
1648                 } else {
1649                     this.context.lineTo(this.coords[i][0], this.coords[i][1])
1650                 }
1651             }
1652
1653             for (var i=this.coords.length - 1; i>=len; --i) {
1654                 this.context.lineTo(this.coords[i][0], this.coords[i][1])
1655             }
1656             this.context.stroke();
1657             this.context.fill();
1658         }
1659     }
1660
1661
1662     /**
1663     * Redraws the line with the correct line width etc
1664     * 
1665     * @param array coords The coordinates of the line
1666     */
1667     RGraph.Line.prototype.RedrawLine = function (coords, color, linewidth)
1668     {
1669         if (this.Get('chart.noredraw')) {
1670             return;
1671         }
1672
1673         this.context.beginPath();
1674         this.context.strokeStyle = (typeof(color) == 'object' && color ? color[0] : color);
1675         this.context.lineWidth = linewidth;
1676
1677         var len    = coords.length;
1678         var gutter = this.gutter;
1679         var width  = this.canvas.width;
1680         var height = this.canvas.height;
1681         var penUp  = false;
1682
1683         for (var i=0; i<len; ++i) {
1684
1685             var xPos   = coords[i][0];
1686             var yPos   = coords[i][1];
1687
1688             if (i > 0) {
1689                 var prevX = coords[i - 1][0];
1690                 var prevY = coords[i - 1][1];
1691             }
1692
1693
1694             if ((
1695                    (i == 0 && coords[i])
1696                 || (yPos < gutter)
1697                 || (prevY < gutter)
1698                 || (yPos > (height - gutter))
1699                 || (i > 0 && prevX > (width - gutter))
1700                 || (i > 0 && prevY > (height - gutter))
1701                 || prevY == null
1702                 || penUp == true
1703                ) && !this.Get('chart.outofbounds')) {
1704
1705                 this.context.moveTo(coords[i][0], coords[i][1]);
1706                 
1707                 penUp = false;
1708
1709             } else {
1710
1711                 if (this.Get('chart.stepped') && i > 0) {
1712                     this.context.lineTo(coords[i][0], coords[i - 1][1]);
1713                 }
1714                 
1715                 // Don't draw the last bit of a stepped chart. Now DO
1716                 //if (!this.Get('chart.stepped') || i < (coords.length - 1)) {
1717                 this.context.lineTo(coords[i][0], coords[i][1]);
1718                 //}
1719                 penUp = false;
1720             }
1721         }
1722
1723         /**
1724         * If two colors are specified instead of one, go over the up bits
1725         */
1726         if (this.Get('chart.colors.alternate') && typeof(color) == 'object' && color[0] && color[1]) {
1727             for (var i=1; i<len; ++i) {
1728
1729                 var prevX = coords[i - 1][0];
1730                 var prevY = coords[i - 1][1];
1731                 
1732                 this.context.beginPath();
1733                 this.context.strokeStyle = color[coords[i][1] < prevY ? 0 : 1];
1734                 this.context.lineWidth = this.Get('chart.linewidth');
1735                 this.context.moveTo(prevX, prevY);
1736                 this.context.lineTo(coords[i][0], coords[i][1]);
1737                 this.context.stroke();
1738             }
1739         }
1740     }
1741
1742
1743     /**
1744     * This function is used by MSIE only to manually draw the shadow
1745     * 
1746     * @param array coords The coords for the line
1747     */
1748     RGraph.Line.prototype.DrawIEShadow = function (coords, color)
1749     {
1750         var offsetx = this.Get('chart.shadow.offsetx');
1751         var offsety = this.Get('chart.shadow.offsety');
1752         
1753         this.context.lineWidth   = this.Get('chart.linewidth');
1754         this.context.strokeStyle = color;
1755         this.context.beginPath();
1756
1757         for (var i=0; i<coords.length; ++i) {
1758             if (i == 0) {
1759                 this.context.moveTo(coords[i][0] + offsetx, coords[i][1] + offsety);
1760             } else {
1761                 this.context.lineTo(coords[i][0] + offsetx, coords[i][1] + offsety);
1762             }
1763         }
1764
1765         this.context.stroke();
1766     }
1767
1768
1769     /**
1770     * Draw the backdrop
1771     */
1772     RGraph.Line.prototype.DrawBackdrop = function (coords, color)
1773     {
1774         var size = this.Get('chart.backdrop.size');
1775         this.context.lineWidth = size;
1776         this.context.globalAlpha = this.Get('chart.backdrop.alpha');
1777         this.context.strokeStyle = color;
1778         this.context.lineJoin = 'miter';
1779         
1780         this.context.beginPath();
1781             this.context.moveTo(coords[0][0], coords[0][1]);
1782             for (var j=1; j<coords.length; ++j) {
1783                 this.context.lineTo(coords[j][0], coords[j][1]);
1784             }
1785     
1786         this.context.stroke();
1787     
1788         // Reset the alpha value
1789         this.context.globalAlpha = 1;
1790         this.context.lineJoin = 'round';
1791         RGraph.NoShadow(this);
1792     }
1793
1794
1795     /**
1796     * Returns the linewidth
1797     */
1798     RGraph.Line.prototype.GetLineWidth = function (i)
1799     {
1800         var linewidth = this.Get('chart.linewidth');
1801         
1802         if (typeof(linewidth) == 'number') {
1803             return linewidth;
1804         
1805         } else if (typeof(linewidth) == 'object') {
1806             if (linewidth[i]) {
1807                 return linewidth[i];
1808             } else {
1809                 return linewidth[0];
1810             }
1811
1812             alert('[LINE] Error! chart.linewidth should be a single number or an array of one or more numbers');
1813         }
1814     }
1815
1816
1817     /**
1818     * The getPoint() method - used to get the point the mouse is currently over, if any
1819     * 
1820     * @param object e The event object
1821     */
1822     RGraph.Line.prototype.getPoint = function (e)
1823     {
1824         var canvas  = e.target;
1825         var context = canvas.getContext('2d');
1826         var obj     = e.target.__object__;
1827         var mouseXY = RGraph.getMouseXY(e);
1828         var mouseX  = mouseXY[0];
1829         var mouseY  = mouseXY[1];
1830
1831         for (var i=0; i<obj.coords.length; ++i) {
1832         
1833             var xCoord = obj.coords[i][0];
1834             var yCoord = obj.coords[i][1];
1835
1836             if (   mouseX <= (xCoord + 5 + obj.Get('chart.tooltips.coords.adjust')[0])
1837                 && mouseX >= (xCoord - 5 + obj.Get('chart.tooltips.coords.adjust')[0])
1838                 && mouseY <= (yCoord + 5 + obj.Get('chart.tooltips.coords.adjust')[1])
1839                 && mouseY >= (yCoord - 5 + obj.Get('chart.tooltips.coords.adjust')[1])) {
1840
1841                     return [obj, xCoord, yCoord, i];
1842             }
1843         }
1844     }
1845
1846
1847     /**
1848     * Draws the above line labels
1849     */
1850     RGraph.Line.prototype.DrawAboveLabels = function ()
1851     {
1852         var context    = this.context;
1853         var size       = this.Get('chart.labels.above.size');
1854         var font       = this.Get('chart.text.font');
1855         var units_pre  = this.Get('chart.units.pre');
1856         var units_post = this.Get('chart.units.post');
1857
1858         context.beginPath();
1859
1860         // Don't need to check that chart.labels.above is enabled here, it's been done already
1861         for (var i=0; i<this.coords.length; ++i) {
1862             var coords = this.coords[i];
1863             
1864             RGraph.Text(context, font, size, coords[0], coords[1] - 5 - size, RGraph.number_format(this, this.data_arr[i], units_pre, units_post), 'center', 'center', true, null, 'rgba(255, 255, 255, 0.7)');
1865         }
1866         
1867         context.fill();
1868     }