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

commonMain.jetbrains.datalore.plot.config.PlotConfigUtil.kt Maven / Gradle / Ivy

There is a newer version: 4.5.3-alpha1
Show newest version
/*
 * Copyright (c) 2019. JetBrains s.r.o.
 * Use of this source code is governed by the MIT license that can be found in the LICENSE file.
 */

package jetbrains.datalore.plot.config

import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.plot.base.*
import jetbrains.datalore.plot.base.scale.transform.Transforms
import jetbrains.datalore.plot.base.scale.transform.Transforms.ensureApplicableDomain
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.builder.assemble.PlotFacets
import jetbrains.datalore.plot.builder.assemble.TypedScaleMap
import jetbrains.datalore.plot.builder.scale.ContinuousOnlyMapperProvider
import jetbrains.datalore.plot.builder.scale.DiscreteOnlyMapperProvider
import jetbrains.datalore.plot.builder.scale.ScaleProvider
import jetbrains.datalore.plot.builder.scale.ScaleProviderHelper
import jetbrains.datalore.plot.common.data.SeriesUtil
import jetbrains.datalore.plot.config.PlotConfig.Companion.PLOT_COMPUTATION_MESSAGES

object PlotConfigUtil {

    fun toLayersDataByTile(dataByLayer: List, facets: PlotFacets): List> {
        // Plot consists of one or more tiles,
        // each tile consists of layers

        val layersDataByTile: List> =
            if (facets.isDefined) {
                List(facets.numTiles) { ArrayList() }
            } else {
                // Just one tile.
                listOf(ArrayList())
            }

        for (layerData in dataByLayer) {
            if (facets.isDefined) {
                val dataByTile = facets.dataByTile(layerData)
                for ((tileIndex, tileData) in dataByTile.withIndex()) {
                    layersDataByTile[tileIndex].add(tileData)
                }
            } else {
                layersDataByTile[0].add(layerData)
            }
        }
        return layersDataByTile
    }

    // backend
    fun addComputationMessage(accessor: OptionsAccessor, message: String?) {
        require(message != null)
        val computationMessages = ArrayList(
            getComputationMessages(
                accessor
            )
        )
        computationMessages.add(message)
        accessor.update(PLOT_COMPUTATION_MESSAGES, computationMessages)
    }

    // frontend
    fun findComputationMessages(spec: Map): List {
        val result: List =
            when {
                PlotConfig.isPlotSpec(spec) -> getComputationMessages(spec)
                PlotConfig.isGGBunchSpec(spec) -> {
                    val bunchConfig = BunchConfig(spec)
                    bunchConfig.bunchItems.flatMap { getComputationMessages(it.featureSpec) }
                }
                else -> throw RuntimeException("Unexpected plot spec kind: ${PlotConfig.specKind(spec)}")
            }

        return result.distinct()
    }

    private fun getComputationMessages(opts: Map): List {
        return getComputationMessages(OptionsAccessor(opts))
    }

    private fun getComputationMessages(accessor: OptionsAccessor): List {
        return accessor.getList(PLOT_COMPUTATION_MESSAGES).map { it as String }
    }

    private fun getVarBindings(
        layerConfigs: List,
        excludeStatVariables: Boolean
    ): List {
        return layerConfigs
            .flatMap { it.varBindings }
            .filter { !(excludeStatVariables && it.variable.isStat) }
    }

    internal fun createScaleProviders(
        layerConfigs: List,
        scaleConfigs: List>,
        excludeStatVariables: Boolean
    ): Map, ScaleProvider<*>> {

        val aesSet = getVarBindings(layerConfigs, excludeStatVariables).map { it.aes }.toSet() +
                setOf(Aes.X, Aes.Y)

        val scaleProviderByAes = HashMap, ScaleProvider<*>>()

        // Create 'configured' scale providers.
        for (scaleConfig in scaleConfigs) {
            val scaleProvider = scaleConfig.createScaleProvider()
            scaleProviderByAes[scaleConfig.aes] = scaleProvider
        }

        // Append all the rest scale providers.
        return aesSet.associateWith {
            ScaleProviderHelper.getOrCreateDefault(it, scaleProviderByAes)
        }
    }


    private fun associateAesWithMappedVariables(varBindings: List): Map, List> {
        val variablesByMappedAes: MutableMap, MutableList> = HashMap()
        for (varBinding in varBindings) {
            val aes = varBinding.aes
            val variable = varBinding.variable
            variablesByMappedAes.getOrPut(aes) { ArrayList() }.add(variable)
        }
        return variablesByMappedAes
    }

    private fun associateVarBindingsWithData(
        layerConfigs: List,
        excludeStatVariables: Boolean
    ): Map {
        val dataByVarBinding: Map = layerConfigs
            .flatMap { layer ->
                layer.varBindings
                    .filter { !(excludeStatVariables && it.variable.isStat) }
                    .map { it to layer.combinedData }
            }.toMap()

        // Check that all variables in bindings are mapped to data.
        for ((varBinding, data) in dataByVarBinding) {
            val variable = varBinding.variable
            require(data.has(variable)) {
                "Undefined variable: '${variable.name}'. Variables in data frame: ${
                    data.variables().map { "'${it.name}'" }
                }"
            }
        }

        return dataByVarBinding
    }

    internal fun createTransforms(
        layerConfigs: List,
        scaleProviderByAes: Map, ScaleProvider<*>>,
        excludeStatVariables: Boolean
    ): Map, Transform> {
        val dataByVarBinding = associateVarBindingsWithData(
            layerConfigs,
            excludeStatVariables
        )

        val variablesByMappedAes = associateAesWithMappedVariables(
            getVarBindings(layerConfigs, excludeStatVariables)
        )

        // All aes used in bindings.
        val aesSet: Set> = dataByVarBinding.keys.map { it.aes }.toSet()

        // Compute domains for all aes with discrete input.

        // Extract "discrete" aes set.
        val discreteAesSet: MutableSet> = HashSet()
        for (aes in aesSet) {
            val scaleProvider = scaleProviderByAes.getValue(aes)
            if (scaleProvider.discreteDomain) {
                discreteAesSet.add(aes)
            } else if (variablesByMappedAes.containsKey(aes)) {
                val variables = variablesByMappedAes.getValue(aes)
                val anyNotNumericData = variables.any {
                    val data = dataByVarBinding.getValue(VarBinding(it, aes))
                    if (data.isEmpty(it)) {
                        isDiscreteScaleForEmptyData(scaleProvider)
                    } else {
                        !data.isNumeric(it)
                    }
                }
                if (anyNotNumericData) {
                    discreteAesSet.add(aes)
                }
            }
        }

        // If axis is 'discrete' then put all 'positional' aes to 'discrete' aes set.
        val discreteX: Boolean = discreteAesSet.any { Aes.isPositionalX(it) }
        val discreteY: Boolean = discreteAesSet.any { Aes.isPositionalY(it) }
        for (aes in aesSet) {
            if (discreteX && Aes.isPositionalX(aes)) {
                discreteAesSet.add(aes)
            }
            if (discreteY && Aes.isPositionalY(aes)) {
                discreteAesSet.add(aes)
            }
        }

        // Discrete domains from 'data'.
        val discreteDataByVarBinding: Map = dataByVarBinding.filterKeys {
            it.aes in discreteAesSet
        }
        val discreteDomainByAes = HashMap, LinkedHashSet>()
        for ((varBinding, data) in discreteDataByVarBinding) {
            val aes = varBinding.aes
            val variable = varBinding.variable
            val factors = data.distinctValues(variable)
            discreteDomainByAes.getOrPut(aes) { LinkedHashSet() }.addAll(factors)
        }

        // create discrete transforms.
        val discreteTransformByAes = HashMap, DiscreteTransform>()
        for (aes in discreteAesSet) {
            val scaleProvider = scaleProviderByAes.getValue(aes)
            val scaleBreaks = scaleProvider.breaks ?: emptyList()
            val domainValues = discreteDomainByAes.getValue(aes)
            val effectiveDomain = (scaleBreaks + domainValues).distinct()
            val transform = DiscreteTransform(
                domainValues = effectiveDomain,
                domainLimits = (scaleProvider.limits ?: emptyList()).filterNotNull()
            )
            discreteTransformByAes[aes] = transform
        }

        // create continuous transforms.
        val continuousTransformByAes = HashMap, ContinuousTransform>()
        val continuousAesSet = aesSet - discreteAesSet
        for (aes in continuousAesSet) {
            continuousTransformByAes[aes] = scaleProviderByAes.getValue(aes).continuousTransform
        }

        // All 'positional' aes must use the same transform.
        fun axisTransform(axisAes: List>, discrete: Boolean): Transform {
            @Suppress("CascadeIf")
            return if (discrete) {
                val domainValues = LinkedHashSet()
                val domainLimits = LinkedHashSet()
                for (aes in axisAes) {
                    val transform = discreteTransformByAes.getValue(aes)
                    domainValues.addAll(transform.domainValues)
                    domainLimits.addAll(transform.domainLimits)
                }
                DiscreteTransform(domainValues, domainLimits.toList())
            } else if (axisAes.isEmpty()) {
                Transforms.IDENTITY
            } else {
                continuousTransformByAes.getValue(axisAes.first())
            }
        }

        val xAxisTransform = axisTransform(aesSet.filter { Aes.isPositionalX(it) }, discreteX)
        val yAxisTransform = axisTransform(aesSet.filter { Aes.isPositionalY(it) }, discreteY)

        // Replace 'positional' transforms with 'axis' transform
        // and make sure that the mpp contains Aes.X and Aes.Y keys.
        @Suppress("UnnecessaryVariable")
        val allTransformsByAes: Map, Transform> = (discreteTransformByAes + continuousTransformByAes)
            .mapValues { (aes, trans) ->
                when {
                    Aes.isPositionalX(aes) -> xAxisTransform
                    Aes.isPositionalY(aes) -> yAxisTransform
                    else -> trans
                }
            } + mapOf(
            Aes.X to xAxisTransform,
            Aes.Y to yAxisTransform,
        )

        return allTransformsByAes
    }

    internal fun createScales(
        layerConfigs: List,
        transformByAes: Map, Transform>,
        scaleProviderByAes: Map, ScaleProvider<*>>,
        excludeStatVariables: Boolean
    ): TypedScaleMap {

        val dataByVarBinding = associateVarBindingsWithData(
            layerConfigs,
            excludeStatVariables
        )

        val variablesByMappedAes = associateAesWithMappedVariables(
            getVarBindings(layerConfigs, excludeStatVariables)
        )

        // All aes used in bindings.
        val aesSet: Set> = dataByVarBinding.keys.map { it.aes }.toSet()

        // Compute domains for 'continuous' data
        // but exclude all 'positional' aes.
        //
        // Domains for X, Y axis are computed later.
        //      See: PlotAssemblerUtil.computePlotDryRunXYRanges()

        val continuousDomainByAesRaw = HashMap, ClosedRange?>()

        // Continuois domains from 'data'
        for ((varBinding, data) in dataByVarBinding) {
            val aes = varBinding.aes
            val variable = varBinding.variable
            val transform = transformByAes.getValue(aes)

            if (transform is ContinuousTransform && !Aes.isPositionalXY(aes)) {
                continuousDomainByAesRaw[aes] = SeriesUtil.span(
                    continuousDomainByAesRaw[aes],
                    computeContinuousDomain(data, variable, transform)
                )
            }
        }

        // make sure all continuous domains are 'applicable range' (not emprty and not null)
        val continuousDomainByAes = continuousDomainByAesRaw.mapValues {
            val aes = it.key
            val transform: ContinuousTransform = transformByAes.getValue(aes) as ContinuousTransform
            ensureApplicableDomain(it.value, transform)
        }

        // Create scales for all aes.
        fun defaultScaleName(aes: Aes<*>): String {
            return if (variablesByMappedAes.containsKey(aes)) {
                val variables = variablesByMappedAes.getValue(aes)
                val labels = variables.map { it.label }.distinct()
                if (labels.size > 1 && (aes == Aes.X || aes == Aes.Y)) {
                    // Don't show multiple labels on X,Y axis.
                    aes.name
                } else {
                    labels.joinToString()
                }
            } else {
                aes.name
            }
        }

        val scaleByAes = HashMap, Scale<*>>()
        for (aes in aesSet + setOf(Aes.X, Aes.Y)) {
            val defaultName = defaultScaleName(aes)
            val scaleProvider = scaleProviderByAes.getValue(aes)

            @Suppress("MoveVariableDeclarationIntoWhen")
            val transform = transformByAes.getValue(aes)

            val scale = when (transform) {
                is DiscreteTransform -> scaleProvider.createScale(defaultName, transform.domainValues)
                else -> if (continuousDomainByAes.containsKey(aes)) {
                    val continuousDomain = continuousDomainByAes.getValue(aes)
                    scaleProvider.createScale(defaultName, continuousDomain)
                } else {
                    // Positional aes & continuous domain.
                    // The domain doesn't matter - it will be computed later (see: PlotAssemblerUtil.computePlotDryRunXYRanges())
                    scaleProvider.createScale(defaultName, ClosedRange.singleton(0.0))
                }
            }

            scaleByAes[aes] = scale
        }

//        if(FLIP_AXIS_COORD) {
//            val xScale = scaleByAes.getValue(Aes.X)
//            scaleByAes[Aes.X] = scaleByAes.getValue(Aes.Y)
//            scaleByAes[Aes.Y] = xScale
//        }
        return TypedScaleMap(scaleByAes)
    }

    /**
     * ToDo: 'domans' should be computed on 'transformed' data.
     */
    private fun computeContinuousDomain(
        data: DataFrame,
        variable: DataFrame.Variable,
        transform: ContinuousTransform
    ): ClosedRange? {
        return if (!transform.hasDomainLimits()) {
            data.range(variable)
        } else {
            val filtered = data.getNumeric(variable).filter {
                transform.isInDomain(it)
            }
            SeriesUtil.range(filtered)
        }
    }


//    /**
//     * ToDo: move to SeriesUtil (or better place)
//     */
//    private fun ensureApplicableDomain(
//        dataRange: ClosedRange?,
//        transform: ContinuousTransform
//    ): ClosedRange {
//        return when {
//            dataRange == null ->
//                transform.createApplicableDomain(0.0)
//            SeriesUtil.isSubTiny(dataRange) ->
//                transform.createApplicableDomain(dataRange.lowerEnd)
//            else ->
//                dataRange
//        }
//    }

    private fun isDiscreteScaleForEmptyData(scaleProvider: ScaleProvider<*>): Boolean {
        // Empty data is neither 'discrete' nor 'numeric'.
        // Which scale to build?
        if (scaleProvider.discreteDomain) return true

        val mapperProvider = scaleProvider.mapperProvider
        if (mapperProvider is DiscreteOnlyMapperProvider) return true
        if (mapperProvider is ContinuousOnlyMapperProvider) return false

        val breaks = scaleProvider.breaks
        val limits = scaleProvider.limits

        val breaksAreDiscrete = breaks?.let {
            it.any { !(it is Number) }
        } ?: false

        val limitsAreDiscrete = limits?.let {
            // Not a list of 2 numbers.
            when {
                it.size > 2 -> true
                else -> it.filterNotNull().any { !(it is Number) }
            }
        } ?: false

        return breaksAreDiscrete || limitsAreDiscrete
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy