import { Box , ComponentDrawInfo , DateBy , GridConfig , TimelineData , } from './types' ; import Component from './common/Component' ; import Event from './Event' ; import Axis from './Axis' ; import { DATE_COUNT_EXTRA , SN , SN_VERSION } from './common/definitions' ; import AxisScale from './Axis/AxisScale' ; import AxisMilestone from './Axis/AxisMilestone' ; import { Breakpoint } from './extensions/BreakpointAnimation' ; import TimeSpliter from './common/TimeSpliter' ; import { ExtensionManager } from './extensions' ; import { deepFreeze , mergeBox , parseDate } from './common/functions' ; import html2canvas from 'html2canvas' ; typedef EventInfo property { Date } date property { string } title property { [ string ] } description property { [ Date | 'now' ] } endDate property { [ string ] } endText property { [ boolean ] } folded property { [ string ] } foldPlaceholder property interface DrawInfo extends ComponentDrawInfo { events : { date : string ; title : string ; description ? : string ; endDate ? : string | 'now' ; endText ? : string ; folded ? : boolean ; foldPlaceholder ? : string ; } [ ] ; } property property property property property export interface RuntimeInfo { startDate : Date ; endDate : Date ; milestoneBy : DateBy | null ; scaleBy : DateBy | null ; axisLength : number ; } export interface ConstructInfo { canvas : HTMLCanvasElement ; container : HTMLElement ; ext ? : ExtensionManager ; grid ? : GridConfig ; } /**
 * Timeline self. There are 2 way to draw a timeline.
 * 1. draw a timeline base on event list.
 * 2. draw a timeline base an TimelineData.
 * For No.1, there are like:
 * ```js
 * const timeline = new Timeline(...);
 * timeline.event = [...];
 * await timeline.apply();
 * timeline.draw();
 * ```js
 *
 * For No.2, Ensure you had a TimelineData, and run like this for draw a timeline:
 * (The TimelineData is generate by timeline.export())
 * ```js
 * const timeline = new Timeline(...);
 * await timeline.drawFrom(timelineData);
 * ```
 * */ export default abstract class Timeline extends Component { constructor ( props ) { super ( { ext : props . ext || new ExtensionManager , canvas : props . canvas , container : props . container , } ) ; this . grid = props . grid || Timeline . defaultGrid ; this . ext . onConstruct ( this ) ; } name = SN . Timeline ; grid : GridConfig ; see RuntimeInfo runtime : RuntimeInfo = { } as any ; drawInfo : DrawInfo = { box : { x : 0 , y : 0 , width : 0 , height : 0 } , events : [ ] , } ; events : Event [ ] = [ ] ; axis : Axis = null as any ; abstract axisConstructor : typeof Axis ; abstract eventConstructor : typeof Event ; createBox ( ) { this . drawInfo . box = mergeBox ( this . axis . drawInfo . box , ... this . events . map ( event => event . drawInfo . box ) ) ; } param { Partial < RuntimeInfo > } runtime async apply ( runtime ? ) { if ( ! this . drawInfo . events . length ) throw new Error ( 'No event passed the timeline!' ) ; this . initRuntime ( runtime ) ; this . stretchCanvas ( ) ; await this . initAxis ( ) ; await this . axis . apply ( ) ; this . events . forEach ( e => e . destroy ( ) ) ; this . events . length = 0 ; this . initEvents ( ) ; await Promise . all ( this . events . map ( e => e . apply ( ) ) ) ; this . createBox ( ) ; return super . apply ( ) ; } draw ( ) { this . stretchCanvas ( ) ; this . setupRadio ( ) ; this . axis . draw ( ) ; this . events . forEach ( event => event . draw ( ) ) ; return super . draw ( ) ; } hide ( ) { this . events . forEach ( event => event . hide ( ) ) ; this . axis . hide ( ) ; return super . hide ( ) ; } param return { Promise < void > async import ( input ) { const data = typeof input === 'string' ? JSON . parse ( input ) . data : input . data ; const axis = new this . axisConstructor ( this ) ; const event = new this . eventConstructor ( this ) ; this . canvas . width = data . width ; this . canvas . height = data . height ; data . axis . milestonesDrawInfo . forEach ( ( milestoneDrawInfo ) => { if ( 'date' in milestoneDrawInfo . content ) { milestoneDrawInfo . content . date = new Date ( milestoneDrawInfo . content . date ) ; } } ) ; const allComponents = [ ] ; { const { bodyDrawInfo , scalesDrawInfo , milestonesDrawInfo } = data . axis ; allComponents . push ( new axis . bodyConstructor ( this ) . importDrawInfo ( bodyDrawInfo ) ) ; const scalesAndMilestones = [ ] ; scalesDrawInfo . forEach ( ( scaleDrawInfo ) => { scalesAndMilestones . push ( new axis . scaleConstructor ( this ) . importDrawInfo ( scaleDrawInfo ) ) ; } ) ; milestonesDrawInfo . forEach ( ( milestoneDrawInfo ) => { scalesAndMilestones . push ( new axis . milestoneConstructor ( this ) . importDrawInfo ( milestoneDrawInfo ) ) ; } ) ; scalesAndMilestones . sort ( ( comp1 , comp2 ) => comp1 . drawInfo . alignY - comp2 . drawInfo . alignY ) ; allComponents . push ( ... scalesAndMilestones ) ; } const events = Array . from ( data . events ) . sort ( ( e1 , e2 ) => e1 . drawInfo . target . y - e2 . drawInfo . target . y ) ; for ( const { bodyDrawInfo , markDrawInfo , axisDrawInfo } of events ) { allComponents . push ( new event . bodyConstructor ( this ) . importDrawInfo ( bodyDrawInfo ) ) ; if ( axisDrawInfo !== null ) { allComponents . push ( new event . axisConstructor ( this ) . importDrawInfo ( axisDrawInfo ) ) ; } allComponents . push ( new event . markConstructor ( this ) . importDrawInfo ( markDrawInfo ) ) ; } for ( const comp of allComponents ) { await this . ext . breakpoint . block ( Breakpoint . DrawFrom , { protagonist : comp } ) ; comp . draw ( ) ; } } return { TimelineData } export ( ) { const timeline = this . ext . components [ SN . Timeline ] [ 0 ] ; return JSON . parse ( JSON . stringify ( { theme : timeline . theme , version : SN_VERSION , data : { width : timeline . canvas . width , height : timeline . canvas . height , timeline : timeline . drawInfo , runtime : timeline . runtime , events : timeline . events . map ( event => ( { drawInfo : event . drawInfo , bodyDrawInfo : event . body . drawInfo , markDrawInfo : event . mark . drawInfo , axisDrawInfo : event . axis ? event . axis . drawInfo : null , } ) ) , axis : { drawInfo : timeline . axis . drawInfo , bodyDrawInfo : timeline . axis . body . drawInfo , scalesDrawInfo : timeline . axis . scales . map ( scale => scale . drawInfo ) , milestonesDrawInfo : timeline . axis . milestones . map ( milestone => milestone . drawInfo ) , } , } , } ) ) ; } /**
     * Generate a pure image base timeline.
     * It's available after after draw() been called
     * */ async screenshot ( ) { console . log ( 2 , this ) ; return html2canvas ( this . container , { scale : Math . max ( 2 , window . devicePixelRatio ) , } ) ; } /**
     * Download a timeline image for user
     * It's available after after draw() been called
     * */ async downloadImage ( filename = `${ this . theme }-timeline.png` ) { const canvas = await this . screenshot ( ) ; const a = document . createElement ( 'a' ) ; const blob = await new Promise < Blob | null > ( ( resolve => canvas . toBlob ( resolve , 'image/png' ) ) ) ! ; a . target = '_blank' ; a . download = filename ; a . href = window . URL . createObjectURL ( blob ) ; a . click ( ) ; } protected stretchCanvas ( ) { this . canvas . width = this . grid . canvasWidth ; this . canvas . height = this . runtime . axisLength + this . grid . axisStart . y * 2 ; const componentMap = this . ext . components ; for ( const snName in componentMap ) { for ( const component of componentMap [ snName ] ) { const box = component . drawInfo . box ; this . canvas . height = Math . max ( this . canvas . height , box . y + box . height + this . grid . axisStart . y ) ; this . canvas . width = Math . max ( this . canvas . width , box . x + box . width ) ; } } this . container . style . width = `${ this . canvas . width }px` ; } protected initRuntime ( runtime ? ) { this . runtime = Object . create ( runtime || Object . prototype ) ; this . runtime . startDate = ( 'startDate' in this . runtime ) ? parseDate ( this . runtime . startDate ) : this . countStartData ( ) ; this . runtime . endDate = ( 'endDate' in this . runtime ) ? parseDate ( this . runtime . endDate ) : this . countEndData ( ) ; this . runtime . milestoneBy = ( 'milestoneBy' in this . runtime ) ? this . runtime . milestoneBy : this . countMilestoneBy ( ) ; this . runtime . scaleBy = ( 'scaleBy' in this . runtime ) ? this . runtime . scaleBy : this . countScaleBy ( ) ; this . runtime . axisLength = ( 'axisLength' in this . runtime ) ? this . runtime . axisLength : this . countAxisLength ( ) ; this . runtime . startDate = parseDate ( parseDate ( this . runtime . startDate ! ) . getTime ( ) - DATE_COUNT_EXTRA [ this . runtime . scaleBy || DateBy . Day ] ) ; this . runtime . endDate = parseDate ( parseDate ( this . runtime . endDate ! ) . getTime ( ) + DATE_COUNT_EXTRA [ this . runtime . scaleBy || DateBy . Day ] ) ; } protected countStartData ( ) { const events = Array . from ( this . drawInfo . events ) . sort ( ( e1 , e2 ) => Number ( parseDate ( e1 . date ) ) - Number ( parseDate ( e2 . date ) ) ) ; return parseDate ( events [ 0 ] . date ) ; } protected countEndData ( ) { const events = this . drawInfo . events ; const maxStartData = Math . max ( ... events . map ( event => Number ( parseDate ( event . date ) ) ) ) ; const maxEndData = Math . max ( ... events . filter ( event => ( 'endDate' in event ) ) . map ( event => Number ( parseDate ( event . endDate ! ) ) ) ) || - 1 ; return parseDate ( Math . max ( maxStartData , maxEndData ) ) ; } protected countMilestoneBy ( ) { const TWO_WEEK = 1000 * 60 * 60 * 24 * 7 * 2 ; const TWO_MONTH = 1000 * 60 * 60 * 24 * 30 * 2 ; const TWO_QUARTER = 1000 * 60 * 60 * 24 * 30 * 3 * 2 ; const TWO_YEAR = 1000 * 60 * 60 * 24 * 30 * 12 * 2 ; const TWENTY_YEAR = 1000 * 60 * 60 * 24 * 30 * 12 * 20 ; const TWO_CENTURY = 1000 * 60 * 60 * 24 * 30 * 12 * 100 * 2 ; const TWENTY_CENTURY = 1000 * 60 * 60 * 24 * 30 * 12 * 100 * 20 ; const dataScope = parseDate ( this . runtime . endDate ) . getTime ( ) - parseDate ( this . runtime . startDate ) . getTime ( ) ; switch ( true ) { case dataScope > TWENTY_CENTURY : return DateBy . FiveCentury ; case dataScope > TWO_CENTURY : return DateBy . Century ; case dataScope > TWENTY_YEAR : return DateBy . TenYear ; case dataScope > TWO_YEAR : return DateBy . Year ; case dataScope > TWO_QUARTER : return DateBy . Quarter ; case dataScope > TWO_MONTH : return DateBy . Month ; case dataScope > TWO_WEEK : return DateBy . Week ; } return null ; } protected countScaleBy ( ) { switch ( this . runtime . milestoneBy ) { case DateBy . FiveCentury : return DateBy . Century ; case DateBy . Century : return DateBy . TenYear ; case DateBy . TenYear : return DateBy . Year ; case DateBy . Year : return DateBy . Quarter ; case DateBy . Quarter : return DateBy . Month ; case DateBy . Month : return DateBy . Week ; case DateBy . Week : return DateBy . Day ; } return null ; } protected countAxisLength ( ) { const events = this . drawInfo . events ; return 500 + events . length * 50 + events . filter ( e => e . description ) . length * 70 + events . filter ( e => e . endText ) . length * 30 ; } protected initAxis ( ) { if ( ! this . axis ) this . axis = new this . axisConstructor ( this ) ; const startDate = this . runtime . startDate . getTime ( ) ; const endDate = this . runtime . endDate . getTime ( ) ; const timeSpliter = new TimeSpliter ( startDate , endDate ) ; const dateLength = endDate - startDate ; this . axis . drawInfo . length = this . runtime . axisLength ; if ( this . runtime . milestoneBy !== null ) { this . axis . drawInfo . milestones = timeSpliter . split ( this . runtime . milestoneBy ) . map ( date => ( { position : ( endDate - date . getTime ( ) ) / dateLength , content : { date : date . toISOString ( ) , by : this . runtime . milestoneBy ! , } , } ) ) ; } if ( this . runtime . scaleBy !== null ) { this . axis . drawInfo . scales = timeSpliter . split ( this . runtime . scaleBy ) . map ( date => ( parseDate ( this . runtime . endDate ) . getTime ( ) - date . getTime ( ) ) / dateLength ) ; } } protected initEvents ( ) { const events = Array . from ( this . drawInfo . events ) . sort ( ( e1 , e2 ) => parseDate ( e1 . date ) . getTime ( ) - parseDate ( e2 . date ) . getTime ( ) ) ; const dateLength = parseDate ( this . runtime . endDate ) . getTime ( ) - parseDate ( this . runtime . startDate ) . getTime ( ) ; for ( const data of events ) { const event = new this . eventConstructor ( this ) ; event . drawInfo . axisBodyDrawInfo = deepFreeze ( this . axis . body . drawInfo ) ; event . drawInfo . target = { x : this . axis . body . drawInfo . box . x + this . axis . body . drawInfo . box . width / 2 , y : ( parseDate ( this . runtime . endDate ) . getTime ( ) - parseDate ( data . date ) . getTime ( ) ) / dateLength * this . axis . drawInfo . length , } ; event . drawInfo . date = data . date ; event . drawInfo . title = data . title ; event . drawInfo . description = data . description ; event . drawInfo . folded = Boolean ( data . folded ) ; event . drawInfo . foldPlaceholder = data . foldPlaceholder ; event . drawInfo . endText = data . endText ; if ( data . endDate ) { const endDate = parseDate ( data . endDate === 'now' ? this . runtime . endDate : data . endDate ) ; event . drawInfo . endDate = endDate . toISOString ( ) ; event . drawInfo . axisLength = ( endDate . getTime ( ) - parseDate ( data . date ) . getTime ( ) ) / dateLength * this . axis . drawInfo . length ; } this . events . push ( event ) ; } } static defaultGrid : GridConfig = { eventOffset : { x : 20 , y : 0 } , minEventAxisOffset : 25 , markWidth : 15 , scaleHeight : 5 , axisWidth : 12 , axisStart : { x : 450 , y : 10 , } , eventWidth : 300 , canvasWidth : 700 , } ; static is ( comp ) { return comp . name === SN . Timeline ; } /**
     * Mount a HTML element adding canvas and container.
     * The HTML element passed will be cleared.
     * */ static mount ( el , themeName ) { const container = typeof el === 'string' ? document . querySelector ( el ) ! as HTMLElement : el as HTMLElement ; container . innerHTML = '' ; container . classList . add ( 'short-night' , `sn-${ themeName }` , 'sn-container' ) ; const canvas = document . createElement ( 'canvas' ) as HTMLCanvasElement ; canvas . height = 6000 ; canvas . width = 600 ; canvas . classList . add ( 'short-night' , `sn-${ themeName }` , 'sn-canvas' ) ; container . appendChild ( canvas ) ; return { container , canvas } ; } }