933 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			933 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* eslint-disable no-unused-vars */
 | 
						|
import { Cycle, Colors, Dates, isEmpty } from '../utils.js'
 | 
						|
import { Capabilities } from '../capabilities.js'
 | 
						|
import { BitmapLabeledGraphics, FontInfo } from './labeledgraphics.js'
 | 
						|
 | 
						|
export class Ticks {
 | 
						|
    get reservedPrefixes() {
 | 
						|
        return ['decade', 'year', 'month', 'day', 'hour', 'minute', 'second']
 | 
						|
    }
 | 
						|
 | 
						|
    static get largeTickSize() {
 | 
						|
        return 4.2
 | 
						|
    }
 | 
						|
 | 
						|
    get minWidth() {
 | 
						|
        return 10
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        return { year: '2-digit', timeZone: 'UTC' }
 | 
						|
    }
 | 
						|
 | 
						|
    get minLabelWidth() {
 | 
						|
        return 44
 | 
						|
    }
 | 
						|
 | 
						|
    get formatKey() {
 | 
						|
        return this.key
 | 
						|
    }
 | 
						|
 | 
						|
    dateKey(date) {
 | 
						|
        return this.key + date.getFullYear()
 | 
						|
    }
 | 
						|
 | 
						|
    *iter(start, end) {
 | 
						|
        let date = this.iterStart(start)
 | 
						|
        while (date <= end) {
 | 
						|
            yield date
 | 
						|
            date = this.next(date)
 | 
						|
        }
 | 
						|
        yield date
 | 
						|
    }
 | 
						|
 | 
						|
    *iterRanges(range) {
 | 
						|
        for (let date of this.iter(range.start, range.end)) {
 | 
						|
            let next = this.next(date)
 | 
						|
            yield { start: date, end: next }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    selectedRange(timeline, info) {
 | 
						|
        let first = null
 | 
						|
        let last = null
 | 
						|
        let visibleFirst = null
 | 
						|
        let visibleLast = null
 | 
						|
        let units = 0
 | 
						|
        for (let { start, end } of this.iterRanges(info)) {
 | 
						|
            if (timeline.visibleRange(start, end)) {
 | 
						|
                if (first == null) {
 | 
						|
                    first = start
 | 
						|
                }
 | 
						|
                last = end
 | 
						|
            }
 | 
						|
            if (timeline.visibleDate(start) && timeline.visibleDate(end)) {
 | 
						|
                units += 1
 | 
						|
                if (visibleFirst == null) {
 | 
						|
                    visibleFirst = start
 | 
						|
                }
 | 
						|
                visibleLast = end
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (first == null) return info
 | 
						|
        return {
 | 
						|
            start: first,
 | 
						|
            end: last,
 | 
						|
            visibleStart: visibleFirst,
 | 
						|
            visibleEnd: visibleLast,
 | 
						|
            units: units,
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    drawTick(timeline, x, y, date) {
 | 
						|
        let visible = date > timeline.start && date < timeline.end
 | 
						|
        if (!visible) return false
 | 
						|
        timeline.drawTick(x)
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    toLocaleString(date, format) {
 | 
						|
        return date.toLocaleDateString('de', format)
 | 
						|
    }
 | 
						|
 | 
						|
    draw(timeline, range, width, height, available, format, nextFormat, level, extraTicks = false) {
 | 
						|
        let first = null
 | 
						|
        let last = null
 | 
						|
        let keyedFormat = format ? format[this.formatKey] : null
 | 
						|
        let keyedNextFormat = nextFormat ? nextFormat[this.formatKey] : null
 | 
						|
        let redundant = nextFormat ? this.formatKey in nextFormat : false
 | 
						|
        let fullyRedundant = keyedFormat != null && keyedFormat == keyedNextFormat
 | 
						|
        let y = timeline.getY()
 | 
						|
        for (let { start, end } of this.iterRanges(range)) {
 | 
						|
            let x = timeline.toX(start)
 | 
						|
            let xx = x
 | 
						|
            let yy = y + timeline.tickLabelOffset(-1)
 | 
						|
            if (this.drawTick(timeline, x, y, start) && format) {
 | 
						|
                let key = this.dateKey(start)
 | 
						|
                let text = this.toLocaleString(start, format)
 | 
						|
                let align = 'center'
 | 
						|
                if (nextFormat) {
 | 
						|
                    yy = y + timeline.tickLabelOffset(-1, 1)
 | 
						|
                    align = 'left'
 | 
						|
                    timeline.drawTick(x, Ticks.largeTickSize)
 | 
						|
                    let nextX = timeline.toX(end) - 100
 | 
						|
                    if (x < 0 && nextX > -100 && !redundant) {
 | 
						|
                        xx = Math.min(4, nextX)
 | 
						|
                    } else {
 | 
						|
                        xx -= 2
 | 
						|
                    }
 | 
						|
                } else if (level > 0) {
 | 
						|
                    xx = x + available / 2
 | 
						|
                }
 | 
						|
 | 
						|
                if (!fullyRedundant) {
 | 
						|
                    timeline.ensureLabel(key, text, { x: xx, y: yy, align }, FontInfo.small)
 | 
						|
                }
 | 
						|
                if (extraTicks) timeline.drawTick(x, -level)
 | 
						|
            }
 | 
						|
            if (timeline.visibleRange(start, end)) {
 | 
						|
                if (first == null) first = start
 | 
						|
                last = end
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (first == null) return null
 | 
						|
        return { start: first, end: last }
 | 
						|
    }
 | 
						|
 | 
						|
    drawExtra(timeline, range, size) {
 | 
						|
        for (let { start } of this.iterRanges(range)) {
 | 
						|
            if (timeline.visibleDate(start)) {
 | 
						|
                let x = timeline.toX(start)
 | 
						|
                timeline.drawTick(x, -size)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class DecadeTicks extends Ticks {
 | 
						|
    get milliseconds() {
 | 
						|
        return 10 * 365 * 24 * 60 * 60 * 1000
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        return { year: 'numeric', timeZone: 'UTC' }
 | 
						|
    }
 | 
						|
 | 
						|
    selection(timeline, dates, selected) {
 | 
						|
        return dates
 | 
						|
    }
 | 
						|
 | 
						|
    get key() {
 | 
						|
        return 'decade'
 | 
						|
    }
 | 
						|
 | 
						|
    get formatKey() {
 | 
						|
        return 'year'
 | 
						|
    }
 | 
						|
 | 
						|
    iterStart(start) {
 | 
						|
        let modulo = start.getFullYear() % 10
 | 
						|
        let year = start.getFullYear() - modulo
 | 
						|
        return Dates.create(year, 0, 1)
 | 
						|
    }
 | 
						|
 | 
						|
    next(decade) {
 | 
						|
        return Dates.nextYear(decade, 10)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class YearTicks extends Ticks {
 | 
						|
    get milliseconds() {
 | 
						|
        return 365 * 24 * 60 * 60 * 1000
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        if (available < 44) return { year: '2-digit', timeZone: 'UTC' }
 | 
						|
        return { year: 'numeric', timeZone: 'UTC' }
 | 
						|
    }
 | 
						|
 | 
						|
    get minLabelWidth() {
 | 
						|
        return 22
 | 
						|
    }
 | 
						|
 | 
						|
    get key() {
 | 
						|
        return 'year'
 | 
						|
    }
 | 
						|
 | 
						|
    iterStart(start) {
 | 
						|
        return Dates.create(start.getFullYear(), 0, 1)
 | 
						|
    }
 | 
						|
 | 
						|
    next(year) {
 | 
						|
        return Dates.nextYear(year)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class MonthTicks extends Ticks {
 | 
						|
    get milliseconds() {
 | 
						|
        return (365 / 12) * 24 * 60 * 60 * 1000
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        let format = { month: 'narrow', timeZone: 'UTC' }
 | 
						|
        if (available > 44) format.month = 'short'
 | 
						|
        if (available > 66) format.year = '2-digit'
 | 
						|
        if (available > 100) {
 | 
						|
            format.month = 'long'
 | 
						|
            format.year = 'numeric'
 | 
						|
        }
 | 
						|
        return format
 | 
						|
    }
 | 
						|
 | 
						|
    get minLabelWidth() {
 | 
						|
        return 32
 | 
						|
    }
 | 
						|
 | 
						|
    get key() {
 | 
						|
        return 'month'
 | 
						|
    }
 | 
						|
 | 
						|
    dateKey(date) {
 | 
						|
        return this.key + date.getFullYear() + date.getMonth()
 | 
						|
    }
 | 
						|
 | 
						|
    iterStart(start) {
 | 
						|
        return Dates.create(start.getFullYear(), start.getMonth(), 1)
 | 
						|
    }
 | 
						|
 | 
						|
    next(month) {
 | 
						|
        return Dates.nextMonth(month)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class DayTicks extends Ticks {
 | 
						|
    get milliseconds() {
 | 
						|
        return 24 * 60 * 60 * 1000
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        let format = { day: 'numeric', timeZone: 'UTC' }
 | 
						|
        if (available > 44) format.month = 'short'
 | 
						|
        if (available > 100) {
 | 
						|
            format.month = 'long'
 | 
						|
            format.year = '2-digit'
 | 
						|
        }
 | 
						|
        if (available > 150) {
 | 
						|
            format.weekday = 'short'
 | 
						|
        }
 | 
						|
        if (available > 200) {
 | 
						|
            format.year = 'numeric'
 | 
						|
            format.weekday = 'long'
 | 
						|
        }
 | 
						|
        return format
 | 
						|
    }
 | 
						|
 | 
						|
    get key() {
 | 
						|
        return 'day'
 | 
						|
    }
 | 
						|
 | 
						|
    dateKey(date) {
 | 
						|
        return this.key + date.getFullYear() + date.getMonth() + date.getDate()
 | 
						|
    }
 | 
						|
 | 
						|
    iterStart(start) {
 | 
						|
        return Dates.create(start.getFullYear(), start.getMonth(), start.getDate())
 | 
						|
    }
 | 
						|
 | 
						|
    next(date) {
 | 
						|
        return Dates.nextDay(date)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class HourTicks extends Ticks {
 | 
						|
    get milliseconds() {
 | 
						|
        return 60 * 60 * 1000
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        let format = {}
 | 
						|
        if (available > 44) {
 | 
						|
            format.hour = '2-digit'
 | 
						|
        }
 | 
						|
        if (available > 100) {
 | 
						|
            format.day = '2-digit'
 | 
						|
            format.month = '2-digit'
 | 
						|
            format.year = '2-digit'
 | 
						|
        }
 | 
						|
        if (available > 150) {
 | 
						|
            format.weekday = 'short'
 | 
						|
            format.month = 'short'
 | 
						|
        }
 | 
						|
        if (available > 200) {
 | 
						|
            format.day = 'numeric'
 | 
						|
            format.year = 'numeric'
 | 
						|
            format.month = 'long'
 | 
						|
            format.weekday = 'long'
 | 
						|
        }
 | 
						|
        return format
 | 
						|
    }
 | 
						|
 | 
						|
    get key() {
 | 
						|
        return 'hour'
 | 
						|
    }
 | 
						|
 | 
						|
    dateKey(date) {
 | 
						|
        return this.key + date.getFullYear() + date.getMonth() + date.getDate() + date.getHours()
 | 
						|
    }
 | 
						|
 | 
						|
    iterStart(start) {
 | 
						|
        return Dates.create(start.getFullYear(), start.getMonth(), start.getDate(), start.getHours())
 | 
						|
    }
 | 
						|
 | 
						|
    next(date) {
 | 
						|
        return Dates.nextHour(date)
 | 
						|
    }
 | 
						|
 | 
						|
    toLocaleString(date, format) {
 | 
						|
        return date.toLocaleTimeString('de', format)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class MinuteTicks extends Ticks {
 | 
						|
    get milliseconds() {
 | 
						|
        return 60 * 1000
 | 
						|
    }
 | 
						|
 | 
						|
    format(available) {
 | 
						|
        let format = { minute: 'numeric', timeZone: 'UTC' }
 | 
						|
        if (available > 44) {
 | 
						|
            format.hour = 'numeric'
 | 
						|
            format.minute = 'numeric'
 | 
						|
        }
 | 
						|
        if (available > 100) {
 | 
						|
            format.month = 'short'
 | 
						|
            format.year = '2-digit'
 | 
						|
        }
 | 
						|
        if (available > 150) {
 | 
						|
            format.weekday = 'short'
 | 
						|
        }
 | 
						|
        if (available > 200) {
 | 
						|
            format.year = 'numeric'
 | 
						|
            format.weekday = 'long'
 | 
						|
        }
 | 
						|
        return format
 | 
						|
    }
 | 
						|
 | 
						|
    get key() {
 | 
						|
        return 'minute'
 | 
						|
    }
 | 
						|
 | 
						|
    dateKey(date) {
 | 
						|
        return this.key + date.getFullYear() + date.getMonth() + date.getDate() + date.getHours() + date.getMinutes()
 | 
						|
    }
 | 
						|
 | 
						|
    iterStart(start) {
 | 
						|
        return Dates.create(
 | 
						|
            start.getFullYear(),
 | 
						|
            start.getMonth(),
 | 
						|
            start.getDate(),
 | 
						|
            start.getHours(),
 | 
						|
            start.getMinutes()
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    next(date) {
 | 
						|
        return Dates.nextMinute(date)
 | 
						|
    }
 | 
						|
 | 
						|
    toLocaleString(date, format) {
 | 
						|
        return date.toLocaleTimeString('de', format)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class TimeTicks {
 | 
						|
    constructor(...ticks) {
 | 
						|
        this.ticks = ticks
 | 
						|
    }
 | 
						|
 | 
						|
    selectedRange(timeline) {
 | 
						|
        let info = { start: timeline.start, end: timeline.end, units: 0 }
 | 
						|
        for (let ticks of this.ticks) {
 | 
						|
            info = ticks.selectedRange(timeline, info)
 | 
						|
            if (info.units > 1) {
 | 
						|
                timeline.selection = [info.visibleStart, info.visibleEnd]
 | 
						|
                return
 | 
						|
            }
 | 
						|
        }
 | 
						|
        timeline.selection = [info.start, info.end]
 | 
						|
    }
 | 
						|
 | 
						|
    draw(timeline, width, height) {
 | 
						|
        let range = timeline
 | 
						|
        let start = timeline.toX(range.start)
 | 
						|
        let end = timeline.toX(range.end)
 | 
						|
        let size = end - start
 | 
						|
        let duration = timeline.end - timeline.start
 | 
						|
        let formats = new Map()
 | 
						|
        let nextFormats = new Map()
 | 
						|
        let availables = new Map()
 | 
						|
        let previous = null
 | 
						|
        let visible = []
 | 
						|
        for (let ticks of this.ticks) {
 | 
						|
            let amount = ticks.milliseconds / duration
 | 
						|
            let available = amount * size
 | 
						|
            availables.set(ticks, available)
 | 
						|
            if (available < ticks.minWidth) break
 | 
						|
            formats.set(ticks, available < ticks.minLabelWidth ? null : ticks.format(available))
 | 
						|
            nextFormats.set(previous, formats.get(ticks))
 | 
						|
            previous = ticks
 | 
						|
            visible.push(ticks)
 | 
						|
        }
 | 
						|
        let level = 0
 | 
						|
        let ranges = []
 | 
						|
        for (let ticks of visible) {
 | 
						|
            if (range == null) continue
 | 
						|
            range = ticks.draw(
 | 
						|
                timeline,
 | 
						|
                range,
 | 
						|
                width,
 | 
						|
                height,
 | 
						|
                availables.get(ticks),
 | 
						|
                formats.get(ticks),
 | 
						|
                nextFormats.get(ticks),
 | 
						|
                level
 | 
						|
            )
 | 
						|
            if (range) {
 | 
						|
                ranges.push({ ticks, range })
 | 
						|
            }
 | 
						|
            level += 1
 | 
						|
        }
 | 
						|
 | 
						|
        let extraLevel = ranges.length - 1
 | 
						|
        let extraStart = extraLevel
 | 
						|
        for (let { ticks, range } of ranges) {
 | 
						|
            ticks.drawExtra(timeline, range, extraLevel)
 | 
						|
            extraLevel -= 1
 | 
						|
            if (extraLevel <= 0) {
 | 
						|
                continue
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        timeline.drawTick(start, Ticks.largeTickSize)
 | 
						|
        timeline.drawTick(start, -extraStart)
 | 
						|
        timeline.drawTick(end, Ticks.largeTickSize)
 | 
						|
        timeline.drawTick(end, -extraStart)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export class ColorRanges {
 | 
						|
    constructor(label, color, ranges) {
 | 
						|
        this.label = label
 | 
						|
        this.color = color
 | 
						|
        this.ranges = ranges
 | 
						|
    }
 | 
						|
 | 
						|
    draw(timeline, width, height, size = 12) {
 | 
						|
        let minimum = 1 / Capabilities.devicePixelRatio
 | 
						|
        let h2 = size
 | 
						|
        timeline.lineStyle(size, this.color)
 | 
						|
        for (let range of this.ranges) {
 | 
						|
            if (range.to === null) {
 | 
						|
                range.to = Dates.nextDay(range.from)
 | 
						|
            }
 | 
						|
            let start = timeline.toX(range.from)
 | 
						|
            let end = timeline.toX(range.to)
 | 
						|
            if (end < start + minimum) {
 | 
						|
                end = start + minimum
 | 
						|
            }
 | 
						|
            timeline.moveTo(start, h2)
 | 
						|
            timeline.lineTo(end, h2)
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
export default class Timeline extends BitmapLabeledGraphics {
 | 
						|
    constructor(width, height, { ticks = null, baseLine = 0.5, showRange = true, throwDamping = 0.95 } = {}) {
 | 
						|
        super()
 | 
						|
        this.wantedWidth = width
 | 
						|
        this.wantedHeight = height
 | 
						|
        this.extraLeft = 0
 | 
						|
        this.extraRight = 0
 | 
						|
        this.inset = 5
 | 
						|
        this.showRange = showRange
 | 
						|
        this.baseLine = baseLine
 | 
						|
        this.tickHeight = 4
 | 
						|
        this.zoom = 1
 | 
						|
        this.minZoom = 1
 | 
						|
        this.maxZoom = 12000
 | 
						|
        this.scroll = 0
 | 
						|
        this.draggable = false
 | 
						|
        this.deltas = []
 | 
						|
        this.labelDates = []
 | 
						|
        this.colorRanges = []
 | 
						|
        this.rangeColors = new Cycle(Colors.eminence, Colors.steelblue, Colors.ochre, Colors.turquoise)
 | 
						|
        this.callbacks = []
 | 
						|
        this.onTapCallbacks = []
 | 
						|
        this.onDoubleTapCallbacks = []
 | 
						|
        this.onLongPressCallbacks = []
 | 
						|
        this.progress = null
 | 
						|
        this.start = null
 | 
						|
        this.end = null
 | 
						|
        this.selection = null
 | 
						|
        this.autoScroll = false
 | 
						|
        this.direction = -1
 | 
						|
        this.throwDamping = throwDamping
 | 
						|
        this.timeticks = ticks || new TimeTicks(new DecadeTicks(), new YearTicks(), new MonthTicks(), new DayTicks())
 | 
						|
        this.labelPrefix = '__'
 | 
						|
    }
 | 
						|
 | 
						|
    updateSelection() {
 | 
						|
        if (this.visibleDate(this.start) && this.visibleDate(this.end)) {
 | 
						|
            this.selection = [this.start, this.end]
 | 
						|
        } else {
 | 
						|
            this.timeticks.selectedRange(this)
 | 
						|
        }
 | 
						|
 | 
						|
        return this.selection
 | 
						|
    }
 | 
						|
 | 
						|
    addCallback(callback) {
 | 
						|
        this.callbacks.push(callback)
 | 
						|
    }
 | 
						|
 | 
						|
    addTabCallback(callback) {
 | 
						|
        this.onTapCallbacks.push(callback)
 | 
						|
    }
 | 
						|
 | 
						|
    addDoubleTapCallback(callback) {
 | 
						|
        this.onDoubleTapCallbacks.push(callback)
 | 
						|
    }
 | 
						|
 | 
						|
    addLongPressCallback(callback) {
 | 
						|
        this.onLongPressCallbacks.push(callback)
 | 
						|
    }
 | 
						|
 | 
						|
    addLabels(labels) {
 | 
						|
        this.labelDates = labels
 | 
						|
    }
 | 
						|
 | 
						|
    range(start, end) {
 | 
						|
        this.start = start
 | 
						|
        this.end = end
 | 
						|
    }
 | 
						|
 | 
						|
    draw(width, height) {
 | 
						|
        this.wantedWidth = width
 | 
						|
        this.wantedHeight = height
 | 
						|
        this.redraw()
 | 
						|
    }
 | 
						|
 | 
						|
    updateColorRanges(w, h) {
 | 
						|
        let xx = w - this.inset
 | 
						|
        let size = FontInfo.small.fontSize
 | 
						|
        let yy = h - size
 | 
						|
        for (let i = this.colorRanges.length - 1; i >= 0; i--) {
 | 
						|
            let cr = this.colorRanges[i]
 | 
						|
            let label = cr.label
 | 
						|
            cr.draw(this, w, h)
 | 
						|
            let current = this.ensureLabel(
 | 
						|
                'colorRange:' + label,
 | 
						|
                label,
 | 
						|
                { x: xx, y: yy, align: 'right' },
 | 
						|
                FontInfo.small
 | 
						|
            )
 | 
						|
            let r = current.getBounds()
 | 
						|
            xx -= r.width + 16
 | 
						|
 | 
						|
            this.lineStyle(size, cr.color)
 | 
						|
            this.moveTo(xx, yy)
 | 
						|
            this.lineTo(xx + size, yy)
 | 
						|
            xx -= size + 4
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    drawSelectedRamge(selected) {
 | 
						|
        this.lineStyle(2, app.theme.primaryColor)
 | 
						|
        let start = this.toX(selected[0])
 | 
						|
        let end = this.toX(selected[1])
 | 
						|
        this.moveTo(start, 0)
 | 
						|
        this.lineTo(end, 0)
 | 
						|
        this.drawTick(start, -1.5, 0)
 | 
						|
        this.drawTick(end, -1.5, 0)
 | 
						|
    }
 | 
						|
 | 
						|
    redraw() {
 | 
						|
        this.clear()
 | 
						|
        let h = this.wantedHeight
 | 
						|
        let w = this.wantedWidth
 | 
						|
        let y = this.getY()
 | 
						|
        this.prepareLabels()
 | 
						|
        this.updateColorRanges(w, h)
 | 
						|
 | 
						|
        this.lineStyle(2, 0xffffff)
 | 
						|
        if (this.start != null && this.end != null) {
 | 
						|
            this.moveTo(this.toX(this.start), y)
 | 
						|
            this.lineTo(this.toX(this.end), y)
 | 
						|
            this.updateTicksAndLabels(w, h)
 | 
						|
            this.updateSelection()
 | 
						|
            let selected = this.selection
 | 
						|
            if (selected[0] != this.start && selected[1] != this.end) {
 | 
						|
                if (this.showRange) this.drawSelectedRamge(selected)
 | 
						|
            }
 | 
						|
            for (let callback of this.callbacks) {
 | 
						|
                callback(this.scroll, this.zoom, this.selection)
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            this.moveTo(this.inset, y)
 | 
						|
            this.lineTo(w - this.inset, y)
 | 
						|
        }
 | 
						|
 | 
						|
        if (this.progress != null && this.progress < 1) {
 | 
						|
            this.lineStyle(2, 0xccccff)
 | 
						|
            this.moveTo(this.inset, y)
 | 
						|
            this.lineTo((w - this.inset) * this.progress, y)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    totalWidth(bounded = false) {
 | 
						|
        let w = this.wantedWidth - 2 * this.inset
 | 
						|
        return w * this.validZoom(this.zoom, bounded)
 | 
						|
    }
 | 
						|
 | 
						|
    validZoom(zoom, bounded = true) {
 | 
						|
        let overshoot = bounded ? 1.0 : 2.0
 | 
						|
        zoom = Math.max(zoom, this.minZoom / overshoot)
 | 
						|
        zoom = Math.min(zoom, this.maxZoom * overshoot)
 | 
						|
        return zoom
 | 
						|
    }
 | 
						|
 | 
						|
    getY() {
 | 
						|
        return this.wantedHeight * this.baseLine
 | 
						|
    }
 | 
						|
 | 
						|
    toX(date) {
 | 
						|
        let total = this.end - this.start
 | 
						|
        let offset = this.inset + this.scroll
 | 
						|
        let width = this.totalWidth()
 | 
						|
        let delta = date - this.start
 | 
						|
        let ratio = delta / total
 | 
						|
        return offset + ratio * width
 | 
						|
    }
 | 
						|
 | 
						|
    fromX(value) {
 | 
						|
        let total = this.end - this.start
 | 
						|
        let offset = this.inset + this.scroll
 | 
						|
        let width = this.totalWidth()
 | 
						|
        let ratio = (value - offset) / width
 | 
						|
        let time = this.start.getTime() + total * ratio
 | 
						|
        let date = new Date(time)
 | 
						|
        return date
 | 
						|
    }
 | 
						|
 | 
						|
    drawTick(x, direction = 1, y = null) {
 | 
						|
        if (y == null) {
 | 
						|
            y = this.getY()
 | 
						|
        }
 | 
						|
        this.moveTo(x, y)
 | 
						|
        this.lineTo(x, y - this.tickHeight * direction * this.direction)
 | 
						|
    }
 | 
						|
 | 
						|
    prepareLabels() {
 | 
						|
        for (let key of this.labels.keys()) {
 | 
						|
            if (!key.startsWith(this.labelPrefix)) this.labels.get(key).visible = false
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    updateTicksAndLabels(width, height) {
 | 
						|
        this.drawTick(this.toX(this.start))
 | 
						|
        this.drawTick(this.toX(this.end))
 | 
						|
        this.timeticks.draw(this, width, height)
 | 
						|
        this.updateLabels(width, height)
 | 
						|
    }
 | 
						|
 | 
						|
    visibleDate(date, offset = 0) {
 | 
						|
        if (date >= this.start && date <= this.end) {
 | 
						|
            let x = this.toX(date) + offset
 | 
						|
            return x > 0 && x < this.wantedWidth
 | 
						|
        }
 | 
						|
        return false
 | 
						|
    }
 | 
						|
 | 
						|
    visibleRange(start, end) {
 | 
						|
        let x = this.toX(start)
 | 
						|
        if (x > this.wantedWidth) return false
 | 
						|
        x = this.toX(end)
 | 
						|
        if (x < 0) return false
 | 
						|
        return true
 | 
						|
    }
 | 
						|
 | 
						|
    tickLabelOffset(direction = 1, level = 0) {
 | 
						|
        let fs = FontInfo.small.fontSize
 | 
						|
        let dh = fs + level * (fs + 2)
 | 
						|
        return this.direction * direction * dh
 | 
						|
    }
 | 
						|
 | 
						|
    updateLabels(width, height) {
 | 
						|
        let h2 = height / 2
 | 
						|
        if (this.labelDates) {
 | 
						|
            let last = null
 | 
						|
            let y = h2 + this.tickLabelOffset()
 | 
						|
            for (let i = this.labelDates.length - 1; i >= 0; i--) {
 | 
						|
                let [label, date] = this.labelDates[i]
 | 
						|
                let align = 'center' // (last == null) ? 'right' : 'left'
 | 
						|
                let x = this.toX(date)
 | 
						|
                let current = this.ensureLabel(
 | 
						|
                    this.labelPrefix + label,
 | 
						|
                    label,
 | 
						|
                    {
 | 
						|
                        x: x,
 | 
						|
                        y: y,
 | 
						|
                        align,
 | 
						|
                    },
 | 
						|
                    FontInfo.small
 | 
						|
                )
 | 
						|
                let r = current.getBounds()
 | 
						|
                current.visible = !(last != null && r.x + r.width > last.x)
 | 
						|
                if (current.visible) {
 | 
						|
                    this.drawTick(x, -1)
 | 
						|
                    last = r
 | 
						|
                }
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            let start = this.start.toLocaleDateString('de', {
 | 
						|
                year: 'numeric',
 | 
						|
                month: 'numeric',
 | 
						|
                day: 'numeric',
 | 
						|
            })
 | 
						|
            let end = this.end.toLocaleDateString('de', {
 | 
						|
                year: 'numeric',
 | 
						|
                month: 'numeric',
 | 
						|
                day: 'numeric',
 | 
						|
            })
 | 
						|
            this.ensureLabel(this.labelPrefix + 'start', start, {
 | 
						|
                x: this.toX(this.start),
 | 
						|
                y: h2,
 | 
						|
            })
 | 
						|
            this.ensureLabel(this.labelPrefix + 'end', end, {
 | 
						|
                x: this.toX(this.end),
 | 
						|
                y: h2,
 | 
						|
                align: 'right',
 | 
						|
            })
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onZoom(zoom, anchor) {
 | 
						|
        let date = this.fromX(anchor.x)
 | 
						|
        let newZoom = this.validZoom(this.zoom * zoom, false)
 | 
						|
        this.zoom = newZoom
 | 
						|
        let newX = this.toX(date)
 | 
						|
        this.scroll += anchor.x - newX
 | 
						|
    }
 | 
						|
 | 
						|
    onStart(event, interaction) {
 | 
						|
        this.killTweens()
 | 
						|
        this.deltas = []
 | 
						|
        this.validScroll()
 | 
						|
        // if (typeof ThrowPropsPlugin != 'undefined') {
 | 
						|
        //     ThrowPropsPlugin.track(this, 'delta')
 | 
						|
        // }
 | 
						|
    }
 | 
						|
 | 
						|
    onMove(event, interaction) {
 | 
						|
        let delta = interaction.delta()
 | 
						|
        if (delta == null) {
 | 
						|
            return
 | 
						|
        }
 | 
						|
        this.scroll += delta.x
 | 
						|
        if (this.draggable) {
 | 
						|
            this.y += delta.y
 | 
						|
        }
 | 
						|
        while (this.deltas.length > 10) {
 | 
						|
            this.deltas.pop(0)
 | 
						|
        }
 | 
						|
        this.deltas.push(delta.x)
 | 
						|
        if (interaction.current.size > 1) {
 | 
						|
            this.onZoom(delta.zoom, delta.about)
 | 
						|
        }
 | 
						|
        this.redraw()
 | 
						|
    }
 | 
						|
 | 
						|
    onEnd(event, interaction) {
 | 
						|
        // if (typeof ThrowPropsPlugin != 'undefined') {
 | 
						|
        //     let vel = ThrowPropsPlugin.getVelocity(this, 'delta')
 | 
						|
        //     ThrowPropsPlugin.untrack(this)
 | 
						|
        // }
 | 
						|
        this.killTweens()
 | 
						|
        this.redraw()
 | 
						|
        let delta = 0
 | 
						|
        for (let d of this.deltas) {
 | 
						|
            delta += d
 | 
						|
        }
 | 
						|
        if (this.deltas.length > 0) {
 | 
						|
            delta /= this.deltas.length
 | 
						|
        }
 | 
						|
        this.autoScroll = true
 | 
						|
        let anchor = interaction.current.mean()
 | 
						|
        this.keepInBounds(delta, anchor)
 | 
						|
        for (let key of interaction.ended.keys()) {
 | 
						|
            if (interaction.isDoubleTap(key)) {
 | 
						|
                this.onDoubleTap(event, interaction, key)
 | 
						|
            } else if (interaction.isTap(key)) {
 | 
						|
                this.onTap(event, interaction, key)
 | 
						|
            } else if (interaction.isLongPress(key)) {
 | 
						|
                this.onLongPress(event, interaction, key)
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onLongPress(event, interaction, key) {
 | 
						|
        for (let callback of this.onLongPressCallbacks) {
 | 
						|
            callback(event, interaction, key)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onTap(event, interaction, key) {
 | 
						|
        for (let callback of this.onTapCallbacks) {
 | 
						|
            callback(event, interaction, key)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onDoubleTap(event, interaction, key) {
 | 
						|
        for (let callback of this.onDoubleTapCallbacks) {
 | 
						|
            callback(event, interaction, key)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    _scrollMinimum(bounded) {
 | 
						|
        let total = this.totalWidth(bounded)
 | 
						|
        return -total + this.wantedWidth - 2 * this.inset
 | 
						|
    }
 | 
						|
 | 
						|
    _scrollMaximum(bounded) {
 | 
						|
        let total = this.totalWidth(bounded)
 | 
						|
        let limit = this.wantedWidth
 | 
						|
        if (total > limit) return 0
 | 
						|
        let w = limit - 2 * this.inset
 | 
						|
        return (w - total) / 2
 | 
						|
    }
 | 
						|
 | 
						|
    scrollMinimum(bounded) {
 | 
						|
        return this._scrollMinimum(bounded) - this.extraRight
 | 
						|
    }
 | 
						|
 | 
						|
    scrollMaximum(bounded) {
 | 
						|
        return this._scrollMaximum(bounded) + this.extraLeft
 | 
						|
    }
 | 
						|
 | 
						|
    killTweens() {
 | 
						|
        TweenLite.killTweensOf(this)
 | 
						|
        this.autoScroll = false
 | 
						|
    }
 | 
						|
 | 
						|
    validScroll(bounded = true) {
 | 
						|
        let minimum = this.scrollMinimum(bounded)
 | 
						|
        let maximum = this.scrollMaximum(bounded)
 | 
						|
        if (this.scroll < minimum) {
 | 
						|
            this.scroll = minimum
 | 
						|
        }
 | 
						|
        if (this.scroll > maximum) {
 | 
						|
            this.scroll = maximum
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    keepInBounds(delta, anchor) {
 | 
						|
        let bounded = true
 | 
						|
        let minimum = this.scrollMinimum(bounded)
 | 
						|
        let maximum = this.scrollMaximum(bounded)
 | 
						|
        let tweens = {}
 | 
						|
        if (this.zoom > this.maxZoom) {
 | 
						|
            tweens.zoom = this.maxZoom
 | 
						|
            let date = this.fromX(anchor.x)
 | 
						|
            let oldZoom = this.zoom
 | 
						|
            this.zoom = this.maxZoom
 | 
						|
            let newX = this.toX(date)
 | 
						|
            tweens.scroll = this.scroll + anchor.x - newX
 | 
						|
            this.zoom = oldZoom
 | 
						|
        } else {
 | 
						|
            if (this.zoom < this.minZoom) {
 | 
						|
                tweens.zoom = this.minZoom
 | 
						|
            }
 | 
						|
            if (this.scroll > maximum) {
 | 
						|
                tweens.scroll = maximum
 | 
						|
            }
 | 
						|
            if (this.scroll < minimum) {
 | 
						|
                tweens.scroll = minimum
 | 
						|
            }
 | 
						|
        }
 | 
						|
        if (!isEmpty(tweens)) {
 | 
						|
            tweens.onUpdate = () => {
 | 
						|
                this.redraw()
 | 
						|
            }
 | 
						|
            TweenLite.to(this, 0.5, tweens).delay(0.1)
 | 
						|
            return
 | 
						|
        }
 | 
						|
        this.scroll += delta
 | 
						|
        delta *= this.throwDamping
 | 
						|
        this.redraw()
 | 
						|
        if (Math.abs(delta) > 1 && this.autoScroll) {
 | 
						|
            setTimeout(() => this.keepInBounds(delta, anchor), 1000 / 100)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    onMouseWheel(event) {
 | 
						|
        this.killTweens()
 | 
						|
        let direction = event.detail < 0 || event.wheelDelta > 0
 | 
						|
        let anchor = { x: event.clientX, y: event.clientY }
 | 
						|
        const zoomFactor = 1.5
 | 
						|
        this.onZoom(direction ? zoomFactor : 1 / zoomFactor, anchor)
 | 
						|
        this.redraw()
 | 
						|
        this.keepInBounds(0, anchor)
 | 
						|
    }
 | 
						|
 | 
						|
    showRanges(ranges, label = 'Untitled', color = null) {
 | 
						|
        for (let cr of this.colorRanges) {
 | 
						|
            if (cr.label == label) return
 | 
						|
        }
 | 
						|
        while (this.colorRanges.length >= this.rangeColors.length) {
 | 
						|
            this.colorRanges.shift()
 | 
						|
        }
 | 
						|
        this.colorRanges.push(new ColorRanges(label, color, ranges))
 | 
						|
        this.redraw()
 | 
						|
    }
 | 
						|
}
 |