]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add ticks.sampleSize option (#6508)
authorBen McCann <322311+benmccann@users.noreply.github.com>
Wed, 9 Oct 2019 17:25:04 +0000 (10:25 -0700)
committerEvert Timberg <evert.timberg+github@gmail.com>
Wed, 9 Oct 2019 17:25:04 +0000 (13:25 -0400)
docs/axes/cartesian/README.md
docs/general/performance.md [new file with mode: 0644]
src/core/core.scale.js

index c9e126cc07d559894afe5e482e1a882f79eff65a..4a97dfb16b645cc02019e9e9a5a70a2ab03710a4 100644 (file)
@@ -28,6 +28,7 @@ The following options are common to all cartesian axes but do not apply to other
 | ---- | ---- | ------- | -----------
 | `min` | `number` | | User defined minimum value for the scale, overrides minimum value from data.
 | `max` | `number` | | User defined maximum value for the scale, overrides maximum value from data.
+| `sampleSize` | `number` | `ticks.length` | The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length.
 | `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to `maxRotation` before skipping any. Turn `autoSkip` off to show all labels no matter what.
 | `autoSkipPadding` | `number` | `0` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled.
 | `labelOffset` | `number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas*
diff --git a/docs/general/performance.md b/docs/general/performance.md
new file mode 100644 (file)
index 0000000..2d3397e
--- /dev/null
@@ -0,0 +1,8 @@
+# Performance
+
+Chart.js charts are rendered on `canvas` elements, which makes rendering quite fast. For large datasets or performance sensitive applications, you may wish to consider the tips below:
+
+* Set `animation: { duration: 0 }` to disable [animations](../configuration/animations.md).
+* For large datasets:
+  * You may wish to sample your data before providing it to Chart.js. E.g. if you have a data point for each day, you may find it more performant to pass in a data point for each week instead
+  * Set the [`ticks.sampleSize`](../axes/cartesian/README.md#tick-configuration) option in order to render axes more quickly
index 74efdb4395a4774238fe560a253e151a78b5ec4c..44ce92505b980e8ae5c284e2d3369ca3e2509850 100644 (file)
@@ -67,6 +67,19 @@ defaults._set('scale', {
        }
 });
 
+/** Returns a new array containing numItems from arr */
+function sample(arr, numItems) {
+       var result = [];
+       var increment = arr.length / numItems;
+       var i = 0;
+       var len = arr.length;
+
+       for (; i < len; i += increment) {
+               result.push(arr[Math.floor(i)]);
+       }
+       return result;
+}
+
 function getPixelForGridLine(scale, index, offsetGridLines) {
        var length = scale.getTicks().length;
        var validIndex = Math.min(index, length - 1);
@@ -266,7 +279,8 @@ var Scale = Element.extend({
        update: function(maxWidth, maxHeight, margins) {
                var me = this;
                var tickOpts = me.options.ticks;
-               var i, ilen, labels, label, ticks, tick;
+               var sampleSize = tickOpts.sampleSize;
+               var i, ilen, labels, ticks;
 
                // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
                me.beforeUpdate();
@@ -281,6 +295,8 @@ var Scale = Element.extend({
                        bottom: 0
                }, margins);
 
+               me._ticks = null;
+               me.ticks = null;
                me._labelSizes = null;
                me._maxLabelLines = 0;
                me.longestLabelWidth = 0;
@@ -314,35 +330,23 @@ var Scale = Element.extend({
                // Allow modification of ticks in callback.
                ticks = me.afterBuildTicks(ticks) || ticks;
 
-               me.beforeTickToLabelConversion();
-
-               // New implementations should return the formatted tick labels but for BACKWARD
-               // COMPAT, we still support no return (`this.ticks` internally changed by calling
-               // this method and supposed to contain only string values).
-               labels = me.convertTicksToLabels(ticks) || me.ticks;
-
-               me.afterTickToLabelConversion();
-
-               me.ticks = labels;   // BACKWARD COMPATIBILITY
-
-               // IMPORTANT: below this point, we consider that `this.ticks` will NEVER change!
-
-               // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
-               for (i = 0, ilen = labels.length; i < ilen; ++i) {
-                       label = labels[i];
-                       tick = ticks[i];
-                       if (!tick) {
-                               ticks.push(tick = {
-                                       label: label,
+               // Ensure ticks contains ticks in new tick format
+               if ((!ticks || !ticks.length) && me.ticks) {
+                       ticks = [];
+                       for (i = 0, ilen = me.ticks.length; i < ilen; ++i) {
+                               ticks.push({
+                                       value: me.ticks[i],
                                        major: false
                                });
-                       } else {
-                               tick.label = label;
                        }
                }
 
                me._ticks = ticks;
 
+               // Compute tick rotation and fit using a sampled subset of labels
+               // We generally don't need to compute the size of every single label for determining scale size
+               labels = me._convertTicksToLabels(sampleSize ? sample(ticks, sampleSize) : ticks);
+
                // _configure is called twice, once here, once from core.controller.updateLayout.
                // Here we haven't been positioned yet, but dimensions are correct.
                // Variables set in _configure are needed for calculateTickRotation, and
@@ -353,19 +357,28 @@ var Scale = Element.extend({
                me.beforeCalculateTickRotation();
                me.calculateTickRotation();
                me.afterCalculateTickRotation();
-               // Fit
+
                me.beforeFit();
                me.fit();
                me.afterFit();
+
                // Auto-skip
-               me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(me._ticks) : me._ticks;
+               me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(ticks) : ticks;
+
+               if (sampleSize) {
+                       // Generate labels using all non-skipped ticks
+                       labels = me._convertTicksToLabels(me._ticksToDraw);
+               }
+
+               me.ticks = labels;   // BACKWARD COMPATIBILITY
+
+               // IMPORTANT: after this point, we consider that `this.ticks` will NEVER change!
 
                me.afterUpdate();
 
                // TODO(v3): remove minSize as a public property and return value from all layout boxes. It is unused
                // make maxWidth and maxHeight private
                return me.minSize;
-
        },
 
        /**
@@ -673,6 +686,31 @@ var Scale = Element.extend({
                return rawValue;
        },
 
+       _convertTicksToLabels: function(ticks) {
+               var me = this;
+               var labels, i, ilen;
+
+               me.ticks = ticks.map(function(tick) {
+                       return tick.value;
+               });
+
+               me.beforeTickToLabelConversion();
+
+               // New implementations should return the formatted tick labels but for BACKWARD
+               // COMPAT, we still support no return (`this.ticks` internally changed by calling
+               // this method and supposed to contain only string values).
+               labels = me.convertTicksToLabels(ticks) || me.ticks;
+
+               me.afterTickToLabelConversion();
+
+               // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
+               for (i = 0, ilen = ticks.length; i < ilen; ++i) {
+                       ticks[i].label = labels[i];
+               }
+
+               return labels;
+       },
+
        /**
         * @private
         */
@@ -835,11 +873,12 @@ var Scale = Element.extend({
                for (i = 0; i < tickCount; i++) {
                        tick = ticks[i];
 
-                       if (skipRatio > 1 && i % skipRatio > 0) {
-                               // leave tick in place but make sure it's not displayed (#4635)
+                       if (skipRatio <= 1 || i % skipRatio === 0) {
+                               tick._index = i;
+                               result.push(tick);
+                       } else {
                                delete tick.label;
                        }
-                       result.push(tick);
                }
                return result;
        },
@@ -966,7 +1005,7 @@ var Scale = Element.extend({
                                borderDashOffset = gridLines.borderDashOffset || 0.0;
                        }
 
-                       lineValue = getPixelForGridLine(me, i, offsetGridLines);
+                       lineValue = getPixelForGridLine(me, tick._index || i, offsetGridLines);
 
                        // Skip if the pixel is out of the range
                        if (lineValue === undefined) {
@@ -1044,7 +1083,7 @@ var Scale = Element.extend({
                                continue;
                        }
 
-                       pixel = me.getPixelForTick(i) + optionTicks.labelOffset;
+                       pixel = me.getPixelForTick(tick._index || i) + optionTicks.labelOffset;
                        font = tick.major ? fonts.major : fonts.minor;
                        lineHeight = font.lineHeight;
                        lineCount = isArray(label) ? label.length : 1;