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

commonMain.jetbrains.datalore.plot.builder.PlotUtil.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.builder

import jetbrains.datalore.base.gcommon.base.Preconditions.checkState
import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.base.gcommon.collect.Iterables
import jetbrains.datalore.base.gcommon.collect.Sets
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.values.Pair
import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.Aesthetics
import jetbrains.datalore.plot.base.GeomContext
import jetbrains.datalore.plot.base.PositionAdjustment
import jetbrains.datalore.plot.base.aes.AestheticsBuilder
import jetbrains.datalore.plot.base.aes.AestheticsBuilder.Companion.listMapper
import jetbrains.datalore.plot.base.data.DataFrameUtil
import jetbrains.datalore.plot.base.scale.Mappers
import jetbrains.datalore.plot.builder.assemble.GeomContextBuilder
import jetbrains.datalore.plot.common.data.SeriesUtil.isFinite
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sign

object PlotUtil {
    internal fun createLayerPos(layer: GeomLayer, aes: Aesthetics): PositionAdjustment {
        return layer.createPos(object : PosProviderContext {
            override val aesthetics: Aesthetics
                get() = aes

            override val groupCount: Int
                    by lazy {
                        val set = Sets.newHashSet(aes.groups())
                        set.size
                    }
        })
    }

    fun computeLayerDryRunXYRanges(
        layer: GeomLayer,
        aes: Aesthetics
    ): Pair?, ClosedRange?> {
        val geomCtx = GeomContextBuilder().aesthetics(aes).build()

        val rangesAfterPosAdjustment =
            computeLayerDryRunXYRangesAfterPosAdjustment(layer, aes, geomCtx)
        val rangesAfterSizeExpand =
            computeLayerDryRunXYRangesAfterSizeExpand(layer, aes, geomCtx)

        var rangeX = rangesAfterPosAdjustment.first
        if (rangeX == null) {
            rangeX = rangesAfterSizeExpand.first
        } else if (rangesAfterSizeExpand.first != null) {
            rangeX = rangeX.span(rangesAfterSizeExpand.first!!)
        }

        var rangeY = rangesAfterPosAdjustment.second
        if (rangeY == null) {
            rangeY = rangesAfterSizeExpand.second
        } else if (rangesAfterSizeExpand.second != null) {
            rangeY = rangeY.span(rangesAfterSizeExpand.second!!)
        }

        return Pair(rangeX, rangeY)
    }

    private fun combineRanges(aesList: List>, aesthetics: Aesthetics): ClosedRange? {
        var result: ClosedRange? = null
        for (aes in aesList) {
            val range = aesthetics.range(aes)
            if (range != null) {
                result = result?.span(range) ?: range
            }
        }
        return result
    }

    private fun computeLayerDryRunXYRangesAfterPosAdjustment(
        layer: GeomLayer, aes: Aesthetics, geomCtx: GeomContext
    ): Pair?, ClosedRange?> {
        val posAesX = Iterables.toList(Aes.affectingScaleX(layer.renderedAes()))
        val posAesY = Iterables.toList(Aes.affectingScaleY(layer.renderedAes()))

        val pos = createLayerPos(layer, aes)
        if (pos.isIdentity) {
            // simplified ranges
            val rangeX = combineRanges(posAesX, aes)
            val rangeY = combineRanges(posAesY, aes)
            return Pair(rangeX, rangeY)
        }

        var adjustedMinX = 0.0
        var adjustedMaxX = 0.0
        var adjustedMinY = 0.0
        var adjustedMaxY = 0.0
        var rangesInited = false

        val cardinality = posAesX.size * posAesY.size
        val px = arrayOfNulls(cardinality)
        val py = arrayOfNulls(cardinality)
        for (p in aes.dataPoints()) {
            var i = -1
            for (aesX in posAesX) {
                val valX = p.numeric(aesX)
                for (aesY in posAesY) {
                    val valY = p.numeric(aesY)
                    i++
                    px[i] = valX
                    py[i] = valY
                }
            }

            while (i >= 0) {
                if (px[i] != null && py[i] != null) {
                    val x = px[i]
                    val y = py[i]
                    if (isFinite(x) && isFinite(y)) {
                        val newLoc = pos.translate(DoubleVector(x!!, y!!), p, geomCtx)
                        val adjustedX = newLoc.x
                        val adjustedY = newLoc.y
                        if (rangesInited) {
                            adjustedMinX = min(adjustedX, adjustedMinX)
                            adjustedMaxX = max(adjustedX, adjustedMaxX)
                            adjustedMinY = min(adjustedY, adjustedMinY)
                            adjustedMaxY = max(adjustedY, adjustedMaxY)
                        } else {
                            adjustedMaxX = adjustedX
                            adjustedMinX = adjustedMaxX
                            adjustedMaxY = adjustedY
                            adjustedMinY = adjustedMaxY
                            rangesInited = true
                        }
                    }
                }
                i--
            }
        }

        // X range
        val xRange = if (rangesInited)
            ClosedRange(adjustedMinX, adjustedMaxX)
        else
            null

        val yRange = if (rangesInited)
            ClosedRange(adjustedMinY, adjustedMaxY)
        else
            null
        return Pair(xRange, yRange)
    }

    private fun computeLayerDryRunXYRangesAfterSizeExpand(
        layer: GeomLayer,
        aesthetics: Aesthetics,
        geomCtx: GeomContext
    ): Pair?, ClosedRange?> {
        val renderedAes = layer.renderedAes()
        val computeExpandX = renderedAes.contains(Aes.WIDTH)
        val computeExpandY = renderedAes.contains(Aes.HEIGHT)
        val rangeX = if (computeExpandX)
            computeLayerDryRunRangeAfterSizeExpand(
                Aes.X,
                Aes.WIDTH,
                aesthetics,
                geomCtx
            )
        else
            null
        val rangeY = if (computeExpandY)
            computeLayerDryRunRangeAfterSizeExpand(
                Aes.Y,
                Aes.HEIGHT,
                aesthetics,
                geomCtx
            )
        else
            null

        return Pair(rangeX, rangeY)
    }

    private fun computeLayerDryRunRangeAfterSizeExpand(
        locationAes: Aes, sizeAes: Aes, aesthetics: Aesthetics, geomCtx: GeomContext
    ): ClosedRange? {
        val locations = aesthetics.numericValues(locationAes).iterator()
        val sizes = aesthetics.numericValues(sizeAes).iterator()

        val resolution = geomCtx.getResolution(locationAes)
        val minMax = doubleArrayOf(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY)

        for (i in 0 until aesthetics.dataPointCount()) {
            if (!locations.hasNext()) {
                throw IllegalStateException("Index is out of bounds: $i for $locationAes")
            }
            if (!sizes.hasNext()) {
                throw IllegalStateException("Index is out of bounds: $i for $sizeAes")
            }
            val loc = locations.next()
            val size = sizes.next()
            if (isFinite(loc) && isFinite(size)) {
                val expand = resolution * (size!! / 2)
                updateExpandedMinMax(loc!!, expand, minMax)
            }
        }

        return if (minMax[0] <= minMax[1])
            ClosedRange(minMax[0], minMax[1])
        else
            null
    }

    private fun updateExpandedMinMax(value: Double, expand: Double, expandedMinMax: DoubleArray) {
        expandedMinMax[0] = min(value - expand, expandedMinMax[0])
        expandedMinMax[1] = max(value + expand, expandedMinMax[1])
    }

    fun createLayerDryRunAesthetics(layer: GeomLayer): Aesthetics {
        val dryRunMapperByAes = HashMap, (Double?) -> Double?>()
        for (aes in layer.renderedAes()) {
            if (aes.isNumeric) {
                // safe cast: 'numeric' aes is always 
                @Suppress("UNCHECKED_CAST")
                dryRunMapperByAes[aes as Aes] = Mappers.IDENTITY
            }
        }

        val mappers = prepareLayerAestheticMappers(layer, dryRunMapperByAes)
        return createLayerAesthetics(layer, mappers, emptyMap())
    }

    internal fun prepareLayerAestheticMappers(
        layer: GeomLayer,
        sharedNumericMappers: Map, (Double?) -> Double?>
    ): Map, (Double?) -> Any?> {

        val mappers = HashMap, (Double?) -> Any?>(sharedNumericMappers)
        for (aes in layer.renderedAes()) {
            var mapper: ((Double?) -> Any?)? = sharedNumericMappers[aes]
            if (mapper == null) {
                // positional aes share their mappers
                if (Aes.isPositionalX(aes)) {
                    mapper = sharedNumericMappers[Aes.X]
                } else if (Aes.isPositionalY(aes)) {
                    mapper = sharedNumericMappers[Aes.Y]
                }
            }
            if (mapper == null && layer.hasBinding(aes)) {
                mapper = layer.scaleMap[aes].mapper
            }

            if (mapper != null) {
                mappers[aes] = mapper
            }
        }
        return mappers
    }

    internal fun createLayerAesthetics(
        layer: GeomLayer,
        sharedMappers: Map, (Double?) -> Any?>,
        overallNumericDomains: Map, ClosedRange>
    ): Aesthetics {

        val aesBuilder = AestheticsBuilder()
        aesBuilder.group(layer.group)
        for ((aes, domain) in overallNumericDomains) {
            sharedMappers[aes]?.let { mapper ->
                val range = ClosedRange(
                    mapper(domain.lowerEnd) as Double,
                    mapper(domain.upperEnd) as Double
                )
                aesBuilder.overallRange(aes, range)
            }
        }

        var hasPositionalConstants = false
        for (aes in layer.renderedAes()) {
            if (Aes.isPositional(aes) && layer.hasConstant(aes)) {
                hasPositionalConstants = true
                break
            }
        }

        val data = layer.dataFrame
        var dataPointCount: Int? = null
        for (aes in layer.renderedAes()) {
            @Suppress("UNCHECKED_CAST", "NAME_SHADOWING")
            val aes = aes as Aes

            val mapperOption = sharedMappers[aes]
            if (layer.hasConstant(aes)) {
                // Constant overrides binding
                val v = layer.getConstant(aes)
                aesBuilder.constantAes(aes, asAesValue(aes, v, mapperOption))
            } else {
                // No constant - look-up aes mapping
                if (layer.hasBinding(aes)) {
                    checkState(mapperOption != null, "No scale mapper defined for aesthetic $aes")

                    // variable at this point must be either STAT or TRANSFORM (but not ORIGIN)
                    val transformVar = DataFrameUtil.transformVarFor(aes)
                    checkState(data.has(transformVar), "Undefined var $transformVar for aesthetic $aes")
                    val numericValues = data.getNumeric(transformVar)

                    if (dataPointCount == null) {
                        dataPointCount = numericValues.size
                    } else {
                        checkState(
                            dataPointCount == numericValues.size,
                            "" + aes + " expected data size=" + dataPointCount + " was size=" + numericValues.size
                        )
                    }

                    if (dataPointCount == 0 && hasPositionalConstants) {
                        // put constant instead of empty list
                        aesBuilder.constantAes(aes, layer.aestheticsDefaults.defaultValue(aes))
                    } else {
                        val integerFunction = listMapper(numericValues, mapperOption as (Double?) -> Any?)
                        aesBuilder.aes(aes, integerFunction)
                    }
                } else {
                    // apply default
                    val v = layer.getDefault(aes)
                    aesBuilder.constantAes(
                        aes,
                        asAesValue(aes, v, mapperOption)
                    )
                }
            }
        }

        if (dataPointCount != null && dataPointCount > 0) {
            aesBuilder.dataPointCount(dataPointCount)
        } else if (hasPositionalConstants) {
            // some geoms (point, abline etc) can be plotted with only constants
            aesBuilder.dataPointCount(1)
        }

        return aesBuilder.build()
    }

    private fun  asAesValue(aes: Aes<*>, dataValue: T, mapperOption: ((Double?) -> T?)?): T {
        return if (aes.isNumeric && mapperOption != null) {
            mapperOption(dataValue as? Double)
                ?: throw IllegalArgumentException("Can't map $dataValue to aesthetic $aes")
        } else dataValue
    }

    fun rangeWithExpand(layer: GeomLayer, aes: Aes, range: ClosedRange?): ClosedRange? {
        if (range == null) return null

        // expand X-range to ensure that the data is placed some distance away from the axes.
        // see: https://ggplot2.tidyverse.org/current/scale_continuous.html - expand
//        val mulExp = getMultiplicativeExpand(layer, aes)
//        val addExp = getAdditiveExpand(layer, aes)

        val scale = layer.scaleMap[aes]
        val mulExp = scale.multiplicativeExpand
        val addExp = scale.additiveExpand
        val lowerEndpoint = range.lowerEnd
        val upperEndpoint = range.upperEnd

        val length = upperEndpoint - lowerEndpoint
        var lowerExpand = addExp + length * mulExp
        var upperExpand = lowerExpand
        if (layer.rangeIncludesZero(aes)) {
            // zero-based plots (like bar) - do not 'expand' on the zero-end
            if (lowerEndpoint == 0.0 ||
                upperEndpoint == 0.0 ||
                sign(lowerEndpoint) == sign(upperEndpoint)
            ) {
                if (lowerEndpoint >= 0) {
                    lowerExpand = 0.0
                } else {
                    upperExpand = 0.0
                }
            }
        }

        return ClosedRange(lowerEndpoint - lowerExpand, upperEndpoint + upperExpand)
    }

//    private fun getMultiplicativeExpand(layer: GeomLayer, aes: Aes): Double {
//        val scale = findBoundScale(layer, aes)
//        return scale?.multiplicativeExpand ?: 0.0
//    }
//
//    private fun getAdditiveExpand(layer: GeomLayer, aes: Aes): Double {
//        val scale = findBoundScale(layer, aes)
//        return scale?.additiveExpand ?: 0.0
//    }
//
//    private fun findBoundScale(layer: GeomLayer, aes: Aes<*>): Scale<*>? {
//        if (layer.hasBinding(aes)) {
//            return layer.scaleMap[aes]
//        }
//        if (Aes.isPositional(aes)) {
//            val horizontal = Aes.isPositionalX(aes)
//            for (rendered in layer.renderedAes()) {
//                if (layer.hasBinding(rendered)) {
//                    if (horizontal && Aes.isPositionalX(rendered) || !horizontal && Aes.isPositionalY(rendered)) {
//                        return layer.scaleMap[aes]
//                    }
//                }
//            }
//        }
//        return null
//    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy