diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ContinuousOnlyMapperProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ContinuousOnlyMapperProvider.kt new file mode 100644 index 00000000000..ff254987144 --- /dev/null +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ContinuousOnlyMapperProvider.kt @@ -0,0 +1,15 @@ +/* + * 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.scale + +import jetbrains.datalore.plot.builder.scale.provider.MapperProviderBase + +abstract class ContinuousOnlyMapperProvider(naValue: T) : MapperProviderBase(naValue) { + override fun createDiscreteMapper(domainValues: Collection<*>): GuideMapper { + val domainRepr = domainValues.joinToString(limit = 3) { "'$it'" } + throw IllegalStateException("[${this::class.simpleName}] Can't create mapper for discrete domain: $domainRepr") + } +} diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/MapperProviderAdapter.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DiscreteOnlyMapperProvider.kt similarity index 56% rename from plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/MapperProviderAdapter.kt rename to plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DiscreteOnlyMapperProvider.kt index 3aa5b2a8efb..fdab9ae9ce7 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/MapperProviderAdapter.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/DiscreteOnlyMapperProvider.kt @@ -7,19 +7,15 @@ package jetbrains.datalore.plot.builder.scale import jetbrains.datalore.base.gcommon.collect.ClosedRange import jetbrains.datalore.plot.base.ContinuousTransform +import jetbrains.datalore.plot.builder.scale.provider.MapperProviderBase -open class MapperProviderAdapter : MapperProvider { - override fun createDiscreteMapper(domainValues: Collection<*>): GuideMapper { - throw IllegalStateException("Can't create mapper for discrete domain: ${domainValues.map { "'$it'" } - .joinToString(limit = 3)}") - } - +abstract class DiscreteOnlyMapperProvider(naValue: T) : MapperProviderBase(naValue) { override fun createContinuousMapper( domain: ClosedRange, lowerLimit: Double?, upperLimit: Double?, trans: ContinuousTransform ): GuideMapper { - throw IllegalStateException("Can't create mapper for continuous domain $domain") + throw IllegalStateException("[${this::class.simpleName}] Can't create mapper for continuous domain $domain") } } diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProvider.kt index a57ef1a9916..a69a33dc4f3 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProvider.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProvider.kt @@ -12,8 +12,9 @@ import jetbrains.datalore.plot.base.Scale interface ScaleProvider { val discreteDomain: Boolean val breaks: List? - val discreteDomainLimits: List? + val limits: List? // when 'continuous' limits, NULL means undefined upper or lower limit. val continuousTransform: ContinuousTransform + val mapperProvider: MapperProvider /** * Create scale for discrete input (domain) diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt index 4e58d1e03c2..ab5b843e989 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/ScaleProviderBuilder.kt @@ -139,8 +139,7 @@ class ScaleProviderBuilder(private val aes: Aes) { return MyScaleProvider(this) } - private class MyScaleProvider(b: ScaleProviderBuilder) : - ScaleProvider { + private class MyScaleProvider(b: ScaleProviderBuilder) : ScaleProvider { private val myName: String? = b.myName @@ -148,19 +147,14 @@ class ScaleProviderBuilder(private val aes: Aes) { private val myLabelFormat: String? = b.myLabelFormat private val myMultiplicativeExpand: Double? = b.myMultiplicativeExpand private val myAdditiveExpand: Double? = b.myAdditiveExpand - private val myLimits: List? = b.myLimits?.let { ArrayList(it) } private val discreteDomainReverse: Boolean = b.myDiscreteDomainReverse - private val myBreaksGenerator: BreaksGenerator? = b.myBreaksGenerator - private val myAes: Aes = b.aes - private val mapperProvider: MapperProvider = b.mapperProvider - override val breaks: List? = b.myBreaks?.let { ArrayList(it) } override val discreteDomain: Boolean = b.myDiscreteDomain - - // This is only to use by 'discrete' transforms/scales - override val discreteDomainLimits: List? = myLimits?.filterNotNull() + override val mapperProvider: MapperProvider = b.mapperProvider + override val breaks: List? = b.myBreaks?.let { ArrayList(it) } + override val limits: List? = b.myLimits?.let { ArrayList(it) } override val continuousTransform: ContinuousTransform = b.myContinuousTransform @@ -191,7 +185,7 @@ class ScaleProviderBuilder(private val aes: Aes) { mapper ) - val discreteLimits = discreteDomainLimits?.let { + val discreteLimits = limits?.filterNotNull()?.let { if (discreteDomainReverse) { it.reversed() } else { @@ -216,9 +210,9 @@ class ScaleProviderBuilder(private val aes: Aes) { var lowerLimit: Double? = null var upperLimit: Double? = null - if (myLimits != null) { + if (limits != null) { var lower = true - for (limit in myLimits) { + for (limit in limits) { if (limit is Number) { val v = limit.toDouble() if (v.isFinite()) { @@ -263,7 +257,7 @@ class ScaleProviderBuilder(private val aes: Aes) { .build() } - if (myLimits != null) { + if (limits != null) { val with = scale.with() if (lowerLimit != null) { with.lowerLimit(lowerLimit) diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/AlphaMapperProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/AlphaMapperProvider.kt index 8d041861d4e..6cbc057b7de 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/AlphaMapperProvider.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/AlphaMapperProvider.kt @@ -10,9 +10,9 @@ import jetbrains.datalore.plot.base.Aes.Companion.ALPHA import jetbrains.datalore.plot.builder.scale.DefaultNaValue class AlphaMapperProvider( - range: ClosedRange, - naValue: Double) : - LinearNormalizingMapperProvider(range, naValue) { + range: ClosedRange, + naValue: Double +) : LinearNormalizingMapperProvider(range, naValue) { companion object { private val DEF_RANGE = ClosedRange(0.1, 1.0) diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/ColorGradient2MapperProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/ColorGradient2MapperProvider.kt index 67c9cf67f01..09b77a3a5df 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/ColorGradient2MapperProvider.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/ColorGradient2MapperProvider.kt @@ -9,6 +9,7 @@ import jetbrains.datalore.base.gcommon.collect.ClosedRange import jetbrains.datalore.base.values.Color import jetbrains.datalore.plot.base.ContinuousTransform import jetbrains.datalore.plot.base.scale.MapperUtil +import jetbrains.datalore.plot.builder.scale.ContinuousOnlyMapperProvider import jetbrains.datalore.plot.builder.scale.GuideMapper import jetbrains.datalore.plot.builder.scale.mapper.ColorMapper import jetbrains.datalore.plot.builder.scale.mapper.GuideMappers @@ -16,8 +17,9 @@ import jetbrains.datalore.plot.common.data.SeriesUtil import kotlin.math.max import kotlin.math.min -class ColorGradient2MapperProvider(low: Color?, mid: Color?, high: Color?, midpoint: Double?, naValue: Color) : - MapperProviderBase(naValue) { +class ColorGradient2MapperProvider( + low: Color?, mid: Color?, high: Color?, midpoint: Double?, naValue: Color +) : ContinuousOnlyMapperProvider(naValue) { private val myLow: Color private val myMid: Color diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/DirectlyProportionalMapperProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/DirectlyProportionalMapperProvider.kt index 9252247f6f0..33bc4bfe50e 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/DirectlyProportionalMapperProvider.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/DirectlyProportionalMapperProvider.kt @@ -8,6 +8,7 @@ package jetbrains.datalore.plot.builder.scale.provider import jetbrains.datalore.base.gcommon.collect.ClosedRange import jetbrains.datalore.plot.base.ContinuousTransform import jetbrains.datalore.plot.base.scale.MapperUtil +import jetbrains.datalore.plot.builder.scale.ContinuousOnlyMapperProvider import jetbrains.datalore.plot.builder.scale.GuideMapper import jetbrains.datalore.plot.builder.scale.mapper.GuideMappers @@ -17,7 +18,7 @@ import jetbrains.datalore.plot.builder.scale.mapper.GuideMappers open class DirectlyProportionalMapperProvider( private val max: Double, naValue: Double -) : MapperProviderBase(naValue) { +) : ContinuousOnlyMapperProvider(naValue) { override fun createContinuousMapper( domain: ClosedRange, diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/IdentityDiscreteMapperProvider.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/IdentityDiscreteMapperProvider.kt index b4cbc9c5c7a..ab68429caae 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/IdentityDiscreteMapperProvider.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/IdentityDiscreteMapperProvider.kt @@ -5,12 +5,14 @@ package jetbrains.datalore.plot.builder.scale.provider +import jetbrains.datalore.plot.builder.scale.DiscreteOnlyMapperProvider import jetbrains.datalore.plot.builder.scale.GuideMapper import jetbrains.datalore.plot.builder.scale.mapper.GuideMappers open class IdentityDiscreteMapperProvider( - private val inputConverter: (Any?) -> T?, naValue: T -) : MapperProviderBase(naValue) { + private val inputConverter: (Any?) -> T?, + naValue: T +) : DiscreteOnlyMapperProvider(naValue) { override fun createDiscreteMapper(domainValues: Collection<*>): GuideMapper { val outputValues = ArrayList() diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/MapperProviderBase.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/MapperProviderBase.kt index 46ac5cfc4ca..0858d4b6661 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/MapperProviderBase.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/scale/provider/MapperProviderBase.kt @@ -5,6 +5,6 @@ package jetbrains.datalore.plot.builder.scale.provider -import jetbrains.datalore.plot.builder.scale.MapperProviderAdapter +import jetbrains.datalore.plot.builder.scale.MapperProvider -abstract class MapperProviderBase(protected val naValue: T) : MapperProviderAdapter() +abstract class MapperProviderBase(protected val naValue: T) : MapperProvider diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigUtil.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigUtil.kt index ba5e31cffeb..cd9277f150b 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigUtil.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/PlotConfigUtil.kt @@ -12,6 +12,8 @@ import jetbrains.datalore.plot.base.scale.transform.Transforms 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 @@ -168,13 +170,18 @@ object PlotConfigUtil { // Extract "discrete" aes set. val discreteAesSet: MutableSet> = HashSet() for (aes in aesSet) { - if (scaleProviderByAes.getValue(aes).discreteDomain) { + 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)) - !data.isNumeric(it) + if (data.isEmpty(it)) { + isDiscreteScaleForEmptyData(scaleProvider) + } else { + !data.isNumeric(it) + } } if (anyNotNumericData) { discreteAesSet.add(aes) @@ -215,7 +222,7 @@ object PlotConfigUtil { val effectiveDomain = (scaleBreaks + domainValues).distinct() val transform = DiscreteTransform( domainValues = effectiveDomain, - domainLimits = scaleProvider.discreteDomainLimits ?: emptyList() + domainLimits = (scaleProvider.limits ?: emptyList()).filterNotNull() ) discreteTransformByAes[aes] = transform } @@ -392,4 +399,31 @@ object PlotConfigUtil { 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 + } } diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt index 20d8d4dd200..7af45aa3d47 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/ScaleConfig.kt @@ -5,7 +5,6 @@ package jetbrains.datalore.plot.config -import jetbrains.datalore.base.gcommon.base.Preconditions.checkArgument import jetbrains.datalore.base.values.Color import jetbrains.datalore.plot.base.Aes import jetbrains.datalore.plot.base.scale.Mappers.nullable @@ -239,8 +238,8 @@ class ScaleConfig(options: Map) : OptionsAccessor(options) { fun aesOrFail(options: Map): Aes<*> { val accessor = OptionsAccessor(options) - checkArgument(accessor.has(AES), "Required parameter '$AES' is missing") - return Option.Mapping.toAes(accessor.getString(AES)!!) + require(accessor.has(AES)) { "Required parameter '$AES' is missing" } + return Option.Mapping.toAes(accessor.getStringSafe(AES)) } fun createIdentityMapperProvider(aes: Aes, naValue: T): MapperProvider {