@@ -487,7 +487,11 @@ axes.expand = function(ax, data, options) {
487487
488488axes . autoBin = function ( data , ax , nbins , is2d ) {
489489 var datamin = Lib . aggNums ( Math . min , null , data ) ,
490- datamax = Lib . aggNums ( Math . max , null , data ) ;
490+ datamax = Lib . aggNums ( Math . max , null , data ) ,
491+ blankcount = 0 ,
492+ datacount ,
493+ i ;
494+
491495 if ( ax . type === 'category' ) {
492496 return {
493497 start : datamin - 0.5 ,
@@ -548,16 +552,16 @@ axes.autoBin = function(data, ax, nbins, is2d) {
548552 if ( typeof dummyax . dtick === 'number' ) {
549553 var edgecount = 0 ,
550554 midcount = 0 ,
551- intcount = 0 ,
552- blankcount = 0 ;
553- for ( var i = 0 ; i < data . length ; i ++ ) {
555+ intcount = 0 ;
556+
557+ for ( i = 0 ; i < data . length ; i ++ ) {
554558 if ( data [ i ] % 1 === 0 ) intcount ++ ;
555559 else if ( ! isNumeric ( data [ i ] ) ) blankcount ++ ;
556560
557561 if ( nearEdge ( data [ i ] ) ) edgecount ++ ;
558562 if ( nearEdge ( data [ i ] + dummyax . dtick / 2 ) ) midcount ++ ;
559563 }
560- var datacount = data . length - blankcount ;
564+ datacount = data . length - blankcount ;
561565
562566 if ( intcount === datacount && ax . type !== 'date' ) {
563567 // all integers: if bin size is <1, it's because
@@ -586,6 +590,12 @@ axes.autoBin = function(data, ax, nbins, is2d) {
586590 binend = binstart + bincount * dummyax . dtick ;
587591 }
588592 else {
593+ // month ticks - should be the only nonlinear kind we have
594+ // at this point.
595+ if ( dummyax . dtick . charAt ( 0 ) === 'M' ) {
596+ binstart = autoShiftMonthBins ( binstart , data , dummyax . dtick , datamin ) ;
597+ }
598+
589599 // calculate the endpoint for nonlinear ticks - you have to
590600 // just increment until you're done
591601 binend = binstart ;
@@ -602,6 +612,79 @@ axes.autoBin = function(data, ax, nbins, is2d) {
602612} ;
603613
604614
615+ function autoShiftMonthBins ( binStart , data , dtick , dataMin ) {
616+ var exactYears = 0 ,
617+ exactMonths = 0 ,
618+ exactDays = 0 ,
619+ blankCount = 0 ,
620+ dataCount ,
621+ di ,
622+ d ,
623+ year ,
624+ month ;
625+
626+ for ( var i = 0 ; i < data . length ; i ++ ) {
627+ di = data [ i ] ;
628+ if ( ! isNumeric ( di ) ) {
629+ blankCount ++ ;
630+ continue ;
631+ }
632+ d = new Date ( di ) ,
633+ year = d . getUTCFullYear ( ) ;
634+ if ( di === Date . UTC ( year , 0 , 1 ) ) {
635+ exactYears ++ ;
636+ }
637+ else {
638+ month = d . getUTCMonth ( ) ;
639+ if ( di === Date . UTC ( year , month , 1 ) ) {
640+ exactMonths ++ ;
641+ }
642+ else if ( di === Date . UTC ( year , month , d . getUTCDate ( ) ) ) {
643+ exactDays ++ ;
644+ }
645+ }
646+ }
647+
648+ dataCount = data . length - blankCount ;
649+
650+ // include bigger exact dates in the smaller ones
651+ exactMonths += exactYears ;
652+ exactDays += exactMonths ;
653+
654+ // unmber of data points that needs to be an exact value
655+ // to shift that increment to (near) the bin center
656+ var threshold = 0.8 * dataCount ;
657+
658+ if ( exactDays > threshold ) {
659+ var numMonths = Number ( dtick . substr ( 1 ) ) ;
660+
661+ if ( ( exactYears > threshold ) && ( numMonths % 12 === 0 ) ) {
662+ // The exact middle of a non-leap-year is 1.5 days into July
663+ // so if we start the bins here, all but leap years will
664+ // get hover-labeled as exact years.
665+ binStart = axes . tickIncrement ( binStart , 'M6' , 'reverse' ) + ONEDAY * 1.5 ;
666+ }
667+ else if ( exactMonths > threshold ) {
668+ // Months are not as clean, but if we shift half the *longest*
669+ // month (31/2 days) then 31-day months will get labeled exactly
670+ // and shorter months will get labeled with the correct month
671+ // but shifted 12-36 hours into it.
672+ binStart = axes . tickIncrement ( binStart , 'M1' , 'reverse' ) + ONEDAY * 15.5 ;
673+ }
674+ else {
675+ // Shifting half a day is exact, but since these are month bins it
676+ // will always give a somewhat odd-looking label, until we do something
677+ // smarter like showing the bin boundaries (or the bounds of the actual
678+ // data in each bin)
679+ binStart -= ONEDAY / 2 ;
680+ }
681+ var nextBinStart = axes . tickIncrement ( binStart , dtick ) ;
682+
683+ if ( nextBinStart <= dataMin ) return nextBinStart ;
684+ }
685+ return binStart ;
686+ }
687+
605688// ----------------------------------------------------
606689// Ticks and grids
607690// ----------------------------------------------------
@@ -919,6 +1002,7 @@ function autoTickRound(ax) {
9191002// for pure powers of 10
9201003// numeric ticks always have constant differences, other datetime ticks
9211004// can all be calculated as constant number of milliseconds
1005+ var THREEDAYS = 3 * ONEDAY ;
9221006axes . tickIncrement = function ( x , dtick , axrev ) {
9231007 var axSign = axrev ? - 1 : 1 ;
9241008
@@ -930,10 +1014,23 @@ axes.tickIncrement = function(x, dtick, axrev) {
9301014
9311015 // Dates: months (or years)
9321016 if ( tType === 'M' ) {
933- var y = new Date ( x ) ;
934- // is this browser consistent? setUTCMonth edits a date but
935- // returns that date's milliseconds
936- return y . setUTCMonth ( y . getUTCMonth ( ) + dtSigned ) ;
1017+ /*
1018+ * set(UTC)Month does not (and CANNOT) always preserve day, since
1019+ * months have different lengths. The worst example of this is:
1020+ * d = new Date(1970,0,31); d.setMonth(1) -> Feb 31 turns into Mar 3
1021+ *
1022+ * But we want to be able to iterate over the last day of each month,
1023+ * regardless of what its number is.
1024+ * So shift 3 days forward, THEN set the new month, then unshift:
1025+ * 1/31 -> 2/28 (or 29) -> 3/31 -> 4/30 -> ...
1026+ *
1027+ * Note that odd behavior still exists if you start from the 26th-28th:
1028+ * 1/28 -> 2/28 -> 3/31
1029+ * but at least you can't shift any dates into the wrong month,
1030+ * and ticks on these days incrementing by month would be very unusual
1031+ */
1032+ var y = new Date ( x + THREEDAYS ) ;
1033+ return y . setUTCMonth ( y . getUTCMonth ( ) + dtSigned ) - THREEDAYS ;
9371034 }
9381035
9391036 // Log scales: Linear, Digits
0 commit comments