commonMain.jetbrains.datalore.plot.builder.PlotUtil.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lets-plot-common Show documentation
Show all versions of lets-plot-common Show documentation
Lets-Plot JVM package without rendering part
/*
* 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.collect.ClosedRange
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.plot.base.*
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 = aes.groups().toSet()
set.size
}
})
}
fun computeLayerDryRunXYRanges(
layer: GeomLayer, aes: Aesthetics
): Pair?, ClosedRange?> {
val geomCtx = GeomContextBuilder().aesthetics(aes).build()
val rangesAfterPosAdjustment =
computeLayerDryRunXYRangesAfterPosAdjustment(layer, aes, geomCtx)
val (xRangeAfterSizeExpand, yRangeAfterSizeExpand) =
computeLayerDryRunXYRangesAfterSizeExpand(layer, aes, geomCtx)
var rangeX = rangesAfterPosAdjustment.first
if (rangeX == null) {
rangeX = xRangeAfterSizeExpand
} else if (xRangeAfterSizeExpand != null) {
rangeX = rangeX.span(xRangeAfterSizeExpand)
}
var rangeY = rangesAfterPosAdjustment.second
if (rangeY == null) {
rangeY = yRangeAfterSizeExpand
} else if (yRangeAfterSizeExpand != null) {
rangeY = rangeY.span(yRangeAfterSizeExpand)
}
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 = Aes.affectingScaleX(layer.renderedAes())
val posAesY = 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 mappers = prepareLayerAestheticMappers(
layer,
xAesMapper = Mappers.IDENTITY,
yAesMapper = Mappers.IDENTITY
)
return createLayerAesthetics(layer, mappers)
}
internal fun prepareLayerAestheticMappers(
layer: GeomLayer,
xAesMapper: (Double?) -> Double?,
yAesMapper: (Double?) -> Double?,
): Map, (Double?) -> Any?> {
val mappers = HashMap, (Double?) -> Any?>()
val renderedAes = layer.renderedAes() + listOf(Aes.X, Aes.Y)
for (aes in renderedAes) {
var mapper: ((Double?) -> Any?)? = when {
aes == Aes.SLOPE -> Mappers.mul(yAesMapper(1.0)!! / xAesMapper(1.0)!!)
// positional aes share their mappers
Aes.isPositionalX(aes) -> xAesMapper
Aes.isPositionalY(aes) -> yAesMapper
layer.hasBinding(aes) -> layer.scaleMap[aes].mapper
else -> null // rendered but has no binding - just ignore.
}
mapper?.run {
mappers[aes] = this
}
}
return mappers
}
internal fun createLayerAesthetics(
layer: GeomLayer,
sharedMappers: Map, (Double?) -> Any?>,
): Aesthetics {
val aesBuilder = AestheticsBuilder()
aesBuilder.group(layer.group)
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)) {
check(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)
check(data.has(transformVar)) { "Undefined var $transformVar for aesthetic $aes" }
val numericValues = data.getNumeric(transformVar)
if (dataPointCount == null) {
dataPointCount = numericValues.size
} else {
check(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)
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
}
/**
* Expand X/Y-range to ensure that the data is placed some distance away from the axes.
*/
internal fun rangeWithExpand(
range: ClosedRange?,
scale: Scale<*>,
includeZero: Boolean
): ClosedRange? {
if (range == null) return null
val mulExp = scale.multiplicativeExpand
val addExp = scale.additiveExpand
// Compute expands in terms of the original data.
// Otherwise, can easily run into Infinities then using 'log10' transform
val continuousTransform: ContinuousTransform? = if (scale.isContinuousDomain) {
scale.transform as ContinuousTransform
} else {
null
}
val lowerEndpoint = continuousTransform?.applyInverse(range.lowerEnd) ?: range.lowerEnd
val upperEndpoint = continuousTransform?.applyInverse(range.upperEnd) ?: range.upperEnd
val length = upperEndpoint - lowerEndpoint
var lowerExpand = addExp + length * mulExp
var upperExpand = lowerExpand
if (includeZero) {
// 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
}
}
}
val lowerEndWithExpand = (lowerEndpoint - lowerExpand).let {
val transformed = continuousTransform?.apply(it) ?: it
if (transformed.isNaN()) {
range.lowerEnd
} else {
transformed
}
}
val upperEndWithExpand = (upperEndpoint + upperExpand).let {
val transformed = continuousTransform?.apply(it) ?: it
if (transformed.isNaN()) {
range.upperEnd
} else {
transformed
}
}
return ClosedRange(lowerEndWithExpand, upperEndWithExpand)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy