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

tri.covid19.coda.history.HistoryPanelModel.kt Maven / Gradle / Ivy

/*-
 * #%L
 * coda-app
 * --
 * Copyright (C) 2020 - 2022 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.history

import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.getProperty
import tornadofx.property
import tri.area.usa.UsaAreaLookup
import tri.covid19.ACTIVE
import tri.covid19.CASES
import tri.covid19.DEATHS
import tri.covid19.RECOVERED
import tri.covid19.coda.utils.ChartDataSeries
import tri.covid19.coda.utils.series
import tri.util.DateRange
import tri.util.javaTrim
import tri.covid19.coda.data.CovidTimeSeriesSources.countryData
import tri.covid19.coda.data.CovidTimeSeriesSources.usCbsaData
import tri.covid19.coda.data.CovidTimeSeriesSources.usCountyData
import tri.covid19.coda.data.CovidTimeSeriesSources.usStateData
import tri.covid19.coda.data.perCapita
import tri.timeseries.*
import java.time.LocalDate
import kotlin.reflect.KMutableProperty1

const val COUNTRIES = "Countries and Global Regions"
const val STATES = "US States and Territories"
const val COUNTIES = "US Counties"
const val CBSA = "US CBSA"
val METRIC_OPTIONS = listOf(CASES, DEATHS, RECOVERED, ACTIVE)

/** UI model for history panel. */
class HistoryPanelModel(var onChange: () -> Unit = {}) {

    val regionTypes = listOf(COUNTRIES, STATES, COUNTIES, CBSA)
    var regionLimit by property(10)
    var skipFirst by property(0)
    var minPopulation by property(10000)
    var maxPopulation by property(Int.MAX_VALUE)

    val selectedRegionType = SimpleStringProperty(regionTypes[1]).apply { addListener { _ -> onChange() } }
    val includeRegionActive = SimpleBooleanProperty(false).apply { addListener { _ -> onChange() } }
    val excludeRegionActive = SimpleBooleanProperty(false).apply { addListener { _ -> onChange() } }
    val parentRegion = SimpleStringProperty("United States").apply { addListener { _ -> onChange() } }
    val includeRegion = SimpleStringProperty("").apply { addListener { _ -> if (includeRegionActive.get()) onChange() } }
    val excludeRegion = SimpleStringProperty("").apply { addListener { _ -> if (excludeRegionActive.get()) onChange() } }

    var selectedMetric by property(METRIC_OPTIONS[0])
    var perDay by property(false)
    var perCapita by property(false)
    var logScale by property(false)
    var smooth by property(7)
    var extraSmooth by property(false)
    var sort by property(TimeSeriesSort.ALL)

    //region JAVAFX UI PROPERTIES

    private fun  property(prop: KMutableProperty1<*, T>) = getProperty(prop).apply { addListener { _ -> onChange() } }

    val _regionLimit = property(HistoryPanelModel::regionLimit)
    val _skipFirst = property(HistoryPanelModel::skipFirst)
    val _minPopulation = property(HistoryPanelModel::minPopulation)
    val _maxPopulation = property(HistoryPanelModel::maxPopulation)
    val _selectedMetric = property(HistoryPanelModel::selectedMetric)

    val _perCapita = property(HistoryPanelModel::perCapita)
    val _perDay = property(HistoryPanelModel::perDay)
    val _smooth = property(HistoryPanelModel::smooth)
    val _extraSmooth = property(HistoryPanelModel::extraSmooth)
    val _sort = property(HistoryPanelModel::sort)

    val _logScale = property(HistoryPanelModel::logScale)

    //endregion

    //region INCLUDE/EXCLUDE

    /** Test when a region should be included in the plot. */
    fun include(region: String) = when (includeRegionActive.get()) {
        true -> includeRegion.get().filterOptions.any { it in region.toLowerCase() }
        else -> false
    }

    /** Test when a region should be excluded from the plot. */
    fun exclude(region: String) = when (excludeRegionActive.get()) {
        true -> excludeRegion.get().filterOptions.none { it in region.toLowerCase() }
        else -> true
    }

    private val String.filterOptions: List
        get() = split(",").filter { it.isNotEmpty() }.map { it.javaTrim().toLowerCase() }

    //endregion

    //region DATA

    /** Get historical data for current config. Matching "includes" are first. */
    internal fun historicalData(metric: String? = null): List {
        if (metric == null) {
            val sMetrics = data()
                    .asSequence()
                    .filter { parentRegion.value.isNullOrEmpty() || UsaAreaLookup.area(it.areaId).parent == UsaAreaLookup.area(parentRegion.value) }
                    .filter { it.metric == if (perCapita) selectedMetric.perCapita else selectedMetric }
                    .filter { UsaAreaLookup.area(it.areaId).population.let { it == null || it in minPopulation..maxPopulation } }
                    .filter { exclude(it.areaId) }
                    .sortedByDescending { it.sortMetric }
                    .toList()
            return (sMetrics.filter { include(it.areaId) } + sMetrics).take(regionLimit + skipFirst).drop(skipFirst)
        } else {
            val regions = historicalData(null).map { it.areaId }
            return data().filter { it.metric == if (perCapita) metric.perCapita else metric }
                .filter { it.areaId in regions }
                .sortedBy { regions.indexOf(it.areaId) }
        }
    }

    private val TimeSeries.sortMetric
        get() = when(sort) {
            TimeSeriesSort.ALL -> lastValue
            TimeSeriesSort.LAST14 -> lastValue - values.getOrElse(values.size - 14) { 0.0 }
            TimeSeriesSort.LAST7 -> lastValue - values.getOrElse(values.size - 7) { 0.0 }
            TimeSeriesSort.POPULATION -> UsaAreaLookup.area(areaId).population?.toDouble() ?: 0.0
            TimeSeriesSort.PEAK7 -> values.deltas().movingAverage(7).maxOrNull() ?: 0.0
            TimeSeriesSort.PEAK14 -> values.deltas().movingAverage(14).maxOrNull() ?: 0.0
        }

    internal fun data() = when (selectedRegionType.get()) {
        COUNTRIES -> countryData(includeGlobal = true)
        STATES -> usStateData(includeUS = true)
        COUNTIES -> usCountyData()
        CBSA -> usCbsaData()
        else -> throw IllegalStateException()
    }

    //endregion

    //region

    /** Smooth using current settings. */
    internal val List.smoothed: List
        get() {
            var res = this
            if (smooth != 1) {
                res = res.map { it.movingAverage(smooth, false) }
                if (extraSmooth) {
                    res = res.map { it.movingAverage(3, false).movingAverage(3, false) }
                }
            }
            return res
        }

    /** Get smoothed version of data. */
    internal fun smoothedData(metric: String? = null) = historicalData(metric).smoothed

    /** Plot counts by date. */
    internal fun historicalDataSeries(): Pair> {
        var metrics = smoothedData()
        if (perDay) {
            metrics = metrics.map { it.deltas() }
        }
        val domain = metrics.dateRangeOrNull() ?: DateRange(LocalDate.now(), LocalDate.now())
        return domain to metrics.map { series(it.areaId, domain, it) }
    }

    /** Plot growth vs counts. */
    internal fun hubbertDataSeries() = smoothedData().map { it.hubbertSeries(1) }
                .map { series(it.first.areaId, it.first.domain.shift(1, 0), it.first, it.second) }

    //endregion
}

/** Options for sorting time series retrieved for history panel. */
enum class TimeSeriesSort {
    ALL,
    LAST7,
    LAST14,
    PEAK7,
    PEAK14,
    POPULATION
}

/** Creates Hubbert series from monotonic metric. */
fun TimeSeries.hubbertSeries(window: Int): Pair {
    val totals = movingAverage(window).restrictNumberOfStartingZerosTo(0)
    val growths = totals.symmetricGrowth()
    return totals to growths
}

/** Creates doubling-change series from monotonic metric. */
fun TimeSeries.changeDoublingDataSeries(window: Int): Pair {
    return movingAverage(window).doublingTimes() to movingAverage(window).deltas()
}

/** Creates total-doubling series from monotonic metric. */
fun TimeSeries.doublingTotalDataSeries(window: Int): Pair {
    return movingAverage(window) to movingAverage(window).doublingTimes()
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy