From c80f16871119901bb45db32753a8d66b9ce32806 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Tue, 3 Sep 2019 15:42:51 -0700 Subject: [PATCH] Add ticks.sampleSize option --- docs/axes/cartesian/README.md | 1 + docs/general/performance.md | 8 +++ samples/scales/time/financial.html | 2 +- src/core/core.scale.js | 101 ++++++++++++++++++++--------- 4 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 docs/general/performance.md diff --git a/docs/axes/cartesian/README.md b/docs/axes/cartesian/README.md index c9e126cc07d..4a97dfb16b6 100644 --- a/docs/axes/cartesian/README.md +++ b/docs/axes/cartesian/README.md @@ -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 index 00000000000..2d3397e6eea --- /dev/null +++ b/docs/general/performance.md @@ -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 diff --git a/samples/scales/time/financial.html b/samples/scales/time/financial.html index 084bacc3622..6051b51518b 100644 --- a/samples/scales/time/financial.html +++ b/samples/scales/time/financial.html @@ -76,7 +76,7 @@ var now = moment(); var data = []; var lessThanDay = unitLessThanDay(); - for (; data.length < 60 && date.isBefore(now); date = date.clone().add(1, unit).startOf(unit)) { + for (; data.length < 8000 && date.isBefore(now); date = date.clone().add(1, unit).startOf(unit)) { if (outsideMarketHours(date)) { if (!lessThanDay || !beforeNineThirty(date)) { date = date.clone().add(date.isoWeekday() >= 5 ? 8 - date.isoWeekday() : 1, 'day'); diff --git a/src/core/core.scale.js b/src/core/core.scale.js index 8f532ca754f..47cc4c9eda6 100644 --- a/src/core/core.scale.js +++ b/src/core/core.scale.js @@ -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); @@ -263,7 +276,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(); @@ -278,6 +292,8 @@ var Scale = Element.extend({ bottom: 0 }, margins); + me._ticks = null; + me.ticks = null; me._labelSizes = null; me._maxLabelLines = 0; me.longestLabelWidth = 0; @@ -311,35 +327,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 @@ -350,19 +354,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; - }, /** @@ -670,6 +683,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 */ @@ -832,11 +870,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; }, @@ -963,7 +1002,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) { @@ -1041,7 +1080,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;