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

tri.covid19.coda.utils.LogAxis.kt Maven / Gradle / Ivy

/*-
 * #%L
 * coda-app
 * --
 * Copyright (C) 2020 - 2021 Elisha Peterson
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package tri.covid19.coda.utils

import javafx.animation.KeyFrame
import javafx.animation.KeyValue
import javafx.animation.Timeline
import javafx.beans.binding.DoubleBinding
import javafx.beans.property.DoubleProperty
import javafx.beans.property.SimpleDoubleProperty
import javafx.scene.chart.ValueAxis
import javafx.util.Duration
import tri.util.numberFormat
import kotlin.math.ceil
import kotlin.math.floor
import kotlin.math.log10
import kotlin.math.pow


/** Log axis for plotting JavaFX charts. */
class LogAxis : ValueAxis(1.0, 100.0) {

    private val logUpperBound = SimpleDoubleProperty()
    private val logLowerBound = SimpleDoubleProperty()

    private val ANIMATION_TIME = 2000.0
    private val lowerRangeTimeline = Timeline()
    private val upperRangeTimeline = Timeline()

    init {
        logLowerBound.bindToLogOf(lowerBoundProperty())
        logUpperBound.bindToLogOf(upperBoundProperty())
        autoRangingProperty().set(true)
    }

    /** Validates bounds that support logs. */
    private fun validateBounds(lowerBound: Double, upperBound: Double) {
        check(lowerBound > 0 && upperBound > 0 && upperBound > lowerBound) { "Invalid log range: $lowerBound to $upperBound" }
    }

    //region AXIS METHODS

    override fun getRange() = doubleArrayOf(lowerBound, upperBound)

    override fun autoRange(minValue: Double, maxValue: Double, length: Double, labelSize: Double): DoubleArray {
        val min = largestPowerOfTenBelow(if (minValue <= 0.0) 1.0 else minValue)
        val max = smallestPowerOfTenAbove(maxValue)
        return doubleArrayOf(min, if (min == max) min*10 else max)
    }

    private fun largestPowerOfTenBelow(x: Double) = 10.0.pow(floor(log10(x)))
    private fun smallestPowerOfTenAbove(x: Double) = 10.0.pow(ceil(log10(x)))

    override fun setRange(range: Any?, animate: Boolean) {
        if (range is DoubleArray) {
            validateBounds(range[0], range[1])
            if (animate) {
//                try {
                lowerRangeTimeline.keyFrames.clear()
                upperRangeTimeline.keyFrames.clear()
                lowerRangeTimeline.keyFrames.addAll(KeyFrame(Duration.ZERO, KeyValue(lowerBoundProperty(), lowerBoundProperty().get())),
                        KeyFrame(Duration(ANIMATION_TIME), KeyValue(lowerBoundProperty(), range[0])))
                upperRangeTimeline.keyFrames.addAll(KeyFrame(Duration.ZERO, KeyValue(upperBoundProperty(), upperBoundProperty().get())),
                        KeyFrame(Duration(ANIMATION_TIME), KeyValue(upperBoundProperty(), range[1])))
                lowerRangeTimeline.play()
                upperRangeTimeline.play()
            } else {
                lowerBoundProperty().set(range[0])
                upperBoundProperty().set(range[1])
            }
        }
    }

    override fun getValueForDisplay(displayPosition: Double): Number {
        val delta = logUpperBound.get() - logLowerBound.get()
        return when {
            side.isVertical -> 10.0.pow((displayPosition - height) / -height * delta + logLowerBound.get())
            else -> 10.0.pow(displayPosition / width * delta + logLowerBound.get())
        }
    }

    override fun getDisplayPosition(value: Number): Double {
        val delta = logUpperBound.get() - logLowerBound.get()
        val deltaV = log10(value.toDouble()) - logLowerBound.get()
        return when {
            side.isVertical -> (1.0 - deltaV / delta) * height
            else -> deltaV / delta * width
        }
    }

    override fun calculateTickValues(length: Double, range: Any?) = mutableListOf().apply {
        if (range is DoubleArray) {
            var i = log10(maxOf(range[0], 1E-5))
            while (i <= log10(range[1])) {
                add(10.0.pow(i))
                i++
            }
            lastOrNull()?.let { if (it != range[1]) add(it) }
        }
    }

    override fun calculateMinorTickMarks() = calculateTickValues(0.0, range)
            .flatMap { m -> (0..minorTickCount).map { m.toDouble() * 10.0.pow(it/minorTickCount.toDouble()) } }

    override fun getTickMarkLabel(value: Number) = numberFormat(integerDigitRange = 1..10).format(value)

}


/** Binds property to log value of another property. */
private fun DoubleProperty.bindToLogOf(base: DoubleProperty) {
    bind(object : DoubleBinding() {
        init { super.bind(base) }
        override fun computeValue() = log10(base.get())
    })
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy