Chart = root.Chart,
helpers = Chart.helpers;
+ var time = {
+ units: [
+ 'millisecond',
+ 'second',
+ 'minute',
+ 'hour',
+ 'day',
+ 'week',
+ 'month',
+ 'quarter',
+ 'year',
+ ],
+ unit: {
+ 'millisecond': {
+ display: 'SSS [ms]', // 002 ms
+ maxStep: 1000,
+ },
+ 'second': {
+ display: 'h:mm:ss a', // 11:20:01 AM
+ maxStep: 60,
+ },
+ 'minute': {
+ display: 'h:mm:ss a', // 11:20:01 AM
+ maxStep: 60,
+ },
+ 'hour': {
+ display: 'MMM D, hA', // Sept 4, 5PM
+ maxStep: 24,
+ },
+ 'day': {
+ display: 'll', // Sep 4 2015
+ maxStep: 7,
+ },
+ 'week': {
+ display: 'll', // Week 46, or maybe "[W]WW - YYYY" ?
+ maxStep: 4.3333,
+ },
+ 'month': {
+ display: 'MMM YYYY', // Sept 2015
+ maxStep: 12,
+ },
+ 'quarter': {
+ display: '[Q]Q - YYYY', // Q3
+ maxStep: 4,
+ },
+ 'year': {
+ display: 'YYYY', // 2015
+ maxStep: false,
+ },
+ }
+ };
+
var defaultConfig = {
display: true,
position: "bottom",
drawTicks: true, // draw ticks extending towards the label
},
- time: {
- format: false, // http://momentjs.com/docs/#/parsing/string-format/
- unit: false, // week, month, year, etc.
- aggregation: 'average',
- display: false, //http://momentjs.com/docs/#/parsing/string-format/
- unitFormats: {
- 'millisecond': 'h:mm:ss SSS', // 11:20:01 002
- 'second': 'h:mm:ss', // 11:20:01
- 'minute': 'h:mm:ss a', // 11:20:01 AM
- 'hour': 'MMM D, hA', // Sept 4, 5PM
- 'day': 'll', // Sep 4 2015 8:30 PM
- 'week': '[W]WW - YYYY', // Week 46
- 'month': 'MMM YYYY', // Sept 2015
- 'quarter': '[Q]Q - YYYY', // Q3
- 'year': 'YYYY' // 2015
- }
+ tick: {
+ format: false, // false == date objects or use pattern string from http://momentjs.com/docs/#/parsing/string-format/
+ unit: false, // false == automatic or override with week, month, year, etc.
+ round: false, // none, or override with week, month, year, etc.
+ displayFormat: false, // defaults to unit's corresponding unitFormat below or override using pattern string from http://momentjs.com/docs/#/displaying/format/
},
// scale numbers
fontSize: 12,
fontStyle: "normal",
fontColor: "#666",
- fontFamily: "Helvetica Neue"
+ fontFamily: "Helvetica Neue",
+ maxRotation: 45,
}
};
return this.options.position == "top" || this.options.position == "bottom";
},
parseTime: function(label) {
- if (typeof this.options.time.format !== 'string' && this.options.time.format.call) {
- return this.options.time.format(label);
- } else {
- return moment(label, this.options.time.format);
+ // Date objects
+ if (typeof label.getMonth === 'function' || typeof label == 'number') {
+ return moment(label);
+ }
+ // Moment support
+ if (label.isValid && label.isValid()) {
+ return label;
}
+ // Custom parsing (return an instance of moment)
+ if (typeof this.options.tick.format !== 'string' && this.options.tick.format.call) {
+ return this.options.tick.format(label);
+ }
+ // Moment format parsing
+ return moment(label, this.options.tick.format);
},
- buildLabels: function(index) {
- // Actual labels on the grid
- this.labels = [];
- // A map of original labelIndex to time labelIndex
- this.timeLabelIndexMap = {};
- // The time formatted versions of the labels for use by tooltips
- this.data.timeLabels = [];
+ generateTicks: function(index) {
- var definedMoments = [];
+ this.ticks = [];
+ this.labelMoments = [];
- // Format each point into a moment
+ // Parse each label into a moment
this.data.labels.forEach(function(label, index) {
- definedMoments.push(this.parseTime(label));
+ var labelMoment = this.parseTime(label);
+ if (this.options.tick.round) {
+ labelMoment.startOf(this.options.tick.round);
+ }
+ this.labelMoments.push(labelMoment);
}, this);
+ // Find the first and last moments, and range
+ this.firstTick = moment.min.call(this, this.labelMoments).clone();
+ this.lastTick = moment.max.call(this, this.labelMoments).clone();
- // Find or set the unit of time
- if (!this.options.time.unit) {
-
+ // Set unit override if applicable
+ if (this.options.tick.unit) {
+ this.tickUnit = this.options.tick.unit || 'day';
+ this.displayFormat = time.unit.day.display;
+ this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit, true));
+ } else {
// Determine the smallest needed unit of the time
- helpers.each([
- 'millisecond',
- 'second',
- 'minute',
- 'hour',
- 'day',
- 'week',
- 'month',
- 'quarter',
- 'year',
- ], function(format) {
- if (this.timeUnit) {
+ var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
+ var labelCapacity = innerWidth / this.options.labels.fontSize + 4;
+ var buffer = this.options.tick.round ? 0 : 2;
+
+ this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, true) + buffer);
+ var done;
+
+ helpers.each(time.units, function(format) {
+ if (this.tickRange <= labelCapacity) {
return;
}
- var start;
- helpers.each(definedMoments, function(mom) {
- if (!start) {
- start = mom[format]();
- }
+ this.tickUnit = format;
+ this.tickRange = Math.ceil(this.lastTick.diff(this.firstTick, this.tickUnit) + buffer);
+ this.displayFormat = time.unit[format].display;
- if (mom[format]() !== start) {
- this.timeUnit = format;
- if (!this.displayFormat) {
- this.displayFormat = this.options.time.unitFormats[format];
- }
- }
- }, this);
}, this);
- } else {
- this.timeUnit = this.options.time.unit;
}
- if (!this.timeUnit) {
- this.timeUnit = 'day';
- this.displayFormat = this.options.time.unitFormats.day;
- }
+ this.firstTick.startOf(this.tickUnit);
+ this.lastTick.endOf(this.tickUnit);
- if (this.options.time.display) {
- this.displayFormat = this.options.time.display;
+ // Tick displayFormat override
+ if (this.options.tick.displayFormat) {
+ this.displayFormat = this.options.tick.displayFormat;
}
-
- // Find the first and last moments
- this.firstMoment = moment.min.call(this, definedMoments);
- this.lastMoment = moment.max.call(this, definedMoments);
-
- // Find the length of the timeframe in the desired unit
- var momentRangeLength = this.lastMoment.diff(this.firstMoment, this.timeUnit);
-
- helpers.each(definedMoments, function(definedMoment, index) {
- this.timeLabelIndexMap[index] = momentRangeLength - this.lastMoment.diff(definedMoment, this.timeUnit);
- this.data.timeLabels.push(
- definedMoment
- .format(this.options.time.display ? this.options.time.display : this.displayFormat)
- );
- }, this);
-
// For every unit in between the first and last moment, create a moment and add it to the labels tick
var i = 0;
if (this.options.labels.userCallback) {
- for (; i <= momentRangeLength; i++) {
- this.labels.push(
- this.options.labels.userCallback(this.firstMoment
- .add((!i ? 0 : 1), this.timeUnit)
- .format(this.options.time.display ? this.options.time.display : this.displayFormat)
+ for (; i <= this.tickRange; i++) {
+ this.ticks.push(
+ this.options.labels.userCallback(this.firstTick.clone()
+ .add(i, this.tickUnit)
+ .format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display)
)
);
}
} else {
- for (; i <= momentRangeLength; i++) {
- this.labels.push(this.firstMoment
- .add((!i ? 0 : 1), this.timeUnit)
- .format(this.options.time.display ? this.options.time.display : this.displayFormat)
+ for (; i <= this.tickRange; i++) {
+ this.ticks.push(this.firstTick.clone()
+ .add(i, this.tickUnit)
+ .format(this.options.tick.displayFormat ? this.options.tick.displayFormat : time.unit[this.tickUnit].display)
);
}
}
-
-
},
- getPixelForValue: function(value, index, datasetIndex, includeOffset) {
+ getPixelForValue: function(value, decimal, datasetIndex, includeOffset) {
// This must be called after fit has been run so that
// this.left, this.top, this.right, and this.bottom have been defined
if (this.isHorizontal()) {
var innerWidth = this.width - (this.paddingLeft + this.paddingRight);
- var valueWidth = innerWidth / Math.max((this.labels.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
- var valueOffset = (valueWidth * index) + this.paddingLeft;
+ var valueWidth = innerWidth / Math.max((this.ticks.length - ((this.options.gridLines.offsetGridLines) ? 0 : 1)), 1);
+ var valueOffset = (innerWidth * decimal) + this.paddingLeft;
if (this.options.gridLines.offsetGridLines && includeOffset) {
valueOffset += (valueWidth / 2);
return this.left + Math.round(valueOffset);
} else {
- return this.top + (index * (this.height / this.labels.length));
+ return this.top + (decimal * (this.height / this.ticks.length));
}
},
getPointPixelForValue: function(value, index, datasetIndex) {
- // This function references the timeLaabelIndexMap to know which index in the timeLabels corresponds to the index of original labels
- return this.getPixelForValue(value, this.timeLabelIndexMap[index], datasetIndex, true);
+
+ var offset = this.labelMoments[index].diff(this.firstTick, this.tickUnit, true);
+ return this.getPixelForValue(value, offset / this.tickRange, datasetIndex);
},
// Functions needed for bar charts
calculateBaseWidth: function() {
- return (this.getPixelForValue(null, 1, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing);
+ return (this.getPixelForValue(null, this.ticks.length / 100, 0, true) - this.getPixelForValue(null, 0, 0, true)) - (2 * this.options.categorySpacing);
},
calculateBarWidth: function(barDatasetCount) {
//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
return (baseWidth / barDatasetCount);
},
calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) {
+
var xWidth = this.calculateBaseWidth(),
xAbsolute = this.getPixelForValue(null, elementIndex, datasetIndex, true) - (xWidth / 2),
barWidth = this.calculateBarWidth(barDatasetCount);
return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2;
},
- calculateLabelRotation: function(maxHeight, margins) {
+ calculateTickRotation: function(maxHeight, margins) {
//Get the width of each grid by calculating the difference
//between x offsets between 0 and 1.
var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
this.ctx.font = labelFont;
- var firstWidth = this.ctx.measureText(this.labels[0]).width;
- var lastWidth = this.ctx.measureText(this.labels[this.labels.length - 1]).width;
+ var firstWidth = this.ctx.measureText(this.ticks[0]).width;
+ var lastWidth = this.ctx.measureText(this.ticks[this.ticks.length - 1]).width;
var firstRotated;
var lastRotated;
this.labelRotation = 0;
if (this.options.display) {
- var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels);
+ var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
var cosRotation;
var sinRotation;
var firstRotatedWidth;
//Allow 3 pixels x2 padding either side for label readability
// only the index matters for a dataset scale, but we want a consistent interface between scales
- var datasetWidth = Math.floor(this.getPixelForValue(0, 1) - this.getPixelForValue(0, 0)) - 6;
+ var datasetWidth = Math.floor(this.getPixelForValue(null, 1 / this.ticks.length) - this.getPixelForValue(null, 0)) - 6;
//Max label rotation can be set or default to 90 - also act as a loop counter
while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.labels.maxRotation) {
this.labelRotation++;
this.labelWidth = cosRotation * originalLabelWidth;
+
}
} else {
this.labelWidth = 0;
this.height = maxHeight;
}
- this.buildLabels();
- this.calculateLabelRotation(maxHeight, margins);
+ this.generateTicks();
+ this.calculateTickRotation(maxHeight, margins);
var minSize = {
width: 0,
};
var labelFont = helpers.fontString(this.options.labels.fontSize, this.options.labels.fontStyle, this.options.labels.fontFamily);
- var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.labels);
+ var longestLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
// Width
if (this.isHorizontal()) {
var isRotated = this.labelRotation !== 0;
var skipRatio = false;
- if ((this.options.labels.fontSize + 4) * this.labels.length > (this.width - (this.paddingLeft + this.paddingRight))) {
- skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.labels.length) / (this.width - (this.paddingLeft + this.paddingRight)));
+ if ((this.options.labels.fontSize + 4) * this.ticks.length > (this.width - (this.paddingLeft + this.paddingRight))) {
+ skipRatio = 1 + Math.floor(((this.options.labels.fontSize + 4) * this.ticks.length) / (this.width - (this.paddingLeft + this.paddingRight)));
}
- helpers.each(this.labels, function(label, index) {
- // Blank labels
- if ((skipRatio > 1 && index % skipRatio > 0) || (label === undefined || label === null)) {
+ helpers.each(this.ticks, function(tick, index) {
+ // Blank ticks
+ if ((skipRatio > 1 && index % skipRatio > 0) || (tick === undefined || tick === null)) {
return;
}
- var xLineValue = this.getPixelForValue(label, index, null, false); // xvalues for grid lines
- var xLabelValue = this.getPixelForValue(label, index, null, true); // x values for labels (need to consider offsetLabel option)
+ var xLineValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, false); // xvalues for grid lines
+ var xLabelValue = this.getPixelForValue(null, (1 / (this.ticks.length - 1)) * index, null, true); // x values for ticks (need to consider offsetLabel option)
if (this.options.gridLines.show) {
if (index === 0) {
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
- // Draw the label area
+ // Draw the tick area
this.ctx.beginPath();
if (this.options.gridLines.drawTicks) {
this.ctx.font = this.font;
this.ctx.textAlign = (isRotated) ? "right" : "center";
this.ctx.textBaseline = (isRotated) ? "middle" : "top";
- this.ctx.fillText(label, 0, 0);
+ this.ctx.fillText(tick, 0, 0);
this.ctx.restore();
}
}, this);