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

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

There is a newer version: 4.5.3-alpha1
Show newest version
/*
 * Copyright (c) 2022. 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.plot.base.*
import jetbrains.datalore.plot.base.scale.transform.Transforms
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.builder.scale.ContinuousOnlyMapperProvider
import jetbrains.datalore.plot.builder.scale.DiscreteOnlyMapperProvider
import jetbrains.datalore.plot.builder.scale.MapperProvider
import jetbrains.datalore.plot.builder.scale.ScaleProvider
import jetbrains.datalore.plot.config.PlotConfigUtil.createPlotAesBindingSetup

internal object PlotConfigTransforms {
    internal fun createTransforms(
        layerConfigs: List,
        scaleProviderByAes: Map, ScaleProvider<*>>,
        mapperProviderByAes: Map, MapperProvider<*>>,
        excludeStatVariables: Boolean
    ): Map, Transform> {
        // X,Y scale - always.
        check(scaleProviderByAes.containsKey(Aes.X))
        check(scaleProviderByAes.containsKey(Aes.Y))
        check(mapperProviderByAes.containsKey(Aes.X))
        check(mapperProviderByAes.containsKey(Aes.Y))

        val setup = createPlotAesBindingSetup(layerConfigs, excludeStatVariables)

        // All aes used in bindings and x/y aes.
        // Exclude "stat positional" because we don't know which of axis they will use (i.e. orientation="y").
        val aesSet = setup.mappedAesWithoutStatPositional() + setOf(Aes.X, Aes.Y)
        val xAesSet = aesSet.filter { Aes.isPositionalX(it) }.toSet()
        val yAesSet = aesSet.filter { Aes.isPositionalY(it) }.toSet()

        val dataByVarBinding = setup.dataByVarBinding
        val variablesByMappedAes = setup.variablesByMappedAes

        // 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, mapperProviderByAes.getValue(aes))
                    } 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 { it in xAesSet }
        val discreteY: Boolean = discreteAesSet.any { it in yAesSet }
        if (discreteX) {
            discreteAesSet.addAll(xAesSet)
        }
        if (discreteY) {
            discreteAesSet.addAll(yAesSet)
        }

        // 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 = if (discreteDomainByAes.containsKey(aes)) {
                discreteDomainByAes.getValue(aes)
            } else if (aes in setOf(Aes.X, Aes.Y)) {
                // Aes x/y are always in the list, thus it's possible there is no data associated with x/y aes.
                emptySet()
            } else {
                throw IllegalStateException("No discrete data found for aes $aes")
            }
            val effectiveDomain = (scaleBreaks + domainValues).distinct()

            val transformDomainValues = if (scaleProvider.discreteDomainReverse) {
                effectiveDomain.reversed()
            } else {
                effectiveDomain
            }
            val transformDomainLimits = (scaleProvider.limits ?: emptyList()).filterNotNull().let {
                if (scaleProvider.discreteDomainReverse) {
                    it.reversed()
                } else {
                    it
                }
            }

            val transform = DiscreteTransform(
                domainValues = transformDomainValues, domainLimits = transformDomainLimits
            )
            discreteTransformByAes[aes] = transform
        }

        // Create continuous transforms.
        val continuousTransformByAes = HashMap, ContinuousTransform>()
        val continuousAesSet = aesSet - discreteAesSet
        for (aes in continuousAesSet) {
            if (Aes.isPositionalXY(aes) && !(aes == Aes.X || aes == Aes.Y)) {
                // Exclude all 'positional' aes except X, Y.
                continue
            }
            val scaleProvider = scaleProviderByAes.getValue(aes)
            val transform = scaleProvider.continuousTransform
            val limits = toContinuousLims(scaleProvider.limits, transform)
            val effectiveTransform = limits?.let {
                Transforms.continuousWithLimits(transform, it)
            } ?: transform
            continuousTransformByAes[aes] = effectiveTransform
        }

        // All 'positional' aes must use the same transform.
        fun joinDiscreteTransforms(axisAes: List>): Transform {
            return DiscreteTransform.join(axisAes.map { discreteTransformByAes.getValue(it) })
        }

        val xAxisTransform = when (discreteX) {
            true -> joinDiscreteTransforms(xAesSet.toList())
            false -> continuousTransformByAes.getValue(Aes.X)
        }
        val yAxisTransform = when (discreteY) {
            true -> joinDiscreteTransforms(yAesSet.toList())
            false -> continuousTransformByAes.getValue(Aes.Y)
        }

        // Replace all 'positional' transforms with the 'axis' transform.
        val transformByPositionalAes: Map, Transform> =
            xAesSet.associateWith { xAxisTransform } +
                    yAesSet.associateWith { yAxisTransform }

        return discreteTransformByAes + continuousTransformByAes + transformByPositionalAes
    }

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

        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
    }

    /**
     * rawLims : A pair of 2 "nullable" numbers (the second num can be omitted).
     */
    private fun toContinuousLims(rawLims: List?, transform: ContinuousTransform): Pair? {
        if (rawLims == null) return null
        val lims2 = rawLims.take(2)
        val lims2d = lims2.map {
            if (it != null) {
                require(it is Number && it.toDouble().isFinite()) { "Numbers expected: limits=$lims2" }
                it.toDouble().let { if (transform.isInDomain(it)) it else null }
            } else {
                null
            }
        }

        val limsSorted = when (lims2d.filterNotNull().size) {
            0 -> null
            2 -> {
                @Suppress("UNCHECKED_CAST") (lims2d as List).sorted()
            }
            else -> lims2d + listOf(null)
        }

        return limsSorted?.let { Pair(it[0], it[1]) }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy