Skip to content

Commit

Permalink
Make autoskip aware of major ticks
Browse files Browse the repository at this point in the history
  • Loading branch information
benmccann committed Sep 8, 2019
1 parent 995efa5 commit bbf5ce5
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 131 deletions.
120 changes: 92 additions & 28 deletions src/core/core.scale.js
Expand Up @@ -201,6 +201,76 @@ function parseTickFontOptions(options) {
return {minor: minor, major: major};
}

function calculateSpacing(majorIndices, ticks, axisLength, ticksLimit) {
var evenMajorSpacing = majorIndices.length > 1 ? majorIndices.reduce(function(acc, val, idx, arr) {
var diff = idx === 0 ? acc : arr[idx] - arr[idx - 1];
return acc && acc === diff ? diff : false;
}, majorIndices[1] - majorIndices[0]) : false;
var spacing = (ticks.length - 1) / ticksLimit;
var factors, factor, i, ilen;

// If the major ticks are evenly spaced apart, place the minor ticks
// so that they divide the major ticks into even chunks
if (evenMajorSpacing) {
factors = helpers.math._factorize(evenMajorSpacing);
for (i = 0, ilen = factors.length - 1; i < ilen; i++) {
factor = factors[i];
if (factor > spacing) {
return factor;
}
}
}
return Math.max(spacing, 1);
}

function getMajorIndices(ticks) {
var result = [];
var i, ilen;
for (i = 0, ilen = ticks.length; i < ilen; i++) {
if (ticks[i].major) {
result.push(i);
}
}
return result;
}

function skipMajors(ticks, majorIndices, spacing) {
var ticksToKeep = {};
var i;

spacing = Math.ceil(spacing);
for (i = 0; i < majorIndices.length; i += spacing) {
ticksToKeep[majorIndices[i]] = 1;
}
for (i = 0; i < ticks.length; i++) {
if (!ticksToKeep[i]) {
delete ticks[i].label;
}
}
}

function skip(ticks, spacing, majorStart, majorEnd) {
var ticksToKeep = {};
var start = valueOrDefault(majorStart, 0);
var end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length);
var length, i, tick;

spacing = Math.ceil(spacing);
if (majorEnd) {
length = majorEnd - majorStart;
spacing = length / Math.floor(length / spacing);
}
for (i = 0, tick = start; tick < end; i++) {
tick = Math.round(start + i * spacing);
ticksToKeep[tick] = 1;
}
for (i = Math.max(start, 0); i < end; i++) {
if (!ticksToKeep[i]) {
delete ticks[i].label;
}
}
}

var Scale = Element.extend({
/**
* Get the padding needed for the scale
Expand Down Expand Up @@ -355,7 +425,7 @@ var Scale = Element.extend({
me.fit();
me.afterFit();
// Auto-skip
me._ticksToDraw = tickOpts.display && tickOpts.autoSkip ? me._autoSkip(me._ticks) : me._ticks;
me._ticksToDraw = tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto') ? me._autoSkip(me._ticks) : me._ticks;

me.afterUpdate();

Expand Down Expand Up @@ -806,39 +876,33 @@ var Scale = Element.extend({
*/
_autoSkip: function(ticks) {
var me = this;
var optionTicks = me.options.ticks;
var tickCount = ticks.length;
var skipRatio = false;
var maxTicks = optionTicks.maxTicksLimit;

// Total space needed to display all ticks. First and last ticks are
// drawn as their center at end of axis, so tickCount-1
var ticksLength = me._tickSize() * (tickCount - 1);

var tickOpts = me.options.ticks;
var axisLength = me._length;
var result = [];
var i, tick;

if (ticksLength > axisLength) {
skipRatio = 1 + Math.floor(ticksLength / axisLength);
var ticksLimit = tickOpts.maxTicksLimit || axisLength / me._tickSize() + 1;
var majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : [];
var first = majorIndices[0];
var last = majorIndices[majorIndices.length - 1];
var i, ilen, spacing, avgMajorSpacing;

// If there are too many major ticks to display them all
if (majorIndices.length > ticksLimit) {
skipMajors(ticks, majorIndices, majorIndices.length / ticksLimit);
return ticks;
}

// if they defined a max number of optionTicks,
// increase skipRatio until that number is met
if (tickCount > maxTicks) {
skipRatio = Math.max(skipRatio, 1 + Math.floor(tickCount / maxTicks));
}
spacing = calculateSpacing(majorIndices, ticks, axisLength, ticksLimit);

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)
delete tick.label;
if (majorIndices.length > 0) {
for (i = 0, ilen = majorIndices.length - 1; i < ilen; i++) {
skip(ticks, spacing, majorIndices[i], majorIndices[i + 1]);
}
result.push(tick);
avgMajorSpacing = majorIndices.length > 1 ? (last - first) / (majorIndices.length - 1) : null;
skip(ticks, spacing, helpers.isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first);
skip(ticks, spacing, last, helpers.isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing);
return ticks;
}
return result;
skip(ticks, spacing);
return ticks;
},

/**
Expand Down
121 changes: 35 additions & 86 deletions src/scales/scale.time.js
Expand Up @@ -5,8 +5,8 @@ var defaults = require('../core/core.defaults');
var helpers = require('../helpers/index');
var Scale = require('../core/core.scale');

var resolve = helpers.options.resolve;
var valueOrDefault = helpers.valueOrDefault;
var factorize = helpers.math._factorize;

// Integer constants are from the ES6 spec.
var MIN_INTEGER = Number.MIN_SAFE_INTEGER || -9007199254740991;
Expand All @@ -16,42 +16,42 @@ var INTERVALS = {
millisecond: {
common: true,
size: 1,
steps: factorize(1000)
steps: 1000
},
second: {
common: true,
size: 1000,
steps: factorize(60)
steps: 60
},
minute: {
common: true,
size: 60000,
steps: factorize(60)
steps: 60
},
hour: {
common: true,
size: 3600000,
steps: factorize(24)
steps: 24
},
day: {
common: true,
size: 86400000,
steps: factorize(10)
steps: 30
},
week: {
common: false,
size: 604800000,
steps: factorize(4)
steps: 4
},
month: {
common: true,
size: 2.628e9,
steps: factorize(12)
steps: 12
},
quarter: {
common: false,
size: 7.884e9,
steps: factorize(4)
steps: 4
},
year: {
common: true,
Expand Down Expand Up @@ -248,31 +248,6 @@ function parse(scale, input) {
return value;
}

/**
* Returns the number of unit to skip to be able to display up to `capacity` number of ticks
* in `unit` for the given `min` / `max` range and respecting the interval steps constraints.
*/
function determineStepSize(min, max, unit, capacity) {
var range = max - min;
var interval = INTERVALS[unit];
var milliseconds = interval.size;
var steps = interval.steps;
var i, ilen, factor;

if (!steps) {
return Math.ceil(range / (capacity * milliseconds));
}

for (i = 0, ilen = steps.length; i < ilen; ++i) {
factor = steps[i];
if (Math.ceil(range / (milliseconds * factor)) <= capacity) {
break;
}
}

return factor;
}

/**
* Figures out what unit results in an appropriate number of auto-generated ticks
*/
Expand All @@ -282,7 +257,7 @@ function determineUnitForAutoTicks(minUnit, min, max, capacity) {

for (i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) {
interval = INTERVALS[UNITS[i]];
factor = interval.steps ? interval.steps[interval.steps.length - 1] : MAX_INTEGER;
factor = interval.steps ? interval.steps / 2 : MAX_INTEGER;

if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) {
return UNITS[i];
Expand All @@ -296,10 +271,9 @@ function determineUnitForAutoTicks(minUnit, min, max, capacity) {
* Figures out what unit to format a set of ticks with
*/
function determineUnitForFormatting(scale, ticks, minUnit, min, max) {
var ilen = UNITS.length;
var i, unit;

for (i = ilen - 1; i >= UNITS.indexOf(minUnit); i--) {
for (i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) {
unit = UNITS[i];
if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= ticks.length - 1) {
return unit;
Expand All @@ -309,17 +283,9 @@ function determineUnitForFormatting(scale, ticks, minUnit, min, max) {
return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0];
}

function determineMajorUnit(unit) {
for (var i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) {
if (INTERVALS[UNITS[i]].common) {
return UNITS[i];
}
}
}

/**
* Generates a maximum of `capacity` timestamps between min and max, rounded to the
* `minor` unit, aligned on the `major` unit and using the given scale time `options`.
* `minor` unit using the given scale time `options`.
* Important: this method can return ticks outside the min and max range, it's the
* responsibility of the calling code to clamp values if needed.
*/
Expand All @@ -328,51 +294,33 @@ function generate(scale, min, max, capacity) {
var options = scale.options;
var timeOpts = options.time;
var minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, capacity);
var major = determineMajorUnit(minor);
var stepSize = valueOrDefault(timeOpts.stepSize, timeOpts.unitStepSize);
var stepSize = resolve([timeOpts.stepSize, timeOpts.unitStepSize, 1]);
var weekday = minor === 'week' ? timeOpts.isoWeekday : false;
var majorTicksEnabled = options.ticks.major.enabled;
var interval = INTERVALS[minor];
var first = min;
var last = max;
var ticks = [];
var time;

if (!stepSize) {
stepSize = determineStepSize(min, max, minor, capacity);
}

// For 'week' unit, handle the first day of week option
if (weekday) {
first = +adapter.startOf(first, 'isoWeek', weekday);
last = +adapter.startOf(last, 'isoWeek', weekday);
}

// Align first/last ticks on unit
// Align first ticks on unit
first = +adapter.startOf(first, weekday ? 'day' : minor);
last = +adapter.startOf(last, weekday ? 'day' : minor);

// Make sure that the last tick include max
if (last < max) {
last = +adapter.add(last, 1, minor);
// Prevent browser from freezing in case user options request millions of milliseconds
if (adapter.diff(max, min, minor) > 100000 * stepSize) {
throw min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor;
}

time = first;

if (majorTicksEnabled && major && !weekday && !timeOpts.round) {
// Align the first tick on the previous `minor` unit aligned on the `major` unit:
// we first aligned time on the previous `major` unit then add the number of full
// stepSize there is between first and the previous major time.
time = +adapter.startOf(time, major);
time = +adapter.add(time, ~~((first - time) / (interval.size * stepSize)) * stepSize, minor);
for (time = first; time < max; time = +adapter.add(time, stepSize, minor)) {
ticks.push(time);
}

for (; time < last; time = +adapter.add(time, stepSize, minor)) {
ticks.push(+time);
if (time === max || options.bounds === 'ticks') {
ticks.push(time);
}

ticks.push(+time);

return ticks;
}

Expand Down Expand Up @@ -609,18 +557,17 @@ module.exports = Scale.extend({
var timeOpts = options.time;
var timestamps = me._timestamps;
var ticks = [];
var capacity = me.getLabelCapacity(min);
var source = options.ticks.source;
var distribution = options.distribution;
var i, ilen, timestamp;

switch (options.ticks.source) {
case 'data':
if (source === 'data' || (source === 'auto' && distribution === 'series')) {
timestamps = timestamps.data;
break;
case 'labels':
} else if (source === 'labels') {
timestamps = timestamps.labels;
break;
case 'auto':
default:
timestamps = generate(me, min, max, me.getLabelCapacity(min), options);
} else {
timestamps = generate(me, min, max, capacity, options);
}

if (options.bounds === 'ticks' && timestamps.length) {
Expand All @@ -645,8 +592,11 @@ module.exports = Scale.extend({

// PRIVATE
me._unit = timeOpts.unit || determineUnitForFormatting(me, ticks, timeOpts.minUnit, me.min, me.max);
me._majorUnit = determineMajorUnit(me._unit);
me._table = buildLookupTable(me._timestamps.data, min, max, options.distribution);
// Make sure the major unit fits. Usually it will just be the next largest unit
// But if you have a lot of ticks it could be larger. E.g. if you have 8000 day ticks the majorUnit may be year
me._majorUnit = !options.ticks.major.enabled || me._unit === 'year' ? undefined
: determineUnitForAutoTicks(UNITS[UNITS.indexOf(me._unit) + 1], me.min, me.max, capacity);
me._table = buildLookupTable(me._timestamps.data, min, max, distribution);
me._offsets = computeOffsets(me._table, ticks, min, max, options);

if (options.ticks.reverse) {
Expand Down Expand Up @@ -690,10 +640,9 @@ module.exports = Scale.extend({
var majorFormat = formats[majorUnit];
var tick = ticks[index];
var tickOpts = options.ticks;
var majorTickOpts = tickOpts.major;
var major = majorTickOpts.enabled && majorUnit && majorFormat && tick && tick.major;
var major = majorUnit && majorFormat && tick && tick.major;
var label = adapter.format(time, format ? format : major ? majorFormat : minorFormat);
var nestedTickOpts = major ? majorTickOpts : tickOpts.minor;
var nestedTickOpts = major ? tickOpts.major : tickOpts.minor;
var formatter = helpers.options.resolve([
nestedTickOpts.callback,
nestedTickOpts.userCallback,
Expand Down

0 comments on commit bbf5ce5

Please sign in to comment.