}
});
+function labelsFromTicks(ticks) {
+ var labels = [];
+ var i, ilen;
+
+ for (i = 0, ilen = ticks.length; i < ilen; ++i) {
+ labels.push(ticks[i].label);
+ }
+
+ return labels;
+}
+
module.exports = function(Chart) {
function computeTextSize(context, tick, font) {
};
},
+ /**
+ * Returns the scale tick objects ({label, major})
+ * @since 2.7
+ */
+ getTicks: function() {
+ return this._ticks;
+ },
+
// These methods are ordered by lifecyle. Utilities then follow.
// Any function defined here is inherited by all scale types.
// Any function can be extended by the scale type
},
update: function(maxWidth, maxHeight, margins) {
var me = this;
+ var i, ilen, labels, label, ticks, tick;
// Update Lifecycle - Probably don't want to ever extend or overwrite this function ;)
me.beforeUpdate();
me.determineDataLimits();
me.afterDataLimits();
- // Ticks
+ // Ticks - `this.ticks` is now DEPRECATED!
+ // Internal ticks are now stored as objects in the PRIVATE `this._ticks` member
+ // and must not be accessed directly from outside this class. `this.ticks` being
+ // around for long time and not marked as private, we can't change its structure
+ // without unexpected breaking changes. If you need to access the scale ticks,
+ // use scale.getTicks() instead.
+
me.beforeBuildTicks();
- me.buildTicks();
+
+ // New implementations should return an array of objects but for BACKWARD COMPAT,
+ // we still support no return (`this.ticks` internally set by calling this method).
+ ticks = me.buildTicks() || [];
+
me.afterBuildTicks();
me.beforeTickToLabelConversion();
- me.convertTicksToLabels();
+
+ // New implementations should return the formatted tick labels but for BACKWARD
+ // COMPAT, we still support no return (`this.ticks` internally changed by calling
+ // this method and supposed to contain only string values).
+ labels = me.convertTicksToLabels(ticks) || me.ticks;
+
me.afterTickToLabelConversion();
+ me.ticks = labels; // BACKWARD COMPATIBILITY
+
+ // IMPORTANT: from this point, we consider that `this.ticks` will NEVER change!
+
+ // BACKWARD COMPAT: synchronize `_ticks` with labels (so potentially `this.ticks`)
+ for (i = 0, ilen = labels.length; i < ilen; ++i) {
+ label = labels[i];
+ tick = ticks[i];
+ if (!tick) {
+ ticks.push(tick = {
+ label: label,
+ major: false
+ });
+ } else {
+ tick.label = label;
+ }
+ }
+
+ me._ticks = ticks;
+
// Tick Rotation
me.beforeCalculateTickRotation();
me.calculateTickRotation();
var me = this;
var context = me.ctx;
var tickOpts = me.options.ticks;
+ var labels = labelsFromTicks(me._ticks);
// Get the width of each grid by calculating the difference
// between x offsets between 0 and 1.
var labelRotation = tickOpts.minRotation || 0;
- if (me.options.display && me.isHorizontal()) {
- var originalLabelWidth = helpers.longestText(context, tickFont.font, me.ticks, me.longestTextCache);
+ if (labels.length && me.options.display && me.isHorizontal()) {
+ var originalLabelWidth = helpers.longestText(context, tickFont.font, labels, me.longestTextCache);
var labelWidth = originalLabelWidth;
var cosRotation;
var sinRotation;
height: 0
};
+ var labels = labelsFromTicks(me._ticks);
+
var opts = me.options;
var tickOpts = opts.ticks;
var scaleLabelOpts = opts.scaleLabel;
// Don't bother fitting the ticks if we are not showing them
if (tickOpts.display && display) {
- var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, me.ticks, me.longestTextCache);
- var tallestLabelHeightInLines = helpers.numberOfLabelLines(me.ticks);
+ var largestTextWidth = helpers.longestText(me.ctx, tickFont.font, labels, me.longestTextCache);
+ var tallestLabelHeightInLines = helpers.numberOfLabelLines(labels);
var lineSpace = tickFont.size * 0.5;
var tickPadding = me.options.ticks.padding;
minSize.height = Math.min(me.maxHeight, minSize.height + labelHeight + tickPadding);
me.ctx.font = tickFont.font;
- var firstTick = me.ticks[0];
- var firstLabelWidth = computeTextSize(me.ctx, firstTick, tickFont.font);
-
- var lastTick = me.ticks[me.ticks.length - 1];
- var lastLabelWidth = computeTextSize(me.ctx, lastTick, tickFont.font);
+ var firstLabelWidth = computeTextSize(me.ctx, labels[0], tickFont.font);
+ var lastLabelWidth = computeTextSize(me.ctx, labels[labels.length - 1], tickFont.font);
// Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned which means that the right padding is dominated
// by the font height
var me = this;
if (me.isHorizontal()) {
var innerWidth = me.width - (me.paddingLeft + me.paddingRight);
- var tickWidth = innerWidth / Math.max((me.ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
+ var tickWidth = innerWidth / Math.max((me._ticks.length - ((me.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
var pixel = (tickWidth * index) + me.paddingLeft;
if (includeOffset) {
return finalVal;
}
var innerHeight = me.height - (me.paddingTop + me.paddingBottom);
- return me.top + (index * (innerHeight / (me.ticks.length - 1)));
+ return me.top + (index * (innerHeight / (me._ticks.length - 1)));
},
// Utility for getting the pixel location of a percentage of scale
var labelRotationRadians = helpers.toRadians(me.labelRotation);
var cosRotation = Math.cos(labelRotationRadians);
var longestRotatedLabel = me.longestLabelWidth * cosRotation;
+ var tickCount = me._ticks.length;
var itemsToDraw = [];
if (isHorizontal) {
skipRatio = false;
- if ((longestRotatedLabel + optionTicks.autoSkipPadding) * me.ticks.length > (me.width - (me.paddingLeft + me.paddingRight))) {
- skipRatio = 1 + Math.floor(((longestRotatedLabel + optionTicks.autoSkipPadding) * me.ticks.length) / (me.width - (me.paddingLeft + me.paddingRight)));
+ if ((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount > (me.width - (me.paddingLeft + me.paddingRight))) {
+ skipRatio = 1 + Math.floor(((longestRotatedLabel + optionTicks.autoSkipPadding) * tickCount) / (me.width - (me.paddingLeft + me.paddingRight)));
}
// if they defined a max number of optionTicks,
// increase skipRatio until that number is met
- if (maxTicks && me.ticks.length > maxTicks) {
- while (!skipRatio || me.ticks.length / (skipRatio || 1) > maxTicks) {
+ if (maxTicks && tickCount > maxTicks) {
+ while (!skipRatio || tickCount / (skipRatio || 1) > maxTicks) {
if (!skipRatio) {
skipRatio = 1;
}
var yTickStart = options.position === 'bottom' ? me.top : me.bottom - tl;
var yTickEnd = options.position === 'bottom' ? me.top + tl : me.bottom;
- helpers.each(me.ticks, function(tick, index) {
- var label = (tick && tick.value) || tick;
+ helpers.each(me._ticks, function(tick, index) {
+ var label = tick.label;
// If the callback returned a null or undefined value, do not draw this line
if (helpers.isNullOrUndef(label)) {
return;
}
- var isLastTick = me.ticks.length === index + 1;
+ var isLastTick = tickCount === index + 1;
// Since we always show the last tick,we need may need to hide the last shown one before
- var shouldSkip = (skipRatio > 1 && index % skipRatio > 0) || (index % skipRatio === 0 && index + skipRatio >= me.ticks.length);
+ var shouldSkip = (skipRatio > 1 && index % skipRatio > 0) || (index % skipRatio === 0 && index + skipRatio >= tickCount);
if (shouldSkip && !isLastTick || helpers.isNullOrUndef(label)) {
return;
}
return ticks;
}
+function ticksFromTimestamps(values, majorUnit) {
+ var ticks = [];
+ var i, ilen, value, major;
+
+ for (i = 0, ilen = values.length; i < ilen; ++i) {
+ value = values[i];
+ major = majorUnit ? value === +moment(value).startOf(majorUnit) : false;
+
+ ticks.push({
+ value: value,
+ major: major
+ });
+ }
+
+ return ticks;
+}
+
module.exports = function(Chart) {
var defaultConfig = {
}
}
- me.ticks = ticks;
me.min = min;
me.max = max;
// PRIVATE
me._unit = unit;
me._majorUnit = majorUnit;
- me._displayFormat = formats[unit];
- me._majorDisplayFormat = formats[majorUnit];
+ me._minorFormat = formats[unit];
+ me._majorFormat = formats[majorUnit];
me._table = buildLookupTable(ticks, min, max, ticksOpts.mode === 'linear');
+
+ return ticksFromTimestamps(ticks, majorUnit);
},
getLabelForIndex: function(index, datasetIndex) {
var options = me.options;
var time = tick.valueOf();
var majorUnit = me._majorUnit;
- var majorFormat = me._majorDisplayFormat;
+ var majorFormat = me._majorFormat;
var majorTime = tick.clone().startOf(me._majorUnit).valueOf();
var major = majorUnit && majorFormat && time === majorTime;
- var formattedTick = tick.format(major ? majorFormat : me._displayFormat);
+ var label = tick.format(major ? majorFormat : me._minorFormat);
var tickOpts = major ? options.ticks.major : options.ticks.minor;
var formatter = helpers.valueOrDefault(tickOpts.callback, tickOpts.userCallback);
- if (formatter) {
- formattedTick = formatter(formattedTick, index, ticks);
- }
-
- return {
- value: formattedTick,
- major: major,
- time: time,
- };
+ return formatter ? formatter(label, index, ticks) : label;
},
- convertTicksToLabels: function() {
- var ticks = this.ticks;
+ convertTicksToLabels: function(ticks) {
+ var labels = [];
var i, ilen;
for (i = 0, ilen = ticks.length; i < ilen; ++i) {
- ticks[i] = this.tickFormatFunction(moment(ticks[i]));
+ labels.push(this.tickFormatFunction(moment(ticks[i].value), i, ticks));
}
+
+ return labels;
},
/**
},
getPixelForTick: function(index) {
- return index >= 0 && index < this.ticks.length ?
- this.getPixelForOffset(this.ticks[index].time) :
+ var ticks = this.getTicks();
+ return index >= 0 && index < ticks.length ?
+ this.getPixelForOffset(ticks[index].value) :
null;
},
getLabelCapacity: function(exampleTime) {
var me = this;
- me._displayFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation
+ me._minorFormat = me.options.time.displayFormats.millisecond; // Pick the longest format for guestimation
- var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []).value;
+ var exampleLabel = me.tickFormatFunction(moment(exampleTime), 0, []);
var tickLabelWidth = me.getLabelWidth(exampleLabel);
var innerWidth = me.isHorizontal() ? me.width : me.height;
return scale;
}
- function getTicksValues(ticks) {
- return ticks.map(function(tick) {
- return tick.value;
- });
+ function getTicksLabels(scale) {
+ return scale.ticks;
}
function fetchTickPositions(scale) {
var scaleOptions = Chart.scaleService.getScaleDefaults('time');
var scale = createScale(mockData, scaleOptions);
scale.update(1000, 200);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
// `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
};
var scale = createScale(mockData, Chart.scaleService.getScaleDefaults('time'));
scale.update(1000, 200);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
// `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
var xScale = chart.scales.xScale0;
xScale.update(800, 200);
- var ticks = getTicksValues(xScale.ticks);
+ var ticks = getTicksLabels(xScale);
// `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
var tScale = chart.scales.tScale0;
tScale.update(800, 200);
- var ticks = getTicksValues(tScale.ticks);
+ var ticks = getTicksLabels(tScale);
// `ticks.bounds === 'data'`: first and last ticks removed since outside the data range
expect(ticks).toEqual(['Jan 2', 'Jan 3', 'Jan 4', 'Jan 5', 'Jan 6', 'Jan 7', 'Jan 8', 'Jan 9', 'Jan 10']);
var xScale = chart.scales.xScale0;
// Counts down because the lines are drawn top to bottom
- expect(xScale.ticks[0].value).toBe('Jan 2');
- expect(xScale.ticks[1].value).toBe('May 8');
+ expect(xScale.ticks[0]).toBe('Jan 2');
+ expect(xScale.ticks[1]).toBe('May 8');
});
it('should build ticks using the config unit', function() {
var scale = createScale(mockData, config);
scale.update(2500, 200);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
expect(ticks).toEqual(['8PM', '9PM', '10PM', '11PM', 'Jan 2', '1AM', '2AM', '3AM', '4AM', '5AM', '6AM', '7AM', '8AM', '9AM', '10AM', '11AM', '12PM', '1PM', '2PM', '3PM', '4PM', '5PM', '6PM', '7PM', '8PM', '9PM']);
});
}, Chart.scaleService.getScaleDefaults('time'));
var scale = createScale(mockData, config);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
expect(ticks).toEqual(['Jan 2015', 'Jan 2', 'Jan 3']);
});
var scale = createScale(mockData, config);
scale.update(800, 200);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
// last date is feb 15 because we round to start of week
expect(ticks).toEqual(['Dec 28, 2014', 'Jan 4, 2015', 'Jan 11, 2015', 'Jan 18, 2015', 'Jan 25, 2015', 'Feb 2015', 'Feb 8, 2015', 'Feb 15, 2015']);
var scale = createScale(mockData, config);
scale.update(2500, 200);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
expect(ticks).toEqual(['8PM', '10PM']);
});
config.time.min = '2014-12-29T04:00:00';
var scale = createScale(mockData, config);
- expect(scale.ticks[0].value).toEqual('Dec 31');
+ expect(scale.ticks[0]).toEqual('Dec 31');
});
it('should use the max option', function() {
var scale = createScale(mockData, config);
- expect(scale.ticks[scale.ticks.length - 1].value).toEqual('Jan 5');
+ expect(scale.ticks[scale.ticks.length - 1]).toEqual('Jan 5');
});
});
}, Chart.scaleService.getScaleDefaults('time'));
var scale = createScale(mockData, config);
- var ticks = getTicksValues(scale.ticks);
+ var ticks = getTicksLabels(scale);
expect(ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']);
});
it('should be bounded by nearest step\'s year start and end', function() {
var scale = this.scale;
- var step = scale.ticks[1].time - scale.ticks[0].time;
+ var ticks = scale.getTicks();
+ var step = ticks[1].value - ticks[0].value;
var stepsAmount = Math.floor((scale.max - scale.min) / step);
expect(scale.getValueForPixel(scale.left)).toBeCloseToTime({
it('should build the correct ticks', function() {
// Where 'correct' is a two year spacing.
- expect(getTicksValues(this.scale.ticks)).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']);
+ expect(getTicksLabels(this.scale)).toEqual(['2005', '2007', '2009', '2011', '2013', '2015', '2017', '2019']);
});
it('should have ticks with accurate labels', function() {
var scale = this.scale;
- var ticks = scale.ticks;
+ var ticks = scale.getTicks();
var pixelsPerYear = scale.width / 14;
for (var i = 0; i < ticks.length - 1; i++) {
var offset = 2 * pixelsPerYear * i;
expect(scale.getValueForPixel(scale.left + offset)).toBeCloseToTime({
- value: moment(ticks[i].value + '-01-01'),
+ value: moment(ticks[i].label + '-01-01'),
unit: 'day',
threshold: 0.5,
});
expect(scale.min).toEqual(+moment('2017', 'YYYY'));
expect(scale.max).toEqual(+moment('2042', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2017', '2019', '2020', '2025', '2042']);
});
it ('should not add ticks for min and max if they extend the labels range', function() {
expect(scale.min).toEqual(+moment('2012', 'YYYY'));
expect(scale.max).toEqual(+moment('2051', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2017', '2019', '2020', '2025', '2042']);
});
it ('should not duplicate ticks if min and max are the labels limits', function() {
expect(scale.min).toEqual(+moment('2017', 'YYYY'));
expect(scale.max).toEqual(+moment('2042', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2017', '2019', '2020', '2025', '2042']);
});
it ('should correctly handle empty `data.labels`', function() {
expect(scale.min).toEqual(+moment().startOf('day'));
expect(scale.max).toEqual(+moment().endOf('day') + 1);
- expect(getTicksValues(scale.ticks)).toEqual([]);
+ expect(getTicksLabels(scale)).toEqual([]);
});
});
expect(scale.min).toEqual(+moment('2017', 'YYYY'));
expect(scale.max).toEqual(+moment('2043', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2017', '2018', '2019', '2020', '2025', '2042', '2043']);
});
it ('should not add ticks for min and max if they extend the labels range', function() {
expect(scale.min).toEqual(+moment('2012', 'YYYY'));
expect(scale.max).toEqual(+moment('2051', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2017', '2018', '2019', '2020', '2025', '2042', '2043']);
});
it ('should not duplicate ticks if min and max are the labels limits', function() {
expect(scale.min).toEqual(+moment('2017', 'YYYY'));
expect(scale.max).toEqual(+moment('2043', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2017', '2018', '2019', '2020', '2025', '2042', '2043']);
});
it ('should correctly handle empty `data.labels`', function() {
expect(scale.min).toEqual(+moment('2018', 'YYYY'));
expect(scale.max).toEqual(+moment('2043', 'YYYY'));
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'2018', '2020', '2043']);
});
});
expect(scale.max).toEqual(+moment('02/23 11:00', 'MM/DD HH:mm'));
expect(scale.getPixelForValue('02/20 08:00')).toBeCloseToPixel(scale.left);
expect(scale.getPixelForValue('02/23 11:00')).toBeCloseToPixel(scale.left + scale.width);
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(getTicksLabels(scale)).toEqual([
'Feb 21', 'Feb 22', 'Feb 23']);
});
});
});
var scale = chart.scales.x;
- var ticks = scale.ticks;
+ var ticks = scale.getTicks();
- expect(scale.min).toEqual(ticks[0].time);
- expect(scale.max).toEqual(ticks[ticks.length - 1].time);
- expect(scale.getPixelForValue('02/20 08:00')).toBeCloseToPixel(77);
- expect(scale.getPixelForValue('02/23 11:00')).toBeCloseToPixel(412);
- expect(getTicksValues(scale.ticks)).toEqual([
+ expect(scale.min).toEqual(ticks[0].value);
+ expect(scale.max).toEqual(ticks[ticks.length - 1].value);
+ expect(scale.getPixelForValue('02/20 08:00')).toBeCloseToPixel(60);
+ expect(scale.getPixelForValue('02/23 11:00')).toBeCloseToPixel(426);
+ expect(getTicksLabels(scale)).toEqual([
'Feb 20', 'Feb 21', 'Feb 22', 'Feb 23', 'Feb 24']);
});
});
expect(scale.max).toEqual(+moment(max, 'MM/DD HH:mm'));
expect(scale.getPixelForValue(min)).toBeCloseToPixel(scale.left);
expect(scale.getPixelForValue(max)).toBeCloseToPixel(scale.left + scale.width);
- scale.ticks.forEach(function(tick) {
- expect(tick.time >= +moment(min, 'MM/DD HH:mm')).toBeTruthy();
- expect(tick.time <= +moment(max, 'MM/DD HH:mm')).toBeTruthy();
+ scale.getTicks().forEach(function(tick) {
+ expect(tick.value >= +moment(min, 'MM/DD HH:mm')).toBeTruthy();
+ expect(tick.value <= +moment(max, 'MM/DD HH:mm')).toBeTruthy();
});
});
it ('should shrink scale to the min/max range', function() {
expect(scale.max).toEqual(+moment(max, 'MM/DD HH:mm'));
expect(scale.getPixelForValue(min)).toBeCloseToPixel(scale.left);
expect(scale.getPixelForValue(max)).toBeCloseToPixel(scale.left + scale.width);
- scale.ticks.forEach(function(tick) {
- expect(tick.time >= +moment(min, 'MM/DD HH:mm')).toBeTruthy();
- expect(tick.time <= +moment(max, 'MM/DD HH:mm')).toBeTruthy();
+ scale.getTicks().forEach(function(tick) {
+ expect(tick.value >= +moment(min, 'MM/DD HH:mm')).toBeTruthy();
+ expect(tick.value <= +moment(max, 'MM/DD HH:mm')).toBeTruthy();
});
});
});