1. 程式人生 > >Echarts時間座標軸刻度的改進和優化

Echarts時間座標軸刻度的改進和優化

問題介紹:
Echarts外掛的座標軸型別主要分為時間(time)、數值(value)、對數(log)、類目(category)四大類,當採用二維的直角座標系時,一般x軸通過採用類目軸,而y軸通常採用value軸。官方的有關的直角座標系例子,大多遵循這個原則配置的。這樣配置其實只適用於離散型資料,而對於時間型資料,雖然有時間軸的支援,但是效果卻差強人意,甚至存在展示的缺陷。

如圖1所示為日的時間型資料,從圖上可以看出柱狀圖的柱子跟y軸有重疊,x軸刻度值少的問題。如圖2、3所示為月和年的時間型資料,問題跟圖1差不多,但還有一個不一樣的問題,就是刻度值,不管是月還是年的資料,刻度值都顯示的日的刻度值,這個對於使用者來說,體驗應該是不好的。更好的體驗應該是,月的時間型資料,顯示的刻度值應該都是月的刻度值;年的時間型資料,顯示的應該都是年的刻度值。如圖4、5、6,是本人優化和處理後的顯示效果圖。

總的來說,主要存在以下三個問題
(1)不同時間格式下,計算出來的刻度不符合預期;
(2)資料的最小值容易與座標軸重疊,最大值容易顯示在圖表之外;
(3)計算出來的刻度個數偏少,資料之間容易重疊。

這裡寫圖片描述

圖1 時間格式為日的時間軸展示

這裡寫圖片描述
圖2 時間格式為月的時間軸展示

這裡寫圖片描述
圖3 時間格式為年的時間軸展示

這裡寫圖片描述
圖4 時間格式為日的時間軸展示

這裡寫圖片描述
圖5 時間格式為月的時間軸展示

這裡寫圖片描述
圖6 時間格式為年的時間軸展示

Echarts時間型刻度的計算方法:

通過閱讀Echarts原始碼,可以瞭解Echarts對於時間型刻度的計算方法: 在Echarts的架構中, echarts/scale/Time模組負責對時間刻度進行處理和計算,但求解刻度的演算法還是採用線性比例尺的演算法,即將時間型資料轉為時間戳(變為純數字),然後通過線性比例尺求刻度的方法計算得出時間戳型別的刻度,最後通過時間轉換函式,將時間戳刻度,轉為時間型刻度。而且在原始碼裡面,將資料的最小值和最大值,預設設定為刻度的最小值和最大值,並且刻度個數通常只有五個,偏少。

改進和優化方法:
針對前文總結出的三點缺陷,現提出以下的改進和優化方法:
(1)針對不同時間格式下刻度的計算問題,借鑑d3.js的時間比例尺介面和方法,並將其整合到Echarts原始碼中。d3.js針對不同時間格式,採用不同函式計算時間刻度,針對年、月、日、小時分別有year、month、day、hour等函式。不同時間格式函式處理刻度方法稍有不同。具體方法下文會詳細介紹。
(2)針對最小值容易與座標軸重疊,最大值容易顯示在圖形之外問題。採用保留刻度法予以解決。保留刻度法:即資料最小值與刻度最小值之差為正且小於步長20%,或者資料最小值與刻度最小值之差為負,則最小刻度需要再向下增加一個刻度;若資料最大值與刻度最大值之差為負且絕對值小於步長20%,或者資料最小值與刻度最小值之差為正, 則最大刻度需要再向上增加一個刻度。
(3)針對計算出來的刻度個數偏少問題,採用自適應寬度的方法,力求最大的刻度個數。
規定刻度標籤的旋轉45度,每一個標籤佔用30px的寬度,這樣,如果圖表的寬為600px,則理論上可以在座標軸上最大放置20個刻度。

具體實現:
(1)通過除錯d3.js原始碼和研究,發現d3.js處理時間比例尺的介面主要是以下程式碼,程式碼中newInterval可以看做一個建構函式,傳入不同的引數,都會返回interval物件,但interval物件的方法卻因為引數和變數值的不一樣,而有不同的功能。比如:year物件專門處理只與年相關的比例尺,month物件則專門處理與月相關的比例尺,minute物件則專門處理與分鐘相關的比例尺…這些物件有1個方法,是計算比例尺的關鍵,它就是range(start,stop,step)函式,它接受三個引數,start:資料的開始點(資料最小值),stop:資料的終點(資料最大值),step:相鄰刻度的步長。以年為例,假設現在資料最小值是2011,最大值是2028年,步長是2。則通過range函式,計算出來的刻度是[1293811200000, 1356969600000, 1420041600000, 1483200000000, 1546272000000, 1609430400000, 1672502400000, 1735660800000, 1798732800000],接著再將時間戳格式化[“2011-01-01 00:00:00”, “2013-01-01 00:00:00”, “2015-01-01 00:00:00”, “2017-01-01 00:00:00”, “2019-01-01 00:00:00”, “2021-01-01 00:00:00”, “2023-01-01 00:00:00”, “2025-01-01 00:00:00”, “2027-01-01 00:00:00”]。從結果可以看出,計算出來的刻度值只是在年份上遞增,滿足我們的預期,同理,其他月、日、小時的時間格式比例尺計算方法也跟這個類似。

 function(module, exports, __webpack_require__) {
    var t0$1 = new Date;
    var t1$1 = new Date;
    var timeInterval = {};

    function newInterval(floori, offseti, count, field) {

      function interval(date) {
        return floori(date = new Date(+date)), date;
      }

      interval.floor = interval;

      interval.ceil = function(date) {
        return floori(date = new Date(date - 1)), offseti(date, 1), floori(date), date;
      };

      interval.round = function(date) {
        var d0 = interval(date),
            d1 = interval.ceil(date);
        return date - d0 < d1 - date ? d0 : d1;
      };

      interval.offset = function(date, step) {
        return offseti(date = new Date(+date), step == null ? 1 : Math.floor(step)), date;
      };

      interval.range = function(start, stop, step) {
        var range = [];
        start = interval.ceil(start);
        step = step == null ? 1 : Math.floor(step);
        if (!(start < stop) || !(step > 0)) return range; // also handles Invalid Date
        do range.push(new Date(+start).getTime()); while (offseti(start, step), floori(start), start < stop)
        return range;
      };

      interval.filter = function(test) {
        return newInterval(function(date) {
          if (date >= date) while (floori(date), !test(date)) date.setTime(date - 1);
        }, function(date, step) {
          if (date >= date) {
            if (step < 0) while (++step <= 0) {
              while (offseti(date, -1), !test(date)) {} // eslint-disable-line no-empty
            } else while (--step >= 0) {
              while (offseti(date, +1), !test(date)) {} // eslint-disable-line no-empty
            }
          }
        });
      };

      if (count) {
        interval.count = function(start, end) {
          t0$1.setTime(+start), t1$1.setTime(+end);
          floori(t0$1), floori(t1$1);
          return Math.floor(count(t0$1, t1$1));
        };

        interval.every = function(step) {
          step = Math.floor(step);
          return !isFinite(step) || !(step > 0) ? null
              : !(step > 1) ? interval
              : interval.filter(field
                  ? function(d) { return field(d) % step === 0; }
                  : function(d) { return interval.count(0, d) % step === 0; });
        };
      }

      return interval;
    }

    var millisecond = newInterval(function() {
      // noop
    }, function(date, step) {
      date.setTime(+date + step);
    }, function(start, end) {
      return end - start;
    });

    // An optimized implementation for this simple case.
    millisecond.every = function(k) {
      k = Math.floor(k);
      if (!isFinite(k) || !(k > 0)) return null;
      if (!(k > 1)) return millisecond;
      return newInterval(function(date) {
        date.setTime(Math.floor(date / k) * k);
      }, function(date, step) {
        date.setTime(+date + step * k);
      }, function(start, end) {
        return (end - start) / k;
      });
    };

    var milliseconds = millisecond.range;

    var durationSecond$1 = 1e3;
    var durationMinute$1 = 6e4;
    var durationHour$1 = 36e5;
    var durationDay$1 = 864e5;
    var durationWeek$1 = 6048e5;

    var second = newInterval(function(date) {
      date.setTime(Math.floor(date / durationSecond$1) * durationSecond$1);
    }, function(date, step) {
      date.setTime(+date + step * durationSecond$1);
    }, function(start, end) {
      return (end - start) / durationSecond$1;
    }, function(date) {
      return date.getUTCSeconds();
    });

    var seconds = second.range;

    var minute = newInterval(function(date) {
      date.setTime(Math.floor(date / durationMinute$1) * durationMinute$1);
    }, function(date, step) {
      date.setTime(+date + step * durationMinute$1);
    }, function(start, end) {
      return (end - start) / durationMinute$1;
    }, function(date) {
      return date.getMinutes();
    });

    var minutes = minute.range;

    var hour = newInterval(function(date) {
      var offset = date.getTimezoneOffset() * durationMinute$1 % durationHour$1;
      if (offset < 0) offset += durationHour$1;
      date.setTime(Math.floor((+date - offset) / durationHour$1) * durationHour$1 + offset);
    }, function(date, step) {
      date.setTime(+date + step * durationHour$1);
    }, function(start, end) {
      return (end - start) / durationHour$1;
    }, function(date) {
      return date.getHours();
    });

    var hours = hour.range;

    var day = newInterval(function(date) {
      date.setHours(0, 0, 0, 0);
    }, function(date, step) {
      date.setDate(date.getDate() + step);
    }, function(start, end) {
      return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute$1) / durationDay$1;
    }, function(date) {
      return date.getDate() - 1;
    });

    var days = day.range;

    function weekday(i) {
      return newInterval(function(date) {
        date.setDate(date.getDate() - (date.getDay() + 7 - i) % 7);
        date.setHours(0, 0, 0, 0);
      }, function(date, step) {
        date.setDate(date.getDate() + step * 7);
      }, function(start, end) {
        return (end - start - (end.getTimezoneOffset() - start.getTimezoneOffset()) * durationMinute$1) / durationWeek$1;
      });
    }

    var sunday = weekday(0);
    var monday = weekday(1);
    var tuesday = weekday(2);
    var wednesday = weekday(3);
    var thursday = weekday(4);
    var friday = weekday(5);
    var saturday = weekday(6);

    var sundays = sunday.range;
    var mondays = monday.range;
    var tuesdays = tuesday.range;
    var wednesdays = wednesday.range;
    var thursdays = thursday.range;
    var fridays = friday.range;
    var saturdays = saturday.range;

    var month = newInterval(function(date) {
      date.setDate(1);
      date.setHours(0, 0, 0, 0);
    }, function(date, step) {
      date.setMonth(date.getMonth() + step);
    }, function(start, end) {
      return end.getMonth() - start.getMonth() + (end.getFullYear() - start.getFullYear()) * 12;
    }, function(date) {
      return date.getMonth();
    });

    var months = month.range;

    var year = newInterval(function(date) {
      date.setMonth(0, 1);
      date.setHours(0, 0, 0, 0);
    }, function(date, step) {
      date.setFullYear(date.getFullYear() + step);
    }, function(start, end) {
      return end.getFullYear() - start.getFullYear();
    }, function(date) {
      return date.getFullYear();
    });

    // An optimized implementation for this simple case.
    year.every = function(k) {
      return !isFinite(k = Math.floor(k)) || !(k > 0) ? null : newInterval(function(date) {
        date.setFullYear(Math.floor(date.getFullYear() / k) * k);
        date.setMonth(0, 1);
        date.setHours(0, 0, 0, 0);
      }, function(date, step) {
        date.setFullYear(date.getFullYear() + step * k);
      });
    };

    var years = year.range;
    timeInterval.timeYear = year;
    timeInterval.timeYears = years;
    timeInterval.timeMonths = months;
    timeInterval.timeMonth = month;
    timeInterval.timeSecond = second;
    timeInterval.timeSeconds = seconds;
    timeInterval.timeDay = day;
    timeInterval.timeDays = days;   
    timeInterval.timeHour = hour;
    timeInterval.timeHours = hours; 
    timeInterval.timeMinute = minute;
    timeInterval.timeMinutes = minutes;

    module.exports = timeInterval;
 },       

(2)針對第二個問題的解決方法,主要是對(1)中算出的刻度進一步處理,說白了,就是給刻度的兩個端點適量留一些空白,以便不影響柱狀圖等圖形顯示。具體程式碼如下所示:

        /**
           * 優化並調整刻度的最大刻度和最小刻度,
           * 若存在,直接返回刻度值
           * @param {Object} params 
           * @param {Array.<Number>} params.ticks: 包含刻度的陣列
           * @param {Array.<Number>} params.extent: 最大刻度和最小刻度陣列
           * @param {Array.<Number>} params.niceTickExtent: 更新後的最大刻度和最小刻度陣列
           * @param {Number} params.splitNum: 刻度軸的分割數目
           * @param {Function} params.offset: 時間軸時,為時間的偏移函式;數值軸時為null
           * @param {Number} params.step: 時間軸時,刻度之間的步長
           * @param {Number} params.intervalPrecision: 數值軸時,刻度值的精度
        */
        helper.adjustTickExtent = function (params) {
            var ticks = params.ticks,
            extent = params.extent,
            niceTickExtent = params.niceTickExtent,
            splitNum = params.splitNum;
            var context = this;
            var interval;
            var name = extent[0] + '@#' + extent[1] + '&' + splitNum;
            // 快取存在直接返回         
            if (this.cache[name]) {
                return this.cache[name];
            }   
            if (processOnlyNum (params, context)) {
                return ticks;
            }   
            preprocessTicks(params);                    
            calTickMin(params, extent[0]);
            calTickMax(params, extent[1]);                                          
            setAdjustExtent.call(this, niceTickExtent, extent, ticks, splitNum);
            return ticks;

            /**
               * 當資料最大值和最小值相等時
               * 此時刻度值數目只有三個
               * @param {Object} params: 包含引數值的物件
               * @param {Object} context: 執行環境的上下文
            */
            function processOnlyNum (params, context) {
                var ticks = params.ticks;
                var offset = params.offset;
                var adjustInterval = 1;
                var extent = params.extent;
                var step = params.step;
                var onlyNum = params.onlyNum;
                var intervalPrecision = params.intervalPrecision;
                //onlyNum表示資料只有一條
                if (onlyNum != null) {
                    if (offset === null) {
                        ticks.length = 0;
                        adjustInterval = extent[1] - extent[0];
                        ticks[0] = extent[0];
                        ticks[1] = extent[1];
                        onlyNum == extent[0] ? ticks.unshift(extent[0] - adjustInterval) : ticks.push(extent[1] + adjustInterval);              
                    } else {
                        ticks.length = 0;
                        ticks[0] = offset(onlyNum, -step).getTime(); 
                        ticks[1] = onlyNum;
                        ticks[2] = offset(onlyNum, step).getTime(); 
                    }       
                    setAdjustExtent.call(context, niceTickExtent, extent, ticks, splitNum);
                    return true;
                } 
                return false;
            }

            /**
               * 預處理刻度,功能:
               *(1)移除刻度中數值等於extent[0]和extent[1]的刻度
               *(2)將只包含一個刻度和兩個刻度的刻度陣列擴充套件成多個
               * @param {Object} params: 包含各種引數的物件
            */
            function preprocessTicks(params) {  
                var ticks = params.ticks;
                var offset = params.offset;
                var extent = params.extent;
                var step = params.step;
                var intervalPrecision = params.intervalPrecision;
                //ticks等於1,只有可能是時間軸的情況
                if (ticks.length == 1) {            
                    ticks.unshift(offset(ticks[0], -step).getTime()); 
                    ticks.push(offset(ticks[ticks.length - 1], step).getTime());                    
                } else if ((ticks.length == 2 || ticks.length == 3) && offset == null) {
                    //當刻度的最小值是資料的最小值時,根據step求出最小刻度
                    tick = ticks[0];                                        
                    if (tick == extent[0] && (tick % step != 0)) {
                       tick = roundNumber(tick - (tick % step) , intervalPrecision);
                       ticks[0] = tick; 
                    }
                    //當刻度的最大值是資料的最大值時,根據step求出最大刻度
                    tick = ticks[ticks.length - 1];
                    if (tick == extent[1] && (tick % step != 0)) {                  
                        tick = roundNumber(tick + step - (tick % step) , intervalPrecision);
                        ticks[ticks.length - 1] = tick;
                    }               
                } else if (ticks.length > 3) {
                    ticks[0] == extent[0] && ticks.shift();
                    ticks[ticks.length - 1] == extent[1] && ticks.pop();
                }               
            }

            /**
               * 計算刻度的最小刻度
               * @param {Object} params: 包含各種引數的物件
               * @param {Number} min:資料的最小值         
            */          
            function calTickMin(params, min) {  
                var ticks = params.ticks;
                var offset = params.offset;                         
                var step = params.step;         
                var intervalPrecision = params.intervalPrecision;   
                var interval = offset === null ? step : (ticks[1] - ticks[0]);                      
                var i = 0, tickMin, differ, tick;           
                while (true) {
                    i++;
                    if (i == 3) {
                        break;
                    }
                    tickMin = ticks[0];
                    differ = min - tickMin;                                
                    if (differ > 0 && differ >= interval * 0.2 ) {                  
                        break;
                    }   
                    /*
                      * 若資料最小值與刻度最小值之差為正且小於步長20%,
                      * 或者資料最小值與刻度最小值之差為負
                      * 則最小刻度需要再向下增加一個刻度    
                    */                      
                    if ((differ > 0 && differ < interval * 0.2) || differ <= 0) {   
                        //數值軸
                        if (offset == null) {
                            tick = roundNumber(tickMin - step, intervalPrecision);
                        //時間軸
                        } else {
                            tick = offset(tickMin, -step).getTime();
                        }                                   
                        ticks.unshift(tick);                
                    }
                }   
            }

            /**
               * 計算刻度的最小刻度
               * @param {Object} params: 包含各種引數的物件
               * @param {Number} min:資料的最小值           
            */  
            function calTickMax(params, max, interval) {
                var ticks = params.ticks;
                var offset = params.offset;                         
                var step = params.step;         
                var intervalPrecision = params.intervalPrecision;
                var interval = offset === null ? step : (ticks[1] - ticks[0]);                  
                var i = 0, tickMax, differ, tick;       
                while (true) {  
                    i++;    
                    //防止陷入死迴圈
                    if (i == 3) {
                        break;
                    }               
                    tickMax = ticks[ticks.length - 1];
                    differ = max - tickMax;
                    if (differ < 0 && Math.abs(differ) >= interval * 0.2) {                 
                        break;
                    }
                    /*
                      * 若資料最大值與刻度最大值之差為負且絕對值小於步長20%,
                      * 或者資料最小值與刻度最小值之差為正
                      * 則最大刻度需要再向上增加一個刻度    
                    */                                  
                    if (differ >= 0 || (differ < 0 && Math.abs(differ) < interval * 0.2)) {
                        if (offset == null) {
                            tick = roundNumber(tickMax + step, intervalPrecision);
                        } else {
                            tick = offset(tickMax, step).getTime();
                        }
                        ticks.push(tick);           
                    } 
                }
            }

            /**
             * 設定extent,並存入快取
             * @param {Array} niceTickExtent
             * @param {Array} extent
             * @param {Array} ticks
             * @param {Array} splitNum
             */
            function setAdjustExtent(niceTickExtent, extent, ticks, splitNum) {
                //修正軸的extent                
                niceTickExtent[0] = extent[0] = ticks[0];
                niceTickExtent[1] = extent[1] = ticks[ticks.length - 1];    
                var name = extent[0] + '@#' + extent[1] + '&' + splitNum;
                this.cache[name] = ticks;
            }           
        };      

(3)第三個問題,相對來說,簡單很多,採用比較暴力的方法,規定標籤統一傾斜45度展示,每一個標籤在x軸上佔用40px。這樣可以計算出一根軸上最大容納的標籤數目。方法主要擴充套件在axisHelper物件上。

        /**
           * 計算出軸能容納的最大標籤個數
           * @param {Number} width:軸的總寬
           * @return {Number} num: 容納的標籤數目
        */
      axisHelper.getMaxTickNum = function (width) {
          var perWidth = 40px;
          var num = Math.floor(width / perWidth);
          num = num < 1 ? 1 : num;
          return num;
    };
        axisHelper.niceScaleExtent = function (scale, model) {
            var extent = axisHelper.getScaleExtent(scale, model);
            var axis = model.axis;
            var fixMin = model.getMin() != null;
            var fixMax = model.getMax() != null;
            var splitNumber = model.get('splitNumber');
            var mulDimension = model.get('mulDimension');
            //  時間軸刻度自適應
            if (axis.model.get('tickMax') && axis.type != 'category' && axis.dim == 'x') {
                splitNumber = axisHelper.getMaxTickNum(axis._extent[1] - axis._extent[0], mulDimension);    


                scale.splitNumber = splitNumber;
            }
            if (scale.type === 'log') {
                scale.base = model.get('logBase');
            }       
            scale.setExtent(extent[0], extent[1]);
            scale.niceExtent({
                splitNumber: splitNumber,
                fixMin: fixMin,
                fixMax: fixMax,
                minInterval: scale.type === 'interval' ? model.get('minInterval') : null
            });

            // If some one specified the min, max. And the default calculated interval
            // is not good enough. He can specify the interval. It is often appeared
            // in angle axis with angle 0 - 360. Interval calculated in interval scale is hard
            // to be 60.
            // FIXME
            var interval = model.get('interval');
            if (interval != null) {
                scale.setInterval && scale.setInterval(interval);
            }
        };

(4)為了實現這些問題,還有一些初始化工作需要處理,主要有一下程式碼實現:

/**
           * 根據條件計算出最佳的時間刻度值
           * @param {Object} params: 包含引數的集合
           * @param {Array} params.extent: 刻度的範圍
           * @param {Array} params.niceTickExtent: 更新後的刻度範圍
           * @param {Number} params.splitNum: 刻度分割數目
           * @param {String} params.defaultFormat: 初始的時間格式
           * @param {Number} params.onlyNum: 是否只有單個數值          
        */
        helper.processTimeTicks = function (params) {
            var ticks = [], timeIntervalPro;
            var extent = params.extent;
            var splitNum = params.splitNum;
            var format = selectTimeFormat(extent, splitNum, timeInterval, params.defaultFormat);

            timeIntervalPro = Math.ceil(timeInterval[format].count(extent[0], extent[1]) / splitNum);
            timeIntervalPro = timeIntervalPro < 1 ? 1 : timeIntervalPro;
            ticks = timeInterval[format+'s'](extent[0], extent[1], timeIntervalPro);
            //調整刻度的最大刻度和最小刻度
            ticks = this.adjustTickExtent({
                ticks: ticks,
                extent: extent, 
                niceTickExtent: params.niceTickExtent,
                splitNum: splitNum,
                offset: timeInterval[format].offset,
                step: timeIntervalPro,
                onlyNum: params.onlyNum
            });
            return ticks;
            /**
              * 判斷使用哪種時間格式求時間刻度
              * @param {Array} extent: 刻度的範圍
              * @param {Number} splitNum: 刻度分割數目
              * @param {Function} timeInterval: 處理時間刻度的函式集合
              * @param {String} defaultFormat: 初始的時間格式
            */
            function selectTimeFormat(extent, splitNum, timeInterval, defaultFormat)  {
                var format = 'timeSecond';
                if (defaultFormat == 'timeYear') {
                    return 'timeYear';
                }
                if (timeInterval.timeYear.count(extent[0], extent[1]) >= splitNum) {
                    format = 'timeYear';
                } else if (timeInterval.timeMonth.count(extent[0], extent[1]) >= splitNum) {
                    format = 'timeMonth';
                } else if (timeInterval.timeDay.count(extent[0], extent[1]) >= splitNum) {
                    format = 'timeDay';
                } else if (timeInterval.timeHour.count(extent[0], extent[1]) >= splitNum) {
                    format = 'timeHour';
                } else if (timeInterval.timeMinute.count(extent[0], extent[1]) >= splitNum) {
                    format = 'timeMinute';
                } else {
                    format = 'timeSecond';
                }
                format = correctResult(format, defaultFormat);
                return format;
                /**
                   * 判斷出的時間格式,可能不滿足展示要求,
                   * 需要重新進行修正
                   * @param {String} format: 判斷出的時間格式
                   * @param {String} defaultFormat: 初始化的時間格式
                   * @return {String} format: 修正後的時間格式
                */
                function correctResult(format, defaultFormat) {
                    if (defaultFormat == 'timeDay') {
                        if (!/timeYear|timeMonth|timeDay/.test(format)) {
                            format = 'timeDay';
                        }
                    } else if (defaultFormat == 'timeMonth') {
                        if (!/timeYear|timeMonth/.test(format)) {
                            format = 'timeMonth';
                        }
                    } else if (defaultFormat == 'timeSecond') {
                        if (!/timeHour|timeMinute|timeSecond/.test(format)) {
                            format = 'timeSecond';
                        }
                    }
                    return format;
                }
            }
        };
        /**
         * 得到時間格式對應的時間型別
         * @param {String} timeFormat: 時間格式
         * @return {String} type: 時間型別:year、month、day、second;
         */
        helper.getTimeType = function(timeFormat) {
            var type = null;
            if (['month', 'year', 'day', 'second'].indexOf(timeFormat) > -1) {
                type = timeFormat;
            } else if (timeFormat == 'hh:mm:ss' || timeFormat == 'yyyy-MM-dd hh:mm:ss' || timeFormat == 'yyyy/MM/dd hh:mm:ss') {
                type = 'second';
            } else if (timeFormat == 'yyyy') {
                type = 'year';
            } else if (timeFormat == 'yyyy/MM' || timeFormat == 'yyyyMM' || timeFormat == 'yyyy-MM') {
                type = 'month';
            } else if (/(yyyy\-MM\-dd)|(yyyy\/MM\/dd)|(dd\/MM\/yyyy)|(yyyyMMdd)/i.test(timeFormat)) {
                type = 'day';
            } else {
                type = 'second';
            }
            return type;
        };
        helper.intervalScaleGetTicks = function (extent, niceTickExtent, params) {
            var ticks = [], timeIntervalPro;
            var splitNum = params && params.splitNum || 5;
            var mulDimension = params && params.mulDimension;
            var timeFormat = params && params.timeFormat;
            var interval = params.interval;
            var intervalPrecision = params.intervalPrecision;
            var type = params.type;
            // If interval is 0, return [];
            if (!interval) {
                return ticks;
            }
            splitNum == 1 && (splitNum += 2) || 
            splitNum == 2 && (splitNum += 1);
            var name = extent[0] + '@#' + extent[1] + '&' + splitNum;
            if (this.cache[name]) {
                return this.cache[name];
            }
            //沈才良新增 修復時間處於年和月的顯示錯誤
            if (type == 'time' && params) { 
                timeFormat = this.getTimeType(timeFormat);
                if (['year', 'month', 'day', 'second'].indexOf(timeFormat) > -1) {
                    ticks = this.processTimeTicks({
                        extent: extent, 
                        niceTickExtent: niceTickExtent, 
                        splitNum: splitNum, 
                        defaultFormat: 'time' + timeFormat.slice(0, 1).toUpperCase() + timeFormat.slice(1), 
                        onlyNum: params.onlyNum
                    });
                    return ticks;
                }
            }
            // Consider this case: using dataZoom toolbox, zoom and zoom.
            var safeLimit = 10000;

            if (extent[0] < niceTickExtent[0]) {
                ticks.push(extent[0]);
            }
            var tick = niceTickExtent[0];

            while (tick <= niceTickExtent[1]) {
                ticks.push(tick);
                // Avoid rounding error
                tick = roundNumber(tick + interval, intervalPrecision);
                if (tick === ticks[ticks.length - 1]) {
                    // Consider out of safe float point, e.g.,
                    // -3711126.9907707 + 2e-10 === -3711126.9907707
                    break;
                }
                if (ticks.length > safeLimit) {
                    return [];
                }
            }
            // Consider this case: the last item of ticks is smaller
            // than niceTickExtent[1] and niceTickExtent[1] === extent[1].
            if (extent[1] > (ticks.length ? ticks[ticks.length - 1] : niceTickExtent[1])) {
                ticks.push(extent[1]);
            }       
            }           
            return ticks;       
        };

(5)Echarts的年時間軸option配置項,主要是設定兩個引數”timeFormat”、 “tickMax”。”timeFormat”表示時間軸的格式,可以為”yyyy”、“yyyy-MM”、“yyyy-MM-dd”“hh:mm:ss”等,tickMax則表示是否啟用最大刻度個數,blooean型別:true表示啟用。

        var option ={
            "color":[
                "#4cc5f4",
                "#fa5879",
                "#f8e402",              

            ],

            "tooltip":{
                "trigger": "item"
            },
            "legend":{
                "show":false,
                "data":[
                    "2013-01",
                    "2013-07",
                    "2013-09",
                    "2013-11",
                    "2013-03",
                    "2013-05",
                    "2013-02",
                    "2013-04",
                    "2013-06",
                    "2013-08",
                    "2013-10",
                    "2013-12"
                ]
            },
            "calculable":true,
            "xAxis":[
                {
                    "type":"time",                                      
                    "timeFormat": 'yyyy',               
                    "rotate":45,
                    "tickMax": true,
                     min: 'dataMin',
                     max: 'dataMax',
                    "axisLabel":{
                        "tooltip":{
                            "show":true                         
                        },
                        formatter: function (params) {
                            return new Date(params).Format('yyyy');
                        }
                    },