2015-09-11 06:48:48 +02:00
( function ( ) {
"use strict" ;
var root = this ,
Chart = root . Chart ,
helpers = Chart . helpers ;
2015-09-15 19:40:01 +02:00
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 ,
} ,
}
} ;
2015-09-11 06:48:48 +02:00
var defaultConfig = {
position : "bottom" ,
2015-09-24 05:52:31 +02:00
time : {
2015-09-15 19:40:01 +02:00
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/
2015-09-11 06:48:48 +02:00
} ,
} ;
2015-09-23 03:31:26 +02:00
var TimeScale = Chart . Scale . extend ( {
2015-09-11 06:48:48 +02:00
parseTime : function ( label ) {
2015-09-15 19:40:01 +02:00
// Date objects
if ( typeof label . getMonth === 'function' || typeof label == 'number' ) {
return moment ( label ) ;
}
// Moment support
if ( label . isValid && label . isValid ( ) ) {
return label ;
2015-09-11 06:48:48 +02:00
}
2015-09-15 19:40:01 +02:00
// Custom parsing (return an instance of moment)
2015-09-24 05:52:31 +02:00
if ( typeof this . options . time . format !== 'string' && this . options . time . format . call ) {
return this . options . time . format ( label ) ;
2015-09-15 19:40:01 +02:00
}
// Moment format parsing
2015-09-24 05:52:31 +02:00
return moment ( label , this . options . time . format ) ;
2015-09-11 06:48:48 +02:00
} ,
2015-09-15 19:40:01 +02:00
generateTicks : function ( index ) {
2015-09-11 06:48:48 +02:00
2015-09-15 19:40:01 +02:00
this . ticks = [ ] ;
this . labelMoments = [ ] ;
2015-09-11 06:48:48 +02:00
2015-09-15 19:40:01 +02:00
// Parse each label into a moment
2015-09-11 06:48:48 +02:00
this . data . labels . forEach ( function ( label , index ) {
2015-09-15 19:40:01 +02:00
var labelMoment = this . parseTime ( label ) ;
2015-09-24 05:52:31 +02:00
if ( this . options . time . round ) {
labelMoment . startOf ( this . options . time . round ) ;
2015-09-15 19:40:01 +02:00
}
this . labelMoments . push ( labelMoment ) ;
2015-09-11 06:48:48 +02:00
} , this ) ;
2015-09-15 19:40:01 +02:00
// 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 ( ) ;
2015-09-11 06:48:48 +02:00
2015-09-15 19:40:01 +02:00
// Set unit override if applicable
2015-09-24 05:52:31 +02:00
if ( this . options . time . unit ) {
this . tickUnit = this . options . time . unit || 'day' ;
2015-09-15 19:40:01 +02:00
this . displayFormat = time . unit . day . display ;
this . tickRange = Math . ceil ( this . lastTick . diff ( this . firstTick , this . tickUnit , true ) ) ;
} else {
2015-09-11 06:48:48 +02:00
// Determine the smallest needed unit of the time
2015-09-15 19:40:01 +02:00
var innerWidth = this . width - ( this . paddingLeft + this . paddingRight ) ;
2015-09-23 03:31:26 +02:00
var labelCapacity = innerWidth / this . options . ticks . fontSize + 4 ;
2015-09-24 05:52:31 +02:00
var buffer = this . options . time . round ? 0 : 2 ;
2015-09-15 19:40:01 +02:00
this . tickRange = Math . ceil ( this . lastTick . diff ( this . firstTick , true ) + buffer ) ;
var done ;
helpers . each ( time . units , function ( format ) {
if ( this . tickRange <= labelCapacity ) {
2015-09-11 06:48:48 +02:00
return ;
}
2015-09-15 19:40:01 +02:00
this . tickUnit = format ;
this . tickRange = Math . ceil ( this . lastTick . diff ( this . firstTick , this . tickUnit ) + buffer ) ;
this . displayFormat = time . unit [ format ] . display ;
2015-09-11 06:48:48 +02:00
} , this ) ;
}
2015-09-15 19:40:01 +02:00
this . firstTick . startOf ( this . tickUnit ) ;
this . lastTick . endOf ( this . tickUnit ) ;
2015-09-18 19:31:25 +02:00
this . smallestLabelSeparation = this . width ;
var i = 0 ;
for ( i = 1 ; i < this . labelMoments . length ; i ++ ) {
this . smallestLabelSeparation = Math . min ( this . smallestLabelSeparation , this . labelMoments [ i ] . diff ( this . labelMoments [ i - 1 ] , this . tickUnit , true ) ) ;
}
2015-09-11 06:48:48 +02:00
2015-09-15 19:40:01 +02:00
// Tick displayFormat override
2015-09-24 05:52:31 +02:00
if ( this . options . time . displayFormat ) {
this . displayFormat = this . options . time . displayFormat ;
2015-09-11 06:48:48 +02:00
}
2015-09-23 03:31:26 +02:00
// For every unit in between the first and last moment, create a moment and add it to the ticks tick
if ( this . options . ticks . userCallback ) {
2015-09-21 19:52:17 +02:00
for ( i = 0 ; i <= this . tickRange ; i ++ ) {
2015-09-15 19:40:01 +02:00
this . ticks . push (
2015-09-23 03:31:26 +02:00
this . options . ticks . userCallback ( this . firstTick . clone ( )
2015-09-15 19:40:01 +02:00
. add ( i , this . tickUnit )
2015-09-24 05:52:31 +02:00
. format ( this . options . time . displayFormat ? this . options . time . displayFormat : time . unit [ this . tickUnit ] . display )
2015-09-11 06:48:48 +02:00
)
) ;
}
} else {
2015-09-21 19:52:17 +02:00
for ( i = 0 ; i <= this . tickRange ; i ++ ) {
2015-09-15 19:40:01 +02:00
this . ticks . push ( this . firstTick . clone ( )
. add ( i , this . tickUnit )
2015-09-24 05:52:31 +02:00
. format ( this . options . time . displayFormat ? this . options . time . displayFormat : time . unit [ this . tickUnit ] . display )
2015-09-11 06:48:48 +02:00
) ;
}
}
} ,
2015-09-24 05:52:31 +02:00
getSmallestDataDistance : function ( ) {
return this . smallestLabelSeparation ;
} ,
getPixelForValue : function ( value , datasetIndex , includeOffset ) {
2015-09-11 06:48:48 +02:00
// This must be called after fit has been run so that
// this.left, this.top, this.right, and this.bottom have been defined
2015-09-24 05:52:31 +02:00
var decimal = 0.5 ;
2015-09-11 06:48:48 +02:00
if ( this . isHorizontal ( ) ) {
var innerWidth = this . width - ( this . paddingLeft + this . paddingRight ) ;
2015-09-18 19:31:25 +02:00
var valueWidth = innerWidth / Math . max ( this . ticks . length - 1 , 1 ) ;
2015-09-15 19:40:01 +02:00
var valueOffset = ( innerWidth * decimal ) + this . paddingLeft ;
2015-09-11 06:48:48 +02:00
return this . left + Math . round ( valueOffset ) ;
} else {
2015-09-15 19:40:01 +02:00
return this . top + ( decimal * ( this . height / this . ticks . length ) ) ;
2015-09-11 06:48:48 +02:00
}
} ,
2015-09-24 05:52:31 +02:00
// getPointPixelForValue: function(value, index, datasetIndex) {
// 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() {
// var base = this.getPixelForValue(null, this.smallestLabelSeparation / this.tickRange, 0, true) - this.getPixelForValue(null, 0, 0, true);
// var spacing = 2 * this.options.categorySpacing;
// if (base < spacing * 2) {
// var mod = Math.min((spacing * 2) / base, 1.5);
// base = (base / 2) * mod;
// return base;
// }
// return base - spacing;
// },
// calculateBarWidth: function(barDatasetCount) {
// //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset
// var baseWidth = this.calculateBaseWidth() - ((barDatasetCount - 1) * this.options.spacing);
// if (this.options.stacked) {
// return Math.max(baseWidth, 1);
// }
// return Math.max((baseWidth / barDatasetCount), 1);
// },
// calculateBarX: function(barDatasetCount, datasetIndex, elementIndex) {
// var xWidth = this.calculateBaseWidth(),
// offset = this.labelMoments[elementIndex].diff(this.firstTick, this.tickUnit, true),
// xAbsolute = this.getPixelForValue(null, offset / this.tickRange, datasetIndex, true) - (xWidth / 2),
// barWidth = this.calculateBarWidth(barDatasetCount);
// if (this.options.stacked) {
// return xAbsolute + barWidth / 2;
// }
// return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * this.options.spacing) + barWidth / 2;
// },
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// 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.ticks.fontSize, this.options.ticks.fontStyle, this.options.ticks.fontFamily);
// this.ctx.font = labelFont;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// 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;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// this.paddingRight = lastWidth / 2 + 3;
// this.paddingLeft = firstWidth / 2 + 3;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// this.labelRotation = 0;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// if (this.options.display) {
// var originalLabelWidth = helpers.longestText(this.ctx, labelFont, this.ticks);
// var cosRotation;
// var sinRotation;
// var firstRotatedWidth;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// this.labelWidth = originalLabelWidth;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// //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
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// var datasetWidth = Math.floor(this.getPixelForValue(null, 1 / this.ticks.length) - this.getPixelForValue(null, 0)) - 6;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// //Max label rotation can be set or default to 90 - also act as a loop counter
// while (this.labelWidth > datasetWidth && this.labelRotation <= this.options.ticks.maxRotation) {
// cosRotation = Math.cos(helpers.toRadians(this.labelRotation));
// sinRotation = Math.sin(helpers.toRadians(this.labelRotation));
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// firstRotated = cosRotation * firstWidth;
// lastRotated = cosRotation * lastWidth;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// // We're right aligning the text now.
// if (firstRotated + this.options.ticks.fontSize / 2 > this.yLabelWidth) {
// this.paddingLeft = firstRotated + this.options.ticks.fontSize / 2;
// }
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// this.paddingRight = this.options.ticks.fontSize / 2;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// if (sinRotation * originalLabelWidth > maxHeight) {
// // go back one step
// this.labelRotation--;
// break;
// }
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// this.labelRotation++;
// this.labelWidth = cosRotation * originalLabelWidth;
2015-09-11 06:48:48 +02:00
2015-09-15 19:40:01 +02:00
2015-09-23 03:31:26 +02:00
// }
// } else {
// this.labelWidth = 0;
// this.paddingRight = 0;
// this.paddingLeft = 0;
// }
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// if (margins) {
// this.paddingLeft -= margins.left;
// this.paddingRight -= margins.right;
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// this.paddingLeft = Math.max(this.paddingLeft, 0);
// this.paddingRight = Math.max(this.paddingRight, 0);
// }
2015-09-11 06:48:48 +02:00
2015-09-23 03:31:26 +02:00
// },
2015-09-11 06:48:48 +02:00
// Fit this axis to the given size
// @param {number} maxWidth : the max width the axis can be
// @param {number} maxHeight: the max height the axis can be
// @return {object} minSize : the minimum size needed to draw the axis
fit : function ( maxWidth , maxHeight , margins ) {
// Set the unconstrained dimension before label rotation
if ( this . isHorizontal ( ) ) {
this . width = maxWidth ;
} else {
this . height = maxHeight ;
}
2015-09-15 19:40:01 +02:00
this . generateTicks ( ) ;
this . calculateTickRotation ( maxHeight , margins ) ;
2015-09-11 06:48:48 +02:00
var minSize = {
width : 0 ,
height : 0 ,
} ;
2015-09-23 03:31:26 +02:00
var labelFont = helpers . fontString ( this . options . ticks . fontSize , this . options . ticks . fontStyle , this . options . ticks . fontFamily ) ;
2015-09-15 19:40:01 +02:00
var longestLabelWidth = helpers . longestText ( this . ctx , labelFont , this . ticks ) ;
2015-09-11 06:48:48 +02:00
// Width
if ( this . isHorizontal ( ) ) {
minSize . width = maxWidth ;
} else if ( this . options . display ) {
2015-09-23 03:31:26 +02:00
var labelWidth = this . options . ticks . show ? longestLabelWidth + 6 : 0 ;
minSize . width = Math . min ( labelWidth , maxWidth ) ;
2015-09-11 06:48:48 +02:00
}
// Height
if ( this . isHorizontal ( ) && this . options . display ) {
2015-09-23 03:31:26 +02:00
var labelHeight = ( Math . sin ( helpers . toRadians ( this . labelRotation ) ) * longestLabelWidth ) + 1.5 * this . options . ticks . fontSize ;
minSize . height = Math . min ( this . options . ticks . show ? labelHeight : 0 , maxHeight ) ;
2015-09-11 06:48:48 +02:00
} else if ( this . options . display ) {
minSize . height = maxHeight ;
}
this . width = minSize . width ;
this . height = minSize . height ;
return minSize ;
} ,
// Actualy draw the scale on the canvas
// @param {rectangle} chartArea : the area of the chart to draw full grid lines on
draw : function ( chartArea ) {
if ( this . options . display ) {
var setContextLineSettings ;
// Make sure we draw text in the correct color
2015-09-23 03:31:26 +02:00
this . ctx . fillStyle = this . options . ticks . fontColor ;
2015-09-11 06:48:48 +02:00
if ( this . isHorizontal ( ) ) {
setContextLineSettings = true ;
var yTickStart = this . options . position == "bottom" ? this . top : this . bottom - 10 ;
var yTickEnd = this . options . position == "bottom" ? this . top + 10 : this . bottom ;
var isRotated = this . labelRotation !== 0 ;
var skipRatio = false ;
2015-09-23 03:31:26 +02:00
if ( ( this . options . ticks . fontSize + 4 ) * this . ticks . length > ( this . width - ( this . paddingLeft + this . paddingRight ) ) ) {
skipRatio = 1 + Math . floor ( ( ( this . options . ticks . fontSize + 4 ) * this . ticks . length ) / ( this . width - ( this . paddingLeft + this . paddingRight ) ) ) ;
2015-09-11 06:48:48 +02:00
}
2015-09-15 19:40:01 +02:00
helpers . each ( this . ticks , function ( tick , index ) {
// Blank ticks
if ( ( skipRatio > 1 && index % skipRatio > 0 ) || ( tick === undefined || tick === null ) ) {
2015-09-11 06:48:48 +02:00
return ;
}
2015-09-15 19:40:01 +02:00
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)
2015-09-11 06:48:48 +02:00
if ( this . options . gridLines . show ) {
if ( index === 0 ) {
// Draw the first index specially
this . ctx . lineWidth = this . options . gridLines . zeroLineWidth ;
this . ctx . strokeStyle = this . options . gridLines . zeroLineColor ;
setContextLineSettings = true ; // reset next time
} else if ( setContextLineSettings ) {
this . ctx . lineWidth = this . options . gridLines . lineWidth ;
this . ctx . strokeStyle = this . options . gridLines . color ;
setContextLineSettings = false ;
}
xLineValue += helpers . aliasPixel ( this . ctx . lineWidth ) ;
2015-09-15 19:40:01 +02:00
// Draw the tick area
2015-09-11 06:48:48 +02:00
this . ctx . beginPath ( ) ;
if ( this . options . gridLines . drawTicks ) {
this . ctx . moveTo ( xLineValue , yTickStart ) ;
this . ctx . lineTo ( xLineValue , yTickEnd ) ;
}
// Draw the chart area
if ( this . options . gridLines . drawOnChartArea ) {
this . ctx . moveTo ( xLineValue , chartArea . top ) ;
this . ctx . lineTo ( xLineValue , chartArea . bottom ) ;
}
// Need to stroke in the loop because we are potentially changing line widths & colours
this . ctx . stroke ( ) ;
}
2015-09-23 03:31:26 +02:00
if ( this . options . ticks . show ) {
2015-09-11 06:48:48 +02:00
this . ctx . save ( ) ;
this . ctx . translate ( xLabelValue , ( isRotated ) ? this . top + 12 : this . top + 8 ) ;
this . ctx . rotate ( helpers . toRadians ( this . labelRotation ) * - 1 ) ;
this . ctx . font = this . font ;
this . ctx . textAlign = ( isRotated ) ? "right" : "center" ;
this . ctx . textBaseline = ( isRotated ) ? "middle" : "top" ;
2015-09-15 19:40:01 +02:00
this . ctx . fillText ( tick , 0 , 0 ) ;
2015-09-11 06:48:48 +02:00
this . ctx . restore ( ) ;
}
} , this ) ;
} else {
// Vertical
if ( this . options . gridLines . show ) { }
2015-09-23 03:31:26 +02:00
if ( this . options . ticks . show ) {
// Draw the ticks
2015-09-11 06:48:48 +02:00
}
}
}
}
} ) ;
Chart . scaleService . registerScaleType ( "time" , TimeScale , defaultConfig ) ;
} ) . call ( this ) ;