Skip to content

Commit

Permalink
Add ticks.sampleSize option
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccann committed Sep 11, 2019
1 parent 995efa5 commit 7b66d0c
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 42 deletions.
1 change: 1 addition & 0 deletions docs/axes/cartesian/README.md
Expand Up @@ -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*
Expand Down
8 changes: 8 additions & 0 deletions 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
120 changes: 78 additions & 42 deletions src/core/core.scale.js
Expand Up @@ -67,6 +67,22 @@ defaults._set('scale', {
}
});

/** Returns a new array containing a random numItems from arr */
function sample(arr, numItems) {
var shuffled = arr.slice(0);
var i = arr.length;
var min = i - numItems;
var tmp, index;

while (i-- > min) {
index = Math.floor((i + 1) * Math.random());
tmp = shuffled[index];
shuffled[index] = shuffled[i];
shuffled[i] = tmp;
}
return shuffled.slice(min);
}

function getPixelForGridLine(scale, index, offsetGridLines) {
var length = scale.getTicks().length;
var validIndex = Math.min(index, length - 1);
Expand Down Expand Up @@ -263,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();
Expand Down Expand Up @@ -306,39 +323,28 @@ var Scale = Element.extend({

// New implementations should return an array of objects but for BACKWARD COMPAT,
// we still support no return (`this.ticks` internally set by calling this method).
ticks = me.buildTicks() || [];
ticks = me.buildTicks();

// 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,
if (ticks) {
ticks = me.afterBuildTicks(ticks);
} else {
// Support old implementations (that modified `this.ticks` directly in buildTicks)
me.ticks = me.afterBuildTicks(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
me._ticks = sampleSize ? sample(ticks, sampleSize) : ticks;

labels = me._convertTicksToLabels(me._ticks);

// _configure is called twice, once here, once from core.controller.updateLayout.
// Here we haven't been positioned yet, but dimensions are correct.
Expand All @@ -350,19 +356,29 @@ 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._ticks = ticks;
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(ticks) : ticks;

if (tickOpts.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;

},

/**
Expand Down Expand Up @@ -439,13 +455,7 @@ var Scale = Element.extend({
buildTicks: helpers.noop,
afterBuildTicks: function(ticks) {
var me = this;
// ticks is empty for old axis implementations here
if (isArray(ticks) && ticks.length) {
return helpers.callback(me.options.afterBuildTicks, [me, ticks]);
}
// Support old implementations (that modified `this.ticks` directly in buildTicks)
me.ticks = helpers.callback(me.options.afterBuildTicks, [me, me.ticks]) || me.ticks;
return ticks;
return helpers.callback(me.options.afterBuildTicks, [me, ticks]) || ticks;
},

beforeTickToLabelConversion: function() {
Expand Down Expand Up @@ -670,6 +680,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
*/
Expand Down Expand Up @@ -832,11 +867,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;
},
Expand Down Expand Up @@ -963,7 +999,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) {
Expand Down Expand Up @@ -1041,7 +1077,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;
Expand Down

0 comments on commit 7b66d0c

Please sign in to comment.