3 * Copyright 2006 Dan Vanderkam (danvdk@gmail.com)
4 * MIT-licensed (http://opensource.org/licenses/MIT)
8 * @fileoverview Based on PlotKit.CanvasRenderer, but modified to meet the
11 * In particular, support for:
14 * - dygraphs attribute system
18 * The DygraphCanvasRenderer class does the actual rendering of the chart onto
19 * a canvas. It's based on PlotKit.CanvasRenderer.
20 * @param {Object} element The canvas to attach to
21 * @param {Object} elementContext The 2d context of the canvas (injected so it
22 * can be mocked for testing.)
23 * @param {Layout} layout The DygraphLayout object for this graph.
27 /*jshint globalstrict: true */
28 /*global Dygraph:false,RGBColorParser:false */
35 * This gets called when there are "new points" to chart. This is generally the
36 * case when the underlying data being charted has changed. It is _not_ called
37 * in the common case that the user has zoomed or is panning the view.
39 * The chart canvas has already been created by the Dygraph object. The
40 * renderer simply gets a drawing context.
42 * @param {Dygraph} dygraph The chart to which this renderer belongs.
43 * @param {HTMLCanvasElement} element The <canvas> DOM element on which to draw.
44 * @param {CanvasRenderingContext2D} elementContext The drawing context.
45 * @param {DygraphLayout} layout The chart's DygraphLayout object.
47 * TODO(danvk): remove the elementContext property.
49 var DygraphCanvasRenderer
= function(dygraph
, element
, elementContext
, layout
) {
50 this.dygraph_
= dygraph
;
53 this.element
= element
;
54 this.elementContext
= elementContext
;
55 this.container
= this.element
.parentNode
;
57 this.height
= this.element
.height
;
58 this.width
= this.element
.width
;
60 // --- check whether everything is ok before we return
61 // NOTE(konigsberg): isIE is never defined in this object. Bug of some sort.
62 if (!this.isIE
&& !(DygraphCanvasRenderer
.isSupported(this.element
)))
63 throw "Canvas is not supported.";
66 this.area
= layout
.getPlotArea();
67 this.container
.style
.position
= "relative";
68 this.container
.style
.width
= this.width
+ "px";
70 // Set up a clipping area for the canvas (and the interaction canvas).
71 // This ensures that we don't overdraw.
72 if (this.dygraph_
.isUsingExcanvas_
) {
73 this._createIEClipArea();
75 // on Android 3 and 4, setting a clipping area on a canvas prevents it from
76 // displaying anything.
77 if (!Dygraph
.isAndroid()) {
78 var ctx
= this.dygraph_
.canvas_ctx_
;
80 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
83 ctx
= this.dygraph_
.hidden_ctx_
;
85 ctx
.rect(this.area
.x
, this.area
.y
, this.area
.w
, this.area
.h
);
92 * This just forwards to dygraph.attr_.
93 * TODO(danvk): remove this?
96 DygraphCanvasRenderer
.prototype.attr_
= function(name
, opt_seriesName
) {
97 return this.dygraph_
.attr_(name
, opt_seriesName
);
101 * Clears out all chart content and DOM elements.
102 * This is called immediately before render() on every frame, including
103 * during zooms and pans.
106 DygraphCanvasRenderer
.prototype.clear
= function() {
109 // VML takes a while to start up, so we just poll every this.IEDelay
111 if (this.clearDelay
) {
112 this.clearDelay
.cancel();
113 this.clearDelay
= null;
115 context
= this.elementContext
;
118 // TODO(danvk): this is broken, since MochiKit.Async is gone.
119 // this.clearDelay = MochiKit.Async.wait(this.IEDelay);
120 // this.clearDelay.addCallback(bind(this.clear, this));
125 context
= this.elementContext
;
126 context
.clearRect(0, 0, this.width
, this.height
);
130 * Checks whether the browser supports the <canvas> tag.
133 DygraphCanvasRenderer
.isSupported
= function(canvasName
) {
136 if (typeof(canvasName
) == 'undefined' || canvasName
=== null) {
137 canvas
= document
.createElement("canvas");
141 canvas
.getContext("2d");
144 var ie
= navigator
.appVersion
.match(/MSIE (\d\.\d)/);
145 var opera
= (navigator
.userAgent
.toLowerCase().indexOf("opera") != -1);
146 if ((!ie
) || (ie
[1] < 6) || (opera
))
154 * This method is responsible for drawing everything on the chart, including
155 * lines, error bars, fills and axes.
156 * It is called immediately after clear() on every frame, including during pans
160 DygraphCanvasRenderer
.prototype.render
= function() {
161 // attaches point.canvas{x,y}
162 this._updatePoints();
164 // actually draws the chart.
165 this._renderLineChart();
168 DygraphCanvasRenderer
.prototype._createIEClipArea
= function() {
169 var className
= 'dygraph-clip-div';
170 var graphDiv
= this.dygraph_
.graphDiv
;
172 // Remove old clip divs.
173 for (var i
= graphDiv
.childNodes
.length
-1; i
>= 0; i
--) {
174 if (graphDiv
.childNodes
[i
].className
== className
) {
175 graphDiv
.removeChild(graphDiv
.childNodes
[i
]);
179 // Determine background color to give clip divs.
180 var backgroundColor
= document
.bgColor
;
181 var element
= this.dygraph_
.graphDiv
;
182 while (element
!= document
) {
183 var bgcolor
= element
.currentStyle
.backgroundColor
;
184 if (bgcolor
&& bgcolor
!= 'transparent') {
185 backgroundColor
= bgcolor
;
188 element
= element
.parentNode
;
191 function createClipDiv(area
) {
192 if (area
.w
=== 0 || area
.h
=== 0) {
195 var elem
= document
.createElement('div');
196 elem
.className
= className
;
197 elem
.style
.backgroundColor
= backgroundColor
;
198 elem
.style
.position
= 'absolute';
199 elem
.style
.left
= area
.x
+ 'px';
200 elem
.style
.top
= area
.y
+ 'px';
201 elem
.style
.width
= area
.w
+ 'px';
202 elem
.style
.height
= area
.h
+ 'px';
203 graphDiv
.appendChild(elem
);
206 var plotArea
= this.area
;
217 w
: this.width
- plotArea
.x
,
223 x
: plotArea
.x
+ plotArea
.w
, y
: 0,
224 w
: this.width
-plotArea
.x
- plotArea
.w
,
231 y
: plotArea
.y
+ plotArea
.h
,
232 w
: this.width
- plotArea
.x
,
233 h
: this.height
- plotArea
.h
- plotArea
.y
239 * Returns a predicate to be used with an iterator, which will
240 * iterate over points appropriately, depending on whether
241 * connectSeparatedPoints is true. When it's false, the predicate will
242 * skip over points with missing yVals.
244 DygraphCanvasRenderer
._getIteratorPredicate
= function(connectSeparatedPoints
) {
245 return connectSeparatedPoints
?
246 DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
:
250 DygraphCanvasRenderer
._predicateThatSkipsEmptyPoints
=
251 function(array
, idx
) {
252 return array
[idx
].yval
!== null;
256 * Draws a line with the styles passed in and calls all the drawPointCallbacks.
257 * @param {Object} e The dictionary passed to the plotter function.
260 DygraphCanvasRenderer
._drawStyledLine
= function(e
,
261 color
, strokeWidth
, strokePattern
, drawPoints
,
262 drawPointCallback
, pointSize
) {
264 // TODO(konigsberg): Compute attributes outside this method call.
265 var stepPlot
= g
.getOption("stepPlot", e
.setName
);
267 if (!Dygraph
.isArrayLike(strokePattern
)) {
268 strokePattern
= null;
271 var drawGapPoints
= g
.getOption('drawGapEdgePoints', e
.setName
);
273 var points
= e
.points
;
274 var setName
= e
.setName
;
275 var iter
= Dygraph
.createIterator(points
, 0, points
.length
,
276 DygraphCanvasRenderer
._getIteratorPredicate(
277 g
.getOption("connectSeparatedPoints", setName
)));
279 var stroking
= strokePattern
&& (strokePattern
.length
>= 2);
281 var ctx
= e
.drawingContext
;
284 ctx
.installPattern(strokePattern
);
287 var pointsOnLine
= DygraphCanvasRenderer
._drawSeries(
288 e
, iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
, color
);
289 DygraphCanvasRenderer
._drawPointsOnLine(
290 e
, pointsOnLine
, drawPointCallback
, color
, pointSize
);
293 ctx
.uninstallPattern();
300 * This does the actual drawing of lines on the canvas, for just one series.
301 * Returns a list of [canvasx, canvasy] pairs for points for which a
302 * drawPointCallback should be fired. These include isolated points, or all
303 * points if drawPoints=true.
304 * @param {Object} e The dictionary passed to the plotter function.
307 DygraphCanvasRenderer
._drawSeries
= function(e
,
308 iter
, strokeWidth
, pointSize
, drawPoints
, drawGapPoints
, stepPlot
, color
) {
310 var prevCanvasX
= null;
311 var prevCanvasY
= null;
312 var nextCanvasY
= null;
313 var isIsolated
; // true if this point is isolated (no line segments)
314 var point
; // the point being processed in the while loop
315 var pointsOnLine
= []; // Array of [canvasx, canvasy] pairs.
316 var first
= true; // the first cycle through the while loop
318 var ctx
= e
.drawingContext
;
320 ctx
.strokeStyle
= color
;
321 ctx
.lineWidth
= strokeWidth
;
323 // NOTE: we break the iterator's encapsulation here for about a 25% speedup.
324 var arr
= iter
.array_
;
325 var limit
= iter
.end_
;
326 var predicate
= iter
.predicate_
;
328 for (var i
= iter
.start_
; i
< limit
; i
++) {
331 while (i
< limit
&& !predicate(arr
, i
)) {
334 if (i
== limit
) break;
338 // FIXME: The 'canvasy != canvasy' test here catches NaN values but the test
339 // doesn't catch Infinity values. Could change this to
340 // !isFinite(point.canvasy), but I assume it avoids isNaN for performance?
341 if (point
.canvasy
=== null || point
.canvasy
!= point
.canvasy
) {
342 if (stepPlot
&& prevCanvasX
!== null) {
343 // Draw a horizontal line to the start of the missing data
344 ctx
.moveTo(prevCanvasX
, prevCanvasY
);
345 ctx
.lineTo(point
.canvasx
, prevCanvasY
);
347 prevCanvasX
= prevCanvasY
= null;
350 if (drawGapPoints
|| !prevCanvasX
) {
353 nextCanvasY
= iter
.hasNext
? iter
.peek
.canvasy
: null;
355 var isNextCanvasYNullOrNaN
= nextCanvasY
=== null ||
356 nextCanvasY
!= nextCanvasY
;
357 isIsolated
= (!prevCanvasX
&& isNextCanvasYNullOrNaN
);
359 // Also consider a point to be "isolated" if it's adjacent to a
360 // null point, excluding the graph edges.
361 if ((!first
&& !prevCanvasX
) ||
362 (iter
.hasNext
&& isNextCanvasYNullOrNaN
)) {
368 if (prevCanvasX
!== null) {
371 ctx
.moveTo(prevCanvasX
, prevCanvasY
);
372 ctx
.lineTo(point
.canvasx
, prevCanvasY
);
375 ctx
.lineTo(point
.canvasx
, point
.canvasy
);
378 ctx
.moveTo(point
.canvasx
, point
.canvasy
);
380 if (drawPoints
|| isIsolated
) {
381 pointsOnLine
.push([point
.canvasx
, point
.canvasy
, point
.idx
]);
383 prevCanvasX
= point
.canvasx
;
384 prevCanvasY
= point
.canvasy
;
393 * This fires the drawPointCallback functions, which draw dots on the points by
394 * default. This gets used when the "drawPoints" option is set, or when there
395 * are isolated points.
396 * @param {Object} e The dictionary passed to the plotter function.
399 DygraphCanvasRenderer
._drawPointsOnLine
= function(
400 e
, pointsOnLine
, drawPointCallback
, color
, pointSize
) {
401 var ctx
= e
.drawingContext
;
402 for (var idx
= 0; idx
< pointsOnLine
.length
; idx
++) {
403 var cb
= pointsOnLine
[idx
];
406 e
.dygraph
, e
.setName
, ctx
, cb
[0], cb
[1], color
, pointSize
, cb
[2]);
412 * Attaches canvas coordinates to the points array.
415 DygraphCanvasRenderer
.prototype._updatePoints
= function() {
419 // TODO(bhs): this loop is a hot-spot for high-point-count charts. These
420 // transformations can be pushed into the canvas via linear transformation
422 // NOTE(danvk): this is trickier than it sounds at first. The transformation
423 // needs to be done before the .moveTo() and .lineTo() calls, but must be
424 // undone before the .stroke() call to ensure that the stroke width is
425 // unaffected. An alternative is to reduce the stroke width in the
426 // transformed coordinate space, but you can't specify different values for
427 // each dimension (as you can with .scale()). The speedup here is ~12%.
428 var sets
= this.layout
.points
;
429 for (var i
= sets
.length
; i
--;) {
430 var points
= sets
[i
];
431 for (var j
= points
.length
; j
--;) {
432 var point
= points
[j
];
433 point
.canvasx
= this.area
.w
* point
.x
+ this.area
.x
;
434 point
.canvasy
= this.area
.h
* point
.y
+ this.area
.y
;
440 * Add canvas Actually draw the lines chart, including error bars.
442 * This function can only be called if DygraphLayout's points array has been
443 * updated with canvas{x,y} attributes, i.e. by
444 * DygraphCanvasRenderer._updatePoints.
446 * @param {string=} opt_seriesName when specified, only that series will
447 * be drawn. (This is used for expedited redrawing with highlightSeriesOpts)
448 * @param {CanvasRenderingContext2D} opt_ctx when specified, the drawing
449 * context. However, lines are typically drawn on the object's
453 DygraphCanvasRenderer
.prototype._renderLineChart
= function(opt_seriesName
, opt_ctx
) {
454 var ctx
= opt_ctx
|| this.elementContext
;
457 var sets
= this.layout
.points
;
458 var setNames
= this.layout
.setNames
;
461 this.colors
= this.dygraph_
.colorsMap_
;
463 // Determine which series have specialized plotters.
464 var plotter_attr
= this.attr_("plotter");
465 var plotters
= plotter_attr
;
466 if (!Dygraph
.isArrayLike(plotters
)) {
467 plotters
= [plotters
];
470 var setPlotters
= {}; // series name -> plotter fn.
471 for (i
= 0; i
< setNames
.length
; i
++) {
472 setName
= setNames
[i
];
473 var setPlotter
= this.attr_("plotter", setName
);
474 if (setPlotter
== plotter_attr
) continue; // not specialized.
476 setPlotters
[setName
] = setPlotter
;
479 for (i
= 0; i
< plotters
.length
; i
++) {
480 var plotter
= plotters
[i
];
481 var is_last
= (i
== plotters
.length
- 1);
483 for (var j
= 0; j
< sets
.length
; j
++) {
484 setName
= setNames
[j
];
485 if (opt_seriesName
&& setName
!= opt_seriesName
) continue;
487 var points
= sets
[j
];
489 // Only throw in the specialized plotters on the last iteration.
491 if (setName
in setPlotters
) {
493 p
= setPlotters
[setName
];
495 // Don't use the standard plotters in this case.
500 var color
= this.colors
[setName
];
501 var strokeWidth
= this.dygraph_
.getOption("strokeWidth", setName
);
504 ctx
.strokeStyle
= color
;
505 ctx
.lineWidth
= strokeWidth
;
511 strokeWidth
: strokeWidth
,
512 dygraph
: this.dygraph_
,
513 axis
: this.dygraph_
.axisPropertiesForSeries(setName
),
516 seriesCount
: sets
.length
,
517 singleSeriesName
: opt_seriesName
,
518 allSeriesPoints
: sets
526 * Standard plotters. These may be used by clients via Dygraph.Plotters.
527 * See comments there for more details.
529 DygraphCanvasRenderer
._Plotters
= {
530 linePlotter
: function(e
) {
531 DygraphCanvasRenderer
._linePlotter(e
);
534 fillPlotter
: function(e
) {
535 DygraphCanvasRenderer
._fillPlotter(e
);
538 errorPlotter
: function(e
) {
539 DygraphCanvasRenderer
._errorPlotter(e
);
544 * Plotter which draws the central lines for a series.
547 DygraphCanvasRenderer
._linePlotter
= function(e
) {
549 var setName
= e
.setName
;
550 var strokeWidth
= e
.strokeWidth
;
552 // TODO(danvk): Check if there's any performance impact of just calling
553 // getOption() inside of _drawStyledLine. Passing in so many parameters makes
554 // this code a bit nasty.
555 var borderWidth
= g
.getOption("strokeBorderWidth", setName
);
556 var drawPointCallback
= g
.getOption("drawPointCallback", setName
) ||
557 Dygraph
.Circles
.DEFAULT
;
558 var strokePattern
= g
.getOption("strokePattern", setName
);
559 var drawPoints
= g
.getOption("drawPoints", setName
);
560 var pointSize
= g
.getOption("pointSize", setName
);
562 if (borderWidth
&& strokeWidth
) {
563 DygraphCanvasRenderer
._drawStyledLine(e
,
564 g
.getOption("strokeBorderColor", setName
),
565 strokeWidth
+ 2 * borderWidth
,
573 DygraphCanvasRenderer
._drawStyledLine(e
,
584 * Draws the shaded error bars/confidence intervals for each series.
585 * This happens before the center lines are drawn, since the center lines
586 * need to be drawn on top of the error bars for all series.
589 DygraphCanvasRenderer
._errorPlotter
= function(e
) {
591 var setName
= e
.setName
;
592 var errorBars
= g
.getOption("errorBars") || g
.getOption("customBars");
593 if (!errorBars
) return;
595 var fillGraph
= g
.getOption("fillGraph", setName
);
597 g
.warn("Can't use fillGraph option with error bars");
600 var ctx
= e
.drawingContext
;
602 var fillAlpha
= g
.getOption('fillAlpha', setName
);
603 var stepPlot
= g
.getOption("stepPlot", setName
);
604 var points
= e
.points
;
606 var iter
= Dygraph
.createIterator(points
, 0, points
.length
,
607 DygraphCanvasRenderer
._getIteratorPredicate(
608 g
.getOption("connectSeparatedPoints", setName
)));
612 // setup graphics context
615 var prevYs
= [-1, -1];
616 // should be same color as the lines but only 15% opaque.
617 var rgb
= new RGBColorParser(color
);
619 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' + fillAlpha
+ ')';
620 ctx
.fillStyle
= err_color
;
623 var isNullUndefinedOrNaN
= function(x
) {
624 return (x
=== null ||
629 while (iter
.hasNext
) {
630 var point
= iter
.next();
631 if ((!stepPlot
&& isNullUndefinedOrNaN(point
.y
)) ||
632 (stepPlot
&& !isNaN(prevY
) && isNullUndefinedOrNaN(prevY
))) {
638 newYs
= [ point
.y_bottom
, point
.y_top
];
641 newYs
= [ point
.y_bottom
, point
.y_top
];
643 newYs
[0] = e
.plotArea
.h
* newYs
[0] + e
.plotArea
.y
;
644 newYs
[1] = e
.plotArea
.h
* newYs
[1] + e
.plotArea
.y
;
647 ctx
.moveTo(prevX
, prevYs
[0]);
648 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
649 ctx
.lineTo(point
.canvasx
, prevYs
[1]);
651 ctx
.moveTo(prevX
, prevYs
[0]);
652 ctx
.lineTo(point
.canvasx
, newYs
[0]);
653 ctx
.lineTo(point
.canvasx
, newYs
[1]);
655 ctx
.lineTo(prevX
, prevYs
[1]);
659 prevX
= point
.canvasx
;
665 * Draws the shaded regions when "fillGraph" is set. Not to be confused with
668 * For stacked charts, it's more convenient to handle all the series
669 * simultaneously. So this plotter plots all the points on the first series
670 * it's asked to draw, then ignores all the other series.
674 DygraphCanvasRenderer
._fillPlotter
= function(e
) {
675 // Skip if we're drawing a single series for interactive highlight overlay.
676 if (e
.singleSeriesName
) return;
678 // We'll handle all the series at once, not one-by-one.
679 if (e
.seriesIndex
!== 0) return;
682 var setNames
= g
.getLabels().slice(1); // remove x-axis
684 // getLabels() includes names for invisible series, which are not included in
685 // allSeriesPoints. We remove those to make the two match.
686 // TODO(danvk): provide a simpler way to get this information.
687 for (var i
= setNames
.length
; i
>= 0; i
--) {
688 if (!g
.visibility()[i
]) setNames
.splice(i
, 1);
691 var anySeriesFilled
= (function() {
692 for (var i
= 0; i
< setNames
.length
; i
++) {
693 if (g
.getOption("fillGraph", setNames
[i
])) return true;
698 if (!anySeriesFilled
) return;
700 var ctx
= e
.drawingContext
;
701 var area
= e
.plotArea
;
702 var sets
= e
.allSeriesPoints
;
703 var setCount
= sets
.length
;
705 var fillAlpha
= g
.getOption('fillAlpha');
706 var stackedGraph
= g
.getOption("stackedGraph");
707 var colors
= g
.getColors();
709 // For stacked graphs, track the baseline for filling.
711 // The filled areas below graph lines are trapezoids with two
712 // vertical edges. The top edge is the line segment being drawn, and
713 // the baseline is the bottom edge. Each baseline corresponds to the
714 // top line segment from the previous stacked line. In the case of
715 // step plots, the trapezoids are rectangles.
718 var prevStepPlot
; // for different line drawing modes (line/step) per series
720 // process sets in reverse order (needed for stacked graphs)
721 for (var setIdx
= setCount
- 1; setIdx
>= 0; setIdx
--) {
722 var setName
= setNames
[setIdx
];
723 if (!g
.getOption('fillGraph', setName
)) continue;
725 var stepPlot
= g
.getOption('stepPlot', setName
);
726 var color
= colors
[setIdx
];
727 var axis
= g
.axisPropertiesForSeries(setName
);
728 var axisY
= 1.0 + axis
.minyval
* axis
.yscale
;
729 if (axisY
< 0.0) axisY
= 0.0;
730 else if (axisY
> 1.0) axisY
= 1.0;
731 axisY
= area
.h
* axisY
+ area
.y
;
733 var points
= sets
[setIdx
];
734 var iter
= Dygraph
.createIterator(points
, 0, points
.length
,
735 DygraphCanvasRenderer
._getIteratorPredicate(
736 g
.getOption("connectSeparatedPoints", setName
)));
738 // setup graphics context
740 var prevYs
= [-1, -1];
742 // should be same color as the lines but only 15% opaque.
743 var rgb
= new RGBColorParser(color
);
745 'rgba(' + rgb
.r
+ ',' + rgb
.g
+ ',' + rgb
.b
+ ',' + fillAlpha
+ ')';
746 ctx
.fillStyle
= err_color
;
748 var last_x
, is_first
= true;
749 while (iter
.hasNext
) {
750 var point
= iter
.next();
751 if (!Dygraph
.isOK(point
.y
)) {
753 if (point
.y_stacked
!== null && !isNaN(point
.y_stacked
)) {
754 baseline
[point
.canvasx
] = area
.h
* point
.y_stacked
+ area
.y
;
759 if (!is_first
&& last_x
== point
.xval
) {
766 currBaseline
= baseline
[point
.canvasx
];
768 if (currBaseline
=== undefined
) {
772 lastY
= currBaseline
[0];
774 lastY
= currBaseline
;
777 newYs
= [ point
.canvasy
, lastY
];
780 // Step plots must keep track of the top and bottom of
781 // the baseline at each point.
782 if(prevYs
[0] === -1) {
783 baseline
[point
.canvasx
] = [ point
.canvasy
, axisY
];
785 baseline
[point
.canvasx
] = [ point
.canvasy
, prevYs
[0] ];
788 baseline
[point
.canvasx
] = point
.canvasy
;
792 newYs
= [ point
.canvasy
, axisY
];
795 ctx
.moveTo(prevX
, prevYs
[0]);
797 // Move to top fill point
799 ctx
.lineTo(point
.canvasx
, prevYs
[0]);
801 ctx
.lineTo(point
.canvasx
, newYs
[0]);
803 // Move to bottom fill point
804 if (prevStepPlot
&& currBaseline
) {
805 // Draw to the bottom of the baseline
806 ctx
.lineTo(point
.canvasx
, currBaseline
[1]);
808 ctx
.lineTo(point
.canvasx
, newYs
[1]);
811 ctx
.lineTo(prevX
, prevYs
[1]);
815 prevX
= point
.canvasx
;
817 prevStepPlot
= stepPlot
;