Skip to content

Commit a635144

Browse files
committed
Add ticks.sampleSize option
1 parent ce8ee02 commit a635144

File tree

3 files changed

+93
-48
lines changed

3 files changed

+93
-48
lines changed

docs/axes/cartesian/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The following options are common to all cartesian axes but do not apply to other
2828
| ---- | ---- | ------- | -----------
2929
| `min` | `number` | | User defined minimum value for the scale, overrides minimum value from data.
3030
| `max` | `number` | | User defined maximum value for the scale, overrides maximum value from data.
31+
| `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.
3132
| `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.
3233
| `autoSkipPadding` | `number` | `0` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled.
3334
| `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*

docs/general/performance.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Performance
2+
3+
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:
4+
5+
* Set `animation: { duration: 0 }` to disable [animations](../configuration/animations.md).
6+
* For large datasets:
7+
* 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
8+
* Set the [`ticks.sampleSize`](../axes/cartesian/README.md#tick-configuration) option in order to render axes more quickly

src/core/core.scale.js

Lines changed: 84 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,25 @@ defaults._set('scale', {
6565
}
6666
});
6767

68+
function sample(arr, size) {
69+
var shuffled = arr.slice(0);
70+
var i = arr.length;
71+
var min = i - size;
72+
var tmp, index;
73+
74+
if (size >= min) {
75+
return arr;
76+
}
77+
78+
while (i-- > min) {
79+
index = Math.floor((i + 1) * Math.random());
80+
tmp = shuffled[index];
81+
shuffled[index] = shuffled[i];
82+
shuffled[i] = tmp;
83+
}
84+
return shuffled.slice(min);
85+
}
86+
6887
function getPixelForGridLine(scale, index, offsetGridLines) {
6988
var length = scale.getTicks().length;
7089
var validIndex = Math.min(index, length - 1);
@@ -261,7 +280,7 @@ var Scale = Element.extend({
261280
update: function(maxWidth, maxHeight, margins) {
262281
var me = this;
263282
var tickOpts = me.options.ticks;
264-
var i, ilen, labels, label, ticks, tick;
283+
var i, ilen, labels, ticks;
265284

266285
// Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
267286
me.beforeUpdate();
@@ -304,39 +323,28 @@ var Scale = Element.extend({
304323

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

309328
// Allow modification of ticks in callback.
310-
ticks = me.afterBuildTicks(ticks) || ticks;
311-
312-
me.beforeTickToLabelConversion();
313-
314-
// New implementations should return the formatted tick labels but for BACKWARD
315-
// COMPAT, we still support no return (`this.ticks` internally changed by calling
316-
// this method and supposed to contain only string values).
317-
labels = me.convertTicksToLabels(ticks) || me.ticks;
318-
319-
me.afterTickToLabelConversion();
320-
321-
me.ticks = labels; // BACKWARD COMPATIBILITY
322-
323-
// IMPORTANT: below this point, we consider that `this.ticks` will NEVER change!
324-
325-
// BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
326-
for (i = 0, ilen = labels.length; i < ilen; ++i) {
327-
label = labels[i];
328-
tick = ticks[i];
329-
if (!tick) {
330-
ticks.push(tick = {
331-
label: label,
329+
if (ticks) {
330+
ticks = me.afterBuildTicks(ticks);
331+
} else {
332+
// Support old implementations (that modified `this.ticks` directly in buildTicks)
333+
me.ticks = me.afterBuildTicks(me.ticks) || [];
334+
ticks = [];
335+
for (i = 0, ilen = me.ticks.length; i < ilen; ++i) {
336+
ticks.push({
337+
value: me.ticks[i],
332338
major: false
333339
});
334-
} else {
335-
tick.label = label;
336340
}
337341
}
338342

339-
me._ticks = ticks;
343+
// Compute tick rotation and fit using a sampled subset of labels
344+
// We generally don't need to compute the size of every single label for determining scale size
345+
me._ticks = sample(ticks, tickOpts.sampleSize || ticks.length);
346+
347+
labels = me._convertTicksToLabels(me._ticks);
340348

341349
// _configure is called twice, once here, once from core.controller.updateLayout.
342350
// Here we haven't been positioned yet, but dimensions are correct.
@@ -348,19 +356,29 @@ var Scale = Element.extend({
348356
me.beforeCalculateTickRotation();
349357
me.calculateTickRotation();
350358
me.afterCalculateTickRotation();
351-
// Fit
359+
352360
me.beforeFit();
353361
me.fit();
354362
me.afterFit();
363+
355364
// Auto-skip
356-
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(me._ticks) : me._ticks;
365+
me._ticks = ticks;
366+
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(ticks) : ticks;
367+
368+
if (tickOpts.sampleSize) {
369+
// Generate labels using all non-skipped ticks
370+
labels = me._convertTicksToLabels(me._ticksToDraw);
371+
}
372+
373+
me.ticks = labels; // BACKWARD COMPATIBILITY
374+
375+
// IMPORTANT: after this point, we consider that `this.ticks` will NEVER change!
357376

358377
me.afterUpdate();
359378

360379
// TODO(v3): remove minSize as a public property and return value from all layout boxes. It is unused
361380
// make maxWidth and maxHeight private
362381
return me.minSize;
363-
364382
},
365383

366384
/**
@@ -437,13 +455,7 @@ var Scale = Element.extend({
437455
buildTicks: helpers.noop,
438456
afterBuildTicks: function(ticks) {
439457
var me = this;
440-
// ticks is empty for old axis implementations here
441-
if (helpers.isArray(ticks) && ticks.length) {
442-
return helpers.callback(me.options.afterBuildTicks, [me, ticks]);
443-
}
444-
// Support old implementations (that modified `this.ticks` directly in buildTicks)
445-
me.ticks = helpers.callback(me.options.afterBuildTicks, [me, me.ticks]) || me.ticks;
446-
return ticks;
458+
return helpers.callback(me.options.afterBuildTicks, [me, ticks]) || ticks;
447459
},
448460

449461
beforeTickToLabelConversion: function() {
@@ -621,12 +633,10 @@ var Scale = Element.extend({
621633
*/
622634
handleMargins: function() {
623635
var me = this;
624-
if (me.margins) {
625-
me.margins.left = Math.max(me.paddingLeft, me.margins.left);
626-
me.margins.top = Math.max(me.paddingTop, me.margins.top);
627-
me.margins.right = Math.max(me.paddingRight, me.margins.right);
628-
me.margins.bottom = Math.max(me.paddingBottom, me.margins.bottom);
629-
}
636+
me.margins.left = Math.max(me.paddingLeft, me.margins.left);
637+
me.margins.top = Math.max(me.paddingTop, me.margins.top);
638+
me.margins.right = Math.max(me.paddingRight, me.margins.right);
639+
me.margins.bottom = Math.max(me.paddingBottom, me.margins.bottom);
630640
},
631641

632642
afterFit: function() {
@@ -668,6 +678,31 @@ var Scale = Element.extend({
668678
return rawValue;
669679
},
670680

681+
_convertTicksToLabels: function(ticks) {
682+
var me = this;
683+
var labels, i, ilen;
684+
685+
me.ticks = ticks.map(function(tick) {
686+
return tick.value;
687+
});
688+
689+
me.beforeTickToLabelConversion();
690+
691+
// New implementations should return the formatted tick labels but for BACKWARD
692+
// COMPAT, we still support no return (`this.ticks` internally changed by calling
693+
// this method and supposed to contain only string values).
694+
labels = me.convertTicksToLabels(ticks) || me.ticks;
695+
696+
me.afterTickToLabelConversion();
697+
698+
// BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
699+
for (i = 0, ilen = ticks.length; i < ilen; ++i) {
700+
ticks[i].label = labels[i];
701+
}
702+
703+
return labels;
704+
},
705+
671706
/**
672707
* @private
673708
*/
@@ -830,11 +865,12 @@ var Scale = Element.extend({
830865
for (i = 0; i < tickCount; i++) {
831866
tick = ticks[i];
832867

833-
if (skipRatio > 1 && i % skipRatio > 0) {
834-
// leave tick in place but make sure it's not displayed (#4635)
868+
if (skipRatio <= 1 || i % skipRatio === 0) {
869+
tick._index = i;
870+
result.push(tick);
871+
} else {
835872
delete tick.label;
836873
}
837-
result.push(tick);
838874
}
839875
return result;
840876
},
@@ -961,7 +997,7 @@ var Scale = Element.extend({
961997
borderDashOffset = gridLines.borderDashOffset || 0.0;
962998
}
963999

964-
lineValue = getPixelForGridLine(me, i, offsetGridLines);
1000+
lineValue = getPixelForGridLine(me, tick._index || i, offsetGridLines);
9651001

9661002
// Skip if the pixel is out of the range
9671003
if (lineValue === undefined) {
@@ -1039,7 +1075,7 @@ var Scale = Element.extend({
10391075
continue;
10401076
}
10411077

1042-
pixel = me.getPixelForTick(i) + optionTicks.labelOffset;
1078+
pixel = me.getPixelForTick(tick._index || i) + optionTicks.labelOffset;
10431079
font = tick.major ? fonts.major : fonts.minor;
10441080
lineHeight = font.lineHeight;
10451081
lineCount = helpers.isArray(label) ? label.length : 1;

0 commit comments

Comments
 (0)