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

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

There is a newer version: 0.8.0-RC5
Show newest version
package io.data2viz.scale

import io.data2viz.interpolate.Interpolator
import io.data2viz.interpolate.UnInterpolator
import io.data2viz.math.tickStep
import io.data2viz.interpolate.interpolateNumber
import io.data2viz.interpolate.uninterpolateNumber
import io.data2viz.math.*
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.min


// uninterpolate  [value A .. value B] --> [0 .. 1]
// interpolate [0 .. 1] --> [value A .. value B]


open class LinearScale
    internal constructor(
        interpolateRange: (R, R) -> Interpolator,
        uninterpolateRange: ((R, R) -> UnInterpolator)? = null,
        rangeComparator: Comparator? = null) :
        ContinuousScale(interpolateRange, uninterpolateRange, rangeComparator),
        Tickable,
        NiceableScale {

    val comparator = naturalOrder()

    override fun interpolateDomain(from: Double, to: Double): Interpolator = interpolateNumber(from, to)
    override fun uninterpolateDomain(from: Double, to: Double): UnInterpolator = uninterpolateNumber(from, to)
    override fun domainComparator(): Comparator = comparator

    operator fun invoke(domainValue: Int): R {
        return this(domainValue.toDouble())
    }

    init {
        _domain.clear()
        _domain.addAll(listOf(.0, 1.0))
    }

    /**
     * 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.
     * 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. Nicing is useful if the domain is computed
     * from data, say using extent, and may be irregular. For example, for a domain of [0.201479…, 0.996679…],
     * a nice domain might be [0.2, 1.0]. If the domain has more than two values, nicing the domain only affects
     * the first and last value. See also d3-array’s tickStep.
     *
     * Nicing a scale only modifies the current domain; it does not automatically nice domains that are
     * subsequently set using continuous.domain. You must re-nice the scale af  ter setting the new domain, if desired.
     */
    override fun nice(count: Int) {
        val last = _domain.size - 1
        var step = tickStep(_domain[0], _domain[last], count)
        val start = floor(_domain[0] / step) * step
        val stop = ceil(_domain[last] / step) * step

        if (step != .0) {
            step = tickStep(start, stop, count)
            _domain[0] = floor(start / step) * step
            _domain[last] = ceil(stop / step) * step
            rescale()
        }
    }

    override fun ticks(count: Int): List {
        return io.data2viz.math.ticks(_domain.first(), _domain.last(), count)
    }
}

/**
 * Continuous scales map a continuous, quantitative input domain to a continuous output range.
 *
 * If the range is also numeric, the mapping may be inverted. TODO so it's not invertable by default -> should not implement Invertable
 *
 * A continuous scale is not constructed directly; instead, try a linear, power, log,
 * identity, time or sequential color scale.
 */
abstract class ContinuousScale(
        val interpolateRange: (R, R) -> Interpolator,
        val uninterpolateRange: ((R, R) -> UnInterpolator)? = null,
        val rangeComparator: Comparator? = null) :
        ContinuousDomain,
        ContinuousRangeScale,
        ClampableScale,
        InvertableScale {

    private var rangeToDomain: ((R) -> D)? = null
    private var domainToRange: ((D) -> R)? = null

    protected val _domain: MutableList = arrayListOf()
    protected val _range: MutableList = arrayListOf()

    override var clamp: Boolean = false
        set(value) {
            field = value
            rescale()
        }

    // copy the value (no binding intended)
    override var domain: List
        get() = _domain.toList()
        set(value) {
            _domain.clear()
            _domain.addAll(value)
            rescale()
        }

    // copy the value (no binding intended)
    override var range: List
        get() = _range.toList()
        set(value) {
            _range.clear()
            _range.addAll(value)
            rescale()
        }

    abstract fun interpolateDomain(from: D, to: D): Interpolator
    abstract fun uninterpolateDomain(from: D, to: D): UnInterpolator
    abstract fun domainComparator(): Comparator


    override operator fun invoke(domainValue: D): R {
        if (domainToRange == null) {
            check(_domain.size == _range.size) { "Domains (in) and Ranges (out) must have the same size." }
            val uninterpolateFunc = if (clamp) uninterpolateClamp(::uninterpolateDomain) else ::uninterpolateDomain
            domainToRange =
                    if (_domain.size > 2) polymap(uninterpolateFunc)
                    else bimap(uninterpolateFunc)
        }

        return domainToRange?.invoke(domainValue) ?: throw IllegalStateException()
    }

    // TODO : wrong : clamping is done on interpolateRange function...
    override fun invert(rangeValue: R): D {
        checkNotNull(uninterpolateRange) { "No de-interpolation function for range has been found for this scale. Invert operation is impossible." }

        if (rangeToDomain == null) {
            check(_domain.size == _range.size) { "Domains (in) and Ranges (out) must have the same size." }
            val interpolateFunc = if (clamp) interpolateClamp(::interpolateDomain) else ::interpolateDomain
            rangeToDomain =
                    if (_domain.size > 2 || _range.size > 2) polymapInvert(interpolateFunc, uninterpolateRange!!)
                    else bimapInvert(interpolateFunc, uninterpolateRange!!)
        }

        return rangeToDomain?.invoke(rangeValue) ?: throw IllegalStateException()
    }

    protected fun rescale() {
        rangeToDomain = null
        domainToRange = null
    }

    private fun uninterpolateClamp(uninterpolateFunction: (D, D) -> UnInterpolator): (D, D) -> UnInterpolator {
        return fun(a: D, b: D): UnInterpolator {
            val d = uninterpolateFunction(a, b)
            return fun(value: D) = d(value).coerceToDefault()
        }
    }

    private fun interpolateClamp(interpolateFunction: (D, D) -> Interpolator): (D, D) -> Interpolator {
        return fun(a: D, b: D): Interpolator {
            val r = interpolateFunction(a, b)
            return fun(value: Percent) = r(value.coerceToDefault())
        }
    }

    private fun bimap(deinterpolateDomain: (D, D) -> UnInterpolator): (D) -> R {

        val d0 = _domain[0]
        val d1 = _domain[1]
        val r0 = _range[0]
        val r1 = _range[1]

        val r: Interpolator
        val d: UnInterpolator

        if (domainComparator().compare(d1, d0) < 0) {
            d = deinterpolateDomain(d1, d0)
            r = interpolateRange(r1, r0)
        } else {
            d = deinterpolateDomain(d0, d1)
            r = interpolateRange(r0, r1)
        }

        return { x: D -> r(d(x)) }
    }

    private fun bimapInvert(reinterpolateDomain: (D, D) -> Interpolator,
                            deinterpolateRange: (R, R) -> UnInterpolator): (R) -> D {

        checkNotNull(rangeComparator) { "No RangeComparator has been found for this scale. Invert operation is impossible." }

        val d0 = _domain[0]
        val d1 = _domain[1]
        val r0 = _range[0]
        val r1 = _range[1]

        val r: UnInterpolator
        val d: Interpolator

        if (rangeComparator.compare(r1, r0) < 0) {
            d = reinterpolateDomain(d1, d0)
            r = deinterpolateRange(r1, r0)
        } else {
            d = reinterpolateDomain(d0, d1)
            r = deinterpolateRange(r0, r1)
        }

        return { x: R -> d(r(x)) }
    }

    private fun polymap(uninterpolateDomain: (D, D) -> UnInterpolator): (D) -> R {

        val d0 = _domain.first()
        val d1 = _domain.last()
        val domainReversed = domainComparator().compare(d1, d0) < 0
        val domainValues = if (domainReversed) _domain.reversed() else _domain
        val rangeValues = if (domainReversed) _range.reversed() else _range

        val size = min(_domain.size, _range.size) - 1
        val domainInterpolators = Array(size) { uninterpolateDomain(domainValues[it], domainValues[it + 1]) }
        val rangeInterpolators = Array(size) { interpolateRange(rangeValues[it], rangeValues[it + 1]) }

        return { x ->
            val index = bisectRight(_domain, x, domainComparator(), 1, size) - 1
            rangeInterpolators[index](domainInterpolators[index](x))
        }
    }

    private fun polymapInvert(interpolateDomain: (D, D) -> Interpolator,
                              uninterpolateRange: (R, R) -> UnInterpolator): (R) -> D {

        // TODO  instanceOf Comparable ??
        checkNotNull(rangeComparator) { "No RangeComparator has been found for this scale. Invert operation is impossible." }

        val r0 = _range.first()
        val r1 = _range.last()
        val rangeReversed = rangeComparator.compare(r1, r0) < 0
        val domainValues = if (rangeReversed) _domain.reversed() else _domain
        val rangeValues = if (rangeReversed) _range.reversed() else _range

        val size = min(_domain.size, _range.size) - 1
        val domainInterpolators = Array(size) { interpolateDomain(domainValues[it], domainValues[it + 1]) }
        val rangeInterpolators = Array(size) { uninterpolateRange(rangeValues[it], rangeValues[it + 1]) }

        return { y ->
            val index = bisectRight(rangeValues, y, rangeComparator, 1, size) - 1
            domainInterpolators[index](rangeInterpolators[index](y))
        }
    }
}


// TODO move to array module
/**
 * Returns the insertion point for x in array to maintain sorted order.
 * The arguments lo and hi may be used to specify a subset of the array which should be considered;
 * by default the entire array is used. If x is already present in array, the insertion point will be
 * after (to the right of) any existing entries of x in array.
 * The returned insertion point i partitions the array into two halves so that all v <= x for v in array.slice(lo, i)
 * for the left side and all v > x for v in array.slice(i, hi) for the right side.
 */
fun  bisectRight(list: List, x: T, comparator: Comparator, low: Int = 0, high: Int = list.size): Int {
    var lo = low
    var hi = high
    while (lo < hi) {
        val mid = (lo + hi) / 2
        if (comparator.compare(list[mid], x) > 0)
            hi = mid
        else
            lo = mid + 1
    }
    return lo
}

fun  bisectLeft(list: List, x: T, comparator: Comparator, low: Int = 0, high: Int = list.size): Int {
    var lo = low
    var hi = high
    while (lo < hi) {
        val mid = (lo + hi) / 2
        if (comparator.compare(list[mid], x) < 0)
            lo = mid + 1
        else
            hi = mid
    }
    return lo
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy