Skip to content

Commit a8f9603

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

File tree

3 files changed

+79
-31
lines changed

3 files changed

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

70+
/** Returns a new array containing numItems from arr */
71+
function sample(arr, numItems) {
72+
var result = [];
73+
var increment = arr.length / numItems;
74+
var i = 0;
75+
var len = arr.length;
76+
77+
for (; i < len; i += increment) {
78+
result.push(arr[Math.floor(i)]);
79+
}
80+
return result;
81+
}
82+
7083
function getPixelForGridLine(scale, index, offsetGridLines) {
7184
var length = scale.getTicks().length;
7285
var validIndex = Math.min(index, length - 1);
@@ -263,7 +276,8 @@ var Scale = Element.extend({
263276
update: function(maxWidth, maxHeight, margins) {
264277
var me = this;
265278
var tickOpts = me.options.ticks;
266-
var i, ilen, labels, label, ticks, tick;
279+
var sampleSize = tickOpts.sampleSize;
280+
var i, ilen, labels, ticks;
267281

268282
// Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
269283
me.beforeUpdate();
@@ -278,6 +292,8 @@ var Scale = Element.extend({
278292
bottom: 0
279293
}, margins);
280294

295+
me._ticks = null;
296+
me.ticks = null;
281297
me._labelSizes = null;
282298
me._maxLabelLines = 0;
283299
me.longestLabelWidth = 0;
@@ -311,35 +327,23 @@ var Scale = Element.extend({
311327
// Allow modification of ticks in callback.
312328
ticks = me.afterBuildTicks(ticks) || ticks;
313329

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,
330+
// Ensure ticks contains ticks in new tick format
331+
if ((!ticks || !ticks.length) && me.ticks) {
332+
ticks = [];
333+
for (i = 0, ilen = me.ticks.length; i < ilen; ++i) {
334+
ticks.push({
335+
value: me.ticks[i],
334336
major: false
335337
});
336-
} else {
337-
tick.label = label;
338338
}
339339
}
340340

341341
me._ticks = ticks;
342342

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+
labels = me._convertTicksToLabels(sampleSize ? sample(ticks, sampleSize) : ticks);
346+
343347
// _configure is called twice, once here, once from core.controller.updateLayout.
344348
// Here we haven't been positioned yet, but dimensions are correct.
345349
// Variables set in _configure are needed for calculateTickRotation, and
@@ -350,19 +354,28 @@ var Scale = Element.extend({
350354
me.beforeCalculateTickRotation();
351355
me.calculateTickRotation();
352356
me.afterCalculateTickRotation();
353-
// Fit
357+
354358
me.beforeFit();
355359
me.fit();
356360
me.afterFit();
361+
357362
// Auto-skip
358-
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(me._ticks) : me._ticks;
363+
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(ticks) : ticks;
364+
365+
if (sampleSize) {
366+
// Generate labels using all non-skipped ticks
367+
labels = me._convertTicksToLabels(me._ticksToDraw);
368+
}
369+
370+
me.ticks = labels; // BACKWARD COMPATIBILITY
371+
372+
// IMPORTANT: after this point, we consider that `this.ticks` will NEVER change!
359373

360374
me.afterUpdate();
361375

362376
// TODO(v3): remove minSize as a public property and return value from all layout boxes. It is unused
363377
// make maxWidth and maxHeight private
364378
return me.minSize;
365-
366379
},
367380

368381
/**
@@ -670,6 +683,31 @@ var Scale = Element.extend({
670683
return rawValue;
671684
},
672685

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

835-
if (skipRatio > 1 && i % skipRatio > 0) {
836-
// leave tick in place but make sure it's not displayed (#4635)
873+
if (skipRatio <= 1 || i % skipRatio === 0) {
874+
tick._index = i;
875+
result.push(tick);
876+
} else {
837877
delete tick.label;
838878
}
839-
result.push(tick);
840879
}
841880
return result;
842881
},
@@ -963,7 +1002,7 @@ var Scale = Element.extend({
9631002
borderDashOffset = gridLines.borderDashOffset || 0.0;
9641003
}
9651004

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

9681007
// Skip if the pixel is out of the range
9691008
if (lineValue === undefined) {
@@ -1041,7 +1080,7 @@ var Scale = Element.extend({
10411080
continue;
10421081
}
10431082

1044-
pixel = me.getPixelForTick(i) + optionTicks.labelOffset;
1083+
pixel = me.getPixelForTick(tick._index || i) + optionTicks.labelOffset;
10451084
font = tick.major ? fonts.major : fonts.minor;
10461085
lineHeight = font.lineHeight;
10471086
lineCount = isArray(label) ? label.length : 1;

0 commit comments

Comments
 (0)