Home Reference Source

src/timeline.esdoc.js

/*!
 * Typedef for jQuery Timeline's ESDoc
 * @version: 2.1.0
 */

/** @type {string} [NAME="Timeline"] */
/** @type {string} VERSION */
/** @type {string} DATA_KEY */
/** @type {string} EVENT_KEY */
/** @type {string} PREFIX */
/** @type {number} MIN_POINTER_SIZE */
/** @type {Object} JQUERY_NO_CONFLICT */

/**
 * In principle, this option conforms to the specification of options in "Date.prototype.toLocaleString()".
 * However, there includes some extensions of this plugin original.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
 * 
 * @typedef {Object} LocaleOptions
 * @property {boolean} [hour12=false] - Whether to use 12-hour time (as opposed to 24-hour time). Possible values are true and false.
 * @property {string} [localeMatcher] - 
 * @property {string} [timeZone] - 
 * @property {string} [hourCycle] - 
 * @property {string} [formatMatcher] - 
 * @property {string} [weekday] - The representation of the weekday. Possible values are "narrow", "short", "long".
 * @property {string} [era] - The representation of the era. Possible values are "narrow", "short", "long".
 * @property {string} [year] - The representation of the year. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "zerofill".
 * @property {string} [month] - The representation of the month. Possible values are "numeric", "2-digit", "narrow", "short", "long".
 * @property {string} [day] - The representation of the day. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "ordinal".
 * @property {string} [hour] - The representation of the hour. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "fulltime".
 * @property {string} [minute] - The representation of the minute. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "fulltime".
 * @property {string} [second] - The representation of the second. Possible values are "numeric", "2-digit". Then an extended value by this plugin is "fulltime".
 * @property {string} [timeZoneName] - The representation of the time zone name. Possible values are "short", "long".
 */

/**
 * 
 * @typedef {Object} Headline
 * @property {boolean} [display=true] - Whether to display headline
 * @property {string} [title] - 
 * @property {boolean} [range=true] - Hide if false
 * @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
 * @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
 * @since 2.0.0
 */

/**
 * 
 * @typedef {Object} Footer
 * @property {boolean} [display=true] - Whether to display headline
 * @property {string} [content] - 
 * @property {boolean} [range=false] - Visible if true
 * @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
 * @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
 * @since 2.0.0
 */

/**
 * 
 * @typedef {Object} Sidebar
 * @property {boolean} [sticky=false] - Whether does sticky the sidebar by using "display: sticky" of CSS.
 * @property {boolean} [overlay=false] - 
 * @property {array.<String>} [list] - Define the contents of the row of the sidebar. Appropriate escaping is necessary when using HTML.
 * @since 2.0.0
 */

/**
 * Can define the ruler position to top or bottom and both
 * 
 * @typedef {Object} RulerOptions
 * @property {array.<String>} [lines] - Multiple tick marks can be set, and array elements are set in order from the top. Set same scale of Default.scale if omitted this. c.g. [ 'year', 'month', 'day', 'weekday' ]
 * @property {number} [height=30] - The height of a row of rulers
 * @property {number} [fontSize=14] - 
 * @property {string} [color="#777777"] - 
 * @property {string} [background="#FFFFFF"] - 
 * @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
 * @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
 * @since 2.0.0
 */

/**
 * You can set the upper and lower ruler individually
 *
 * @typedef {Object} Ruler
 * @property {RulerOptions} [top] - The upper ruler configuration. The upper ruler is hidden if omitted.
 * @property {RulerOptions} [bottom] - The lower ruler configuration. The lower ruler is hidden if omitted.
 * @since 2.0.0
 */

/**
 *
 *
 * @typedef {Object} EventMeta
 * @property {boolean} [display=false] - 
 * @property {string} [scale="day"] - 
 * @property {string} [locale="en-US"] - This value is an argument "locales" of `dateObj.toLocaleString([locales[, options]])`
 * @property {LocaleOptions} [format] - This value is an argument "options" of `dateObj.toLocaleString([locales[, options]])`
 * @property {string} [content] - This is value for if you want to show custom content on the meta
 * @since 2.0.0
 */

/**
 * Various effect settings to the timeline object displayed in the DOM
 *
 * @typedef {Object} Effects
 * @property {boolean} [presentTime=false] - Whether to show marking a present time on the timeline container.
 * @property {boolean} [hoverEvent=true] - Whether to show the effect when individual events on the timeline container are mouse over.
 * @property {boolean} [stripedGridRow=true] - 
 * @property {string} [horizontalGridStyle="solid"] - The style of horizontal grid line on the Timeline container. possible values are "solid", "dotted", "none".
 * @property {string} [verticalGridStyle="solid"] - The style of vertical grid line on the Timeline container. possible values are "solid", "dotted", "none".
 * @since 2.0.0
 */

/**
 * Color scheme to overwrite defaults UI color of the event node
 *
 * @typedef {Object} EventColors
 * @property {string} [text="#343A40"] - Defaults to text color of the event node
 * @property {string} [border="#6C757D"] - Defaults to border color of the event node
 * @property {string} [background="#E7E7E7"] - Defaults to background color of the event node
 * @since 2.0.0
 */

/**
 * Color scheme to overwrite defaults UI color of the timeline instance
 *
 * @typedef {Object} ThemeColors
 * @property {string} [name="default"] - 
 * @property {string} [text="#343A40"] - Defaults to basic text color
 * @property {string} [subtext="#707070"] - 
 * @property {string} [offtext="#BBBBBB"] - 
 * @property {string} [modesttext="#969696"] - 
 * @property {string} [line="#6C757D"] - Defaults to basic border color
 * @property {string} [offline="#DDDDDD"] - 
 * @property {string} [activeline="#DC3545"] - 
 * @property {string} [background="#FFFFFF"] - Defaults to background color
 * @property {string} [invertbg="#121212"] - 
 * @property {string} [striped1="#F7F7F7"] - 
 * @property {string} [striped2="#F0F0F0"] - 
 * @property {string} [active="#F73333"] - 
 * @property {string} [marker="#2C7CFF"] - 
 * @property {string} [gridbase="#333333"] - 
 * @since 2.1.0
 */

/**
 * An option to overwrite defaults UI color of all event nodes
 *
 * @typedef {Object} ColorScheme
 * @property {ThemeColors} [theme] - Color scheme to overwrite defaults UI color of the timeline instance
 * @property {EventColors} [event] - Color scheme to overwrite defaults UI color of the event node
 * @property {function} [hookEventColors] - You can declare a function to set colors with referring the data each event node
 * @since 2.0.0
 */

/**
 * Default options for generating the timeline by the jQuery.Timeline plugin.
 * Those defaults are overridden to undefined settings of the timeline configuration.
 *
 * @typedef {Object} Default
 * @property {string} [type="bar"] - View type of timeline event is either "bar" or "point" or "mixed"
 * @property {string} [scale="day"] - Timetable's minimum level scale is either "year", "month", "week", "day", "hour", "minute"
 * @property {string} [startDatetime="currently"] - Beginning date time of timetable on the timeline. format is ( "^d{4}(/|-)d{2}(/|-)d{2}\sd{2}:d{2}:d{2}$" ) or "currently"
 * @property {string} [endDatetime="auto"] - Ending date time of timetable on the timeline. format is ( "^d{4}(/|-)d{2}(/|-)d{2}\sd{2}:d{2}:d{2}$" ) or "auto"
 * @property {Headline} [headline] - Settings for the content customize in the headline
 * @property {Footer} [footer] - Settings for the content customize in the footer
 * @property {number|string} [range=3] - Override the scale range of the timeline to be rendered when endDatetime is undefined or "auto"
 * @property {Sidebar} [sidebar] - Settings for the content of the sidebar
 * @property {number|string} [rows="auto"] - Rows of timeline event area
 * @property {number} [rowHeight=48] - Height of one row
 * @property {number|string} [width="auto"] - Fixed width (pixel) of timeline view
 * @property {number|string} [height="auto"] - Fixed height (pixel) of timeline view; Defaults to ( rows * rowHeight )
 * @property {number} [minGridSize=30] - Override value of minimum size (pixel) of timeline grid
 * @property {number} [marginHeight=2] - Margin (pixel) top and bottom of events on the timeline
 * @property {Ruler} [ruler] - Settings of the ruler
 * @property {number|string} [rangeAlign="latest"] - Possible values are "left", "center", "right", "current", "latest" and specific event id
 * @property {string} [loader="default"] - Custom loader definition, possible values are "default", false and selector of loader element
 * @property {boolean} [hideScrollbar=false] - Whether or not to display the scroll bar displayed when the width of the timeline overflows (even if it is set to non-display, it will not function depending on the browser)
 * @property {EventMeta} [eventMeta] - Display meta of range on event node when the timeline type is "bar"
 * @property {array.<Object>} [eventData] - You can declare the events with object format as default events you want to place
 * @property {Effects} [effects] - You can declare effective styles as view of the timeline object
 * @property {ColorScheme} [colorScheme] - Can overwrite defaults UI color of the event nodes
 * @property {string} [storage="session"] - Specification of Web storage to cache event data, defaults to sessionStorage
 * @property {boolean} [reloadCacheKeep=true] - Whether to load cached events during reloading, the cache is discarded if false
 * @property {boolean} [zoom=false] - Whether to use the ability to zoom the scale of the timeline by double clicking on any scale on the ruler
 * @property {boolean} [wrapScale=true] - Whether wrapping new scale in the timeline container when zoom
 * @property {string} [engine="canvas"] - Choose dependent module to core as rendering engine. It'll be "canvas" or "d3.js"; Maybe add in future version
 * @property {boolean} [debug=false] - Enable to debug mode if true then output logs for debugging to console; defaults to false
 * @since 2.0.0
 */

/**
 * The limited grid number per scale of timeline
 *
 * @typedef {Object} LimitScaleGrids
 * @property {number} [millennium=100] - In other words it's 100000 years
 * @property {number} [century=500] - In other words it's 50000 years
 * @property {number} [decade=500] - In other words it's 5000 years
 * @property {number} [lustrum=500] - In other words it's 2500 years
 * @property {number} [year=500] - In other words it's 500 years
 * @property {number} [month=540] - In other words it's 45 years
 * @property {number} [week=530] - In other words it's 10 years
 * @property {number} [day=366] - In other words it's about 1 years
 * @property {number} [hour=720] - In other words it's 30 days
 * @property {number} [quarterHour=720] - In other words it's 7.5 days
 * @property {number} [halfHour=720] - In other words it's 15 days
 * @property {number} [minute=720] - In other words it's 12 hours
 * @property {number} [second=900] - In other words it's 15 minutes
 * @since 2.0.0
 */

/**
 * 
 *
 * @typedef {Object} RelationOption
 * @property {number} [before] - Set target eventID to connect the relation line to the event (leftward on the timeline) in chronological before from oneself event.
 * @property {number} [after] - Set target eventID to connect the relation line to the event (rightward on the timeline) in chronological after from oneself event.
 * @property {number} [linesize] - 
 * @property {string} [linecolor] - 
 * @property {number|string|boolean} [curve] - Whether the connection line is curved if the connection events are not on the same horizontal. If you specify a boolean value or a shorthand (0 or 1 only), it will be automatically curved. As with the previous version, it is also possible to specify the type of curve using defined preset values.
 */

/**
 * The preset as default of event parameters on the timeline
 *
 * @typedef {Object} EventParams
 * @property {string} uid - An unique id of event data, this can not define because this value is automatically generate as data for cache only
 * @property {?number} [eventId] - It is an ID that identifies an event for you to manipulate event data via each method. If omitted, consecutive numbers are automatically assigned.
 * @property {number} x - Can not define because this value is automatically generate as data for cache only
 * @property {number} y - Can not define because this value is automatically generate as data for cache only
 * @property {number} width - Can not define because this value is automatically generate as data for cache only
 * @property {number} height - Can not define because this value is automatically generate as data for cache only
 * @property {string} start - Can not define because this value is automatically generate as data for cache only
 * @property {string} end - Can not define because this value is automatically generate as data for cache only
 * @property {number} row - Can not define because this value is automatically generate as data for cache only
 * @property {string} [bgColor="#E7E7E7"] - 
 * @property {string} [color="#343A40"] - 
 * @property {string} [bdColor="#6C757D"] - 
 * @property {string} [label] - 
 * @property {string} [content] - 
 * @property {string} [category] - 
 * @property {string} [image] - 
 * @property {number} [margin] - 
 * @property {string} [rangeMeta] - 
 * @property {number|string} [size="normal"] - Define the diameter size of pointer when type of the timeline is "point". Possible values are "large", "normal", "small" and value of pixel.
 * @property {Object} [extend] - The specified key/value pair is replaced with the data attribute of the event element.
 * @property {boolean} [remote=false] - 
 * @property {RelationOption} [relation] - Setting for connecting events by relation lines when the timeline type is "point".
 * @property {function} [callback] - Callback processing that binds to openEvent method when this event is clicked.
 * @since 2.0.0
 */

/*
 * Binding Custom Events
 *
 * @typedef {Object} Event
 * @property {string} INITIALIZED
 * @property {string} HIDE
 * @property {string} SHOW
 * @property {string} CLICK_EVENT
 * @property {string} FOCUSIN_EVENT
 * @property {string} FOCUSOUT_EVENT
 * @property {string} TOUCHSTART_TIMELINE
 * @property {string} TOUCHMOVE_TIMELINE
 * @property {string} TOUCHEND_TIMELINE
 * @property {string} MOUSEENTER_POINTER
 * @property {string} MOUSELEAVE_POINTER
 * @property {string} ZOOMIN_SCALE
 * @since 2.0.0
 */

/*
 * Class name of the timeline elements created by the plugin
 *
 * @typedef {Object} ClassName
 * @property {string} TIMELINE_CONTAINER
 * @property {string} TIMELINE_MAIN
 * @property {string} TIMELINE_HEADLINE
 * @property {string} TIMELINE_HEADLINE_WRAPPER
 * @property {string} HEADLINE_TITLE
 * @property {string} RANGE_META
 * @property {string} RANGE_SPAN
 * @property {string} TIMELINE_EVENT_CONTAINER
 * @property {string} TIMELINE_BACKGROUND_GRID
 * @property {string} TIMELINE_RELATION_LINES
 * @property {string} TIMELINE_EVENTS
 * @property {string} TIMELINE_EVENT_NODE
 * @property {string} TIMELINE_EVENT_LABEL
 * @property {string} TIMELINE_EVENT_THUMBNAIL
 * @property {string} TIMELINE_RULER_LINES
 * @property {string} TIMELINE_RULER_ITEM
 * @property {string} TIMELINE_SIDEBAR
 * @property {string} TIMELINE_SIDEBAR_MARGIN
 * @property {string} TIMELINE_SIDEBAR_ITEM
 * @property {string} TIMELINE_FOOTER
 * @property {string} TIMELINE_FOOTER_CONTENT
 * @property {string} VIEWER_EVENT_TITLE
 * @property {string} VIEWER_EVENT_CONTENT
 * @property {string} VIEWER_EVENT_META
 * @property {string} VIEWER_EVENT_IMAGE_WRAPPER
 * @property {string} VIEWER_EVENT_IMAGE
 * @property {string} VIEWER_EVENT_TYPE_POINTER
 * @property {string} HIDE_SCROLLBAR
 * @property {string} HIDE
 * @property {string} RULER_ITEM_ALIGN_LEFT
 * @property {string} STICKY_LEFT
 * @property {string} OVERLAY
 * @property {string} ALIGN_SELF_RIGHT
 * @property {string} PRESENT_TIME_MARKER
 * @property {string} LOADER_CONTAINER
 * @property {string} LOADER_ITEM
 * @since 2.0.0
 */

/*
 * Selectors assigned on the timeline element
 *
 * @typedef {Object} Selector
 * @property {string} EVENT_NODE
 * @property {string} EVENT_VIEW
 * @property {string} RULER_TOP
 * @property {string} RULER_BOTTOM
 * @property {string} TIMELINE_CONTAINER
 * @property {string} TIMELINE_MAIN
 * @property {string} TIMELINE_RULER_TOP
 * @property {string} TIMELINE_EVENT_CONTAINER
 * @property {string} TIMELINE_RULER_BOTTOM
 * @property {string} TIMELINE_RULER_ITEM
 * @property {string} TIMELINE_RELATION_LINES
 * @property {string} TIMELINE_EVENTS
 * @property {string} TIMELINE_SIDEBAR
 * @property {string} TIMELINE_SIDEBAR_ITEM
 * @property {string} TIMELINE_EVENT_NODE
 * @property {string} VIEWER_EVENT_TYPE_POINTER
 * @property {string} LOADER
 * @property {string} DEFAULT_EVENTS
 * @since 2.0.0
 */

/**
 * Pluin Core Class
 * @access public
 * @since 2.0.0
 */
class Timeline {
    constructor( element, config ) {
        /** @type {Object} */
        this._config        = this._getConfig( config )
        /** @type {Object} */
        this._element       = element
        /** @type {?string} */
        this._selector      = null
        /** @type {boolean} */
        this._isInitialized = false
        /** @type {boolean} */
        this._isCached      = false
        /** @type {boolean} */
        this._isCompleted   = false
        /** @type {boolean} */
        this._isShown       = false
        /** @type {Object} */
        this._instanceProps = {}
        /** @type {?Object} */
        this._observer      = null
    }
    
    // Getters
    
    /** @type {string} */
    static get VERSION() {
        return VERSION
    }
    
    /** @type {Default} */
    static get Default() {
        return Default
    }
    
    // Private
    
    /**
     * Define the default options of this plugin
     * @private
     * @param {Object} config - Initial options
     * @return {Object} Config overrided initial options to default config
     */
    _getConfig( config ) {
        config = {
            ...Default,
            ...config
        }
        return config
    }
    
    /**
     * Filter the scale key name for LimitScaleGrids
     * @private
     * @param {string} key
     * @return {string} Filtered scale key name
     */
    _filterScaleKeyName( key ) {
        let filteredKey = null
        
        switch( true ) {
            case /^quarter-?(|hour)$/i.test( key ):
                filteredKey = 'quarterHour'
                break
            case /^half-?(|hour)$/i.test( key ):
                filteredKey = 'halfHour'
                break
            default:
                filteredKey = key
        }
        return filteredKey
    }
    
    /**
     * Initialize the plugin
     * @private
     */
    _init() {
        this._debug( '_init' )
        
        let _elem       = this._element,
            _selector   = `${_elem.tagName}${( _elem.id ? `#${_elem.id}` : '' )}${( _elem.className ? `.${_elem.className.replace(/\s/g, '.')}` : '' )}`
        
        this._selector = _selector.toLowerCase()
        
        if ( this._isInitialized || this._isCompleted ) {
            return
        }
        
        this.showLoader()
        
        this._calcVars()
        
        if ( ! this._verifyMaxRenderableRange() ) {
            throw new RangeError( `Timeline display period exceeds maximum renderable range.` )
        }
        
        this.sleep( _sleep ).then(() => {
            
            if ( ! this._isInitialized ) {
                
                this._renderView()
                
                const afterInitEvent = $.Event( Event.INITIALIZED, { _elem } )
                
                $(_elem).trigger( afterInitEvent )
                
                $(_elem).off( Event.INITIALIZED )
            }
            
            if ( ! this._isCached ) {
                this._loadEvent()
            }
            
            if ( this._isCached ) {
                this._placeEvent()
            }
            
            // Assign events for the timeline
            $(document).on(
                Event.CLICK_EVENT,
                `${this._selector} ${Selector.EVENT_NODE}`,
                ( event ) => this.openEvent( event )
            )
            $(_elem).on(
                Event.FOCUSIN_EVENT,
                Selector.TIMELINE_EVENT_NODE,
                ( event ) => this._activeEvent( event )
            )
            $(_elem).on(
                Event.FOCUSOUT_EVENT,
                Selector.TIMELINE_EVENT_NODE,
                ( event ) => this._activeEvent( event )
            )
            if ( /^point(|er)$/i.test( this._config.type ) ) {
                $(_elem).on(
                    Event.MOUSEENTER_POINTER,
                    Selector.VIEWER_EVENT_TYPE_POINTER,
                    ( event ) => this._hoverPointer( event )
                )
                $(_elem).on(
                    Event.MOUSELEAVE_POINTER,
                    Selector.VIEWER_EVENT_TYPE_POINTER,
                    ( event ) => this._hoverPointer( event )
                )
            }
            if ( this._config.zoom ) {
                $(_elem).on(
                    Event.ZOOMIN_SCALE,
                    Selector.TIMELINE_RULER_ITEM,
                    ( event ) => this.zoomScale( event )
                )
                
            }
            
            this._isCompleted = true
            
            this.alignment()
            
        })
    }
    
    /**
     * Calculate each properties of the timeline instance
     * @private
     */
    _calcVars() {
        let _opts  = this._config,
            _props = {}
        
        _props.begin      = this.supplement( null, this._getPluggableDatetime( _opts.startDatetime, 'first' ) )
        _props.end        = this.supplement( null, this._getPluggableDatetime( _opts.endDatetime, 'last' ) )
        _props.scaleSize  = this.supplement( null, _opts.minGridSize, this.validateNumeric )
        _props.rows       = this._getPluggableRows()
        _props.rowSize    = this.supplement( null, _opts.rowHeight, this.validateNumeric )
        _props.width      = this.supplement( null, _opts.width, this.validateNumeric )
        _props.height     = this.supplement( null, _opts.height, this.validateNumeric )
        
        this._instanceProps = _props // pre-cache
        
        if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
            // For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
            let _temp            = this._verifyScale( _opts.scale ),
                _values          = Object.values( _temp ),
                _averageDays     = this.numRound( _values.reduce( ( a, v ) => a + v, 0 ) / _values.length, 4 ), // Average days within the range
                _baseDaysOfScale = /^years?$/i.test( _opts.scale ) ? 365 : 30,
                _totalWidth      = 0
            
//console.log( '!', _opts.scale, _temp, _vals )
            _values.forEach( ( days ) => {
                _totalWidth += this.numRound( ( days * _props.scaleSize ) / _baseDaysOfScale, 2 )
            })
            
            _props.scale         = _averageDays * ( 24 * 60 * 60 * 1000 )
            _props.grids         = _values.length
            _props.variableScale = _temp
            _props.fullwidth     = _totalWidth
        } else {
            // In case of fixed length scale (:> 固定長スケールの場合
            _props.scale         = this._verifyScale( _opts.scale )
            _props.grids         = Math.ceil( ( _props.end - _props.begin ) / _props.scale )
            _props.variableScale = null
            _props.fullwidth     = _props.grids * _props.scaleSize
        }
        _props.fullheight = _props.rows * _props.rowSize
        // Define visible size according to full size of timeline (:> タイムラインのフルサイズに準じた可視サイズを定義
        _props.visibleWidth  = _props.width > 0  ? `${( _props.width <= _props.fullwidth ? _props.width : _props.fullwidth )}px` : '100%'
        _props.visibleHeight = _props.height > 0 ? `${( _props.height <= _props.fullheight ? _props.height : _props.fullheight )}px` : 'auto'
        
        for ( let _prop in _props ) {
            if ( _prop === 'width' || _prop === 'height' || _prop === 'variableScale' ) {
                continue
            }
            if ( this.is_empty( _props[_prop] ) ) {
                throw new TypeError( `Property "${_prop}" cannot set because undefined or invalid variable.` )
            }
        }
        
        if ( _props.fullwidth < 2 || _props.fullheight < 2 ) {
            throw new TypeError( `The range of the timeline to be rendered is incorrect.` )
        }
        
        this._instanceProps = _props
    }
    
    /**
     * Retrieve the pluggable datetime as milliseconds from specified keyword
     * @private
     * @param {string} key - Any one of "current", "auto", or datetime string
     * @param {string} [round_type] - 
     * @return {number} This value unit is milliseconds
     */
    _getPluggableDatetime( key, round_type = '' ) {
        let _opts        = this._config,
            _date        = null,
            getFirstDate = ( dateObj, scale ) => {
                let _tmpDate
                
                switch ( true ) {
                    case /^millenniums?|millennia$/i.test( scale ):
                    case /^century$/i.test( scale ):
                    case /^dec(ade|ennium)$/i.test( scale ):
                    case /^lustrum$/i.test( scale ):
                    case /^years?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), 0, 1 )
                        break
                    case /^months?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), 1 )
                        break
                    case /^(week|day)s?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() )
                        break
                    case /^(|half|quarter)-?hours?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours() )
                        break
                    case /^minutes?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes() )
                        break
                    case /^seconds?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds() )
                        break
                }
                return _tmpDate
            },
            getLastDate  = ( dateObj, scale ) => {
                let _tmpDate
                
                switch ( true ) {
                    case /^millenniums?|millennia$/i.test( scale ):
                    case /^century$/i.test( scale ):
                    case /^dec(ade|ennium)$/i.test( scale ):
                    case /^lustrum$/i.test( scale ):
                    case /^years?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear() + 1, 0, 1 )
                        break
                    case /^months?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )
                        break
                    case /^(week|day)s?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate() + 1 )
                        break
                    case /^(|half|quarter)-?hours?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours() + 1 )
                        break
                    case /^minutes?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes() + 1 )
                        break
                    case /^seconds?$/i.test( scale ):
                        _tmpDate = new Date( dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate(), dateObj.getHours(), dateObj.getMinutes(), dateObj.getSeconds() + 1 )
                        break
                }
                return new Date( _tmpDate.getTime() - 1 )
            },
            is_remapping = /^\d{1,2}(|(-|\/).+)$/.test( key.toString() )
//console.log( '!_getPluggableDatetime:', key, round_type, is_remapping )
        
        switch ( true ) {
            case /^current(|ly)$/i.test( key ):
                _date = new Date()
//console.log( '!_getPluggableDatetime::currently:', _opts.scale, this.getHigherScale( _opts.scale ), key, _date.getTime() )
                break
            case /^auto$/i.test( key ): {
                let _ms          = null,
                    _higherScale = this.getHigherScale( _opts.scale )
                
                if ( /^current(|ly)$/i.test( _opts.startDatetime ) ) {
                    _date = new Date()
                    //if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
                        _date = getFirstDate( _date, _opts.scale )
                    //}
                } else {
                    _date = this.getCorrectDatetime( _opts.startDatetime )
                }
                
                if ( _opts.range || _opts.range > 0 ) {
                    if ( /^years?$/i.test( _higherScale ) ) {
                        _ms = 365.25 * 24 * 60 * 60 * 1000
                    } else
                    if ( /^months?$/i.test( _higherScale ) ) {
                        _ms = 30.44 * 24 * 60 * 60 * 1000
                    } else {
                        _ms = this._verifyScale( _higherScale )
                    }
                    _date.setTime( _date.getTime() + ( _ms * _opts.range ) )
                } else {
                    if ( /^years?$/i.test( _opts.scale ) ) {
                        _ms = 365.25 * 24 * 60 * 60 * 1000
                    } else
                    if ( /^months?$/i.test( _opts.scale ) ) {
                        _ms = 30.44 * 24 * 60 * 60 * 1000
                    } else {
                        _ms = this._verifyScale( _opts.scale )
                    }
                    _date.setTime( _date.getTime() + ( _ms * LimitScaleGrids[this._filterScaleKeyName( _opts.scale )] ) )
                }
// console.log( '!_getPluggableDatetime::auto:', _opts.scale, this.getHigherScale( _opts.scale ), key, _date.getTime() )
                break
            }
            default:
                _date = this.getCorrectDatetime( key )
                break
        }
        
        if ( ! is_remapping ) {
            is_remapping = _date.getFullYear() < 100
        }
        
        if ( ! this.is_empty( round_type ) ) {
            if ( 'first' === round_type ) {
//console.log( '!_getPluggableDatetime::first:before:', key, _date, is_remapping )
                _date = getFirstDate( _date, _opts.scale )
//console.log( '!_getPluggableDatetime::first:after:', key, _date, is_remapping )
            } else
            if ( 'last' === round_type ) {
//console.log( '!_getPluggableDatetime::last:before:', key, _date, is_remapping )
                _date = getLastDate( _date, _opts.scale )
//console.log( '!_getPluggableDatetime::last:after:', key, _date, is_remapping )
            }
        }
        
        if ( is_remapping ) {
            _date.setFullYear( String( _date.getFullYear() ).substr(-2) )
        }
        
//console.log( '!_getPluggableDatetime::return:', _date )
        return _date.getTime()
    }
    
    /**
     * Retrieve the pluggable parameter as an object
     * @private
     * @param {string} str_like_params - Strings that can be parsed as javascript objects
     * @return {Object}
     */
    _getPluggableParams( str_like_params ) {
        let params = {}
        
        if ( typeof str_like_params === 'string' && str_like_params ) {
            try {
                params = JSON.parse( JSON.stringify( ( new Function( `return ${str_like_params}` ) )() ) )
                if ( params.hasOwnProperty( 'extend' ) ) {
                    params.extend = JSON.parse( JSON.stringify( ( new Function( `return ${params.extend}` ) )() ) )
                }
            } catch( e ) {
                console.warn( 'Can not parse to object therefor invalid param.' )
            }
        }
        return params
    }
    
    /**
     * Retrieve the pluggable rows of the timeline
     * @private
     * @return {number}
     */
    _getPluggableRows() {
        let _opts      = this._config,
            fixed_rows = this.supplement( 'auto', _opts.rows, this.validateNumeric )
        
        if ( fixed_rows === 'auto' ) {
            fixed_rows = _opts.sidebar.list.length
        }
        return fixed_rows > 0 ? fixed_rows : 1
    }
    
    /**
     * Verify the allowed scale, then retrieve that scale's millisecond if allowed
     * @private
     * @param {string} scale - 
     * @return {number|boolean} Return false if specified an invalid scale
     */
    _verifyScale( scale ) {
        let _opts  = this._config,
            _props = this._instanceProps,
            _ms    = -1
        
        if ( typeof scale === 'undefined' || typeof scale !== 'string' ) {
            return false
        }
        switch ( true ) {
            case /^millisec(|ond)s?$/i.test( scale ):
                // Millisecond (:> ミリ秒
                _ms = 1
                break
            case /^seconds?$/i.test( scale ):
                // Second (:> 秒
                _ms = 1000
                break
            case /^minutes?$/i.test( scale ):
                // Minute (:> 分
                _ms = 60 * 1000
                break
            case /^quarter-?(|hour)$/i.test( scale ):
                // Quarter of an hour (:> 15分
                _ms = 15 * 60 * 1000
                break
            case /^half-?(|hour)$/i.test( scale ):
                // Half an hour (:> 30分
                _ms = 30 * 60 * 1000
                break
            case /^hours?$/i.test( scale ):
                // Hour (:> 時(時間)
                _ms = 60 * 60 * 1000
                break
            case /^days?$/i.test( scale ):
                // Day (:> 日
                _ms = 24 * 60 * 60 * 1000
                break
            case /^weeks?$/i.test( scale ):
                // Week (:> 週
                _ms = 7 * 24 * 60 * 60 * 1000
                break
            case /^months?$/i.test( scale ):
                // Month (is the variable length scale) (:> 月(可変長スケール)
//console.log( '!_verifyScale::month:', this._instanceProps, _opts.scale )
                if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
                    return this._diffDate( _props.begin, _props.end, scale )
                } else {
                    _ms = 30.44 * 24 * 60 * 60 * 1000
                    break
                }
            case /^years?$/i.test( scale ):
                // Year (is the variable length scale) (:> 年(可変長スケール)
                if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
                    return this._diffDate( _props.begin, _props.end, scale )
                } else {
                    _ms = 365.25 * 24 * 60 * 60 * 1000
                    break
                }
            case /^lustrum$/i.test( scale ):
                // Lustrum (is the variable length scale, but currently does not support) (:> 五年紀 (可変長スケールだが現在サポートしてない)
                // 5y = 1826 or 1827; 1826 * 24 * 60 * 60 = 15766400, 1827 * 24 * 60 * 60 = 157852800 | avg.= 157788000
                //_ms = ( ( 3.1536 * Math.pow( 10, 8 ) ) / 2 ) * 1000 // <--- Useless by info of wikipedia
                _ms = 157788000 * 1000
                break
            case /^dec(ade|ennium)$/i.test( scale ):
                // Decade (is the variable length scale, but currently does not support) (:> 十年紀 (可変長スケールだが現在サポートしてない)
                // 10y = 3652 or 3653; 3652 * 24 * 60 * 60 = 315532800, 3653 * 24 * 60 * 60 = 157852800 | avg. = 315576000
                // _ms = ( 3.1536 * Math.pow( 10, 8 ) ) * 1000 // <--- Useless by info of wikipedia
                _ms = 315576000 * 1000
                break
            case /^century$/i.test( scale ):
                // Century (:> 世紀(百年紀)
                // 100y = 36525; 36525 * 24 * 60 * 60 = 3155760000
                _ms = 3155760000 * 1000
                break
            case /^millenniums?|millennia$/i.test( scale ):
                // Millennium (:> 千年紀
                // 100y = 365250
                //_ms = ( 3.1536 * Math.pow( 10, 10 ) ) * 1000
                _ms = 3155760000 * 10 * 1000
                break
            default:
                console.warn( 'Specified an invalid scale.' )
                _ms = -1
        }
        return _ms > 0 ? _ms : false
    }
    
    /**
     * Verify the display period of the timeline does not exceed the maximum renderable range
     * @private
     * @return {boolean}
     */
    _verifyMaxRenderableRange() {
// console.log( this._instanceProps.grids, '/', LimitScaleGrids[this._filterScaleKeyName( this._config.scale )] )
        return this._instanceProps.grids <= LimitScaleGrids[this._filterScaleKeyName( this._config.scale )]
    }
    
    /**
     * Render the view of timeline container
     * @private
     */
    _renderView() {
        this._debug( '_renderView' )
        
        let _elem          = this._element,
            _opts          = this._config,
            _props         = this._instanceProps,
            _tl_container  = $('<div></div>', { class: ClassName.TIMELINE_CONTAINER, style: `width: ${_props.visibleWidth}; height: ${_props.visibleHeight};` }),
            _tl_main       = $('<div></div>', { class: ClassName.TIMELINE_MAIN })
        
//console.log( _elem, _opts, _props )
        if ( $(_elem).length == 0 ) {
            throw new TypeError( 'Does not exist the element to render a timeline container.' )
        }
        
        if ( _opts.debug ) {
            console.info( `Timeline:{ fullWidth: ${_props.fullwidth}px,`, `fullHeight: ${_props.fullheight}px,`, `viewWidth: ${_props.visibleWidth}`, `viewHeight: ${_props.visibleHeight} }` )
        }
        
        $(_elem).css( 'position', 'relative' ) // initialize; not .empty()
        if ( _opts.hideScrollbar ) {
            _tl_container.addClass( ClassName.HIDE_SCROLLBAR )
        }
        
        // Create the timeline headline (:> タイムラインの見出しを生成
        $(_elem).prepend( this._createHeadline() )
        
        // Create the timeline event container (:> タイムラインのイベントコンテナを生成
        _tl_main.append( this._createEventContainer() )
        
        // Create the timeline ruler (:> タイムラインの目盛を生成
        if ( ! this.is_empty( _opts.ruler.top ) ) {
            _tl_main.prepend( this._createRuler( 'top' ) )
        }
        if ( ! this.is_empty( _opts.ruler.bottom ) ) {
            _tl_main.append( this._createRuler( 'bottom' ) )
        }
        
        // Create the timeline side index (:> タイムラインのサイドインデックスを生成
        let margin = {
                top    : parseInt( _tl_main.find( Selector.RULER_TOP ).height(), 10 ) - 1,
                bottom : parseInt( _tl_main.find( Selector.RULER_BOTTOM ).height(), 10 ) - 1
            }
        
        if ( _opts.sidebar.list.length > 0 ) {
            _tl_container.prepend( this._createSideIndex( margin ) )
        }
        
        // Append the timeline container in the timeline element (:> タイムライン要素にタイムラインコンテナを追加
        _tl_container.append( _tl_main )
        $(_elem).append( _tl_container )
        
        // Create the timeline footer (:> タイムラインのフッタを生成
        $(_elem).append( this._createFooter() )
        
        this._isShown = true
    }
    
    /**
     * Create the headline of the timeline
     * @private
     * @return {Object} Generated DOM element
     */
    _createHeadline() {
        let _opts    = this._config,
            _props   = this._instanceProps,
            _display = this.supplement( Default.headline.display, _opts.headline.display, this.validateBoolean ),
            _title   = this.supplement( null, _opts.headline.title ),
            _range   = this.supplement( Default.headline.range, _opts.headline.range, this.validateBoolean ),
            _locale  = this.supplement( Default.headline.locale, _opts.headline.locale ),
            _format  = this.supplement( Default.headline.format, _opts.headline.format ),
            _begin   = this.supplement( null, _props.begin ),
            _end     = this.supplement( null, _props.end ),
            _tl_headline = $('<div></div>', { class: ClassName.TIMELINE_HEADLINE }),
            _wrapper     = $('<div></div>', { class: ClassName.TIMELINE_HEADLINE_WRAPPER })
        
// console.log( '!_createHeadline:', _opts )
        if ( _title ) {
            _wrapper.append( `<h3 class="${ClassName.HEADLINE_TITLE}">${_opts.headline.title}</h3>` )
        }
        if ( _range ) {
            if ( _begin && _end ) {
                let _meta = `${new Date( _begin ).toLocaleString( _locale, _format )}<span class="${ClassName.RANGE_SPAN}"></span>${new Date( _end ).toLocaleString( _locale, _format )}`
                //let _meta = this.getCorrectDatetime( _begin ).toLocaleString( _locale, _format ) +'<span class="jqtl-range-span"></span>'+ this.getCorrectDatetime( _end ).toLocaleString( _locale, _format )
                
                _wrapper.append( `<div class="${ClassName.RANGE_META}">${_meta}</div>` )
            }
        }
        if ( ! _display ) {
            _tl_headline.addClass( ClassName.HIDE )
        }
        return _tl_headline.append( _wrapper )
    }
    
    /**
     * Create the event container of the timeline
     * @private
     * @return {Object} Generated DOM element
     */
    _createEventContainer() {
        let _opts         = this._config,
            _props        = this._instanceProps,
            _actualHeight = _props.fullheight + Math.ceil( _props.rows / 2 ),
            _container    = $('<div></div>', { class: ClassName.TIMELINE_EVENT_CONTAINER, style: `height:${_actualHeight}px;` }),
            _events_bg    = $(`<canvas width="${( _props.fullwidth - 1 )}" height="${_actualHeight}" class="${ClassName.TIMELINE_BACKGROUND_GRID}"></canvas>`),
            _events_lines = $(`<canvas width="${( _props.fullwidth - 1 )}" height="${_actualHeight}" class="${ClassName.TIMELINE_RELATION_LINES}"></canvas>`),
            _events_body  = $('<div></div>', { class: ClassName.TIMELINE_EVENTS }),
            _cy           = 0,
            ctx_grid      = _events_bg[0].getContext('2d'),
            drawRowRect   = ( pos_y, color ) => {
                color = this.supplement( '#FFFFFF', color )
                // console.log( 0, pos_y, _fullwidth, _size_row, color )
                ctx_grid.fillStyle = color
                ctx_grid.fillRect( 0, pos_y + 0.5, _props.fullwidth, _props.rowSize + 1.5 )
                ctx_grid.stroke()
            },
            drawHorizontalLine = ( pos_y, is_dotted ) => {
                is_dotted = this.supplement( false, is_dotted )
                // console.log( pos_y, is_dotted )
                ctx_grid.strokeStyle = 'rgba( 51, 51, 51, 0.1 )'
                ctx_grid.lineWidth = 1
                ctx_grid.filter = 'url(#crisp)'
                ctx_grid.beginPath()
                if ( is_dotted ) {
                    ctx_grid.setLineDash([ 1, 2 ])
                } else {
                    ctx_grid.setLineDash([])
                }
                ctx_grid.moveTo( 0, pos_y + 0.5 )
                ctx_grid.lineTo( _props.fullwidth, pos_y + 0.5 )
                ctx_grid.closePath()
                ctx_grid.stroke()
            },
            drawVerticalLine = ( pos_x, is_dotted ) => {
                is_dotted = this.supplement( false, is_dotted )
                // console.log( pos_x, is_dotted )
                ctx_grid.strokeStyle = 'rgba( 51, 51, 51, 0.025 )'
                ctx_grid.lineWidth = 1
                ctx_grid.filter = 'url(#crisp)'
                ctx_grid.beginPath()
                if ( is_dotted ) {
                    ctx_grid.setLineDash([ 1, 2 ])
                } else {
                    ctx_grid.setLineDash([])
                }
                ctx_grid.moveTo( pos_x - 0.5, 0 )
                ctx_grid.lineTo( pos_x - 0.5, _props.fullheight )
                ctx_grid.closePath()
                ctx_grid.stroke()
            }
        
        _cy = 0
        for ( let i = 0; i < _props.rows; i++ ) {
            _cy += i % 2 == 0 ? 1 : 0
            let _pos_y = ( i * _props.rowSize ) + _cy
            drawRowRect( _pos_y, i % 2 == 0 ? '#FEFEFE' : '#F8F8F8' )
        }
        _cy = 0
        for ( let i = 1; i < _props.rows; i++ ) {
            _cy += i % 2 == 0 ? 1 : 0
            let _pos_y = ( i * _props.rowSize ) + _cy
            drawHorizontalLine( _pos_y, true )
        }
        if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
            // For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
            let _bc = /^years?$/i.test( _opts.scale ) ? 365 : 30,
                _sy = 0
            
            for ( let _key of Object.keys( _props.variableScale ) ) {
                _sy += this.numRound( ( _props.variableScale[_key] * _props.scaleSize ) / _bc, 2 )
                drawVerticalLine( _sy, false )
            }
        } else {
            // In case of fixed length scale (:> 固定長スケールの場合
            for ( let i = 1; i < _props.grids; i++ ) {
                drawVerticalLine( ( i * _props.scaleSize ), false )
            }
        }
        
        return _container.append( _events_bg ).append( _events_lines ).append( _events_body )
    }
    
    /**
     * Create the ruler of the timeline
     * @private
     * @param {string} position - Either "top" or "bottom" as the position of the ruler
     * @return {Object} Generated DOM element
     */
    _createRuler( position ) {
        let _opts       = this._config,
            _props      = this._instanceProps,
            ruler_line  = this.supplement( [ _opts.scale ], _opts.ruler[position].lines, ( def, val ) => Array.isArray( val ) && val.length > 0 ? val : def ),
            line_height = this.supplement( Default.ruler.top.height, _opts.ruler[position].height ),
            font_size   = this.supplement( Default.ruler.top.fontSize, _opts.ruler[position].fontSize ),
            text_color  = this.supplement( Default.ruler.top.color, _opts.ruler[position].color ),
            background  = this.supplement( Default.ruler.top.background, _opts.ruler[position].background ),
            locale      = this.supplement( Default.ruler.top.locale, _opts.ruler[position].locale ),
            format      = this.supplement( Default.ruler.top.format, _opts.ruler[position].format ),
            ruler_opts  = { lines: ruler_line, height: line_height, fontSize: font_size, color: text_color, background, locale, format },
            _fullwidth  = _props.fullwidth - 1,
            _fullheight = ruler_line.length * line_height,
            _ruler      = $('<div></div>', { class: `${PREFIX}ruler-${position}`, style: `height:${_fullheight}px;` }),
            _ruler_bg   = $(`<canvas class="${PREFIX}ruler-bg-${position}" width="${_fullwidth}" height="${_fullheight}"></canvas>`),
            _ruler_body = $('<div></div>', { class: `${PREFIX}ruler-content-${position}` }),
            _finalLines = 0,
            ctx_ruler   = _ruler_bg[0].getContext('2d')
            
//console.log( grids, size_per_grid, scale, begin, min_scale, ruler, position, ruler_line, line_height, ctx_ruler.canvas.width, ctx_ruler.canvas.height )
        // Draw background of ruler
        ctx_ruler.fillStyle = background
        ctx_ruler.fillRect( 0, 0, ctx_ruler.canvas.width, ctx_ruler.canvas.height )
        
        // Draw stroke of ruler
        ctx_ruler.strokeStyle = 'rgba( 51, 51, 51, 0.1 )'
        ctx_ruler.lineWidth = 1
        ctx_ruler.filter = 'url(#crisp)'
        ruler_line.some( ( line_scale, idx ) => {
            if ( /^(quarter|half)-?(|hour)$/i.test( line_scale ) ) {
                return true // break
            }
            
            ctx_ruler.beginPath()
            
            // Draw rows
            //let _line_x = position === 'top' ? 0 : ctx_ruler.canvas.width,
            let _line_y = position === 'top' ? line_height * ( idx + 1 ) - 0.5 : line_height * idx + 0.5
            
            ctx_ruler.moveTo( 0, _line_y )
            ctx_ruler.lineTo( ctx_ruler.canvas.width, _line_y )
            
            // Draw cols
            let _line_grids = null,
                _grid_x     = 0,
                _correction = -1.5
            
            if ( /^(year|month)s?$/i.test( _opts.scale ) ) {
                // For scales where the value of quantity per unit is variable length (:> 単位あたりの量の値が可変長であるスケールの場合
                _line_grids = this._filterVariableScale( line_scale )
//console.log( '!_createRuler:', line_scale, _line_grids )
                
                for ( let _key of Object.keys( _line_grids ) ) {
                    _grid_x += this.numRound( _line_grids[_key], 2 )
                    
                    ctx_ruler.moveTo( _grid_x + _correction, position === 'top' ? _line_y - line_height : _line_y )
                    ctx_ruler.lineTo( _grid_x + _correction, position === 'top' ? _line_y : _line_y + line_height )
                }
            } else {
                // In case of fixed length scale (:> 固定長スケールの場合
                _line_grids = this._getGridsPerScale( line_scale )
                
                for ( let _val of _line_grids ) {
                    if ( this.is_empty( _val ) || _val >= _props.grids ) {
                        break
                    }
                    let _grid_width = _val * _props.scaleSize
                    
                    _grid_x += _grid_width
                    if ( Math.ceil( _grid_x ) - _correction >= ctx_ruler.canvas.width ) {
                        break
                    }
                    ctx_ruler.moveTo( _grid_x + _correction, position === 'top' ? _line_y - line_height : _line_y )
                    ctx_ruler.lineTo( _grid_x + _correction, position === 'top' ? _line_y : _line_y + line_height )
                }
            }
            ctx_ruler.closePath()
            ctx_ruler.stroke()
            _ruler_body.append( this._createRulerContent( _line_grids, line_scale, ruler_opts ) )
            _finalLines++
        })
        
        if ( ruler_line.length != _finalLines ) {
            _ruler.css( 'height', `${_finalLines * line_height}px` )
        }
        
        return _ruler.append( _ruler_bg ).append( _ruler_body )
    }
    
    /**
     * Filter to aggregate the grid width of the variable length scale
     * @private
     * @param {string} target_scale - 
     * @return {Object} 
     */
    _filterVariableScale( target_scale ) {
        let _opts  = this._config,
            _props = this._instanceProps,
            _bc    = /^years?$/i.test( _opts.scale ) ? 365 : 30,
            scales = _props.variableScale,
            retObj = {}
        
        for ( let _dt of Object.keys( scales ) ) {
            let _days     = scales[_dt],
                grid_size = this.numRound( ( _days * _props.scaleSize ) / _bc, 2 ),
                _newKey   = null,
                _arr, _temp
//console.log( '!_filterVariableScale:', _dt, this.getCorrectDatetime( _dt ).getFullYear(), _days )
            
            switch ( true ) {
                case /^millenniums?|millennia$/i.test( target_scale ):
                    _newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 1000 )
                    
                    if ( retObj.hasOwnProperty( _newKey ) ) {
                        retObj[_newKey] += grid_size
                    } else {
                        retObj[_newKey] = grid_size
                    }
                    break
                case /^century$/i.test( target_scale ):
                    _newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 100 )
                    
                    if ( retObj.hasOwnProperty( _newKey ) ) {
                        retObj[_newKey] += grid_size
                    } else {
                        retObj[_newKey] = grid_size
                    }
                    break
                case /^dec(ade|ennium)$/i.test( target_scale ):
                    _newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 10 )
                    
                    if ( retObj.hasOwnProperty( _newKey ) ) {
                        retObj[_newKey] += grid_size
                    } else {
                        retObj[_newKey] = grid_size
                    }
                    break
                case /^lustrum$/i.test( target_scale ):
                    _newKey = Math.ceil( this.getCorrectDatetime( _dt ).getFullYear() / 5 )
                    
                    if ( retObj.hasOwnProperty( _newKey ) ) {
                        retObj[_newKey] += grid_size
                    } else {
                        retObj[_newKey] = grid_size
                    }
                    break
                case /^years?$/i.test( target_scale ):
                    _newKey = `${this.getCorrectDatetime( _dt ).getFullYear()}`
                    
                    if ( retObj.hasOwnProperty( _newKey ) ) {
                        retObj[_newKey] += grid_size
                    } else {
                        retObj[_newKey] = grid_size
                    }
                    break
                case /^months?$/i.test( target_scale ):
                    retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}`] = grid_size
                    break
                case /^weeks?$/i.test( target_scale ):
                    _arr  = _dt.split(',')
                    _temp = this.getWeek( _arr[0] )
//console.log( '!_filterVariableScale::week:', _dt, _arr[0], _temp )
                    retObj[`${this.getCorrectDatetime( _arr[0] ).getFullYear()},${_temp}`] = grid_size
                    break
                case /^weekdays?$/i.test( target_scale ):
                    _arr  = _dt.split(',')
                    _temp = this.getCorrectDatetime( _arr[0] ).getDay()
                    retObj[`${this.getCorrectDatetime( _arr[0] ).getFullYear()}/${this.getCorrectDatetime( _arr[0] ).getMonth() + 1}/1,${_temp}`] = grid_size
                    break
                case /^days?$/i.test( target_scale ):
                    retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1`] = grid_size
                    break
                case /^hours?$/i.test( target_scale ):
                    retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0`] = grid_size
                    break
                case /^minutes?$/i.test( target_scale ):
                    retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0:00`] = grid_size
                    break
                case /^seconds?$/i.test( target_scale ):
                    retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}/1 0:00:00`] = grid_size
                    break
                default:
                    retObj[`${this.getCorrectDatetime( _dt ).getFullYear()}/${this.getCorrectDatetime( _dt ).getMonth() + 1}`] = grid_size
                    break
            }
        }
        
        return retObj
    }
    
    /**
     * Get the grid number per scale (for fixed length scale)
     * @private
     * @param {string} target_scale - 
     * @return {Object} 
     */
    _getGridsPerScale( target_scale ) {
        //let _opts        = this._config,
        let _props       = this._instanceProps,
            _scopes      = [],
            _scale_grids = {},
            _sep         = '/'
        
        for ( let i = 0; i < _props.grids; i++ ) {
            let _tmp = new Date( _props.begin + ( i * _props.scale ) ),
            //let _tmp = this.getCorrectDatetime( _props.begin + ( i * _props.scale ) ),
                _y   = _tmp.getFullYear(),
                _mil = Math.ceil( _y / 1000 ),
                _cen = Math.ceil( _y / 100 ),
                _dec = Math.ceil( _y / 10 ),
                _lus = Math.ceil( _y / 5 ),
                _m   = _tmp.getMonth() + 1,
                _wd  = _tmp.getDay(), // 0 = Sun, ... 6 = Sat
                _d   = _tmp.getDate(),
                _w   = this.getWeek( `${_y}/${_m}/${_d}` ),
                _h   = _tmp.getHours(),
                _min = _tmp.getMinutes(),
                _s   = _tmp.getSeconds()
// console.log( '!!:', _tmp, `y: ${_y}`, `w: ${_w}`, /* `mil: ${_mil}`, `cen: ${_cen}`, `dec: ${_dec}`, `lus: ${_lus}` */ )
            
            _scopes.push({
                millennium : _mil,
                century    : _cen,
                decade     : _dec,
                lustrum    : _lus,
                year       : _y,
                month      : `${_y}${_sep}${_m}${_sep}1`,
                week       : `${_y},${_w}`,
                weekday    : `${_y}${_sep}${_m}${_sep}${_d},${_wd}`,
                day        : `${_y}${_sep}${_m}${_sep}${_d}`,
                hour       : `${_y}${_sep}${_m}${_sep}${_d} ${_h}`,
                minute     : `${_y}${_sep}${_m}${_sep}${_d} ${_h}:${_min}`,
                second     : `${_y}${_sep}${_m}${_sep}${_d} ${_h}:${_min}:${_s}`,
                datetime   : _tmp.toString()
            })
        }
        _scopes.forEach( ( _scope ) => {
//console.log( _scope[target_scale], idx );
            if ( ! _scale_grids[_scope[target_scale]] ) {
                _scale_grids[_scope[target_scale]] = 1
            } else {
                _scale_grids[_scope[target_scale]]++
            }
        })
//console.log( '!_getGridsPerScale:', target_scale, _scale_grids )
        
        return this.toIterableObject( _scale_grids )
    }
    
    /**
     * Create the content of ruler of the timeline
     * @private
     * @param {Object} _line_grids - 
     * @param {string} line_scale - 
     * @param {RulerOptions} ruler - 
     * @return {Object} Generated DOM element
     */
    _createRulerContent( _line_grids, line_scale, ruler ) {
        let _opts        = this._config,
            _props       = this._instanceProps,
            line_height  = this.supplement( Default.ruler.top.height, ruler.height ),
            font_size    = this.supplement( Default.ruler.top.fontSize, ruler.fontSize ),
            text_color   = this.supplement( Default.ruler.top.color, ruler.color ),
            locale       = this.supplement( Default.ruler.top.locale, ruler.locale, this.validateString ),
            format       = this.supplement( Default.ruler.top.format, ruler.format, this.validateObject ),
            _ruler_lines = $('<div></div>', { class: ClassName.TIMELINE_RULER_LINES, style: `width:100%;height:${line_height}px;` })
        
        for ( let _key of Object.keys( _line_grids ) ) {
            let _item_width      = /^(year|month)s?$/i.test( _opts.scale ) ? _line_grids[_key] : _line_grids[_key] * _props.scaleSize,
                _line            = $('<div></div>', { class: ClassName.TIMELINE_RULER_ITEM, style: `width:${_item_width}px;height:${line_height}px;line-height:${line_height}px;font-size:${font_size}px;color:${text_color};` }),
                _ruler_string    = this.getLocaleString( _key, line_scale, locale, format ),
                _data_ruler_item = ''
//console.log( '!_createRulerContent:', _key, _line_grids[_key], line_scale, locale, format, _item_width, _ruler_string )
            
            _data_ruler_item = `${line_scale}-${( _data_ruler_item === '' ? String( _key ) : _data_ruler_item )}`
            _line.attr( 'data-ruler-item', _data_ruler_item ).html( _ruler_string )
            
            if ( _item_width > this.strWidth( _ruler_string ) ) {
                // Adjust position of ruler item string
//console.log( _item_width, _ruler_string, _ruler_string.length, this.strWidth( _ruler_string ), $(this._element).width() )
                if ( _item_width > $(this._element).width() ) {
                    _line.addClass( ClassName.RULER_ITEM_ALIGN_LEFT )
                }
            }
            
            _ruler_lines.append( _line ).attr( 'data-ruler-scope', line_scale )
        }
        
        return _ruler_lines
    }
    
    /**
     * Create the side indexes of the timeline
     * @private
     * @param {Object} margin - 
     * @param {number} margin.top - 
     * @param {number} margin.bottom - 
     * @return {Object} Generated DOM element
     */
    _createSideIndex( margin ) {
        let _opts    = this._config,
            _props   = this._instanceProps,
            _sticky  = this.supplement( Default.sidebar.sticky, _opts.sidebar.sticky ),
            _overlay = this.supplement( Default.sidebar.overlay, _opts.sidebar.overlay ),
            _sbList  = this.supplement( Default.sidebar.list, _opts.sidebar.list ),
            _wrapper = $('<div></div>', { class: ClassName.TIMELINE_SIDEBAR }),
            _margin  = $('<div></div>', { class: ClassName.TIMELINE_SIDEBAR_MARGIN }),
            _list    = $('<div></div>', { class: ClassName.TIMELINE_SIDEBAR_ITEM }),
            _c       = 0.5
        
        if ( _sticky ) {
            _wrapper.addClass( ClassName.STICKY_LEFT )
        }
        
        if ( _overlay ) {
            _list.addClass( ClassName.OVERLAY )
        }
        
        //_wrapper.css( 'margin-top', margin.top + 'px' ).css( 'margin-bottom', margin.bottom + 'px' )
        if ( margin.top > 0 ) {
            _wrapper.prepend( _margin.clone().css( 'height', `${( margin.top + 1 )}px` ) )
        }
        
        for ( let i = 0; i < _props.rows; i++ ) {
            let _item = _list.clone().html( _sbList[i] )
            
            _wrapper.append( _item )
        }
        _wrapper.find( Selector.TIMELINE_SIDEBAR_ITEM ).css( 'height', `${( _props.rowSize + _c )}px` ).css( 'line-height', `${( _props.rowSize + _c )}px` )
        
        if ( margin.bottom > 0 ) {
            _wrapper.append( _margin.clone().css( 'height', `${( margin.bottom + 1 )}px` ) )
        }
        
        return _wrapper
    }
    
    /**
     * Create the footer of the timeline
     * @private
     * @return {Object} Generated DOM element
     */
    _createFooter() {
        let _opts    = this._config,
            _props   = this._instanceProps,
            _display = this.supplement( Default.footer.display, _opts.footer.display ),
            _content = this.supplement( null, _opts.footer.content ),
            _range   = this.supplement( Default.footer.range, _opts.footer.range ),
            _scale   = this.supplement( Default.footer.scale, _opts.footer.scale ),
            _locale  = this.supplement( Default.footer.locale, _opts.footer.locale ),
            _format  = this.supplement( Default.footer.format, _opts.footer.format ),
            _begin   = this.supplement( null, _props.begin ),
            _end     = this.supplement( null, _props.end ),
            _tl_footer = $('<div></div>', { class: ClassName.TIMELINE_FOOTER })
        
        if ( _range ) {
            if ( _begin && _end ) {
                let _meta = `${new Date( _begin ).toLocaleString( _locale, _format )}<span class="${ClassName.RANGE_SPAN}"></span>${new Date( _end ).toLocaleString( _locale, _format )}`
                //let _meta = this.getCorrectDatetime( _begin ).toLocaleString( _locale, _format ) +'<span class="jqtl-range-span"></span>'+ this.getCorrectDatetime( _end ).toLocaleString( _locale, _format )
                
                _tl_footer.append( `<div class="${ClassName.RANGE_META} ${ClassName.ALIGN_SELF_RIGHT}">${_meta}</div>` )
            }
        }
        if ( _content ) {
            _tl_footer.append( `<div class="${ClassName.TIMELINE_FOOTER_CONTENT}">${_content}</div>` )
        }
        if ( ! _display ) {
            _tl_footer.addClass( ClassName.HIDE )
        }
        
        return _tl_footer
    }
    
    /**
     * Acquire the difference between two dates with the specified scale value
     * @private
     * @param {number} date1 - Number that can be parsed as datetime
     * @param {number} date2 - Number that can be parsed as datetime
     * @param {string} [scale="millisecond"] - 
     * @param {boolean} [absval=false] - 
     * @return {number|boolean}
     */
    _diffDate( date1, date2, scale = 'millisecond', absval = false ) {
        //let _opts  = this._config,
        let _dt1   = this.supplement( null, date1 ),
            _dt2   = this.supplement( null, date2 ),
            diffMS = 0,
            retval = false,
            lastDayOfMonth = ( dateObj ) => {
                let _tmp = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )
                _tmp.setTime( _tmp.getTime() - 1 )
                return _tmp.getDate()
            },
            isLeapYear = ( dateObj ) => {
                let _tmp = new Date( dateObj.getFullYear(), 0, 1 ),
                    sum  = 0
                
                for ( let i = 0; i < 12; i++ ) {
                    _tmp.setMonth(i)
                    sum += lastDayOfMonth( _tmp )
                }
                return sum == 365 ? false : true
            }
        
        if ( ! _dt1 || ! _dt2 ) {
            console.warn( 'Cannot parse date because invalid format or undefined.' )
            return false
        }
        
        diffMS = _dt2 - _dt1
        
        if ( absval ) {
            diffMS = Math.abs( diffMS )
        }
        
        let _bd = new Date( _dt1 ),
            _ed = new Date( _dt2 ),
            _dy = _ed.getFullYear() - _bd.getFullYear(),
            _m  = {}
        
        switch ( true ) {
            case /^years?$/i.test( scale ):
                if ( _dy > 0 ) {
                    for ( let i = 0; i <= _dy; i++ ) {
                        let _cd = new Date( _bd.getFullYear() + i, 0, 1 )
                        _m[`${_bd.getFullYear() + i}`] = isLeapYear( _cd ) ? 366 : 365
                    }
                } else {
                    _m[`${_bd.getFullYear()}`] = isLeapYear( _bd ) ? 366 : 365
                }
                retval = _m
                break
            case /^months?$/i.test( scale ):
                if ( _dy > 0 ) {
                    for ( let i = _bd.getMonth(); i < 12; i++ ) {
                        let _cd = new Date( _bd.getFullYear(), i, 1 )
                        _m[`${_bd.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
                    }
                    if ( _dy > 1 ) {
                        for ( let y = 1; y < _dy; y++ ) {
                            for ( let i = 0; i < 12; i++ ) {
                                let _cd = new Date( _bd.getFullYear() + y, i, 1 )
                                _m[`${_bd.getFullYear() + y}/${i + 1}`] = lastDayOfMonth( _cd )
                            }
                        }
                    }
                    for ( let i = 0; i <= _ed.getMonth(); i++ ) {
                        let _cd = new Date( _ed.getFullYear(), i, 1 )
                        _m[`${_ed.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
                    }
                } else {
                    for ( let i = _bd.getMonth(); i <= _ed.getMonth(); i++ ) {
                        let _cd = new Date( _bd.getFullYear(), i, 1 )
                        _m[`${_bd.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
                    }
                }
                retval = _m
                break
            case /^weeks?$/i.test( scale ):
                retval = Math.ceil( diffMS / ( 7 * 24 * 60 * 60 * 1000 ) )
                break
            case /^(|week)days?$/i.test( scale ):
                retval = Math.ceil( diffMS / ( 24 * 60 * 60 * 1000 ) )
                break
            case /^hours?$/i.test( scale ):
                retval = Math.ceil( diffMS / ( 60 * 60 * 1000 ) )
                break
            case /^minutes?$/i.test( scale ):
                retval = Math.ceil( diffMS / ( 60 * 1000 ) )
                break
            case /^seconds?$/i.test( scale ):
                retval = Math.ceil( diffMS / 1000 )
                break
            default:
                retval = diffMS
                break
        }
//console.log( '!_diffDate:', retval )
        
        return retval
    }
    
    /**
     * Load all enabled events markupped on target element to the timeline object
     * @private
     */
    _loadEvent() {
        this._debug( '_loadEvent' )
        
        let _that         = this,
            _elem         = this._element,
            _event_list   = $(_elem).find( Selector.DEFAULT_EVENTS ),
            _cnt          = 0,
            events        = [],
            lastEventId   = 0
        
        _event_list.children().each(function() {
            let _attr = $(this).attr( 'data-timeline-node' )
            
            if ( typeof _attr !== 'undefined' && _attr !== false ) {
                _cnt++
            }
        })
        
        if ( _event_list.length == 0 || _cnt == 0 ) {
            this._debug( 'Enable event does not exist.' )
        }
        
        // Register Event Data
        _event_list.children().each(function() {
            let _evt_params = _that._getPluggableParams( $(this).attr( 'data-timeline-node' ) ),
                _one_event  = {}
            
            if ( ! _that.is_empty( _evt_params ) ) {
                _one_event = _that._registerEventData( this, _evt_params )
                events.push( _one_event )
                lastEventId = Math.max( lastEventId, parseInt( _one_event.eventId, 10 ) )
            }
        });
        // Set event id with auto increment (:> イベントIDを自動採番
        let cacheIds = [] // for checking duplication of id (:> IDの重複チェック用
        events.forEach( ( _evt, _i, _this ) => {
            let _chkId = parseInt( _this[_i].eventId, 10 )
            
            if ( _chkId == 0 || cacheIds.includes( _chkId ) ) {
                lastEventId++
                _this[_i].eventId = lastEventId
            } else {
                _this[_i].eventId = _chkId
            }
            cacheIds.push( _this[_i].eventId )
        });
        
        this._isCached = this._saveToCache( events )
        
    }
    
    /**
     * Register one event data as object
     * @private
     * @param {Object} event_element - 
     * @param {Object} params - 
     * @return {Object} Registered new event data
     */
    _registerEventData( event_element, params ) {
        let _opts     = this._config,
            _props    = this._instanceProps,
            new_event = {
                ...EventParams,
                ...{
                    uid   : this.generateUniqueID(),
                    label : $(event_element).html()
                }
            },
            _relation = {},
            _x, _w, _c //, _pointSize
//console.log( '!_registerEventData:', _opts, params )
        
        if ( params.hasOwnProperty( 'start' ) && ! this.is_empty( params.start ) ) {
            _x = this._getCoordinateX( params.start )
            new_event.x = this.numRound( _x, 2 )
            if ( params.hasOwnProperty( 'end' ) && ! this.is_empty( params.end ) ) {
                _x = this._getCoordinateX( params.end )
                _w = _x - new_event.x
                new_event.width = this.numRound( _w, 2 )
                
                if ( _opts.eventMeta.display ) {
                    if ( this.is_empty( _opts.eventMeta.content ) && ! params.hasOwnProperty( 'rangeMeta' ) ) {
//console.log( '!_registerEventData:', _opts.eventMeta.locale, _opts.eventMeta.format, _opts.scale, params )
                        
                        new_event.rangeMeta += this.getLocaleString( params.start, _opts.eventMeta.scale, _opts.eventMeta.locale, _opts.eventMeta.format )
                        new_event.rangeMeta += ` - ${this.getLocaleString( params.end, _opts.eventMeta.scale, _opts.eventMeta.locale, _opts.eventMeta.format )}`
                    } else {
                        new_event.rangeMeta = _opts.eventMeta.content
                    }
                }
            } else {
                new_event.width = 0
            }
//console.log( 'getX:', _x, 'getW:', _w, event_element )
            if ( params.hasOwnProperty( 'row' ) ) {
                _c = Math.floor( params.row / 2 )
                new_event.y = ( params.row - 1 ) * _opts.rowHeight + new_event.margin + _c
            }
            
            Object.keys( new_event ).forEach( ( _prop ) => {
                switch( true ) {
                    case /^eventId$/i.test( _prop ):
                        if ( params.hasOwnProperty( 'id' ) && this.is_empty( new_event.eventId ) ) {
                            new_event.eventId = parseInt( params.id, 10 )
                        } else {
                            new_event.eventId = parseInt( params[_prop], 10 ) || 0
                        }
                        break
                    case /^(label|content)$/i.test( _prop ):
                        if ( params.hasOwnProperty( _prop ) && ! this.is_empty( params[_prop] ) ) {
                            new_event[_prop] = params[_prop]
                        }
                        // Override the children element to label or content setting
                        if ( $(event_element).children(`.event-${_prop}`).length > 0 ) {
                            new_event[_prop] = $(event_element).children(`.event-${_prop}`).html()
                        }
//console.log( '!_registerEventData:', _prop, params[_prop], new_event[_prop] )
                        break
                    case /^relation$/i.test( _prop ):
                        // For drawing the relation line
                        if ( /^point(|er)$/i.test( _opts.type ) ) {
                            //let _pointSize  = this._getPointerSize( new_event.size, new_event.margin )
                            _relation.x = this.numRound( new_event.x, 2 )
                            _relation.y = this.numRound( ( _props.rowSize * ( ( params.row || 1 ) - 1 ) ) + ( _props.rowSize / 2 ), 2 )
                            
//console.log( '!_registerEventData:', params, new_event.x, new_event.y, _pointSize, _relation )
                            new_event[_prop] = {
                                ...params[_prop],
                                ..._relation
                            }
                        }
                        break
                    default:
                        if ( params.hasOwnProperty( _prop ) && ! this.is_empty( params[_prop] ) ) {
                            new_event[_prop] = params[_prop]
                        }
                        break
                }
            });
        }
//console.log( '!_registerEventData:', new_event )
        return new_event
    }
    
    /**
     * Get the coordinate X on the timeline of any date
     * @private
     * @param {string} date - 
     * @return {number} The pixel value as the coordinate X on timeline
     */
    _getCoordinateX( date ) {
        //let _opts  = this._config,
        let _props = this._instanceProps,
            _date  = this.supplement( null, this._getPluggableDatetime( date ) ),
            coordinate_x = 0
        
        if ( _date ) {
            if ( _date - _props.begin >= 0 && _props.end - _date >= 0 ) {
                // When the given date is within the range of timeline begin and end (:> 指定された日付がタイムラインの開始と終了の範囲内にある場合
                coordinate_x = ( Math.abs( _date - _props.begin ) / _props.scale ) * _props.scaleSize
            } else {
                // When the given date is out of timeline range (:> 指定された日付がタイムラインの範囲外にある場合
                coordinate_x = ( ( _date - _props.begin ) / _props.scale ) * _props.scaleSize
            }
        } else {
            console.warn( 'Cannot parse date because invalid format or undefined.' )
        }
        
        return coordinate_x
    }
    
    /**
     * Cache the event data to the web storage
     * @private
     * @param {Object} data - 
     */
    _saveToCache( data ) {
        let strageEngine = /^local(|Storage)$/i.test( this._config.storage ) ? 'localStorage' : 'sessionStorage',
            is_available = ( strageEngine in window ) && ( ( strageEngine === 'localStorage' ? window.localStorage : window.sessionStorage ) !== null )
        
        if ( is_available ) {
            if ( strageEngine === 'localStorage' ) {
                localStorage.setItem( this._selector, JSON.stringify( data ) )
            } else {
                sessionStorage.setItem( this._selector, JSON.stringify( data ) )
            }
            return true
        } else {
            throw new TypeError( `The storage named "${strageEngine}" can not be available.` )
        }
    }
    
    /**
     * Load the cached event data from the web storage
     * @private
     * @return {Object}
     */
    _loadToCache() {
        let strageEngine = /^local(|Storage)$/i.test( this._config.storage ) ? 'localStorage' : 'sessionStorage',
            is_available = ( strageEngine in window ) && ( ( strageEngine === 'localStorage' ? window.localStorage : window.sessionStorage ) !== null ),
            data         = null
        
        if ( is_available ) {
            if ( strageEngine === 'localStorage' ) {
                data = JSON.parse( localStorage.getItem( this._selector ) )
            } else {
                data = JSON.parse( sessionStorage.getItem( this._selector ) )
            }
        } else {
            throw new TypeError( `The storage named "${strageEngine}" can not be available.` )
        }
        return data
    }
    
    /**
     * Remove the cache data on the web storage
     * @private
     */
    _removeCache() {
        let strageEngine = /^local(|Storage)$/i.test( this._config.storage ) ? 'localStorage' : 'sessionStorage',
            is_available = ( strageEngine in window ) && ( ( strageEngine === 'localStorage' ? window.localStorage : window.sessionStorage ) !== null )
        
        if ( is_available ) {
            if ( strageEngine === 'localStorage' ) {
                localStorage.removeItem( this._selector )
            } else {
                sessionStorage.removeItem( this._selector )
            }
        } else {
            throw new TypeError( `The storage named "${strageEngine}" can not be available.` )
        }
    }
    
    /**
     * Controller method to place event data on timeline
     * @private: 
     */
    _placeEvent() {
        this._debug( '_placeEvent' )
        
        if ( ! this._isCached ) {
            return
        }
        
        let _elem           = this._element,
            _opts           = this._config,
            _evt_container  = $(_elem).find( Selector.TIMELINE_EVENTS ),
            _relation_lines = $(_elem).find( Selector.TIMELINE_RELATION_LINES ),
            events          = this._loadToCache(),
            _sleep          = _opts.debug ? DEBUG_SLEEP : 1
        
        if ( events.length > 0 ) {
            _evt_container.empty()
            events.forEach( ( _evt ) => {
                let _evt_elem = this._createEventNode( _evt )
                
                if ( _evt_elem ) {
                    _evt_container.append( _evt_elem )
                }
            })
        }
        
        if ( /^point(|er)$/i.test( _opts.type ) ) {
            this._drawRelationLine( events )
        }
        
// console.log( '!_placeEvent:', _opts )
        this.sleep( _sleep ).then(() => {
            this.hideLoader()
            _evt_container.fadeIn( 'fast', () => {
                _relation_lines.fadeIn( 'fast' )
            })
        })
        
    }
    
    /**
     * Create an event element on the timeline
     * @private
     * @param {Object} params - 
     * @return {Object} Generated DOM element
     */
    _createEventNode( params ) {
        let _opts     = this._config,
            _props    = this._instanceProps,
            _evt_elem = $('<div></div>', {
                class : ClassName.TIMELINE_EVENT_NODE,
                id    : `evt-${params.eventId}`,
                css   : {
                    left   : `${params.x}px`,
                    top    : `${params.y}px`,
                    width  : `${params.width}px`,
                    height : `${params.height}px`,
                    color  : this.hexToRgbA( params.color ),
                    backgroundColor : this.hexToRgbA( params.bgColor ),
                },
                html  : `<div class="${ClassName.TIMELINE_EVENT_LABEL}">${params.label}</div>`
            })
//console.log( '!_createEventNode:', params )
        
        // Whether this event is within the display range of the timeline (:> タイムライン表示範囲内のイベントかどうか
        // For events excluded, set the width to -1 (:> 除外イベントは幅を -1 に設定する
        if ( params.x >= 0 ) {
            // The event start datetime is over the start datetime of the timeline (:> イベント始点がタイムラインの始点以上
            if ( params.x <= _props.fullwidth ) {
                // The event start datetime is less than or equal to the timeline end datetime (:> イベントの始点がタイムラインの終点以下
                if ( params.x + params.width <= _props.fullwidth ) {
                    // The event end datetime is less than before the timeline end datetime (regular event) (:> イベント終点がタイムラインの終点以下(通常イベント)
                    // OK
                } else {
                    // The event end datetime is after the timeline end datetime (event exceeded end datetime) (:> イベント終点がタイムラインの終点より後(終点超過イベント)
                    params.width = _props.fullwidth - params.x
                }
            } else {
                // The event start datetime is after the timeline end datetime (exclude event) (:> イベント始点がタイムラインの終点より後(除外イベント)
                params.width = -1
            }
        } else {
            // The event start datetime is before the timeline start datetime (:> イベント始点がタイムラインの始点より前
            if ( /^point(|er)$/i.test( _opts.type ) ) {
                // In the case of "point" type, that is an exclude event (:> ポインター型の場合は除外イベント
                params.width = -1
            } else {
                // The case of "bar" type
                if ( params.x + params.width <= 0 ) {
                    // The event end datetime is less than before the timeline start datetime (exclude event) (:> イベント終点がタイムラインの始点より前(除外イベント)
                    params.width = -1
                } else {
                    // The event end datetime is after the timeline start datetime (:> イベント終点がタイムラインの始点より後
                    if ( params.x + params.width <= _props.fullwidth ) {
                        // The event end datetime is less than or equal the timeline end datetime (event exceeded start datetime) (:> イベント終点がタイムラインの終点以下(始点超過イベント)
                        params.width = Math.abs( params.x + params.width )
                        params.x = 0
                    } else {
                        // The event end datetime is after the timeline end datetime (event exceeded both start and end datetime) (:> イベント終点がタイムラインの終点より後(始点・終点ともに超過イベント)
                        params.width = _props.fullwidth
                        params.x = 0
                    }
                }
            }
        }
//console.log( 'x:', params.x, 'w:', params.width, 'x-end:', Math.abs( params.x ) + params.width, 'fw:', _props.fullwidth, 'ps:', params.size )
        
        if ( /^point(|er)$/i.test( _opts.type ) ) {
            if ( params.width < 0 ) {
                return null
            }
            let _pointSize = this._getPointerSize( params.size, params.margin ),
                _shiftX    = this.numRound( params.x - ( _pointSize / 2 ), 2 ),
                _shiftY    = this.numRound( params.y + ( ( params.height - _pointSize ) / 2 ), 2 )
            
//console.log( '!_createEventNode:', params, _pointSize, _shiftX, _shiftY )
            _evt_elem.addClass( ClassName.VIEWER_EVENT_TYPE_POINTER ).css( 'border-color', params.bdColor )
            .css( 'left', `${_shiftX}px` ).css( 'top', `${_shiftY}px` ).css( 'width', `${_pointSize}px` ).css( 'height', `${_pointSize}px` )
            .attr( 'data-base-size', _pointSize ).attr( 'data-base-left', _shiftX ).attr( 'data-base-top', _shiftY )
        } else {
            if ( params.width < 1 ) {
                return null
            }
            _evt_elem.css( 'left', `${params.x}px` ).css( 'width', `${params.width}px` )
        }
        
        _evt_elem.attr( 'data-uid', params.uid )
        
        if ( ! this.is_empty( params.image ) ) {
            if ( /^point(|er)$/i.test( _opts.type ) ) {
                _evt_elem.css( 'background-image', `url(${params.image})` )
            } else {
                let _imgSize = params.height - ( params.margin * 2 )
                _evt_elem.prepend( `<img src="${params.image}" class="${ClassName.TIMELINE_EVENT_THUMBNAIL}" width="${_imgSize}" height="${_imgSize}" />` )
            }
        }
        
        if ( /^bar$/i.test( _opts.type ) && _opts.eventMeta.display ) {
//console.log( '!_createEventNode:', params )
            params.extend.meta = params.rangeMeta
        }
        
        if ( ! this.is_empty( params.extend ) ) {
            for ( let _prop of Object.keys( params.extend ) ) {
                _evt_elem.attr( `data-${_prop}`, params.extend[_prop] )
                if ( _prop === 'toggle' && [ 'popover', 'tooltip' ].includes( params.extend[_prop] ) ) {
                    // for bootstrap's popover or tooltip
                    _evt_elem.attr( 'title', params.label )
                    if ( ! params.extend.hasOwnProperty( 'content' ) ) {
                        _evt_elem.attr( 'data-content', params.content )
                    }
                }
            }
        }
        
        if ( ! this.is_empty( params.callback ) ) {
            _evt_elem.attr( 'data-callback', params.callback )
        }
        
        return _evt_elem
    }
    
    /**
     * Retrieve the diameter size (pixel) of pointer
     * @private
     * @param {number|string} key - 
     * @param {number} margin
     * @return {number} 
     */
    _getPointerSize( key, margin ) {
        //let _opts  = this._config,
        let _props = this._instanceProps,
            _max   = Math.min( _props.scaleSize, _props.rowSize ) - ( margin * 2 ),
            _size  = null
        
        switch ( true ) {
            case /^large$/i.test( key ):
                _size = Math.max( this.numRound( _max * 0.8, 1 ), MIN_POINTER_SIZE )
                break
            case /^normal$/i.test( key ):
                _size = Math.max( this.numRound( _max / 2, 1 ), MIN_POINTER_SIZE )
                break
            case /^small$/i.test( key ):
                _size = Math.max( this.numRound( _max / 4, 1 ), MIN_POINTER_SIZE )
                break
            default:
                _size = Math.max( parseInt( key, 10 ), MIN_POINTER_SIZE )
        }
        
//console.log( '!_getPointerSize:', _props, key, _max, _size )
        return _size
    }
    
    /**
     * Draw the relational lines
     * @private
     * @param {Object} events - 
     */
    _drawRelationLine( events ) {
        let _opts         = this._config,
            _props        = this._instanceProps,
            _canvas       = $(this._element).find( Selector.TIMELINE_RELATION_LINES ),
            ctx_relations = _canvas[0].getContext('2d'),
            drawLine      = ( _sx, _sy, _ex, _ey, evt, _ba ) => {
                let _curveType = {},
                    _radius    = this.numRound( Math.min( _props.scaleSize, _props.rowSize ) / 2, 2 ),
                    _subRadius = this.numRound( this._getPointerSize( evt.size, _opts.marginHeight ) / 2, 2 )
                
                // Defaults
                ctx_relations.strokeStyle = EventParams.bdColor
                ctx_relations.lineWidth   = 2.5
                ctx_relations.filter      = 'url(#crisp)'
                
                for ( let _key of Object.keys( evt.relation ) ) {
                    switch ( true ) {
                        case /^(|line)color$/i.test( _key ):
                            ctx_relations.strokeStyle = evt.relation[_key]
                            break
                        case /^(|line)size$/i.test( _key ):
                            ctx_relations.lineWidth = parseInt( evt.relation[_key], 10 ) || 2.5
                            break
                        case /^curve$/i.test( _key ):
                        if ( /^(r|l)(t|b),?(r|l)?(t|b)?$/i.test( evt.relation[_key] ) ) {
                                let _tmp = evt.relation[_key].split(',')
                                if ( _tmp.length == 2 ) {
                                    _curveType.before = _tmp[0]
                                    _curveType.after  = _tmp[1]
                                } else {
                                    _curveType[_ba] = _tmp[0]
                                }
                            } else
                            if ( ( typeof evt.relation[_key] === 'boolean' && evt.relation[_key] ) || ( typeof evt.relation[_key] === 'number' && Boolean( evt.relation[_key] ) ) ) {
                                // Automatically set the necessary linearity type (:> 自動線形判定
//console.log( _sx, _sy, _ex, _ey, _radius, _ba, _subRadius )
                                if ( _ba === 'before' ) {
                                    // before: targetEvent[ _ex, _ey ] <---- selfEvent[ _sx, _sy ]
                                    if ( _sy > _ey ) {
                                        // 連結点が自分より上にある
                                        if ( _sx > _ex ) {
                                            // 連結点が自分より左にある "(_ex,_ey)└(_sx,_sy)" as "lb"
                                            _curveType[_ba] = 'lb'
                                        } else
                                        if ( _sx < _ex ) {
                                            // 連結点が自分より右にある "⊂ ̄" as "lb+lt"
                                            _curveType[_ba] = 'lb+lt'
                                        } else {
                                            // 連結点が自分の直上 "│" to top
                                            _curveType[_ba] = null
                                        }
                                    } else
                                    if ( _sy < _ey ) {
                                        // 連結点が自分より下にある
                                        if ( _sx > _ex ) {
                                            // 連結点が自分より左にある "(_ex,_ey)┌(_sx,_sy)" as "lt"
                                            _curveType[_ba] = 'lt'
                                        } else
                                        if ( _sx < _ex ) {
                                            // 連結点が自分より右にある "⊂_" as "rt+rb"
                                            _curveType[_ba] = 'lt+lb'
                                        } else {
                                            // 連結点が自分の直下 "│" to bottom
                                            _curveType[_ba] = null
                                        }
                                    } else {
                                        // 連結点が自分と同じ水平上にある(左右どちらか) _sy == _ey; "─" to left or right
                                        _curveType[_ba] = null
                                    }
                                } else
                                if ( _ba === 'after' ) {
                                    // after: selfEvent[ _sx, _sy ] ----> targetEvent[ _ex, _ey ]
                                    if ( _sy < _ey ) {
                                        // Relational endpoint is located "under" self (:> 連結点が自分の下にある
                                        if ( _sx < _ex ) {
                                            // Then relational endpoint is located "right" self (:> 連結点が自分の右にある "(_sx,_sy)┐(_ex,_ey)" as "rt"
                                            _curveType[_ba] = 'rt'
                                        } else
                                        if ( _sx > _ex ) {
                                            // Then relational endpoint is located "left" self (:> 連結点が自分より左にある "_⊃" as "rt+rb"
                                            _curveType[_ba] = 'rt+rb'
                                        } else {
                                            // Relational endpoint is located "just under" self (:> 連結点が自分の直下 "│" to bottom
                                            _curveType[_ba] = null
                                        }
                                    } else
                                    if ( _sy > _ey ) {
                                        // Relational endpoint is located "above" self (:> 連結点が自分より上にある
                                        if ( _sx < _ex ) {
                                            // Then relational endpoint is located "right" self (:> 連結点が自分の右にある "┘" as "rb"
                                            _curveType[_ba] = 'rb'
                                        } else
                                        if ( _sx > _ex ) {
                                            // Then relational endpoint is located "left" self (:> 連結点が自分より左にある " ̄⊃" as "rb+rt"
                                            _curveType[_ba] = 'rb+rt'
                                        } else {
                                            // Relational endpoint is located "just under" self (:> 連結点が自分の直上 "│" to top
                                            _curveType[_ba] = null
                                        }
                                    } else {
                                        // 連結点が自分と同じ水平上にある(左右どちらか) _sy == _ey; "─" to left or right
                                        _curveType[_ba] = null
                                    }
                                }
                            }
                            break
                    }
                }
                if ( Math.abs( _ey - _sy ) > _props.rowSize ) {
                    _ey += Math.floor( Math.abs( _ey - _sy ) / _props.rowSize )
                }
                ctx_relations.beginPath()
                if ( ! this.is_empty( _curveType ) ) {
// console.log( '!_drawLine:', _curveType, _sx, _sy, _ex, _ey, _radius )
                    switch ( true ) {
                        case /^lt$/i.test( _curveType[_ba] ): // "(_ex,_ey)┌(_sx,_sy)"
                            ctx_relations.moveTo( _sx, _sy )
                            if ( Math.abs( _sx - _ex ) > _radius ) {
                                ctx_relations.lineTo( _ex - _radius, _sy ) // "─"
                            }
                            if ( Math.abs( _ey - _sy ) > _radius ) {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy + _radius ) // "┌"
                                ctx_relations.lineTo( _ex, _ey ) // "│"
                            } else {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "┌"
                            }
                            break
                        case /^lb$/i.test( _curveType[_ba] ): // "(_ex,_ey)└(_sx,_sy)"
                            ctx_relations.moveTo( _sx, _sy )
                            if ( Math.abs( _sx - _ex ) > _radius ) {
                                ctx_relations.lineTo( _ex + _radius, _sy ) // "─"
                            }
                            if ( Math.abs( _sy - _ey ) > _radius ) {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy - _radius ) // "└"
                                ctx_relations.lineTo( _ex, _ey ) // "│"
                            } else {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "└"
                            }
                            break
                        case /^rt$/i.test( _curveType[_ba] ): // "(_sx,_sy)┐(_ex,_ey)"
                            ctx_relations.moveTo( _sx, _sy )
                            if ( Math.abs( _ex - _sx ) > _radius ) {
                                ctx_relations.lineTo( _ex - _radius, _sy ) // "─"
                            }
                            if ( Math.abs( _ey - _sy ) > _radius ) {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy + _radius ) // "┐"
                                ctx_relations.lineTo( _ex, _ey )
                            } else {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "┐"
                            }
                            break
                        case /^rb$/i.test( _curveType[_ba] ): // "(_sx,_sy)┘(_ex,_ey)"
                            ctx_relations.moveTo( _sx, _sy )
                            if ( Math.abs( _ex - _sx ) > _radius ) {
                                ctx_relations.lineTo( _ex - _radius, _sy ) // "─"
                            }
                            if ( Math.abs( _sy - _ey ) > _radius ) {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _sy - _radius ) // "┘"
                                ctx_relations.lineTo( _ex, _ey )
                            } else {
                                ctx_relations.quadraticCurveTo( _ex, _sy, _ex, _ey ) // "┘"
                            }
                            break
                        case /^lt\+lb$/i.test( _curveType[_ba] ): // "⊂_"
                        case /^lb\+lt$/i.test( _curveType[_ba] ): // "⊂ ̄"
                            ctx_relations.moveTo( _sx, _sy )
                            //ctx_relations.lineTo( _sx - _subRadius, _sy ) // "─"
                            ctx_relations.lineTo( _sx - _radius, _sy ) // "─"
                            //ctx_relations.bezierCurveTo( _sx - _subRadius - _radius, _sy, _sx - _subRadius - _radius, _ey, _sx - _subRadius, _ey ) // "⊂"
                            ctx_relations.bezierCurveTo( _sx - _radius * 2, _sy, _sx - _radius * 2, _ey, _sx - _radius, _ey ) // "⊂"
                            ctx_relations.lineTo( _ex, _ey ) // "─"
                            break
                        case /^rt\+rb$/i.test( _curveType[_ba] ): // "_⊃"
                        case /^rb\+rt$/i.test( _curveType[_ba] ): // " ̄⊃"
                            ctx_relations.moveTo( _sx, _sy )
                            //ctx_relations.lineTo( _sx + _subRadius, _sy ) // "─"
                            ctx_relations.lineTo( _sx + _radius, _sy ) // "─"
                            //ctx_relations.bezierCurveTo( _sx + _subRadius + _radius, _sy, _sx + _subRadius + _radius, _ey, _sx + _subRadius, _ey ) // "⊃"
                            ctx_relations.bezierCurveTo( _sx + _radius * 2, _sy, _sx + _radius * 2, _ey, _sx + _radius, _ey ) // "⊃"
                            ctx_relations.lineTo( _ex, _ey ) // "─"
                            break
                    }
                } else {
                    ctx_relations.moveTo( _sx, _sy )
                    ctx_relations.lineTo( _ex, _ey )
                }
                //ctx_relations.closePath()
                ctx_relations.stroke()
            }
        
        ctx_relations.clearRect( 0, 0, _canvas[0].width, _canvas[0].height )
//console.log( '!_drawRelationLine:', _props, events, _canvas )
        events.forEach( ( evt ) => {
            let _rel = evt.relation,
                _sx, _sy, _ex, _ey, 
                _targetId, _targetEvent
            
            if ( _rel.hasOwnProperty( 'before' ) ) {
                // before: targetEvent[ _ex, _ey ] <---- selfEvent[ _sx, _sy ]
                // (:> before: 自分を起点( _sx, _sy )として左方向の連結点( _ex, _ey )へ向かう描画方式
                _sx = _rel.x
                _sy = _rel.y
                _targetId = parseInt( _rel.before, 10 )
                if ( _targetId < 0 ) {
                    _ex = 0
                    _ey = _sy
                } else {
                    _targetEvent = events.find( ( _evt ) => parseInt( _evt.eventId, 10 ) == _targetId )
                    if ( ! this.is_empty( _targetEvent ) && _targetEvent.relation ) {
                        _ex = _targetEvent.relation.x < 0 ? 0 : _targetEvent.relation.x
                        _ey = _targetEvent.relation.y
                    }
                }
                if ( _sx >= 0 && _sy >= 0 && _ex >= 0 && _ey >= 0 ) {
                    drawLine( _sx, _sy, _ex, _ey, evt, 'before' )
                }
            }
            if ( _rel.hasOwnProperty( 'after' ) ) {
                // after: selfEvent[ _sx, _sy ] ----> targetEvent[ _ex, _ey ]
                // (:> after: 自分を起点( _sx, _sy )として右方向の連結点( _ex, _ey )へ向かう描画方式
                _sx = _rel.x
                _sy = _rel.y
                _targetId = parseInt( _rel.after, 10 )
                if ( _targetId < 0 ) {
                    _ex = _props.fullwidth
                    _ey = _sy
                } else {
                    _targetEvent = events.find( ( _evt ) => parseInt( _evt.eventId, 10 ) == _targetId )
                    if ( ! this.is_empty( _targetEvent ) && _targetEvent.relation ) {
                        _ex = _targetEvent.relation.x > _props.fullwidth ? _props.fullwidth : _targetEvent.relation.x
                        _ey = _targetEvent.relation.y
                    }
                }
                if ( _sx >= 0 && _sy >= 0 && _ex >= 0 && _ey >= 0 ) {
                    drawLine( _sx, _sy, _ex, _ey, evt, 'after' )
                }
            }
            
        })
        
    }
    
    /**
     * Retrieve the mapping data that placed current events
     * @private
     * @return {number[]}
     */
    _mapPlacedEvents() {
        let _that      = this,
            _tl_events = $(this._element).find( Selector.TIMELINE_EVENTS ).children(),
            _cache     = this._loadToCache(),
            _events    = []
        
        if ( ! this._isCached || this.is_empty( _cache ) ) {
            return _events
        }
        
        _tl_events.each(function() {
            let _uid  = $(this).data( 'uid' ),
                _data = null
            
            if ( _cache ) {
                _data = _cache.find( ( _evt ) => _evt.uid === _uid ) || null
            } else {
                _data = $(this).data()
            }
            
            if ( ! _that.is_empty( _data ) ) {
                _events.push( _data )
            }
        })
//console.log( '!_mapPlacedEvents:', _events )
        
        return _events
    }
    
    /**
     * Event when focus or blur
     * @private
     * @param {Object} event - 
     */
    _activeEvent( event ) {
// console.log( '!_activeEvent:', event )
        let _elem = event.target
        
        if ( 'focusin' === event.type ) {
            $( Selector.TIMELINE_EVENT_NODE ).removeClass( 'active' )
            $(_elem).addClass( 'active' )
        } else
        if ( 'focusout' === event.type ) {
            $(_elem).removeClass( 'active' )
        }
    }
    
    /**
     * Event when hover on the pointer type event
     * @private
     * @param {Object} event - 
     */
    _hoverPointer( event ) {
        let _props = this._instanceProps,
            _elem  = event.target,
            _base  = {
                left  : $(_elem).data( 'baseLeft' ),
                top   : $(_elem).data( 'baseTop' ),
                width : $(_elem).data( 'baseSize' )
            },
            _x     = _base.left,
            _y     = _base.top,
            _w     = _base.width,
            _z     = 5
        
//console.log( '!_hoverPointer:', _props )
        if ( 'mouseenter' === event.type ) {
            _w = Math.max( this.numRound( _w * 1.2, 'ceil' ), Math.min( _props.rowSize, _props.scaleSize ) )
            _x = this.numRound( _x - ( ( _w - _base.width ) / 2 ), 2 )
            _y = this.numRound( _y - ( ( _w - _base.width ) / 2 ), 2 )
            _z = 9
            $(_elem).trigger( Event.FOCUSIN_EVENT )
        } else {
            $(_elem).trigger( Event.FOCUSOUT_EVENT )
        }
        $(_elem).css( 'left', `${_x}px` ).css( 'top', `${_y}px` ).css( 'width', `${_w}px` ).css( 'height', `${_w}px` ).css( 'z-index', _z )
    }

    /**
     * Logger of errors when the method execution
     * @private
     * @param {!string} message - 
     * @param {string} [type='error'] - 
     */
    _error( message, type = 'error' ) {
        if ( message && window.console ) {
            type = window.console[type] ? type : 'error'
            console[type]( message )
        }
    }

    /**
     * Echo the log of plugin for debugging
     * @private
     * @param {string} message - 
     * @param {string} [throwType="Notice"] - 
     */
    _debug( message, throwType = 'Notice' ) {
        if ( ! this._config.debug ) {
            return
        }
        message = this.supplement( null, message )
        if ( message ) {
            let _msg = typeof $(this._element).data( DATA_KEY )[message] !== 'undefined' ? `Called method "${message}".` : message,
                _sty = /^Called method "/.test(_msg) ? 'font-weight:600;color:blue;' : '',
                _rst = ''
            
            if ( window.console && window.console.log ) {
                if ( throwType === 'Notice' ) {
                    window.console.log( '%c%s%c', _sty, _msg, _rst )
                } else {
                    throw new Error( `${_msg}` )
                }
            }
        }
    }
    
    // Public
    
    /**
     * This method is able to call only once after completed an initializing of the plugin
     * @public
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    initialized( ...args ) {
        let _message = this._isInitialized ? 'Skipped because method "initialized" already has been called once' : 'initialized'
        this._debug( _message )
        
        let _elem    = this._element,
            _opts    = this._config,
            _args    = args[0],
            callback = _args.length > 0 && typeof _args[0] === 'function' ? _args[0] : null,
            userdata = _args.length > 1 ? _args.slice(1) : null
        
// console.log( '!initialized:', callback, userdata )
        if ( callback && ! this._isInitialized ) {
            this._debug( 'Fired your callback function after initializing this plugin.' )
            
            callback( _elem, _opts, userdata )
        }
        
        this._isInitialized = true
        
    }
    
    /**
     * Destroy the object to which the plugin is applied
     * @public
     */
    destroy() {
        this._debug( 'destroy' )
        
        $.removeData( this._element, DATA_KEY )
        
        $(window, document, this._element).off( EVENT_KEY )
        
        $(this._element).remove()
        
        this._removeCache()
        
        for ( let _prop of Object.keys( this ) ) {
            this[_prop] = null
            delete this[_prop]
        }
    }
    
    /**
     * @deprecated This method has been deprecated since version 2.0.0
     */
    render() {
        throw new ReferenceError( 'This method named "render" has been deprecated since version 2.0.0' )
    }
    
    /**
     * Show hidden timeline
     * @public
     */
    show() {
        this._debug( 'show' )
        
        let _elem = this._element
        
        if ( ! this._isShown ) {
            $(_elem).removeClass( ClassName.HIDE )
            
            this._isShown = true
        }
    }
    
    /**
     * Hide shown timeline
     * @public
     */
    hide() {
        this._debug( 'hide' )
        
        let _elem = this._element
        
        if ( this._isShown ) {
            $(_elem).addClass( ClassName.HIDE )
            
            this._isShown = false
        }
    }
    
    /**
     * Move shift or expand the range of timeline container as to past direction (to left)
     * @public
     * @param {?Object} options - Options for moving as dateback on the timeline container
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    dateback( ...args ) {
        this._debug( 'dateback' )
        
        let _args    = args[0],
            _opts    = this._config,
            moveOpts = this.supplement( null, _args[0], this.validateObject ),
            callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
            userdata = _args.length > 2 ? _args.slice(2) : null,
            newOpts  = {},
            begin_date, end_date, _tmpDate
        
        if ( this.is_empty( moveOpts ) ) {
            moveOpts = { scale: _opts.scale, range: _opts.range, shift: true }
        } else {
            if ( ! moveOpts.hasOwnProperty('shift') || moveOpts.shift !== false ) {
                moveOpts.shift = true
            }
            if ( ! moveOpts.hasOwnProperty('scale') || ! this._verifyScale( moveOpts.scale ) ) {
                moveOpts.scale = _opts.scale
            }
            if ( ! moveOpts.hasOwnProperty('range') || parseInt( moveOpts.range, 10 ) > LimitScaleGrids[moveOpts.scale] ) {
                moveOpts.range = _opts.range
            }
        }
        _tmpDate   = new Date( _opts.startDatetime )
        begin_date = new Date( _tmpDate.getTime() - ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
        newOpts.startDatetime = begin_date.toString()
        if ( moveOpts.shift ) {
            _tmpDate   = new Date( _opts.endDatetime )
            end_date   = new Date( _tmpDate.getTime() - ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
            newOpts.endDatetime = end_date.toString()
        }
        
        this.reload( [newOpts] )
        
        if ( callback ) {
            this._debug( 'Fired your callback function after datebacking.' )
            
            callback( this._element, _opts, userdata )
        }
    }
    
    /**
     * Move shift or expand the range of timeline container as to futrue direction (to right)
     * @public
     * @param {?Object} options - Options for moving as dateforth on the timeline container
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    dateforth( ...args ) {
        this._debug( 'dateforth' )
        
        let _args        = args[0],
            _opts    = this._config,
            moveOpts = this.supplement( null, _args[0], this.validateObject ),
            callback = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
            userdata = _args.length > 2 ? _args.slice(2) : null,
            newOpts  = {},
            begin_date, end_date, _tmpDate
        
        if ( this.is_empty( moveOpts ) ) {
            moveOpts = { scale: _opts.scale, range: _opts.range, shift: true }
        } else {
            if ( ! moveOpts.hasOwnProperty('shift') || moveOpts.shift !== false ) {
                moveOpts.shift = true
            }
            if ( ! moveOpts.hasOwnProperty('scale') || ! this._verifyScale( moveOpts.scale ) ) {
                moveOpts.scale = _opts.scale
            }
            if ( ! moveOpts.hasOwnProperty('range') || parseInt( moveOpts.range, 10 ) > LimitScaleGrids[moveOpts.scale] ) {
                moveOpts.range = _opts.range
            }
        }
        _tmpDate   = new Date( _opts.endDatetime )
        end_date   = new Date( _tmpDate.getTime() + ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
        newOpts.endDatetime = end_date.toString()
        if ( moveOpts.shift ) {
            _tmpDate   = new Date( _opts.startDatetime )
            begin_date = new Date( _tmpDate.getTime() + ( this._verifyScale( moveOpts.scale ) * parseInt( moveOpts.range, 10 ) ) )
            newOpts.startDatetime = begin_date.toString()
        }
        
        this.reload( [newOpts] )
        
        if ( callback ) {
            this._debug( 'Fired your callback function after dateforthing.' )
            
            callback( this._element, this._config, userdata )
        }
    }
    
    /**
     * Move the display position of the timeline container to the specified position
     * @public
     * @param {?string} position - The preset string of position on timeline you want to align. Allowed values are "left", "begin", "center", "right", "end", "latest", "current", "currently" or number of event id
     * @param {?(number|string)} duration - The duration of alignment animation. Allowed values are "fast", "normal", "slow" or number of milliseconds
     */
    alignment( ...args ) {
        this._debug( 'alignment' )
        
        let _opts         = this._config,
            _props        = this._instanceProps,
            _elem         = this._element,
            _tl_container = $(_elem).find( Selector.TIMELINE_CONTAINER ),
            _movX         = 0,
            _args         = ! this.is_empty( args ) ? args[0] : [],
            position      = _args.length > 0 && typeof _args[0] === 'string' ? _args[0] : _opts.rangeAlign,
            duration      = _args.length > 1 && /^(\d{1,}|fast|normal|slow)$/i.test( _args[1] ) ? _args[1] : 0
        
//console.log( args, _args, position, duration )
        if ( _props.fullwidth <= _elem.scrollWidth ) {
            return
        }
        
        switch ( true ) {
            case /^(left|begin)$/i.test( position ):
                _movX = 0
                break
            case /^center$/i.test( position ):
                _movX = ( _tl_container[0].scrollWidth - _elem.scrollWidth ) / 2 + 1
                break
            case /^(right|end)$/i.test( position ):
                _movX = _tl_container[0].scrollWidth - _elem.scrollWidth + 1
                break
            case /^latest$/i.test( position ): {
                let events    = this._mapPlacedEvents().sort( this.compareValues( 'x' ) ),
                    lastEvent = events[events.length - 1]
                
                _movX = ! this.is_empty( lastEvent ) ? lastEvent.x : 0
                
// console.log( events, lastEvent, _movX, _elem.scrollWidth / 2 )
                // Centering
                if ( _elem.scrollWidth / 2 < _movX ) {
                    _movX -= Math.ceil( _elem.scrollWidth / 2 )
                } else {
                    _movX = 0
                }
                
                // Focus target event
                if ( ! this.is_empty( lastEvent ) ) {
                    $(`${Selector.TIMELINE_EVENT_NODE}[data-uid="${lastEvent.uid}"]`).trigger( Event.FOCUSIN_EVENT )
                }
                break
            }
            case /^\d{1,}$/.test( position ): {
                let events      = this._mapPlacedEvents(),
                    targetEvent = {}
                
                if ( events.length > 0 ) {
                    targetEvent = events.find( ( evt ) => evt.eventId == parseInt( position, 10 ) )
                }
                _movX = ! this.is_empty( targetEvent ) ? targetEvent.x : 0
                
                // Centering
                if ( Math.ceil( _elem.scrollWidth / 2 ) < _movX ) {
                    _movX -= Math.ceil( _elem.scrollWidth / 2 )
                } else {
                    _movX = 0
                }
                
                // Focus target event
                if ( ! this.is_empty( targetEvent ) ) {
                    $(`${Selector.TIMELINE_EVENT_NODE}[data-uid="${targetEvent.uid}"]`).trigger( Event.FOCUSIN_EVENT )
                }
                break
            }
            case /^current(|ly)|now$/i.test( position ):
            default: {
                let _now  = new Date().toString(),
                    _nowX = this.numRound( this._getCoordinateX( _now ), 2 )
                
                if ( _nowX >= 0 ) {
                    if ( _tl_container[0].scrollWidth - _elem.scrollWidth + 1 < _nowX ) {
                        _movX = _tl_container[0].scrollWidth - _elem.scrollWidth + 1
                    } else {
                        _movX = _nowX
                    }
                } else {
                    _movX = 0
                }
                break
            }
        }
//console.log( `!alignment::${position}:`, _props.fullwidth, _props.visibleWidth, _tl_container[0].scrollWidth, _tl_container[0].scrollLeft, _movX )
        if ( duration === '0' ) {
            _tl_container.scrollLeft( _movX )
        } else {
            _tl_container.animate({ scrollLeft: _movX }, duration )
        }
    }
    
    /**
     * @deprecated This method has been deprecated since version 2.0.0
     */
    getOptions() {
        throw new ReferenceError( 'This method named "getOptions" has been deprecated since version 2.0.0' )
    }
    
    /**
     * Add new events to the rendered timeline object
     * @public
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    addEvent( ...args ) {
        this._debug( 'addEvent' )
        
        let _args        = args[0],
            events       = this.supplement( null, _args[0], this.validateArray ),
            callback     = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
            userdata     = _args.length > 2 ? _args.slice(2) : null,
            _cacheEvents = this._loadToCache(),
            lastEventId  = 0,
            add_done     = false
        
        if ( this.is_empty( events ) || ! this._isCompleted ) {
            return
        }
        
        if ( ! this.is_empty( _cacheEvents ) ) {
            _cacheEvents.sort( this.compareValues( 'eventId' ) )
            lastEventId = parseInt( _cacheEvents[_cacheEvents.length - 1].eventId, 10 )
        }
//console.log( '!addEvent::before:', _cacheEvents, lastEventId, callback, userdata )
        
        events.forEach( ( evt ) => {
            let _one_event = this._registerEventData( '<div></div>', evt )
            
            if ( ! this.is_empty( _one_event ) ) {
                _one_event.eventId = Math.max( lastEventId + 1, parseInt( _one_event.eventId, 10 ) )
                _cacheEvents.push( _one_event )
                lastEventId = parseInt( _one_event.eventId, 10 )
                add_done = true
            }
        })
//console.log( '!addEvent::after:', _cacheEvents, lastEventId, callback, userdata )
        if ( ! add_done ) {
            return
        }
        
        this._saveToCache( _cacheEvents )
        
        this._placeEvent()
        
        if ( callback ) {
            this._debug( 'Fired your callback function after replacing events.' )
            
            callback( this._element, this._config, userdata )
        }
    }
    
    /**
     * Remove events from the currently timeline object
     * @public
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    removeEvent( ...args ) {
        this._debug( 'removeEvent' )
        
        let _args        = args[0],
            targets      = this.supplement( null, _args[0], this.validateArray ),
            callback     = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
            userdata     = _args.length > 2 ? _args.slice(2) : null,
            _cacheEvents = this._loadToCache(),
            condition    = {},
            remainEvents = [],
            remove_done  = false
        
        if ( this.is_empty( targets ) || ! this._isCompleted || this.is_empty( _cacheEvents ) ) {
            return
        }
        
        targets.forEach( ( cond ) => {
            switch ( true ) {
                case /^\d{1,}$/.test( cond ):
                    // By matching event ID
                    condition.type  = 'eventId'
                    condition.value = parseInt( cond, 10 )
                    break
                case /^(|\d{1,}(-|\/)\d{1,2}(-|\/)\d{1,2}(|\s\d{1,2}:\d{1,2}(|:\d{1,2})))(|,\d{1,}(-|\/)\d{1,2}(-|\/)\d{1,2}(|\s\d{1,2}:\d{1,2}(|:\d{1,2})))$/.test( cond ): {
                    // By matching range of datetime
                    let _tmp = cond.split(',')
                    
                    condition.type  = 'daterange'
                    condition.value = {}
                    condition.value['from'] = this.is_empty( _tmp[0] ) ? null : new Date( _tmp[0] )
                    condition.value['to']   = this.is_empty( _tmp[1] ) ? null : new Date( _tmp[1] )
                    break
                }
                default:
                    // By matching regex string
                    condition.type  = 'regex'
                    condition.value = new RegExp( cond )
                    break
            }
            _cacheEvents.forEach( ( evt, _idx ) => {
                let is_remove = false
                
                switch ( condition.type ) {
                    case 'eventId': {
                        if ( parseInt( evt.eventId, 10 ) == condition.value ) {
                            is_remove = true
                        }
                        break
                    }
                    case 'daterange': {
                        let _fromX = condition.value.from ? Math.ceil( this._getCoordinateX( condition.value.from.toString() ) ) : 0,
                            _toX   = condition.value.to   ? Math.floor( this._getCoordinateX( condition.value.to.toString() ) ) : _fromX
                        
                        if ( _fromX <= evt.x && evt.x <= _toX ) {
                            is_remove = true
                        }
                        break
                    }
                    case 'regex': {
                        if ( condition.value.test( JSON.stringify( evt ) ) ) {
                            is_remove = true
                        }
                        break
                    }
                }
                if ( ! is_remove ) {
                    remainEvents.push( evt )
                }
            })
        })
        remove_done = remainEvents.length !== _cacheEvents.length
        if ( ! remove_done ) {
            return
        }
        
        this._saveToCache( remainEvents )
        
        this._placeEvent()
        
        if ( callback ) {
            this._debug( 'Fired your callback function after placing additional events.' )
            
            callback( this._element, this._config, userdata )
        }
    }
    
    /**
     * Update events on the currently timeline object
     * @public
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    updateEvent( ...args ) {
        this._debug( 'updateEvent' )
        
        let _args        = args[0],
            events       = this.supplement( null, _args[0], this.validateArray ),
            callback     = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
            userdata     = _args.length > 2 ? _args.slice(2) : null,
            _cacheEvents = this._loadToCache(),
            update_done  = false
        
        if ( this.is_empty( events ) || ! this._isCompleted || this.is_empty( _cacheEvents ) ) {
            return
        }
        
        events.forEach( ( evt ) => {
            let _upc_event = this._registerEventData( '<div></div>', evt ), // Update Candidate
                _old_index = null,
                _old_event = _cacheEvents.find( ( _evt, _idx ) => {
                    _old_index = _idx
                    return _evt.eventId == _upc_event.eventId
                }),
                _new_event = {}
            
            if ( ! this.is_empty( _old_event ) && ! this.is_empty( _upc_event ) ) {
                if ( _upc_event.hasOwnProperty( 'uid' ) ) {
                    delete _upc_event.uid
                }
                _new_event = Object.assign( _new_event, _old_event, _upc_event )
//console.log( _new_event, _old_event, _upc_event, _old_index )
                _cacheEvents[_old_index] = _new_event
                update_done = true
            }
        })
        
        if ( ! update_done ) {
            return
        }
        
        this._saveToCache( _cacheEvents )
        
        this._placeEvent()
        
        if ( callback ) {
            this._debug( 'Fired your callback function after updating events.' )
            
            callback( this._element, this._config, userdata )
        }
    }
    
    /**
     * Reload the timeline with overridable any options
     * @public
     * @param {?Function()} callback - Custom callback fired after calling this method
     * @param {?(number|string|Object)} userdata - Data as object of referable in that callback
     */
    reload( ...args ) {
        this._debug( 'reload' )
        
        let _args        = args[0],
            _upc_options = this.supplement( null, _args[0], this.validateObject ),
            callback     = _args.length > 1 && typeof _args[1] === 'function' ? _args[1] : null,
            userdata     = _args.length > 2 ? _args.slice(2) : null,
            _elem        = this._element,
            $default_evt = $(_elem).find( Selector.DEFAULT_EVENTS ),
            _old_options = this._config,
            _new_options = {}
        
        if ( ! this.is_empty( _upc_options ) ) {
            // _new_options = Object.assign( _new_options, _old_options, _upc_options )
            _new_options = this.mergeDeep( _old_options, _upc_options )
            this._config = _new_options
        }
        
        this._isInitialized = false
        this._isCached      = false
        this._isCompleted   = false
        this._instanceProps = {}
        
        $(_elem).empty().append( $default_evt )
        
        this._calcVars()
        
        if ( ! this._verifyMaxRenderableRange() ) {
            throw new RangeError( `Timeline display period exceeds maximum renderable range.` )
        }
        
        if ( ! this._isInitialized ) {
            this._renderView()
            this._isInitialized = true
        }
        
        if ( this._config.reloadCacheKeep ) {
            let _cacheEvents = this._loadToCache(),
                _renewEvents = []
            
            if ( ! this.is_empty( _cacheEvents ) ) {
                _cacheEvents.forEach( ( evt ) => {
                    delete evt.uid
                    delete evt.x
                    delete evt.Y
                    delete evt.width
                    delete evt.height
                    delete evt.relation.x
                    delete evt.relation.y
                    _renewEvents.push( this._registerEventData( '<div></div>', evt ) )
                })
            }
            this._isCached = this._saveToCache( _renewEvents )
        } else {
            this._loadEvent()
        }
        
        this._placeEvent()
        
        this._isCompleted = true
        
        if ( callback ) {
            this._debug( 'Fired your callback function after reloading timeline.' )
            
            callback( this._element, this._config, userdata )
        }
    }
    
    /**
     * The method that fires when an event on the timeline is clicked
     * Note: You can hook the custom processing with the callback specified in the event parameter
     * @public
     * @param {Object} event - 
     */
    openEvent( event ) {
        this._debug( 'openEvent' )
        
        let _that     = this,
            _self     = event.target,
            $viewer   = $(document).find( Selector.EVENT_VIEW ),
            //eventId   = parseInt( $(_self).attr( 'id' ).replace( 'evt-', '' ), 10 ),
            uid       = $(_self).data( 'uid' ),
            //meta      = this.supplement( null, $(_self).data( 'meta' ) ),
            callback  = this.supplement( null, $(_self).data( 'callback' ) )
//console.log( '!openEvent:', _self, $viewer, eventId, uid, meta, callback )
        
        if ( $viewer.length > 0 ) {
            $viewer.each(function() {
                let _cacheEvents = _that._loadToCache(),
                    _eventData   = _cacheEvents.find( ( event ) => event.uid === uid ),
                    _label       = $('<div></div>', { class: ClassName.VIEWER_EVENT_TITLE }),
                    _content     = $('<div></div>', { class: ClassName.VIEWER_EVENT_CONTENT }),
                    _meta        = $('<div></div>', { class: ClassName.VIEWER_EVENT_META }),
                    _image       = $('<div></div>', { class: ClassName.VIEWER_EVENT_IMAGE_WRAPPER })
                
//console.log( '!openEvent:', $(this), $(_self).html(), _eventData.label )
                
                $(this).empty() // Initialize Viewer
                if ( ! _that.is_empty( _eventData.image ) ) {
                    _image.append( `<img src="${_eventData.image}" class="${ClassName.VIEWER_EVENT_IMAGE}" />` )
                    $(this).append( _image )
                }
                if ( ! _that.is_empty( _eventData.label ) ) {
                    _label.html( _eventData.label )
                    $(this).append( _label )
                }
                if ( ! _that.is_empty( _eventData.content ) ) {
                    _content.html( _eventData.content )
                    $(this).append( _content )
                }
                if ( ! _that.is_empty( _eventData.rangeMeta ) ) {
                    _meta.html( _eventData.rangeMeta )
                    $(this).append( _meta )
                }
                
            })
        }
        
        if ( callback ) {
            this._debug( `The callback "${callback}" was called by the "openEvent" method.` )
            
            try {
                Function.call( null, `return ${callback}` )()
            } catch ( e ) {
                throw new TypeError( e )
            }
        }
    }
    
    /**
     * Be zoomed in scale of the timeline that fires when any scales on the ruler is double clicked
     * @public
     * @param {Object} event - 
     */
    zoomScale( event ) {
        this._debug( 'zoomScale' )
        
        let _elem        = event.target,
            ruler_item   = $(_elem).data( 'ruler-item' ),
            scaleMap     = {
                millennium : { years: 1000, lower: 'century', minGrids: 10 },
                century    : { years: 100,  lower: 'decade',  minGrids: 10 },
                decade     : { years: 10,   lower: 'lustrum', minGrids: 2  },
                lustrum    : { years: 5,    lower: 'year',    minGrids: 5  },
                year       : { years: 1,    lower: 'month',   minGrids: 12 },
                month      : {              lower: 'day',     minGrids: 28 },
                week       : {              lower: 'day',     minGrids: 7  },
                day        : {              lower: 'hour',    minGrids: 24 },
                weekday    : {              lower: 'hour',    minGrids: 24 },
                hour       : {              lower: 'minute',  minGrids: 60 },
                minute     : {              lower: 'second',  minGrids: 60 },
                second     : {              lower: null,      minGrids: 60 }
            },
            getZoomScale = ( ruler_item ) => {
                let [ scale, date_seed ] = ruler_item.split('-'),
                    min_grids            = scaleMap[scale].minGrids,
                    begin_date, end_date, base_year, base_month, week_num, base_day, is_remapping, _tmpDate
                
                switch ( true ) {
                    case /^millennium$/i.test( scale ):
                    case /^century$/i.test( scale ):
                    case /^decade$/i.test( scale ):
                    case /^lustrum$/i.test( scale ):
                        begin_date = `${( ( date_seed - 1 ) * scaleMap[scale].years ) + 1}/1/1`
                        _tmpDate   = new Date( begin_date, 0, 1 ).setFullYear( date_seed * scaleMap[scale].years + 1 )
                        _tmpDate   = new Date( _tmpDate - 1 )
                        end_date   = `${_tmpDate.getFullYear()}/${_tmpDate.getMonth()+1}/${_tmpDate.getDate()} 23:59:59`
                        break
                    case /^year$/i.test( scale ):
                        begin_date = `${date_seed}/1/1`
                        _tmpDate   = new Date( date_seed, 0, 1 ).setFullYear( parseInt( date_seed, 10 ) + 1 )
                        _tmpDate   = new Date( _tmpDate - 1 )
                        end_date   = `${_tmpDate.getFullYear()}/${_tmpDate.getMonth()+1}/${_tmpDate.getDate()} 23:59:59`
                        break
                    case /^month$/i.test( scale ):
                        [ base_year, base_month ] = date_seed.split('/')
                        is_remapping = parseInt( base_year, 10 ) < 100
                        begin_date = new Date( base_year, parseInt( base_month, 10 ) - 1, 1 )
                        if ( begin_date.getMonth() == 11 ) {
                            _tmpDate = new Date( begin_date.getFullYear() + 1, 0, 1 ).setFullYear( parseInt( base_year, 10 ) + 1 )
                        } else {
                            _tmpDate = new Date( begin_date.getFullYear(), begin_date.getMonth() + 1, 1 ).setFullYear( parseInt( base_year, 10 ) )
                        }
                        begin_date = begin_date.toString()
                        end_date   = new Date( _tmpDate - 1 ).toString()
                        break
                    case /^week$/i.test( scale ):
                        [ base_year, week_num ] = date_seed.split(',')
                        is_remapping = parseInt( base_year, 10 ) < 100
                        _tmpDate = new Date( base_year, 0, 1 )
                        if ( is_remapping ) {
                            _tmpDate.setFullYear( base_year )
                        }
                        begin_date = new Date( _tmpDate.getTime() + ( week_num * 7 * 24 * 60 * 60 * 1000 ) ).toString()
                        end_date   = new Date( new Date( begin_date ).getTime() + ( 7 * 24 * 60 * 60 * 1000 ) - 1 ).toString()
                        break
                    case /^day$/i.test( scale ):
                    case /^weekday$/i.test( scale ):
                        if ( 'weekday' === scale ) {
                            let _tmp = date_seed.split(',')
                            date_seed = _tmp[0]
                        }
                        [ base_year, base_month, base_day ] = date_seed.split('/')
                        is_remapping = parseInt( base_year, 10 ) < 100
                        _tmpDate = new Date( base_year, parseInt( base_month, 10 ) - 1, base_day )
                        begin_date = _tmpDate.toString()
                        end_date   = new Date( _tmpDate.getTime() + ( 24 * 60 * 60 * 1000 ) - 1 ).toString()
//console.log( date_seed, base_year, week_num, begin_date, _tmpDate, new Date( _tmpDate ), new Date( _tmpDate - 1 ) )
                        break
                    case /^hour$/i.test( scale ):
                    case /^minute$/i.test( scale ):
                        begin_date = `${date_seed}:00`
                        end_date   = `${date_seed}:59`
                        break
                    default:
                        begin_date = null
                        end_date   = null
                        break
                }
                
                scale = scaleMap.hasOwnProperty( scale ) ? scaleMap[scale].lower : scale
                return [ scale, begin_date, end_date, min_grids ]
            },
            [ to_scale, begin_date, end_date, min_grids ] = getZoomScale( ruler_item ),
            zoom_options = {
                startDatetime : begin_date,
                endDatetime   : end_date,
                scale         : to_scale,
            }
        
        if ( this.is_empty( zoom_options.scale ) ) {
            return
        }
        if ( this._config.wrapScale ) {
            let _wrap = Math.ceil( ( $(this._element).find(Selector.TIMELINE_CONTAINER).width() - $(this._element).find(Selector.TIMELINE_SIDEBAR).width() ) / min_grids ),
                _originMinGridSize
            
            if ( ! this._config.hasOwnProperty( 'originMinGridSize' ) ) {
                // Keep an original minGridSize as cache
                this._config.originMinGridSize = this._config.minGridSize
            }
            _originMinGridSize = this._config.originMinGridSize
            zoom_options.minGridSize = Math.max( _wrap, _originMinGridSize )
        }
// console.log( ruler_item, zoom_options, this._config.wrapScale, this._config.minGridSize )
        
        this.reload( [zoom_options] )
        
    }
    
    /**
     * Show the loader
     * @public
     */
    showLoader() {
        this._debug( 'showLoader' )
        
        let _elem      = this._element,
            _opts      = this._config,
            _container = $(_elem).find( Selector.TIMELINE_CONTAINER ),
            width      = _container.length > 0 ? _container.width() : $(_elem).width(),
            height     = ( _container.length > 0 ? _container.height() : $(_elem).height() ) || 120,
            _loader    = $('<div></div>', { id: 'jqtl-loader', style: `width:${width}px;height:${height}px;` })
        
//console.log( '!showLoader:', width, height, _container.length )
        if ( _opts.loader === false ) {
            return
        }
        
        if ( $(_opts.loader).length == 0 ) {
            let _loading_text = LOADING_MESSAGE.match(/[\uD800-\uDBFF][\uDC00-\uDFFF]|[\s\S]|^$/g).filter( Boolean )
            
            _loading_text.forEach( ( str, idx ) => {
                let _fountain_text = $('<div></div>', { id: `jqtl-loading_${( idx + 1 )}`, class: ClassName.LOADER_ITEM }).text( str )
                _loader.append( _fountain_text )
            })
        } else {
            let _custom_loader = $(_opts.loader).clone().prop( 'hidden', false ).css( 'display', 'block' )
            _loader.append( _custom_loader )
        }
        
        if ( $(_elem).find( Selector.LOADER ).length == 0 ) {
            if ( _container.length > 0 ) {
                _container.append( _loader )
            } else {
                $(_elem).css( 'position', 'relative' ).css( 'min-height', `${height}px` ).append( _loader )
            }
        }
    }
    
    /**
     * Hide the loader
     * @public
     */
    hideLoader() {
        this._debug( 'hideLoader' )
        
        $(this._element).find( Selector.LOADER ).remove()
    }
    
    
    /* ----------------------------------------------------------------------------------------------------------------
     * Utility Api
     * ----------------------------------------------------------------------------------------------------------------
     */
    
    /**
     * Determine empty that like PHP
     * @param {!(number|string|Object|number[]|boolean)} value - Variable you want to check
     * @return {boolean}
     */
    is_empty( value ) {
        if ( value == null ) {
            // typeof null -> object : for hack a bug of ECMAScript
            // Refer: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof
            return true
        }
        switch ( typeof value ) {
            case 'object':
                if ( Array.isArray( value ) ) {
                    // When object is array:
                    return ( value.length === 0 )
                } else {
                    // When object is not array:
                    if ( Object.keys( value ).length > 0 || Object.getOwnPropertySymbols( value ).length > 0 ) {
                        return false
                    } else
                    if ( value.valueOf().length !== undefined ) {
                        return ( value.valueOf().length === 0 )
                    } else
                    if ( typeof value.valueOf() !== 'object' ) {
                        return this.is_empty( value.valueOf() )
                    } else {
                        return true
                    }
                }
            case 'string':
                return ( value === '' )
            case 'number':
                return ( value == 0 )
            case 'boolean':
                return ! value
            case 'undefined':
            case 'null':
                return true
            case 'symbol': // Since ECMAScript6
            case 'function':
            default:
                return false
        }
    }
    
    /**
     * Determine whether variable is an Object
     * @param {!(number|string|Object|boolean)} item - Variable you want to check
     * @return {boolean}
     */
    is_Object( item ) {
        return (item && typeof item === 'object' && ! Array.isArray( item ))
    }
    
    /**
     * Merge two objects deeply as polyfill for instead "$.extend(true,target,source)"
     * @param {!Object} target - 
     * @param {!Object} source - 
     * @return {Object}
     */
    mergeDeep( target, source ) {
        let output = Object.assign( {}, target )
        
        if ( this.is_Object( target ) && this.is_Object( source ) ) {
            for ( const key of Object.keys( source ) ) {
                if ( this.is_Object( source[key] ) ) {
                    if ( ! ( key in target ) ) {
                        Object.assign( output, { [key]: source[key] } )
                    } else {
                        output[key] = this.mergeDeep( target[key], source[key] )
                    }
                } else {
                    Object.assign( output, { [key]: source[key] } )
                }
            }
        }
        return output
    }
    
    /**
     * Determine whether the object is iterable
     * @param {!Object} obj - 
     * @return {boolean}
     */
    is_iterable( obj ) {
        return obj && typeof obj[Symbol.iterator] === 'function'
    }
    
    /**
     * Add an @@iterator method to non-iterable object
     * @param {!Object} obj - 
     * @return {Object}
     */
    toIterableObject( obj ) {
        if ( this.is_iterable( obj ) ) {
            return obj
        }
        
        obj[Symbol.iterator] = () => {
            let index = 0
            
            return {
                next() {
                    if ( obj.length <= index ) {
                        return { done: true }
                    } else {
                        return { value: obj[index++] }
                    }
                }
            }
        }
        
        return obj
    }
    
    /**
     * Await until next process at specific millisec
     * @param {number} [msec=1] - Millisecond
     */
    sleep( msec = 1 ) {
        return new Promise( ( resolve ) => {
            setTimeout( resolve, msec )
        })
    }
    
    /**
     * Supplemental method for validating arguments in local scope
     * @param {!(number|string|Object|boolean)} default_value - 
     * @param {?(number|string|Object|boolean)} opt_arg - 
     * @param {?(number|string|Object|boolean)} opt_callback - function or string of function to call
     * @return {number|string|Object|boolean}
     */
    supplement( default_value, opt_arg, opt_callback ) {
        if ( opt_arg === undefined ) {
            return default_value
        }
        if ( opt_callback === undefined ) {
            return opt_arg
        }
        return opt_callback( default_value, opt_arg )
    }
    
    /**
     * Generate the pluggable unique id
     * @param {number} [digit=1000] - 
     * @return {string}
     */
    generateUniqueID( digit = 1000 ) {
        return new Date().getTime().toString(16) + Math.floor( digit * Math.random() ).toString(16)
    }
    
    /**
     * Round a number with specific digit
     * @param {!number} number - 
     * @param {?number} digit - Defaults to 0
     * @param {string} [round_type="round"] - 
     * @return {number}
     */
    numRound( number, digit, round_type = 'round' ) {
        digit  = this.supplement( 0, digit, this.validateNumeric )
        let _pow = Math.pow( 10, digit )
        
       switch ( true ) {
            case /^ceil$/i.test( round_type ):
                return Math.ceil( number * _pow ) / _pow
            case /^floor$/i.test( round_type ):
                return Math.floor( number * _pow ) / _pow
            case /^round$/i.test( round_type ):
            default:
                return Math.round( number * _pow ) / _pow
        }
    }
    
    /**
     * Convert hex of color code to rgba
     * @param {!string} hex - 
     * @param {number} [alpha=1] - 
     * @return {string}
     */
    hexToRgbA( hex, alpha = 1 ) {
        let _c
        
        if ( /^#([A-Fa-f0-9]{3}){1,2}$/.test( hex ) ) {
            _c = hex.substring(1).split('')
            if ( _c.length == 3 ) {
                _c= [ _c[0], _c[0], _c[1], _c[1], _c[2], _c[2] ]
            }
            _c = `0x${_c.join('')}`
            return `rgba(${[ (_c >> 16) & 255, (_c >> 8) & 255, _c & 255 ].join(',')},${alpha})`
        }
        // throw new Error( 'Bad Hex' )
        return hex
    }
    
    /**
     * Get the correct datetime with remapping to that if the year is 0 - 99
     * @param {!string} datetime_str - 
     * @return {?Object} - Date Object, or null if failed
     */
    getCorrectDatetime( datetime_str ) {
        let normalizeDate = ( dateString ) => {
                // For Safari, IE
                let _d = dateString.replace(/-/g, '/')
                return /^\d{1,4}\/\d{1,2}$/.test( _d ) ? `${_d}/1` : _d
            },
            getDateObject = ( datetime_str ) => {
                let _chk_str = normalizeDate( datetime_str ),
                    _ymd, _his, _parts, _date
                
                switch ( true ) {
                    case /^\d{1,2}(|\/\d{1,2}(|\/\d{1,2}))(| \d{1,2}(|:\d{1,2}(|:\d{1,2})))$/i.test( _chk_str ): {
                        [ _ymd, _his ] = _chk_str.split(' ')
                        _parts = _ymd.split('/')
                        if ( _parts[1] ) {
                            _parts[1] = parseInt( _parts[1], 10 ) - 1 // to month index
                        }
                        if ( _his ) {
                            _parts.push( ..._his.split(':') )
                        }
                        _date = new Date( new Date( ..._parts ).setFullYear( parseInt( _parts[0], 10 ) ) )
                        break
                    }
                    case /^\d+$/.test( _chk_str ):
                        _date = new Date( 1, 0, 1 ).setFullYear( parseInt( _chk_str, 10 ) )
                        break
                    default:
                        _date = new Date( _chk_str.toString() )
                        break
                } 
                return _date
            },
            _checkDate = getDateObject( datetime_str )
        
        if ( isNaN( _checkDate ) || this.is_empty( _checkDate ) ) {
            console.warn( `"${datetime_str}" Cannot parse date because invalid format.` )
            return null
        }
        /*
        let _tempDate = new Date( normalizeDate( datetime_str ) ),
            _chk_date = datetime_str.split( /-|\// )
        
        if ( parseInt( _chk_date[0], 10 ) < 100 ) {
            // Remapping if year is 0-99
            _tempDate.setFullYear( parseInt( _chk_date[0], 10 ) )
        }
        
        return _tempDate
        */
        if ( typeof _checkDate !== 'object' ) {
            _checkDate = new Date( _checkDate )
        }
        return _checkDate
    }
    
    /**
     * Method to get week number as extension of Date object
     * @param {!string} date_str - 
     * @return {number}
     */
    getWeek( date_str ) {
        let targetDate, _str, _onejan,
            _millisecInDay = 24 * 60 * 60 * 1000
        
        if ( /^\d{1,4}(|\/\d{1,2}(|\/\d{1,2}))$/.test( date_str ) ) {
            _str = date_str.split('/')
            if ( ! this.is_empty( _str[1] ) ) {
                _str[1] = parseInt( _str[1], 10 ) - 1 // To month index
            }
            targetDate = new Date( ..._str )
        } else {
            targetDate = new Date( date_str )
        }
        _onejan = new Date( targetDate.getFullYear(), 0, 1 )
        return Math.ceil( ( ( ( targetDate - _onejan ) / _millisecInDay ) + _onejan.getDay() + 1 ) / 7 )
    }
    
    /**
     * Retrieve a first day of the week from week number (Note: added support for daylight savings time but needs improvement as performance has dropped)
     * @param {!number} week_number - 
     * @param {!number} year - defaults to current year
     * @return {object|boolean}
     */
    getFirstDayOfWeek( week_number, year ) {
        if ( this.is_empty( week_number ) ) {
            return false
        }
        year = this.is_empty( year ) ? new Date().getFullYear() : parseInt( year, 10 )
        let firstDayIndex  = this._config.firstDayOfWeek,
            firstDayOfYear = this.getCorrectDatetime( `${year}/1/1` ),
            _weekday       = firstDayOfYear.getDay(),
            _keyDayOfWeek  = firstDayOfYear,
            _offset        = _weekday > firstDayIndex ? _weekday - firstDayIndex : 0,
            _weekNumber    = _offset <= 0 ? 0 : 1,
            hitDate
        if ( _weekNumber == week_number && _weekday == firstDayIndex ) {
            hitDate = firstDayOfYear
        } else {
            for ( let i = _offset; i < _offset + 7; i++ ) {
                if ( i > _offset ) {
                    _keyDayOfWeek = this.modifyDate( firstDayOfYear, i, 'day' )
                }
                if ( _keyDayOfWeek.getDay() == firstDayIndex ) {
                    _weekNumber++
                    break
                }
            }
            if ( _weekNumber == week_number ) {
                hitDate = _keyDayOfWeek
            } else {
                hitDate = this.modifyDate( _keyDayOfWeek, (week_number - _weekNumber) * 7, 'day' )
            }
        }
        return hitDate
    }

    /**
     * Get the datetime shifted from the specified datetime by any fluctuation value
     * @param {!object} datetime - to be date object filtered by getCorrectDatetime method
     * @param {!number} fluctuation - an interval value to shift from given base datetime
     * @param {!string} scale  - the scale of an interval value
     * @return {object|boolean}
     */
    modifyDate( datetime, fluctuation, scale ) {
        if ( this.is_empty( datetime ) || this.is_empty( fluctuation ) || this.is_empty( scale ) || ! this.verifyScale( scale ) ) {
            return false
        }
        let baseDate = this.getCorrectDatetime( datetime ),
            flct     = this.validateNumeric( 0, fluctuation ),
            dateElms = [
                baseDate.getFullYear(),    // 0: year
                baseDate.getMonth(),       // 1: month (index)
                baseDate.getDate(),        // 2: day
                baseDate.getHours(),       // 3: hour
                baseDate.getMinutes(),     // 4: minute
                baseDate.getSeconds(),     // 5: second
                baseDate.getMilliseconds() // 6: millisec
            ],
            tmpDate  = new Date( new Date( ...dateElms ).setFullYear( dateElms[0] ) ),
            isAdjust = false,
            newDate

        switch ( true ) {
            case /^millenniums?|millennia$/i.test( scale ):
                newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 1000) ) )
                break
            case /^century$/i.test( scale ):
                newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 100) ) )
                break
            case /^dec(ade|ennium)$/i.test( scale ):
                newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 10) ) )
                break
            case /^lustrum$/i.test( scale ):
                newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + (flct * 5) ) )
                break
            case /^years?$/i.test( scale ):
                newDate = new Date( tmpDate.setFullYear( tmpDate.getFullYear() + flct ) )
                break
            case /^months?$/i.test( scale ):
                newDate = new Date( tmpDate.setMonth( tmpDate.getMonth() + flct ) )
                break
            case /^weeks?$/i.test( scale ):
                newDate = new Date( tmpDate.setDate( tmpDate.getDate() + (flct * 7) ) )
                newDate.setHours( dateElms[3] )
                newDate.setMinutes( dateElms[4] )
                newDate.setSeconds( dateElms[5] )
                newDate.setMilliseconds( dateElms[6] )
                break
            case /^(|week)days?$/i.test( scale ):
                newDate = new Date( tmpDate.setDate( tmpDate.getDate() + flct ) )
                newDate.setHours( dateElms[3] )
                newDate.setMinutes( dateElms[4] )
                newDate.setSeconds( dateElms[5] )
                newDate.setMilliseconds( dateElms[6] )
                break
            case /^hours?$/i.test( scale ):
                newDate = new Date( tmpDate.setTime( tmpDate.getTime() + ( flct * 60 * 60 * 1000 ) ) )
                newDate.setMinutes( dateElms[4] )
                newDate.setSeconds( dateElms[5] )
                newDate.setMilliseconds( dateElms[6] )
                break
            case /^minutes?$/i.test( scale ):
                newDate = new Date( tmpDate.setTime( tmpDate.getTime() + ( flct * 60 * 1000 ) ) )
                newDate.setSeconds( dateElms[5] )
                newDate.setMilliseconds( dateElms[6] )
                break
            case /^seconds?$/i.test( scale ):
                newDate = new Date( tmpDate.setTime( tmpDate.getTime() + ( flct * 1000 ) ) )
                newDate.setMilliseconds( dateElms[6] )
                break
            default:
                newDate = new Date( tmpDate.setTime( tmpDate.getTime() + flct ) )
                break
        }

        if ( isAdjust ) {
            // Why different time of 1 min 15 sec on 12/01/1847, 0:00:00? (GMT+0001)
            let divide = this.getCorrectDatetime( '1847/12/1 0:01:15' )

            if ( baseDate.getTime() < divide.getTime() && newDate.getTime() >= divide.getTime() ) {
                newDate = new Date( newDate.setTime( newDate.getTime() - (60 * 1000) ) )
            } else
            if ( baseDate.getTime() > divide.getTime() && newDate.getTime() <= divide.getTime() ) {
                newDate = new Date( newDate.setTime( newDate.getTime() - (75 * 1000) ) )
            }
        }
        return newDate
    }

    /**
     * Acquire the difference between two dates with the specified scale value
     * @param {!(number|object)} date1 - integer as milliseconds or object instanceof Date)
     * @param {!(number|object)} date2 - integer as milliseconds or object instanceof Date)
     * @param {string} [scale='millisecond'] - defaults to 'millisecond'
     * @param {boolean} [absval=false] - defaults to false
     * @return {object|boolean}
     */
    diffDate( date1, date2, scale = 'millisecond', absval = false ) {
        let _dt1   = date1 === undefined ? null : date1,
            _dt2   = date2 === undefined ? null : date2,
            diffMS = 0,
            retval = false,
            lastDayOfMonth = ( dateObj ) => {
                let _tmp = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )
                _tmp.setTime( _tmp.getTime() - 1 )
                return _tmp.getDate()
            },
            isLeapYear = ( dateObj ) => {
                let _tmp = new Date( dateObj.getFullYear(), 0, 1 ),
                    sum  = 0

                for ( let i = 0; i < 12; i++ ) {
                    _tmp.setMonth(i)
                    sum += lastDayOfMonth( _tmp )
                }
                return sum == 365 ? false : true
            }

        if ( ! _dt1 || ! _dt2 ) {
            //console.warn( 'Cannot parse date to get difference because undefined.' )
            this._error( 'Cannot parse date to get difference because undefined.', 'warn' )
            return false
        }

        diffMS = _dt2 - _dt1

        if ( isNaN( diffMS ) ) {
            //console.warn( 'Cannot parse date to get difference because invalid format.' )
            this._error( 'Cannot parse date to get difference because invalid format.', 'warn' )
            return false
        }
        if ( absval ) {
            diffMS = Math.abs( diffMS )
        }

        let _bd = _dt1 instanceof Date ? _dt1 : new Date( _dt1 ),
            _ed = _dt2 instanceof Date ? _dt2 : new Date( _dt2 ),
            _dy = _ed.getFullYear() - _bd.getFullYear(),
            _m  = {}

        switch ( true ) {
            case /^millenniums?|millennia$/i.test( scale ): {
                // return { "millennium-number": years,... }
                let _by = _bd.getFullYear(),
                    _ey = _ed.getFullYear(),
                    _bm = Math.ceil( (_by == 0 ? 1 : _by) / 1000 ), // millennium of first ordinal
                    _em = Math.ceil( (_ey == 0 ? 1 : _ey) / 1000 ),
                    _cm = _bm

                _m[_bm] = _em - _bm > 0 ? (_bm * 1000) - _by : _ey - _by
                _cm++
                while ( _cm <= _em ) {
                    _m[_cm] = _em - _cm > 0 ? 1000 : _ey - ((_cm - 1) * 1000)
                    _cm++
                }
                retval = _m
                // return number of milliseconds
                // retval = diffMS
                break
            }
            case /^century$/i.test( scale ): {
                // return { "century-number": years,... }
                let _by = _bd.getFullYear(),
                    _ey = _ed.getFullYear(),
                    _bc = Math.ceil( (_by == 0 ? 1 : _by) / 100 ), // century of first ordinal
                    _ec = Math.ceil( (_ey == 0 ? 1 : _ey) / 100 ),
                    _cc = _bc

                _m[_bc] = _ec - _bc > 0 ? (_bc * 100) - _by : _ey - _by
                _cc++
                while ( _cc <= _ec ) {
                    _m[_cc] = _ec - _cc > 0 ? 100 : _ey - ((_cc - 1) * 100)
                    _cc++
                }
                retval = _m
                // return number of milliseconds
                // retval = diffMS
                break
            }
            case /^dec(ade|ennium)$/i.test( scale ): {
                // return { "decade-number": days,... }
                let _by = _bd.getFullYear(),
                    _ey = _ed.getFullYear(),
                    _cy = _by == 0 ? 1 : _by,
                    _cd, _days

                while ( _cy <= _ey ) {
                    _days = isLeapYear( new Date( _cy, 0, 1 ) ) ? 366 : 365
                    _cd = Math.ceil( _cy / 10 ) // decade of first ordinal
                    if ( Object.hasOwnProperty.call( _m, _cd ) ) {
                        _m[_cd] += _days
                    } else {
                        _m[_cd] = _days
                    }
                    _cy++
                }
                retval = _m
                // return number of milliseconds
                // retval = diffMS
                break
            }
            case /^lustrum$/i.test( scale ): {
                // return { "lustrum-number": days,... }
                let _by = _bd.getFullYear(),
                    _ey = _ed.getFullYear(),
                    _cy = _by == 0 ? 1 : _by,
                    _cl, _days

                while ( _cy <= _ey ) {
                    _days = isLeapYear( new Date( _cy, 0, 1 ) ) ? 366 : 365
                    _cl = Math.ceil( _cy / 5 ) // lustrum of first ordinal
                    if ( Object.hasOwnProperty.call( _m, _cl ) ) {
                        _m[_cl] += _days
                    } else {
                        _m[_cl] = _days
                    }
                    _cy++
                }
                retval = _m
                // return number of milliseconds
                // retval = diffMS
                break
            }
            case /^years?$/i.test( scale ):
                // return { "year": days,... }
                if ( _dy > 0 ) {
                    for ( let i = 0; i <= _dy; i++ ) {
                        let _cd = new Date( _bd.getFullYear() + i, 0, 1 )
                        _m[`${_bd.getFullYear() + i}`] = isLeapYear( _cd ) ? 366 : 365
                    }
                } else {
                    _m[`${_bd.getFullYear()}`] = isLeapYear( _bd ) ? 366 : 365
                }
                retval = _m
                break
            case /^months?$/i.test( scale ):
                // return { "year/month": days,... }
                if ( _dy > 0 ) {
                    for ( let i = _bd.getMonth(); i < 12; i++ ) {
                        let _cd = new Date( _bd.getFullYear(), i, 1 )
                        _m[`${_bd.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
                    }
                    if ( _dy > 1 ) {
                        for ( let y = 1; y < _dy; y++ ) {
                            for ( let i = 0; i < 12; i++ ) {
                                let _cd = new Date( _bd.getFullYear() + y, i, 1 )
                                _m[`${_bd.getFullYear() + y}/${i + 1}`] = lastDayOfMonth( _cd )
                            }
                        }
                    }
                    for ( let i = 0; i <= _ed.getMonth(); i++ ) {
                        let _cd = new Date( _ed.getFullYear(), i, 1 )
                        _m[`${_ed.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
                    }
                } else {
                    for ( let i = _bd.getMonth(); i <= _ed.getMonth(); i++ ) {
                        let _cd = new Date( _bd.getFullYear(), i, 1 )
                        _m[`${_bd.getFullYear()}/${i + 1}`] = lastDayOfMonth( _cd )
                    }
                }
                retval = _m
                break
            case /^weeks?$/i.test( scale ): {
                // return { "year,week": hours,... }
                let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate() ),
                    _cw = this.getWeek( _cd ),
                    _nd = new Date( _cd ),
                    _pd = new Date( _cd ),
                    _newWeek = `${_cd.getFullYear()},${_cw}`

                _nd.setDate( _nd.getDate() + 1 )
                _pd.setDate( _pd.getDate() - 1 )
                _m[_newWeek] = ( _cd - _pd ) / ( 60 * 60 * 1000 ) // hours of first day
                while ( _nd.getTime() <= _ed.getTime() ) {
                    _nd.setDate( _nd.getDate() + 1 )
                    _cd.setDate( _cd.getDate() + 1 )
                    _cw = this.getWeek( _cd )
                    let _newWeekKey = `${_cd.getFullYear()},${_cw}`

                    if ( Object.hasOwnProperty.call( _m, _newWeekKey ) ) {
                        _m[_newWeekKey] += ( _nd - _cd ) / ( 60 * 60 * 1000 )
                    } else {
                        _m[_newWeekKey] = ( _nd - _cd ) / ( 60 * 60 * 1000 )
                    }
                }
                retval = _m
                break
            }
            case /^(|week)days?$/i.test( scale ): {
                // return { "year/month/day": hours,... }
                let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate() ),
                    _nd = new Date( _cd ),
                    _pd = new Date( _cd )

                _nd.setDate( _nd.getDate() + 1 )
                _pd.setDate( _pd.getDate() - 1 )
                _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()}`] = ( _cd - _pd ) / ( 60 * 60 * 1000 )
                while ( _nd.getTime() <= _ed.getTime() ) {
                    _nd.setDate( _nd.getDate() + 1 )
                    _cd.setDate( _cd.getDate() + 1 )
                    _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()}`] = ( _nd - _cd ) / ( 60 * 60 * 1000 )
                }
                retval = _m
                break
            }
            case /^hours?$/i.test( scale ): {
                // return { "year/month/day hour": minutes,... }
                let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate(), _bd.getHours() ),
                    _nd = new Date( _cd ),
                    _pd = new Date( _cd )

                _nd.setHours( _nd.getHours() + 1 )
                _pd.setHours( _pd.getHours() - 1 )
                _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}`] = ( _cd - _pd ) / ( 60 * 1000 )
                while ( _nd.getTime() <= _ed.getTime() ) {
                    _nd.setHours( _nd.getHours() + 1 )
                    _cd.setHours( _cd.getHours() + 1 )
                    _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}`] = ( _nd - _cd ) / ( 60 * 1000 )
                }
                retval = _m
                break
            }
            case /^minutes?$/i.test( scale ): {
                // return { "year/month/day hour:minute": seconds,... }
                let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate(), _bd.getHours(), _bd.getMinutes() ),
                    _nd = new Date( _cd ),
                    _pd = new Date( _cd )

                _nd.setMinutes( _nd.getMinutes() + 1 )
                _pd.setMinutes( _pd.getMinutes() - 1 )
                _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}`] = ( _cd - _pd ) / 1000
                while ( _nd.getTime() <= _ed.getTime() ) {
                    _nd.setMinutes( _nd.getMinutes() + 1 )
                    _cd.setMinutes( _cd.getMinutes() + 1 )
                    _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}`] = ( _nd - _cd ) / 1000
                }
                retval = _m
                break
            }
            case /^seconds?$/i.test( scale ): {
                // return { "year/month/day hour:minute:second": milliseconds,... }
                let _cd = new Date( _bd.getFullYear(), _bd.getMonth(), _bd.getDate(), _bd.getHours(), _bd.getMinutes(), _bd.getSeconds() ),
                    _nd = new Date( _cd ),
                    _pd = new Date( _cd )

                _nd.setSeconds( _nd.getSeconds() + 1 )
                _pd.setSeconds( _pd.getSeconds() - 1 )
                _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}:${_cd.getSeconds()}`] = _cd - _pd
                while ( _nd.getTime() <= _ed.getTime() ) {
                    _nd.setSeconds( _nd.getSeconds() + 1 )
                    _cd.setSeconds( _cd.getSeconds() + 1 )
                    _m[`${_cd.getFullYear()}/${(_cd.getMonth() + 1)}/${_cd.getDate()} ${_cd.getHours()}:${_cd.getMinutes()}:${_cd.getSeconds()}`] = _nd - _cd
                }
                retval = _m
                break
            }
            default:
                // return number of milliseconds
                retval = diffMS
                break
        }

        return retval
    }

    /**
     * Verify whether is allowed scale in the plugin. Then retrieves that values of intervals on the scale if the scale is available and given arguments of date range. And return the base millisecond of scale if it is not the variable length scale (isVLS to false)
     * @param {!string} scale - 
     * @param {number} [begin=null] - begin of range as unit millisecs that got by `Date.getTime()`
     * @param {number} [end=null] - end of range as unit millisecs that got by `Date.getTime()`
     * @param {boolean} [isVLS=false] - whether is variable length scale, defaults to false
     * @return {object|boolean} boolean if no arguments are given after the first argument
     */
    verifyScale( scale, begin = null, end = null, isVLS = false ) {
        let _ms    = -1,
            isBool = this.is_empty( begin ) || this.is_empty( end ),
            retval = isVLS ? this.diffDate( begin, end, scale ) : false

        if ( typeof scale === 'undefined' || typeof scale !== 'string' ) {
            return false
        }
        switch ( true ) {
            case /^millisec(|ond)s?$/i.test( scale ):
                // Millisecond (:> ミリ秒
                _ms = 1
                break
            case /^seconds?$/i.test( scale ):
                // Second (:> 秒
                _ms = 1000
                break
            case /^minutes?$/i.test( scale ):
                // Minute (:> 分
                _ms = 60 * 1000
                break
            case /^quarter-?(|hour)$/i.test( scale ):
                // Quarter of an hour (:> 15分
                _ms = 15 * 60 * 1000
                break
            case /^half-?(|hour)$/i.test( scale ):
                // Half an hour (:> 30分
                _ms = 30 * 60 * 1000
                break
            case /^hours?$/i.test( scale ):
                // Hour (:> 時(時間)
                _ms = 60 * 60 * 1000
                break
            case /^(|week)days?$/i.test( scale ):
                // Day (is the variable length scale by DST) (:> 日 (サマータイムによる可変長スケール)
                _ms = 24 * 60 * 60 * 1000
                break
            case /^weeks?$/i.test( scale ):
                // Week (is the variable length scale by DST) (:> 週 (サマータイムによる可変長スケール)
                _ms = 7 * 24 * 60 * 60 * 1000
                break
            case /^months?$/i.test( scale ):
                // Month (is the variable length scale) (:> 月(可変長スケール)
                _ms = 30.44 * 24 * 60 * 60 * 1000
                break
            case /^years?$/i.test( scale ):
                // Year (is the variable length scale) (:> 年(可変長スケール)
                _ms = 365.25 * 24 * 60 * 60 * 1000
                break
            case /^lustrum$/i.test( scale ):
                // Lustrum (is the variable length scale, but currently does not support) (:> 五年紀 (可変長スケールだが現在サポートしてない)
                // 5y = 1826 or 1827; 1826 * 24 * 60 * 60 = 15766400, 1827 * 24 * 60 * 60 = 157852800 | avg.= 157788000
                //_ms = ( ( 3.1536 * Math.pow( 10, 8 ) ) / 2 ) * 1000 // <--- Useless by info of wikipedia
                _ms = 157788000 * 1000
                break
            case /^dec(ade|ennium)$/i.test( scale ):
                // Decade (is the variable length scale, but currently does not support) (:> 十年紀 (可変長スケールだが現在サポートしてない)
                // 10y = 3652 or 3653; 3652 * 24 * 60 * 60 = 315532800, 3653 * 24 * 60 * 60 = 157852800 | avg. = 315576000
                // _ms = ( 3.1536 * Math.pow( 10, 8 ) ) * 1000 // <--- Useless by info of wikipedia
                _ms = 315576000 * 1000
                break
            case /^century$/i.test( scale ):
                // Century (:> 世紀(百年紀)
                // 100y = 36525; 36525 * 24 * 60 * 60 = 3155760000
                _ms = 3155760000 * 1000
                break
            case /^millenniums?|millennia$/i.test( scale ):
                // Millennium (:> 千年紀
                // 100y = 365250
                //_ms = ( 3.1536 * Math.pow( 10, 10 ) ) * 1000
                _ms = 3155760000 * 10 * 1000
                break
            default:
                //console.warn( `Specified an invalid "${scale}" scale.` )
                this._error( `Specified an invalid "${scale}" scale.`, 'warn' )
                _ms = -1
        }
        if ( isBool ) {
            return _ms > 0
        } else {
            return isVLS ? retval : _ms
        }
    }

    /**
     * Retrieve one higher scale
     * @param {!string} scale - 
     * @return {string} String of higher scale
     */
    getHigherScale( scale ) {
        return this.findScale( scale, 'higher' )
    }

    /**
     * Retrieve one lower scale
     * @param {!string} scale - 
     * @return {string} String of lower scale
     */
    getLowerScale( scale ) {
        return this.findScale( scale, 'lower' )
    }

    /**
     * Find scale matched the specified condition
     * @param {!string} base_scale - 
     * @param {!string} condition - 
     * @return {string|object} matched scale(s)
     */
    findScale( base_scale, condition ) {
        let scalePatternMap = [
                [ 'millisecond', '^millisec(|ond)s?$' ],
                [ 'second', '^seconds?$' ],
                [ 'minute', '^minutes?$' ],
                [ 'hour', '^(|half|quarter)-?(|hour)s?$' ],
                [ 'day', '^(|week)days?$' ],
                [ 'week', '^weeks?$' ],
                [ 'month', '^months?$' ],
                [ 'year', '^years?$' ],
                [ 'lustrum', '^lustrum$' ],
                [ 'decade', '^dec(ade|ennium)$' ],
                [ 'century', '^century$' ],
                [ 'millennium', '^millenniums?|millennia$' ],
            ],
            _idx = scalePatternMap.findIndex( ( elm ) => new RegExp( `${elm[1]}`, 'i' ).test( base_scale ) ),
            _narrows

        switch ( true ) {
            case /^higher$/i.test( condition ):
                _idx = scalePatternMap[(_idx + 1)] ? _idx + 1 : _idx
                return scalePatternMap[_idx][0]
            case /^higher\s?all$/i.test( condition ):
                _narrows = scalePatternMap.slice( _idx + 1 )
                _narrows = _narrows.reduce( ( acc, cur ) => acc.concat( cur[0] ), [] )
                if ( _narrows.includes( 'day' ) ) {
                    _narrows.push( 'weekday' )
                }
                return _narrows
            case /^lower$/i.test( condition ):
                _idx = scalePatternMap[(_idx - 1)] ? _idx - 1 : _idx
                return scalePatternMap[_idx][0]
            case /^lower\s?all$/i.test( condition ):
                _narrows = scalePatternMap.slice( 0, _idx )
                _narrows = _narrows.reduce( ( acc, cur ) => acc.concat( cur[0] ), [] )
                if ( _narrows.includes( 'day' ) ) {
                    _narrows.push( 'weekday' )
                }
                return _narrows
            default:
                return scalePatternMap[_idx][0]
        }
    }

    /**
     * Retrieve the date string of specified locale
     * @param {!string} date_seed - 
     * @param {string} [scale=""] - 
     * @param {string} [locales="en-US"] - 
     * @param {Object} [options={}] - 
     * @return {string} Locale string
     */
    getLocaleString( date_seed, scale = '', locales = 'en-US', options = {} ) {
        function toLocaleStringSupportsLocales() {
            try {
                new Date().toLocaleString( 'i' )
            } catch ( e ) {
                return e.name === "RangeError";
            }
            return false;
        }
        let is_toLocalString = toLocaleStringSupportsLocales(),
            locale_string = '',
            _options = {},
            getOrdinal = ( n ) => {
                let s = [ 'th', 'st', 'nd', 'rd' ], v = n % 100
                return n + ( s[(v - 20)%10] || s[v] || s[0] )
            },
            getZerofill = ( num, digit = 4 ) => {
                let strDuplicate = ( n, str ) => Array( n + 1 ).join( str ),
                    zero = strDuplicate( digit - num.length, '0' )
                
                return String( num ).length == digit ? String( num ) : ( zero + num ).substr( num * -1 )
            },
            parseDatetime = ( date_str ) => {
                let [ _ymd, _his ] = date_str.split(' '),
                    _parts         = []
                
                if ( /^\d{1,4}\/\d{1,2}\/\d{1,2}$/.test( _ymd ) ) {
                    _str = _ymd.split('/')
                    _parts.push( ..._str )
                }
                if ( /^\d{1,2}(|:\d{1,2}(|:\d{1,2}))$/.test( _his ) ) {
                    _str = _his.split(':')
                    _parts.push( ..._str )
                }
                if ( _parts.length > 0 ) {
                    return new Date( ..._parts )
                } else {
                    return new Date( date_str )
                }
            },
            _prop, _temp, _str, _num
        
        for ( _prop in options ) {
            if ( _prop === 'timeZone' || _prop === 'hour12' ) {
                _options[_prop] = options[_prop]
            }
        }
        
        switch ( true ) {
            case /^millenniums?|millennia$/i.test( scale ):
            case /^century$/i.test( scale ):
            case /^dec(ade|ennium)$/i.test( scale ):
            case /^lustrum$/i.test( scale ):
                if ( options.hasOwnProperty( scale ) && options[scale] === 'ordinal' ) {
                    locale_string = getOrdinal( date_seed )
                } else {
                    locale_string = date_seed
                }
                break
            case /^years?$/i.test( scale ):
                if ( is_toLocalString && options.hasOwnProperty( scale ) ) {
                    if ( [ 'numeric', '2-digit' ].includes( options[scale] ) ) {
                        _options.year = options[scale]
                        locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                    } else
                    if ( 'zerofill' === options[scale] ) {
                        locale_string = getZerofill( date_seed )
                    }
                }
                locale_string = this.is_empty( locale_string ) ? this.getCorrectDatetime( date_seed ).getFullYear() : locale_string
                break
            case /^months?$/i.test( scale ):
                if ( is_toLocalString && options.hasOwnProperty( scale ) ) {
                    if ( [ 'numeric', '2-digit', 'narrow', 'short', 'long' ].includes( options[scale] ) ) {
                        _options.month = options[scale]
                        locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                        //locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                    }
                }
                if ( this.is_empty( locale_string ) || isNaN( locale_string ) ) {
                    if ( /^\d{1,2}\/\d{1,2}(|\/\d{1,2})$/.test( date_seed ) ) {
                        _str = date_seed.split('/')
                        _temp = new Date( _str[0], parseInt( _str[1] - 1 ), 1 )
                        locale_string = _temp.toLocaleString( locales, _options )
                    }
                }
                break
            case /^weeks?$/i.test( scale ):
                [ _str, _num ] = date_seed.split(',')
                if ( options.hasOwnProperty( scale ) && options[scale] === 'ordinal' ) {
                    locale_string = getOrdinal( parseInt( _num, 10 ) )
                } else {
                    locale_string = _num
                }
                break
            case /^weekdays?$/i.test( scale ):
                [ _str, _num ] = date_seed.split(',')
                if ( /^\d{1,2}(|\/\d{1,2}(|\/\d{1,2}))$/.test( _str ) ) {
                    _str = _str.split('/')
                    _temp = new Date( ..._str )
                } else {
                    _temp = new Date( _str )
                }
                if ( is_toLocalString ) {
                    _options.weekday = options.hasOwnProperty('weekday') ? options.weekday : 'narrow'
                    locale_string = _temp.toLocaleString( locales, _options )
                    //locale_string = this.getCorrectDatetime( _temp[0] ).toLocaleString( locales, _options )
                } else {
                    let _weekday = [ 'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat' ]
                    locale_string = _weekday[parseInt( _num, 10 )]
                }
                break
            case /^days?$/i.test( scale ):
                if ( /^\d{1,2}(|\/\d{1,2}(|\/\d{1,2}))$/.test( date_seed ) ) {
                    _str = date_seed.split('/')
                    _temp = new Date( ..._str )
                } else {
                    _temp = new Date( date_seed )
                }
                if ( is_toLocalString ) {
                    _options.day = options.hasOwnProperty('day') ? options.day : 'numeric'
                    locales = options.hasOwnProperty('day') ? locales : 'en-US'
                    locale_string = _temp.toLocaleString( locales, _options )
                    //locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                } else {
                    locale_string = _temp.getDate()
                    //locale_string = this.getCorrectDatetime( date_seed ).getDate()
                }
                break
            case /^hours?$/i.test( scale ):
            case /^(half|quarter)-?hours?$/i.test( scale ):
                _temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
                if ( is_toLocalString ) {
                    _options.hour = options.hasOwnProperty('hour') ? options.hour : 'numeric'
                    if ( options.hasOwnProperty('minute') ) {
                        _options.minute = options.hasOwnProperty('minute') ? options.minute : 'numeric'
                    }
                    locale_string = _temp.toLocaleString( locales, _options )
                    //locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                } else {
                    locale_string = _temp.getHours()
                    //locale_string = this.getCorrectDatetime( date_seed ).getHours()
                }
                break
            case /^minutes?$/i.test( scale ):
                _temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
                if ( is_toLocalString ) {
                    _options.minute = options.hasOwnProperty('minute') ? options.minute : 'numeric'
                    if ( options.hasOwnProperty('hour') ) {
                        _options.hour   = options.hasOwnProperty('hour') ? options.hour : 'numeric'
                    }
                    locale_string = _temp.toLocaleString( locales, _options )
                    //locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                } else {
                    locale_string = _temp.getMinutes()
                    //locale_string = this.getCorrectDatetime( date_seed ).getMinutes()
                }
                break
            case /^seconds?$/i.test( scale ):
                _temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
                if ( is_toLocalString ) {
                    _options.second = options.hasOwnProperty('second') ? options.second : 'numeric'
                    if ( options.hasOwnProperty('hour') ) {
                        _options.hour   = options.hasOwnProperty('hour') ? options.hour : 'numeric'
                    }
                    if ( options.hasOwnProperty('minute') ) {
                        _options.minute = options.hasOwnProperty('minute') ? options.minute : 'numeric'
                    }
                    locale_string = _temp.toLocaleString( locales, _options )
                    //locale_string = this.getCorrectDatetime( date_seed ).toLocaleString( locales, _options )
                } else {
                    locale_string = _temp.getSeconds()
                    //locale_string = this.getCorrectDatetime( date_seed ).getSeconds()
                }
                break
            case /^millisec(|ond)s?$/i.test( scale ):
            default:
                _temp = typeof date_seed === 'string' ? parseDatetime( date_seed ) : new Date( date_seed )
                locale_string = _temp.toString()
                //locale_string = this.getCorrectDatetime( date_seed )
                break
        }
        return locale_string
    }

    /**
     * Convert the date-time to custom formatting strings, as like ruby
     * @param {!(number|object)} baseDate - should be a Date object
     * @param {string} [format=''] - 
     * @param {string} [locales='en-US'] - 
     * @return {string}
     */
    datetimeFormat( baseDate, format = '', locales = 'en-US' ) {
        // let _baseDt = Object.prototype.toString.call( baseDate ) === '[object Date]' ? baseDate : this.getCorrectDatetime( baseDate ),
        let _baseDt = baseDate instanceof Date ? baseDate : this.getCorrectDatetime( baseDate ),
            _fmt    = format.toString().split(''),
            _ptn    = 'YyZmBbdwWAaIHMSj'.split(''),
            _cnvStr = '',
            lastDayOfMonth = ( dateObj ) => {
                let _tmp = new Date( dateObj.getFullYear(), dateObj.getMonth() + 1, 1 )

                _tmp.setTime( _tmp.getTime() - 1 )
                return _tmp.getDate()
            }

        if ( this.is_empty( _fmt ) ) {
            return _baseDt.toString()
        }
        _fmt.forEach( ( _str, _i, _orig ) => {
            let _match  = false,
                _repStr = ''

            if ( _ptn.includes( _str ) && ! this.is_empty( _orig[_i - 1] ) && _orig[_i - 1] === '%' ) {
                _match = this.is_empty( _orig[_i - 2] ) || _orig[_i - 2] !== '\\'
            }
            if ( _match ) {
                switch ( _str ) {
                    case 'Y':
                    case 'y':
                    case 'Z': {
                        // year
                        let _year = _baseDt.getFullYear()

                        if ( _str === 'Z' ) {
                            _repStr = _year < 10 ? `000${_year}` : _year < 100 ? `00${_year}` : _year < 1000 ? `0${_year}` : _year
                        } else {
                            _repStr = _str === 'Y' ? _year : _year.toString().slice(-2)
                        }
                        break
                    }
                    case 'm':
                    case 'B':
                    case 'b': {
                        // month
                        if ( _str === 'm' ) {
                            let _month = _baseDt.getMonth() + 1

                            _repStr = _month < 10 ? `0${_month}` : _month
                        } else {
                            let _opts = { month: _str === 'B' ? 'long' : 'short' }

                            _repStr = _baseDt.toLocaleDateString( locales, _opts )
                        }
                        break
                    }
                    case 'd': {
                        // day
                        let _day = _baseDt.getDate()

                        _repStr = _day < 10 ? `0${_day}` : _day
                        break
                    }
                    case 'w':
                    case 'A':
                    case 'a': {
                        // weekday
                        if ( _str === 'w' ) {
                            let _wday = _baseDt.getDay()

                            _repStr = _wday
                        } else {
                            let _opts = { weekday: _str === 'A' ? 'long' : 'short' }

                            _repStr = _baseDt.toLocaleDateString( locales, _opts )
                        }
                        break
                    }
                    case 'W': {
                        // week
                        _repStr = this.getWeek( _baseDt )
                        break
                    }
                    case 'I':
                    case 'H': {
                        // hour
                        let _opts = { hour12: _str === 'I', hour: 'numeric' }

                        _repStr = _baseDt.toLocaleTimeString( locales, _opts )
                        break
                    }
                    case 'M': {
                        // minute
                        _repStr = _baseDt.toLocaleTimeString( locales, { minute: 'numeric' } )
                        break
                    }
                    case 'S': {
                        // second
                        _repStr = _baseDt.toLocaleTimeString( locales, { second: 'numeric' } )
                        break
                    }
                    case 'j': {
                        // day of year
                        let _fdy   = new Date( _baseDt.getFullYear(), 0, 1 ),
                            _month = _baseDt.getMonth(),
                            _days  = 0, _m

                        for ( _m = 0; _m < _month; _m++ ) {
                            _fdy.setMonth( _m )
                            _days += lastDayOfMonth( _fdy )
                        }
                        _repStr = _days + _baseDt.getDate()
                        _repStr = _repStr < 10 ? `00${_repStr}` : _repStr < 100 ? `0${_repStr}` : _repStr
                        break
                    }
                }
                _cnvStr = _cnvStr.substring(0, _cnvStr.length - 1) + _repStr.toString()
            } else {
                _cnvStr += _str
            }
        }, _cnvStr )
        _cnvStr = _cnvStr.toString().replace( /\\/g, '' )
        return _cnvStr
    }

    /**
     * Get the rendering width of the given string
     * @param {!string} str - 
     * @return {number}
     */
    strWidth( str ) {
        let _str_ruler = $( '<span id="jqtl-str-ruler"></span>' ),
            _width     = 0
        if ( $('#jqtl-str-ruler').length == 0 ) {
            $('body').append( _str_ruler )
        }
        _width = $('#jqtl-str-ruler').text( str ).get(0).offsetWidth
        $('#jqtl-str-ruler').empty()
        return _width
    }
    
    /**
     * Sort an array by value of specific property (Note: destructive method)
     * @example
     * Object.sort( this.compareValues( property, order ) )
     *
     * @param {!string} property - To compare a property of object
     * @param {string} [order="asc"] - Order to sort
     * @return {number} Comparison index
     */
    compareValues( property, order = 'asc' ) {
        return ( a, b ) => {
            if ( ! a.hasOwnProperty( property ) || ! b.hasOwnProperty( property ) ) {
                return 0
            }
            
            const varA = typeof a[property] === 'string' ? a[property].toUpperCase() : a[property]
            const varB = typeof b[property] === 'string' ? b[property].toUpperCase() : b[property]
            
            let comparison = 0
            
            if ( varA > varB ) {
                comparison = 1
            } else
            if ( varA < varB ) {
                comparison = -1
            }
            return order === 'desc' ? comparison * -1 : comparison
        }
    }

    /**
     * Getter argument as user data
     * @since v2.1.0
     * @param {!object} userdata - 
     * @return {object}
     */
    getUserArg( userdata ) {
        //console.log( '!_getUserArg:', userdata, typeof userdata, typeof userdata[0], this.is_Object( userdata[0] ) )
        switch( typeof userdata[0] ) {
            case 'string':
            case 'number':
                userdata = [ userdata[0] ]
                break
            case 'object':
                if ( this.is_Object( userdata[0] ) ) {
                    // Object
                    if ( this.is_empty( userdata[0] ) ) {
                        userdata = {}
                    } else {
                        userdata = this.mergeDeep( {}, userdata[0] )
                    }
                } else {
                    // Array
                    if ( this.is_empty( userdata[0] ) ) {
                        userdata = []
                    } else {
                        userdata = userdata[0]
                    }
                }
                break
            default:
                userdata = userdata[0]
                break
        }
        return userdata
    }

    /**
     * Apply custom theme styles
     * @since v2.1.0
     * @return {void}
     */
    applyThemeStyle() {
        let theme    = this._config.colorScheme.theme,
            selector = this._selector,
            styleId  = `${PREFIX}-theme-${selector.replace(/[.#_]/g, '-')}`,
            styleTag = $('<style></style>', { id: styleId }),
            _is      = {},
            _os      = {},
            cssText  = ''
        
        if ( $(`style#${styleId}`).length > 0 ) {
            $(`style#${styleId}`).remove()
        }
        if ( 'default' === theme.name ) {
            return
        }

        _is[Selector.TIMELINE_CONTAINER] = `border:solid 1px ${theme.offline}; background:${theme.background}`
        _is[Selector.HEADLINE_TITLE] = `color:${theme.text}`
        _is[Selector.RANGE_META] = `color:${theme.subtext}`
        _is[Selector.TIMELINE_RULER_TOP] = `outline:solid 1px ${theme.offline}`
        _is[Selector.TIMELINE_RULER_BOTTOM] = `outline:solid 1px ${theme.offline}`
        _is[`${Selector.TIMELINE_RULER_LINES}:nth-child(even)`] = `background-color:${this.hexToRgbA(theme.striped1, 0.25)}`
        _is[Selector.TIMELINE_RULER_ITEM] = `color:${theme.subtext}`
        _is[`${Selector.TIMELINE_RULER_ITEM}:nth-child(even)`] = `background-color:${this.hexToRgbA(theme.striped2, 0.25)}`
        _is[Selector.TIMELINE_EVENT_CONTAINER] = `outline:solid 1px ${theme.offline}`
        _is[`${Selector.TIMELINE_EVENT_NODE}:not(.jqtl-event-type-pointer).active`] = `color:${theme.background};background-color:${theme.active}`
        _is[`${Selector.TIMELINE_EVENT_NODE}:hover`] = `color:${theme.background};background-color:${theme.active}`
        _is[`${Selector.TIMELINE_EVENT_NODE}:hover::after`] = `background-color:${this.hexToRgbA(theme.invertbg, 0.1)}`
        _is[`${Selector.TIMELINE_EVENT_NODE}::before`] = `color:${theme.modesttext}`
        _is[`${Selector.TIMELINE_EVENT_NODE}${Selector.VIEWER_EVENT_TYPE_POINTER}`] = `border:solid 3px ${theme.line}`
        _is[`${Selector.TIMELINE_EVENT_NODE}${Selector.VIEWER_EVENT_TYPE_POINTER}.active`] = `border-color:${theme.activeline}`
        _is[`${Selector.TIMELINE_EVENT_NODE}${Selector.VIEWER_EVENT_TYPE_POINTER}:hover`] = `border-color:${theme.activeline}`
        _is[Selector.TIMELINE_SIDEBAR] = `outline:solid 1px ${theme.offline}`
        _is[`${Selector.TIMELINE_SIDEBAR}> [class^="jqtl-side-index-"]`] = `border-bottom:dotted 1px ${theme.offline};background-color:${theme.background};color:${theme.text}`
        _is[`${Selector.TIMELINE_SIDEBAR} ${Selector.TIMELINE_SIDEBAR_ITEM}:nth-child(odd)`] = `background-color:${theme.striped1}`
        _is[`${Selector.TIMELINE_SIDEBAR} ${Selector.TIMELINE_SIDEBAR_ITEM}:first-child`] = `border-top:solid 1px ${theme.offline}`
        _is[Selector.TIMELINE_SIDEBAR_MARGIN] = `outline:solid 1px ${theme.offline}`
        _is[`${Selector.TIMELINE_SIDEBAR_MARGIN}:first-child`] = `border-bottom:solid 1px ${theme.offline}`
        _is[`${Selector.TIMELINE_SIDEBAR_MARGIN}:last-child`] = `border-top:solid 1px ${theme.offline}`
        _is[Selector.OVERLAY] = `background-color:${this.hexToRgbA(theme.background, 0.65)} !important`
        _is[`${Selector.OVERLAY}:nth-child(odd)`] = `background-color:${this.hexToRgbA(theme.striped1, 0.45)} !important`
        _os[`${Selector.VIEWER_EVENT_TITLE},${Selector.VIEWER_EVENT_CONTENT}`] = `color:${theme.text}`
        _os[`${Selector.VIEWER_EVENT_TITLE}> .event-content`] = `color:${theme.offtext}`
        _os[Selector.VIEWER_EVENT_META] = `color:${theme.offtext}`
        _is[Selector.PRESENT_TIME_MARKER] = `border-left:dotted 1px ${theme.marker}`
        _is[`${Selector.PRESENT_TIME_MARKER}::before,${Selector.PRESENT_TIME_MARKER}::after`] = `background-color:${theme.marker}`
        _is[`${Selector.LOADER_ITEM} span`] = `background:${this.hexToRgbA(theme.text, 0.15)}`
        _os['@keyframes loader'] = `0%{background:${this.hexToRgbA(theme.text, 0.15)}}25%{background:${this.hexToRgbA(theme.text, 0.15)}}50%{background:${this.hexToRgbA(theme.text, 0.15)}}100%{background:${this.hexToRgbA(theme.text, 0.15)}}`

        for ( let _prop of Object.keys( _is ) ) {
            cssText += `${selector} ${_prop}{${_is[_prop]}}`
        }
        for ( let _prop of Object.keys( _os ) ) {
            cssText += `${_prop}{${_os[_prop]}}`
        }
        $('head').append( styleTag.text( cssText ) )
    }

    /**
     * Validator for string
     * @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
     * @param {!(number|string|Object|boolean)} val - Value to validate
     * @return {number|string|Object|boolean}
     */
    validateString( def, val ) {
        return typeof val === 'string' && val !== '' ? val : def
    }
    /**
     * Validator for numeric
     * @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
     * @param {!(number|string|Object|boolean)} val - Value to validate
     * @return {number|string|Object|boolean}
     */
    validateNumeric( def, val ) {
        return typeof val === 'number' ? Number( val ) : def
    }
    /**
     * Validator for boolean
     * @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
     * @param {!(number|string|Object|boolean)} val - Value to validate
     * @return {number|string|Object|boolean}
     */
    validateBoolean( def, val ) {
        return typeof val === 'boolean' || ( typeof val === 'object' && val !== null && typeof val.valueOf() === 'boolean' ) ? val : def
    }
    /**
     * Validator for object
     * @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
     * @param {!(number|string|Object|boolean)} val - Value to validate
     * @return {number|string|Object|boolean}
     */
    validateObject( def, val ) {
        return typeof val === 'object' ? val : def
    }
    /**
     * Validator for array
     * @param {!(number|string|Object|boolean)} def - Define instead this value as default if validation failure
     * @param {!(number|string|Object|boolean)} val - Value to validate
     * @return {number|string|Object|boolean}
     */
    validateArray( def, val ) {
        return Object.prototype.toString.call( val ) === '[object Array]' ? val : def
    }
    
    
    // Static
    
    /**
     * Interface for jQuery
     * @interface
     * @param {?(string|Object)} config - The object of plugin options or string of public method
     * @param {?(...string|...Function())} args - Arguments for public method
     */
    static _jQueryInterface( config, ...args ) {
        return this.each(function () {
            let data = $(this).data( DATA_KEY )
            const _config = {
                ...Default,
                ...$(this).data(),
                ...typeof config === 'object' && config ? config : {}
            }
            
            if ( ! data ) {
                // Apply the plugin and store the instance in data
                data = new Timeline( this, _config )
                $(this).data( DATA_KEY, data )
            }
            
            if ( typeof config === 'string' && config.charAt(0) != '_' ) {
                if ( typeof data[config] === 'undefined' ) {
                    // Call no method
                    throw new ReferenceError( `No method named "${config}"` )
                }
                // Call public method
                data[config]( args )
            } else {
                if ( ! data._isInitialized ) {
                    data._init()
                }
            }
        })
    }
    
} // class end


/* ----------------------------------------------------------------------------------------------------------------
 * For jQuery
 * ----------------------------------------------------------------------------------------------------------------
 */
$.fn[NAME] = Timeline._jQueryInterface
$.fn[NAME].Constructor = Timeline
$.fn[NAME].noConflict = () => {
    $.fn[NAME] = JQUERY_NO_CONFLICT
    return Timeline._jQueryInterface
}

/* ----------------------------------------------------------------------------------------------------------------
 * For ESDoc
 * ----------------------------------------------------------------------------------------------------------------
 */
export {
    /*
    NAME,
    VERSION,
    DATA_KEY,
    EVENT_KEY,
    PREFIX,
    LOADING_MESSAGE,
    MIN_POINTER_SIZE,
    JQUERY_NO_CONFLICT,
    */
    Default,
    LimitScaleGrids,
    EventParams,
    /*
    Event,
    ClassName,
    Selector,
    */
    Timeline
}