Skip to content

Commit 7b66d0c

Browse files
committed
Add ticks.sampleSize option
1 parent 995efa5 commit 7b66d0c

File tree

3 files changed

+87
-42
lines changed

3 files changed

+87
-42
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: 78 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ defaults._set('scale', {
6767
}
6868
});
6969

70+
/** Returns a new array containing a random numItems from arr */
71+
function sample(arr, numItems) {
72+
var shuffled = arr.slice(0);
73+
var i = arr.length;
74+
var min = i - numItems;
75+
var tmp, index;
76+
77+
while (i-- > min) {
78+
index = Math.floor((i + 1) * Math.random());
79+
tmp = shuffled[index];
80+
shuffled[index] = shuffled[i];
81+
shuffled[i] = tmp;
82+
}
83+
return shuffled.slice(min);
84+
}
85+
7086
function getPixelForGridLine(scale, index, offsetGridLines) {
7187
var length = scale.getTicks().length;
7288
var validIndex = Math.min(index, length - 1);
@@ -263,7 +279,8 @@ var Scale = Element.extend({
263279
update: function(maxWidth, maxHeight, margins) {
264280
var me = this;
265281
var tickOpts = me.options.ticks;
266-
var i, ilen, labels, label, ticks, tick;
282+
var sampleSize = tickOpts.sampleSize;
283+
var i, ilen, labels, ticks;
267284

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

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

311328
// Allow modification of ticks in callback.
312-
ticks = me.afterBuildTicks(ticks) || ticks;
313-
314-
me.beforeTickToLabelConversion();
315-
316-
// New implementations should return the formatted tick labels but for BACKWARD
317-
// COMPAT, we still support no return (`this.ticks` internally changed by calling
318-
// this method and supposed to contain only string values).
319-
labels = me.convertTicksToLabels(ticks) || me.ticks;
320-
321-
me.afterTickToLabelConversion();
322-
323-
me.ticks = labels; // BACKWARD COMPATIBILITY
324-
325-
// IMPORTANT: below this point, we consider that `this.ticks` will NEVER change!
326-
327-
// BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
328-
for (i = 0, ilen = labels.length; i < ilen; ++i) {
329-
label = labels[i];
330-
tick = ticks[i];
331-
if (!tick) {
332-
ticks.push(tick = {
333-
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],
334338
major: false
335339
});
336-
} else {
337-
tick.label = label;
338340
}
339341
}
340342

341-
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 = sampleSize ? sample(ticks, sampleSize) : ticks;
346+
347+
labels = me._convertTicksToLabels(me._ticks);
342348

343349
// _configure is called twice, once here, once from core.controller.updateLayout.
344350
// Here we haven't been positioned yet, but dimensions are correct.
@@ -350,19 +356,29 @@ var Scale = Element.extend({
350356
me.beforeCalculateTickRotation();
351357
me.calculateTickRotation();
352358
me.afterCalculateTickRotation();
353-
// Fit
359+
354360
me.beforeFit();
355361
me.fit();
356362
me.afterFit();
363+
357364
// Auto-skip
358-
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!
359376

360377
me.afterUpdate();
361378

362379
// TODO(v3): remove minSize as a public property and return value from all layout boxes. It is unused
363380
// make maxWidth and maxHeight private
364381
return me.minSize;
365-
366382
},
367383

368384
/**
@@ -439,13 +455,7 @@ var Scale = Element.extend({
439455
buildTicks: helpers.noop,
440456
afterBuildTicks: function(ticks) {
441457
var me = this;
442-
// ticks is empty for old axis implementations here
443-
if (isArray(ticks) && ticks.length) {
444-
return helpers.callback(me.options.afterBuildTicks, [me, ticks]);
445-
}
446-
// Support old implementations (that modified `this.ticks` directly in buildTicks)
447-
me.ticks = helpers.callback(me.options.afterBuildTicks, [me, me.ticks]) || me.ticks;
448-
return ticks;
458+
return helpers.callback(me.options.afterBuildTicks, [me, ticks]) || ticks;
449459
},
450460

451461
beforeTickToLabelConversion: function() {
@@ -670,6 +680,31 @@ var Scale = Element.extend({
670680
return rawValue;
671681
},
672682

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

835-
if (skipRatio > 1 && i % skipRatio > 0) {
836-
// leave tick in place but make sure it's not displayed (#4635)
870+
if (skipRatio <= 1 || i % skipRatio === 0) {
871+
tick._index = i;
872+
result.push(tick);
873+
} else {
837874
delete tick.label;
838875
}
839-
result.push(tick);
840876
}
841877
return result;
842878
},
@@ -963,7 +999,7 @@ var Scale = Element.extend({
963999
borderDashOffset = gridLines.borderDashOffset || 0.0;
9641000
}
9651001

966-
lineValue = getPixelForGridLine(me, i, offsetGridLines);
1002+
lineValue = getPixelForGridLine(me, tick._index || i, offsetGridLines);
9671003

9681004
// Skip if the pixel is out of the range
9691005
if (lineValue === undefined) {
@@ -1041,7 +1077,7 @@ var Scale = Element.extend({
10411077
continue;
10421078
}
10431079

1044-
pixel = me.getPixelForTick(i) + optionTicks.labelOffset;
1080+
pixel = me.getPixelForTick(tick._index || i) + optionTicks.labelOffset;
10451081
font = tick.major ? fonts.major : fonts.minor;
10461082
lineHeight = font.lineHeight;
10471083
lineCount = isArray(label) ? label.length : 1;

0 commit comments

Comments
 (0)