Skip to content

Commit fa09223

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

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
@@ -67,6 +67,25 @@ defaults._set('scale', {
6767
}
6868
});
6969

70+
function sample(arr, size) {
71+
var shuffled = arr.slice(0);
72+
var i = arr.length;
73+
var min = i - size;
74+
var tmp, index;
75+
76+
if (size >= min) {
77+
return arr;
78+
}
79+
80+
while (i-- > min) {
81+
index = Math.floor((i + 1) * Math.random());
82+
tmp = shuffled[index];
83+
shuffled[index] = shuffled[i];
84+
shuffled[i] = tmp;
85+
}
86+
return shuffled.slice(min);
87+
}
88+
7089
function getPixelForGridLine(scale, index, offsetGridLines) {
7190
var length = scale.getTicks().length;
7291
var validIndex = Math.min(index, length - 1);
@@ -263,7 +282,7 @@ var Scale = Element.extend({
263282
update: function(maxWidth, maxHeight, margins) {
264283
var me = this;
265284
var tickOpts = me.options.ticks;
266-
var i, ilen, labels, label, ticks, tick;
285+
var i, ilen, labels, ticks;
267286

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

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

311330
// 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,
331+
if (ticks) {
332+
ticks = me.afterBuildTicks(ticks);
333+
} else {
334+
// Support old implementations (that modified `this.ticks` directly in buildTicks)
335+
me.ticks = me.afterBuildTicks(me.ticks) || [];
336+
ticks = [];
337+
for (i = 0, ilen = me.ticks.length; i < ilen; ++i) {
338+
ticks.push({
339+
value: me.ticks[i],
334340
major: false
335341
});
336-
} else {
337-
tick.label = label;
338342
}
339343
}
340344

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

343351
// _configure is called twice, once here, once from core.controller.updateLayout.
344352
// Here we haven't been positioned yet, but dimensions are correct.
@@ -350,19 +358,29 @@ var Scale = Element.extend({
350358
me.beforeCalculateTickRotation();
351359
me.calculateTickRotation();
352360
me.afterCalculateTickRotation();
353-
// Fit
361+
354362
me.beforeFit();
355363
me.fit();
356364
me.afterFit();
365+
357366
// Auto-skip
358-
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(me._ticks) : me._ticks;
367+
me._ticks = ticks;
368+
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(ticks) : ticks;
369+
370+
if (tickOpts.sampleSize) {
371+
// Generate labels using all non-skipped ticks
372+
labels = me._convertTicksToLabels(me._ticksToDraw);
373+
}
374+
375+
me.ticks = labels; // BACKWARD COMPATIBILITY
376+
377+
// IMPORTANT: after this point, we consider that `this.ticks` will NEVER change!
359378

360379
me.afterUpdate();
361380

362381
// TODO(v3): remove minSize as a public property and return value from all layout boxes. It is unused
363382
// make maxWidth and maxHeight private
364383
return me.minSize;
365-
366384
},
367385

368386
/**
@@ -439,13 +457,7 @@ var Scale = Element.extend({
439457
buildTicks: helpers.noop,
440458
afterBuildTicks: function(ticks) {
441459
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;
460+
return helpers.callback(me.options.afterBuildTicks, [me, ticks]) || ticks;
449461
},
450462

451463
beforeTickToLabelConversion: function() {
@@ -623,12 +635,10 @@ var Scale = Element.extend({
623635
*/
624636
handleMargins: function() {
625637
var me = this;
626-
if (me.margins) {
627-
me.margins.left = Math.max(me.paddingLeft, me.margins.left);
628-
me.margins.top = Math.max(me.paddingTop, me.margins.top);
629-
me.margins.right = Math.max(me.paddingRight, me.margins.right);
630-
me.margins.bottom = Math.max(me.paddingBottom, me.margins.bottom);
631-
}
638+
me.margins.left = Math.max(me.paddingLeft, me.margins.left);
639+
me.margins.top = Math.max(me.paddingTop, me.margins.top);
640+
me.margins.right = Math.max(me.paddingRight, me.margins.right);
641+
me.margins.bottom = Math.max(me.paddingBottom, me.margins.bottom);
632642
},
633643

634644
afterFit: 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)