mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Time Scale Rewrite
This commit is contained in:
parent
88d30d8c93
commit
2598446d54
@ -38,12 +38,21 @@
|
||||
var randomColor = function(opacity) {
|
||||
return 'rgba(' + randomColorFactor() + ',' + randomColorFactor() + ',' + randomColorFactor() + ',' + (opacity || '.3') + ')';
|
||||
};
|
||||
var newDate = function(days) {
|
||||
var date = new Date();
|
||||
return date.setDate(date.getDate() + days);
|
||||
};
|
||||
var newTimestamp = function(days) {
|
||||
return Date.now() - days * 100000;
|
||||
};
|
||||
|
||||
var config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
// labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/06/2015 23:00", "01/15/2015 03:00", "01/17/2015 10:00", "01/30/2015 11:00"], // Hours
|
||||
labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days
|
||||
//labels: [newTimestamp(0), newTimestamp(1), newTimestamp(2), newTimestamp(3), newTimestamp(4), newTimestamp(5), newTimestamp(6)], // unix timestamps
|
||||
// labels: [newDate(0), newDate(1), newDate(2), newDate(3), newDate(4), newDate(5), newDate(6)], // Date Objects
|
||||
labels: ["01/01/2015 20:00", "01/02/2015 21:00", "01/03/2015 22:00", "01/05/2015 23:00", "01/07/2015 03:00", "01/08/2015 10:00", "02/1/2015"], // Hours
|
||||
// labels: ["01/01/2015", "01/02/2015", "01/03/2015", "01/06/2015", "01/15/2015", "01/17/2015", "01/30/2015"], // Days
|
||||
// labels: ["12/25/2014", "01/08/2015", "01/15/2015", "01/22/2015", "01/29/2015", "02/05/2015", "02/12/2015"], // Weeks
|
||||
datasets: [{
|
||||
label: "My First dataset",
|
||||
@ -61,14 +70,20 @@
|
||||
xAxes: [{
|
||||
type: "time",
|
||||
display: true,
|
||||
time: {
|
||||
tick: {
|
||||
format: 'MM/DD/YYYY HH:mm',
|
||||
// round: 'day'
|
||||
}
|
||||
}, ],
|
||||
yAxes: [{
|
||||
display: true
|
||||
}]
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
line: {
|
||||
tension: 0
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -5,6 +5,58 @@
|
||||
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",
|
||||
@ -18,22 +70,11 @@
|
||||
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
|
||||
@ -49,7 +90,8 @@
|
||||
fontSize: 12,
|
||||
fontStyle: "normal",
|
||||
fontColor: "#666",
|
||||
fontFamily: "Helvetica Neue"
|
||||
fontFamily: "Helvetica Neue",
|
||||
maxRotation: 45,
|
||||
}
|
||||
};
|
||||
|
||||
@ -58,119 +100,100 @@
|
||||
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);
|
||||
|
||||
|
||||
// Tick displayFormat override
|
||||
if (this.options.tick.displayFormat) {
|
||||
this.displayFormat = this.options.tick.displayFormat;
|
||||
}
|
||||
|
||||
|
||||
if (this.options.time.display) {
|
||||
this.displayFormat = this.options.time.display;
|
||||
}
|
||||
|
||||
|
||||
// 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);
|
||||
@ -178,17 +201,18 @@
|
||||
|
||||
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
|
||||
@ -200,6 +224,7 @@
|
||||
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);
|
||||
@ -211,14 +236,14 @@
|
||||
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;
|
||||
|
||||
@ -228,7 +253,7 @@
|
||||
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;
|
||||
@ -238,7 +263,7 @@
|
||||
//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) {
|
||||
@ -264,6 +289,7 @@
|
||||
this.labelRotation++;
|
||||
this.labelWidth = cosRotation * originalLabelWidth;
|
||||
|
||||
|
||||
}
|
||||
} else {
|
||||
this.labelWidth = 0;
|
||||
@ -292,8 +318,8 @@
|
||||
this.height = maxHeight;
|
||||
}
|
||||
|
||||
this.buildLabels();
|
||||
this.calculateLabelRotation(maxHeight, margins);
|
||||
this.generateTicks();
|
||||
this.calculateTickRotation(maxHeight, margins);
|
||||
|
||||
var minSize = {
|
||||
width: 0,
|
||||
@ -301,7 +327,7 @@
|
||||
};
|
||||
|
||||
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()) {
|
||||
@ -340,17 +366,17 @@
|
||||
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) {
|
||||
@ -366,7 +392,7 @@
|
||||
|
||||
xLineValue += helpers.aliasPixel(this.ctx.lineWidth);
|
||||
|
||||
// Draw the label area
|
||||
// Draw the tick area
|
||||
this.ctx.beginPath();
|
||||
|
||||
if (this.options.gridLines.drawTicks) {
|
||||
@ -391,7 +417,7 @@
|
||||
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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user