All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.data2viz.scale.Time.kt Maven / Gradle / Ivy

package io.data2viz.scale

import io.data2viz.math.tickStep
import io.data2viz.time.*

val dateComparator = Comparator { a, b -> if (a.millisecondsBetween(b) > 0) -1 else if (a.millisecondsBetween(b) < 0) 1 else 0 }

private data class TickInterval(
        val interval: Interval,
        val step: Int,
        val duration: Long
)

private val tickIntervals = listOf(
        TickInterval(timeSecond, 1, durationSecond),
        TickInterval(timeSecond, 5, 5 * durationSecond),
        TickInterval(timeSecond, 15, 15 * durationSecond),
        TickInterval(timeSecond, 30, 30 * durationSecond),
        TickInterval(timeMinute, 1, durationMinute),
        TickInterval(timeMinute, 5, 5 * durationMinute),
        TickInterval(timeMinute, 15, 15 * durationMinute),
        TickInterval(timeMinute, 30, 30 * durationMinute),
        TickInterval(timeHour, 1, durationHour),
        TickInterval(timeHour, 3, 3 * durationHour),
        TickInterval(timeHour, 6, 6 * durationHour),
        TickInterval(timeHour, 12, 12 * durationHour),
        TickInterval(timeDay, 1, durationDay),
        TickInterval(timeDay, 2, 2 * durationDay),
        TickInterval(timeSunday, 1, durationWeek),
        TickInterval(timeMonth, 1, durationMonth),
        TickInterval(timeMonth, 3, 3 * durationMonth),
        TickInterval(timeYear, 1, durationYear)
)

/**
 * Time scales are a variant of linear scales that have a temporal domain: domain values are dates rather than numbers,
 * and invert returns a date.
 * Time scales implement ticks based on calendar intervals, taking the pain out of generating axes for temporal domains.
 */
class TimeScale(interpolateRange: (R, R) -> (Double) -> R,
                   uninterpolateRange: ((R, R) -> (R) -> Double)? = null,
                   rangeComparator: Comparator? = null)
    : ContinuousScale(interpolateRange, uninterpolateRange, rangeComparator),
        NiceableScale,
        Tickable {

    init {
        _domain.clear()
        _domain.addAll(listOf(date(2000, 1, 1), date(2000, 1, 2)))
    }

    override fun uninterpolateDomain(from: Date, to: Date): (Date) -> Double {
        return { date ->
            if (from.millisecondsBetween(to) != 0L)
                ((from.millisecondsBetween(date)) / (from.millisecondsBetween(to)).toDouble())
            else .0
        }
    }

    override fun interpolateDomain(from: Date, to: Date): (Double) -> Date {
        val diff = from.millisecondsBetween(to)
        return { double ->
            val date: Date = date(from)
            val milliseconds = double.toLong() * diff
            date.plusMilliseconds(milliseconds)
            date
        }
    }

    override fun domainComparator(): Comparator = dateComparator

    /**
     * Extends the domain so that it starts and ends on nice round values. This method typically modifies
     * the scale’s domain, and may only extend the bounds to the nearest round value. See continuous.nice for more.
     *
     * An optional tick count argument allows greater control over the step size used to extend the bounds,
     * guaranteeing that the returned ticks will exactly cover the domain.
     *
     * Alternatively, a time interval may be specified to explicitly set the ticks.
     * If an interval is specified, an optional step may also be specified to skip some ticks.
     * For example, time.nice(d3.timeSecond, 10) will extend the domain to an even ten seconds (0, 10, 20, etc.).
     * See time.ticks and interval.every for further detail.
     *
     * Nicing is useful if the domain is computed from data, say using extent, and may be irregular.
     * For example, for a domain of [2009-07-13T00:02, 2009-07-13T23:48],
     * the nice domain is [2009-07-13, 2009-07-14].
     * If the domain has more than two values, nicing the domain only affects the first and last value.
     */
    override fun nice(count: Int) {
        val start = _domain.first()
        val end = _domain.last()
        val interval: Interval = tickInterval(count, start, end)
        niceDomain(end, start, interval)
        rescale()
    }

    private fun tickInterval(count: Int, start: Date, end: Date): Interval {
        val target = start.millisecondsBetween(end) / count
        val intervalIndex = bisectRight(tickIntervals.map { it.duration }, target, naturalOrder())
        val step: Int?
        var interval: Interval = timeYear
        if (intervalIndex == tickIntervals.size) {
            step = tickStep(start.getTime() / durationYear, end.getTime() / durationYear, count).toInt()
        } else if (intervalIndex > 0) {
            val l = target.toDouble() / tickIntervals[intervalIndex - 1].duration
            val l1 = tickIntervals[intervalIndex].duration / target.toDouble()
            val tickInterval = tickIntervals[if (l < l1) intervalIndex - 1 else intervalIndex]
            step = tickInterval.step
            interval = tickInterval.interval
        } else {
            step = tickStep(start.getTime(), end.getTime(), count).toInt()
            interval = timeMillisecond
        }
        if (step > 0) interval = interval.every(step)
        return interval
    }

    private fun niceDomain(end: Date, start: Date, interval: Interval) {
        var first = 0
        var last = _domain.size - 1

        if (end.isBefore(start)) {
            first = domain.size - 1
            last = 0
        }

        val x0 = _domain[first]
        val x1 = _domain[last]

        _domain[first] = interval.floor(x0)
        _domain[last] = interval.ceil(x1)
    }

    /**
     * Returns representative dates from the scale’s domain.
     * The returned tick values are uniformly-spaced (mostly), have sensible values (such as every day at midnight),
     * and are guaranteed to be within the extent of the domain.
     * Ticks are often used to display reference lines, or tick marks, in conjunction with the visualized data.
     *
     * An optional count may be specified to affect how many ticks are generated.
     * If count is not specified, it defaults to 10. The specified count is only a hint; the scale may return more
     * or fewer values depending on the domain.
     *
     * The following time intervals are considered for automatic ticks:
     * 1-, 5-, 15- and 30-second.
     * 1-, 5-, 15- and 30-minute.
     * 1-, 3-, 6- and 12-hour.
     * 1- and 2-day.
     * 1-week.
     * 1- and 3-month.
     * 1-year.
     */
    override fun ticks(count: Int): List {
        var first = 0
        var last = _domain.size - 1

        var start = _domain[first]
        var end = _domain[last]

        if (start.millisecondsBetween(end) == 0L) return listOf()

        val reversed = end.isBefore(start)
        if (reversed) {
            first = _domain.size - 1
            last = 0
            start = _domain[first]
            end = _domain[last]
        }

        val endPlus = date(end)
        endPlus.plusMilliseconds(1)

        val tickInterval = tickInterval(count, start, end)
        val ticks = tickInterval.range(start, endPlus)

        return if (reversed) ticks.reversed() else ticks
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy