Subversion-Projekte lars-tiefland.ci

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
776 lars 1
/**
2
 * This is an experimental Highcharts module that draws long data series on a canvas
3
 * in order to increase performance of the initial load time and tooltip responsiveness.
4
 *
5
 * Compatible with HTML5 canvas compatible browsers (not IE < 9).
6
 *
7
 * Author: Torstein Honsi
8
 *
9
 *
10
 * Development plan
11
 * - Column range.
12
 * - Heatmap.
13
 * - Treemap.
14
 * - Check how it works with Highstock and data grouping.
15
 * - Check inverted charts.
16
 * - Check reversed axes.
17
 * - Chart callback should be async after last series is drawn. (But not necessarily, we don't do
18
     that with initial series animation).
19
 * - Cache full-size image so we don't have to redraw on hide/show and zoom up. But k-d-tree still
20
 *   needs to be built.
21
 * - Test IE9 and IE10.
22
 * - Stacking is not perhaps not correct since it doesn't use the translation given in
23
 *   the translate method. If this gets to complicated, a possible way out would be to
24
 *   have a simplified renderCanvas method that simply draws the areaPath on a canvas.
25
 *
26
 * If this module is taken in as part of the core
27
 * - All the loading logic should be merged with core. Update styles in the core.
28
 * - Most of the method wraps should probably be added directly in parent methods.
29
 *
30
 * Notes for boost mode
31
 * - Area lines are not drawn
32
 * - Point markers are not drawn
33
 * - Zones and negativeColor don't work
34
 * - Columns are always one pixel wide. Don't set the threshold too low.
35
 *
36
 * Optimizing tips for users
37
 * - For scatter plots, use a marker.radius of 1 or less. It results in a rectangle being drawn, which is
38
 *   considerably faster than a circle.
39
 * - Set extremes (min, max) explicitly on the axes in order for Highcharts to avoid computing extremes.
40
 * - Set enableMouseTracking to false on the series to improve total rendering time.
41
 * - The default threshold is set based on one series. If you have multiple, dense series, the combined
42
 *   number of points drawn gets higher, and you may want to set the threshold lower in order to
43
 *   use optimizations.
44
 */
45
/*global document, Highcharts, HighchartsAdapter, setTimeout */
46
(function (H, HA) {
47
 
48
    'use strict';
49
 
50
    var noop = function () { return undefined; },
51
        Color = H.Color,
52
        Series = H.Series,
53
        seriesTypes = H.seriesTypes,
54
        each = H.each,
55
        extend = H.extend,
56
        addEvent = HA.addEvent,
57
        fireEvent = HA.fireEvent,
58
        merge = H.merge,
59
        pick = H.pick,
60
        wrap = H.wrap,
61
        plotOptions = H.getOptions().plotOptions,
62
        CHUNK_SIZE = 50000;
63
 
64
    function eachAsync(arr, fn, callback, chunkSize, i) {
65
        i = i || 0;
66
        chunkSize = chunkSize || CHUNK_SIZE;
67
        each(arr.slice(i, i + chunkSize), fn);
68
        if (i + chunkSize < arr.length) {
69
            setTimeout(function () {
70
                eachAsync(arr, fn, callback, chunkSize, i + chunkSize);
71
            });
72
        } else if (callback) {
73
            callback();
74
        }
75
    }
76
 
77
    // Set default options
78
    each(['area', 'arearange', 'column', 'line', 'scatter'], function (type) {
79
        if (plotOptions[type]) {
80
            plotOptions[type].boostThreshold = 5000;
81
        }
82
    });
83
 
84
    /**
85
     * Override a bunch of methods the same way. If the number of points is below the threshold,
86
     * run the original method. If not, check for a canvas version or do nothing.
87
     */
88
    each(['translate', 'generatePoints', 'drawTracker', 'drawPoints', 'render'], function (method) {
89
        function branch(proceed) {
90
            var letItPass = this.options.stacking && (method === 'translate' || method === 'generatePoints');
91
            if ((this.processedXData || this.options.data).length < (this.options.boostThreshold || Number.MAX_VALUE) ||
92
                    letItPass) {
93
 
94
                // Clear image
95
                if (method === 'render' && this.image) {
96
                    this.image.attr({ href: '' });
97
                    this.animate = null; // We're zooming in, don't run animation
98
                }
99
 
100
                proceed.call(this);
101
 
102
            // If a canvas version of the method exists, like renderCanvas(), run
103
            } else if (this[method + 'Canvas']) {
104
 
105
                this[method + 'Canvas']();
106
            }
107
        }
108
        wrap(Series.prototype, method, branch);
109
 
110
        // A special case for some types - its translate method is already wrapped
111
        if (method === 'translate') {
112
            if (seriesTypes.column) {
113
                wrap(seriesTypes.column.prototype, method, branch);
114
            }
115
            if (seriesTypes.arearange) {
116
                wrap(seriesTypes.arearange.prototype, method, branch);
117
            }
118
        }
119
    });
120
 
121
    /**
122
     * Do not compute extremes when min and max are set.
123
     * If we use this in the core, we can add the hook to hasExtremes to the methods directly.
124
     */
125
    wrap(Series.prototype, 'getExtremes', function (proceed) {
126
        if (!this.hasExtremes()) {
127
            proceed.apply(this, Array.prototype.slice.call(arguments, 1));
128
        }
129
    });
130
    wrap(Series.prototype, 'setData', function (proceed) {
131
        if (!this.hasExtremes(true)) {
132
            proceed.apply(this, Array.prototype.slice.call(arguments, 1));
133
        }
134
    });
135
    wrap(Series.prototype, 'processData', function (proceed) {
136
        if (!this.hasExtremes(true)) {
137
            proceed.apply(this, Array.prototype.slice.call(arguments, 1));
138
        }
139
    });
140
 
141
 
142
    H.extend(Series.prototype, {
143
        pointRange: 0,
144
 
145
        hasExtremes: function (checkX) {
146
            var options = this.options,
147
                data = options.data,
148
                xAxis = this.xAxis && this.xAxis.options,
149
                yAxis = this.yAxis && this.yAxis.options;
150
            return data.length > (options.boostThreshold || Number.MAX_VALUE) && typeof yAxis.min === 'number' && typeof yAxis.max === 'number' &&
151
                (!checkX || (typeof xAxis.min === 'number' && typeof xAxis.max === 'number'));
152
        },
153
 
154
        /**
155
         * If implemented in the core, parts of this can probably be shared with other similar
156
         * methods in Highcharts.
157
         */
158
        destroyGraphics: function () {
159
            var series = this,
160
                points = this.points,
161
                point,
162
                i;
163
 
164
            for (i = 0; i < points.length; i = i + 1) {
165
                point = points[i];
166
                if (point && point.graphic) {
167
                    point.graphic = point.graphic.destroy();
168
                }
169
            }
170
 
171
            each(['graph', 'area'], function (prop) {
172
                if (series[prop]) {
173
                    series[prop] = series[prop].destroy();
174
                }
175
            });
176
        },
177
 
178
        /**
179
         * Create a hidden canvas to draw the graph on. The contents is later copied over
180
         * to an SVG image element.
181
         */
182
        getContext: function () {
183
            var width = this.chart.plotWidth,
184
                height = this.chart.plotHeight;
185
 
186
            if (!this.canvas) {
187
                this.canvas = document.createElement('canvas');
188
                this.image = this.chart.renderer.image('', 0, 0, width, height).add(this.group);
189
                this.ctx = this.canvas.getContext('2d');
190
            } else {
191
                this.ctx.clearRect(0, 0, width, height);
192
            }
193
 
194
            this.canvas.setAttribute('width', width);
195
            this.canvas.setAttribute('height', height);
196
            this.image.attr({
197
                width: width,
198
                height: height
199
            });
200
 
201
            return this.ctx;
202
        },
203
 
204
        /**
205
         * Draw the canvas image inside an SVG image
206
         */
207
        canvasToSVG: function () {
208
            this.image.attr({ href: this.canvas.toDataURL('image/png') });
209
        },
210
 
211
        cvsLineTo: function (ctx, clientX, plotY) {
212
            ctx.lineTo(clientX, plotY);
213
        },
214
 
215
        renderCanvas: function () {
216
            var series = this,
217
                options = series.options,
218
                chart = series.chart,
219
                xAxis = this.xAxis,
220
                yAxis = this.yAxis,
221
                ctx,
222
                i,
223
                c = 0,
224
                xData = series.processedXData,
225
                yData = series.processedYData,
226
                rawData = options.data,
227
                xExtremes = xAxis.getExtremes(),
228
                xMin = xExtremes.min,
229
                xMax = xExtremes.max,
230
                yExtremes = yAxis.getExtremes(),
231
                yMin = yExtremes.min,
232
                yMax = yExtremes.max,
233
                pointTaken = {},
234
                lastClientX,
235
                sampling = !!series.sampling,
236
                points,
237
                r = options.marker && options.marker.radius,
238
                cvsDrawPoint = this.cvsDrawPoint,
239
                cvsLineTo = options.lineWidth ? this.cvsLineTo : false,
240
                cvsMarker = r <= 1 ? this.cvsMarkerSquare : this.cvsMarkerCircle,
241
                enableMouseTracking = options.enableMouseTracking !== false,
242
                lastPoint,
243
                threshold = options.threshold,
244
                yBottom = yAxis.getThreshold(threshold),
245
                hasThreshold = typeof threshold === 'number',
246
                translatedThreshold = yBottom,
247
                doFill = this.fill,
248
                isRange = series.pointArrayMap && series.pointArrayMap.join(',') === 'low,high',
249
                isStacked = !!options.stacking,
250
                cropStart = series.cropStart || 0,
251
                loadingOptions = chart.options.loading,
252
                requireSorting = series.requireSorting,
253
                wasNull,
254
                connectNulls = options.connectNulls,
255
                useRaw = !xData,
256
                minVal,
257
                maxVal,
258
                minI,
259
                maxI,
260
                fillColor = series.fillOpacity ?
261
                        new Color(series.color).setOpacity(pick(options.fillOpacity, 0.75)).get() :
262
                        series.color,
263
                stroke = function () {
264
                    if (doFill) {
265
                        ctx.fillStyle = fillColor;
266
                        ctx.fill();
267
                    } else {
268
                        ctx.strokeStyle = series.color;
269
                        ctx.lineWidth = options.lineWidth;
270
                        ctx.stroke();
271
                    }
272
                },
273
                drawPoint = function (clientX, plotY, yBottom) {
274
                    if (c === 0) {
275
                        ctx.beginPath();
276
                    }
277
 
278
                    if (wasNull) {
279
                        ctx.moveTo(clientX, plotY);
280
                    } else {
281
                        if (cvsDrawPoint) {
282
                            cvsDrawPoint(ctx, clientX, plotY, yBottom, lastPoint);
283
                        } else if (cvsLineTo) {
284
                            cvsLineTo(ctx, clientX, plotY);
285
                        } else if (cvsMarker) {
286
                            cvsMarker(ctx, clientX, plotY, r);
287
                        }
288
                    }
289
 
290
                    // We need to stroke the line for every 1000 pixels. It will crash the browser
291
                    // memory use if we stroke too infrequently.
292
                    c = c + 1;
293
                    if (c === 1000) {
294
                        stroke();
295
                        c = 0;
296
                    }
297
 
298
                    // Area charts need to keep track of the last point
299
                    lastPoint = {
300
                        clientX: clientX,
301
                        plotY: plotY,
302
                        yBottom: yBottom
303
                    };
304
                },
305
 
306
                addKDPoint = function (clientX, plotY, i) {
307
 
308
                    // The k-d tree requires series points. Reduce the amount of points, since the time to build the
309
                    // tree increases exponentially.
310
                    if (enableMouseTracking && !pointTaken[clientX + ',' + plotY]) {
311
                        points.push({
312
                            clientX: clientX,
313
                            plotX: clientX,
314
                            plotY: plotY,
315
                            i: cropStart + i
316
                        });
317
                        pointTaken[clientX + ',' + plotY] = true;
318
                    }
319
                };
320
 
321
            // If we are zooming out from SVG mode, destroy the graphics
322
            if (this.points) {
323
                this.destroyGraphics();
324
            }
325
 
326
            // The group
327
            series.plotGroup(
328
                'group',
329
                'series',
330
                series.visible ? 'visible' : 'hidden',
331
                options.zIndex,
332
                chart.seriesGroup
333
            );
334
 
335
            series.getAttribs();
336
            series.markerGroup = series.group;
337
            addEvent(series, 'destroy', function () {
338
                series.markerGroup = null;
339
            });
340
 
341
            points = this.points = [];
342
            ctx = this.getContext();
343
            series.buildKDTree = noop; // Do not start building while drawing
344
 
345
            // Display a loading indicator
346
            if (rawData.length > 99999) {
347
                chart.options.loading = merge(loadingOptions, {
348
                    labelStyle: {
349
                        backgroundColor: 'rgba(255,255,255,0.75)',
350
                        padding: '1em',
351
                        borderRadius: '0.5em'
352
                    },
353
                    style: {
354
                        backgroundColor: 'none',
355
                        opacity: 1
356
                    }
357
                });
358
                chart.showLoading('Drawing...');
359
                chart.options.loading = loadingOptions; // reset
360
                if (chart.loadingShown === true) {
361
                    chart.loadingShown = 1;
362
                } else {
363
                    chart.loadingShown = chart.loadingShown + 1;
364
                }
365
            }
366
 
367
            // Loop over the points
368
            i = 0;
369
            eachAsync(isStacked ? series.data : (xData || rawData), function (d) {
370
 
371
                var x,
372
                    y,
373
                    clientX,
374
                    plotY,
375
                    isNull,
376
                    low,
377
                    isYInside = true;
378
 
379
                if (useRaw) {
380
                    x = d[0];
381
                    y = d[1];
382
                } else {
383
                    x = d;
384
                    y = yData[i];
385
                }
386
 
387
                // Resolve low and high for range series
388
                if (isRange) {
389
                    if (useRaw) {
390
                        y = d.slice(1, 3);
391
                    }
392
                    low = y[0];
393
                    y = y[1];
394
                } else if (isStacked) {
395
                    x = d.x;
396
                    y = d.stackY;
397
                    low = y - d.y;
398
                }
399
 
400
                isNull = y === null;
401
 
402
                // Optimize for scatter zooming
403
                if (!requireSorting) {
404
                    isYInside = y >= yMin && y <= yMax;
405
                }
406
 
407
                if (!isNull && x >= xMin && x <= xMax && isYInside) {
408
 
409
                    clientX = Math.round(xAxis.toPixels(x, true));
410
 
411
                    if (sampling) {
412
                        if (minI === undefined || clientX === lastClientX) {
413
                            if (!isRange) {
414
                                low = y;
415
                            }
416
                            if (maxI === undefined || y > maxVal) {
417
                                maxVal = y;
418
                                maxI = i;
419
                            }
420
                            if (minI === undefined || low < minVal) {
421
                                minVal = low;
422
                                minI = i;
423
                            }
424
 
425
                        }
426
                        if (clientX !== lastClientX) { // Add points and reset
427
                            if (minI !== undefined) { // then maxI is also a number
428
                                plotY = yAxis.toPixels(maxVal, true);
429
                                yBottom = yAxis.toPixels(minVal, true);
430
                                drawPoint(
431
                                    clientX,
432
                                    hasThreshold ? Math.min(plotY, translatedThreshold) : plotY,
433
                                    hasThreshold ? Math.max(yBottom, translatedThreshold) : yBottom
434
                                );
435
                                addKDPoint(clientX, plotY, maxI);
436
                                if (yBottom !== plotY) {
437
                                    addKDPoint(clientX, yBottom, minI);
438
                                }
439
                            }
440
 
441
 
442
                            minI = maxI = undefined;
443
                            lastClientX = clientX;
444
                        }
445
                    } else {
446
                        plotY = Math.round(yAxis.toPixels(y, true));
447
                        drawPoint(clientX, plotY, yBottom);
448
                        addKDPoint(clientX, plotY, i);
449
                    }
450
                }
451
                wasNull = isNull && !connectNulls;
452
 
453
                i = i + 1;
454
 
455
                if (i % CHUNK_SIZE === 0) {
456
                    series.canvasToSVG();
457
                }
458
 
459
            }, function () {
460
 
461
                var loadingDiv = chart.loadingDiv,
462
                    loadingShown = +chart.loadingShown;
463
 
464
                stroke();
465
                series.canvasToSVG();
466
 
467
                fireEvent(series, 'renderedCanvas');
468
 
469
                // Do not use chart.hideLoading, as it runs JS animation and will be blocked by buildKDTree.
470
                // CSS animation looks good, but then it must be deleted in timeout. If we add the module to core,
471
                // change hideLoading so we can skip this block.
472
                if (loadingShown === 1) {
473
                    extend(loadingDiv.style, {
474
                        transition: 'opacity 250ms',
475
                        opacity: 0
476
                    });
477
 
478
                    chart.loadingShown = false;
479
                    setTimeout(function () {
480
                        if (loadingDiv.parentNode) { // In exporting it is falsy
481
                            loadingDiv.parentNode.removeChild(loadingDiv);
482
                        }
483
                        chart.loadingDiv = chart.loadingSpan = null;
484
                    }, 250);
485
                }
486
                if (loadingShown) {
487
                    chart.loadingShown = loadingShown - 1;
488
                }
489
 
490
                // Pass tests in Pointer.
491
                // TODO: Replace this with a single property, and replace when zooming in
492
                // below boostThreshold.
493
                series.directTouch = false;
494
                series.options.stickyTracking = true;
495
 
496
                delete series.buildKDTree; // Go back to prototype, ready to build
497
                series.buildKDTree();
498
 
499
             // Don't do async on export, the exportChart, getSVGForExport and getSVG methods are not chained for it.
500
            }, chart.renderer.forExport ? Number.MAX_VALUE : undefined);
501
        }
502
    });
503
 
504
    seriesTypes.scatter.prototype.cvsMarkerCircle = function (ctx, clientX, plotY, r) {
505
        ctx.moveTo(clientX, plotY);
506
        ctx.arc(clientX, plotY, r, 0, 2 * Math.PI, false);
507
    };
508
 
509
    // Rect is twice as fast as arc, should be used for small markers
510
    seriesTypes.scatter.prototype.cvsMarkerSquare = function (ctx, clientX, plotY, r) {
511
        ctx.moveTo(clientX, plotY);
512
        ctx.rect(clientX - r, plotY - r, r * 2, r * 2);
513
    };
514
    seriesTypes.scatter.prototype.fill = true;
515
 
516
    extend(seriesTypes.area.prototype, {
517
        cvsDrawPoint: function (ctx, clientX, plotY, yBottom, lastPoint) {
518
            if (lastPoint && clientX !== lastPoint.clientX) {
519
                ctx.moveTo(lastPoint.clientX, lastPoint.yBottom);
520
                ctx.lineTo(lastPoint.clientX, lastPoint.plotY);
521
                ctx.lineTo(clientX, plotY);
522
                ctx.lineTo(clientX, yBottom);
523
            }
524
        },
525
        fill: true,
526
        fillOpacity: true,
527
        sampling: true
528
    });
529
 
530
    extend(seriesTypes.column.prototype, {
531
        cvsDrawPoint: function (ctx, clientX, plotY, yBottom) {
532
            ctx.rect(clientX - 1, plotY, 1, yBottom - plotY);
533
        },
534
        fill: true,
535
        sampling: true
536
    });
537
 
538
    /**
539
     * Return a point instance from the k-d-tree
540
     */
541
    wrap(Series.prototype, 'searchPoint', function (proceed) {
542
        var point = proceed.apply(this, [].slice.call(arguments, 1)),
543
            ret = point;
544
 
545
        if (point && !(point instanceof this.pointClass)) {
546
            ret = (new this.pointClass()).init(this, this.options.data[point.i]);
547
            ret.dist = point.dist;
548
            ret.category = ret.x;
549
            ret.plotX = point.plotX;
550
            ret.plotY = point.plotY;
551
        }
552
        return ret;
553
    });
554
}(Highcharts, HighchartsAdapter));