commonMain.jetbrains.datalore.plot.config.LayerConfig.kt Maven / Gradle / Ivy
/*
* 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.base.Preconditions.checkArgument
import jetbrains.datalore.base.gcommon.base.Preconditions.checkState
import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.DataFrame
import jetbrains.datalore.plot.base.GeomKind
import jetbrains.datalore.plot.base.Stat
import jetbrains.datalore.plot.base.data.DataFrameUtil
import jetbrains.datalore.plot.base.data.DataFrameUtil.variables
import jetbrains.datalore.plot.base.stat.Stats
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.builder.assemble.PosProvider
import jetbrains.datalore.plot.builder.sampling.Sampling
import jetbrains.datalore.plot.builder.tooltip.TooltipSpecification
import jetbrains.datalore.plot.config.ConfigUtil.createAesMapping
import jetbrains.datalore.plot.config.DataMetaUtil.createDataFrame
import jetbrains.datalore.plot.config.Option.Geom.Choropleth.GEO_POSITIONS
import jetbrains.datalore.plot.config.Option.Layer.GEOM
import jetbrains.datalore.plot.config.Option.Layer.MAP_JOIN
import jetbrains.datalore.plot.config.Option.Layer.NONE
import jetbrains.datalore.plot.config.Option.Layer.SHOW_LEGEND
import jetbrains.datalore.plot.config.Option.Layer.STAT
import jetbrains.datalore.plot.config.Option.Layer.TOOLTIPS
import jetbrains.datalore.plot.config.Option.PlotBase.DATA
import jetbrains.datalore.plot.config.Option.PlotBase.MAPPING
class LayerConfig(
layerOptions: Map,
sharedData: DataFrame,
plotMappings: Map<*, *>,
plotDiscreteAes: Set<*>,
val geomProto: GeomProto,
private val clientSide: Boolean
) : OptionsAccessor(
layerOptions,
initDefaultOptions(layerOptions, geomProto)
) {
val stat: Stat
val statKind: StatKind = StatKind.safeValueOf(getStringSafe(STAT))
val explicitGroupingVarName: String?
val posProvider: PosProvider
private val myCombinedData: DataFrame
val varBindings: List
val constantsMap: Map, Any>
private val mySamplings: List?
val tooltips: TooltipSpecification
var ownData: DataFrame? = null
private set
private var myOwnDataUpdated = false
val combinedData: DataFrame
get() {
checkState(!myOwnDataUpdated)
return myCombinedData
}
val isLegendDisabled: Boolean
get() = if (hasOwn(SHOW_LEGEND)) {
!getBoolean(SHOW_LEGEND, true)
} else false
val samplings: List?
get() {
checkState(!clientSide)
return mySamplings
}
val isLiveMap: Boolean
get() = geomProto.geomKind == GeomKind.LIVE_MAP
init {
val (layerMappings, layerData) = createDataFrame(
options = this,
commonData = sharedData,
commonDiscreteAes = plotDiscreteAes,
commonMappings = plotMappings,
isClientSide = clientSide
)
if (!clientSide) {
update(MAPPING, layerMappings)
}
stat = StatProto.createStat(statKind, OptionsAccessor(mergedOptions))
val consumedAesSet = HashSet(geomProto.renders())
if (!clientSide) {
consumedAesSet.addAll(stat.consumes())
}
// mapping (inherit from plot) + 'layer' mapping
val combinedMappingOptions = (plotMappings + layerMappings).filterKeys {
// Only keep those mapping options which can be consumed by this layer.
// ToDo: report to user that some mappings are not applicable to this layer.
if (it == Option.Mapping.GROUP) {
true
} else if (it is String) {
val aes = Option.Mapping.toAes(it)
consumedAesSet.contains(aes)
} else {
false
}
}
// If layer has no mapping then no data is needed.
val dropData: Boolean = (combinedMappingOptions.isEmpty() &&
// Exception: CorrelationStat doesn't "consume" aesthetics and this logic is not applicable.
// ToDo: Remove CorrelationStat
statKind != StatKind.CORR &&
// Do not touch GeoDataframe - empty mapping is OK in this case.
!GeoConfig.isGeoDataframe(layerOptions, DATA)
)
var combinedData =
if (dropData) {
DataFrame.Builder.emptyFrame()
} else if (!(sharedData.isEmpty || layerData.isEmpty) && sharedData.rowCount() == layerData.rowCount()) {
DataFrameUtil.appendReplace(sharedData, layerData)
} else if (!layerData.isEmpty) {
layerData
} else {
sharedData
}
var aesMappings: Map, DataFrame.Variable>
if (clientSide && GeoConfig.isApplicable(layerOptions, combinedMappingOptions)) {
val geoConfig = GeoConfig(
geomProto.geomKind,
combinedData,
layerOptions,
combinedMappingOptions
)
combinedData = geoConfig.dataAndCoordinates
aesMappings = geoConfig.mappings
} else {
aesMappings = createAesMapping(combinedData, combinedMappingOptions)
}
if (clientSide) {
// add stat default mappings
val statDefMapping = Stats.defaultMapping(stat)
// Only keys (aes) in 'statDefMapping' that are not already present in 'aesMappinds'.
aesMappings = statDefMapping + aesMappings
}
// drop from aes mapping constant that were defined explicitly.
val explicitConstantAes = Option.Mapping.REAL_AES_OPTION_NAMES
.filter { hasOwn(it) }
.map { Option.Mapping.toAes(it) }
aesMappings = aesMappings - explicitConstantAes
// init AES constants excluding mapped AES
constantsMap = LayerConfigUtil.initConstants(this, aesMappings.keys)
// grouping
explicitGroupingVarName = initGroupingVarName(combinedData, combinedMappingOptions)
posProvider = LayerConfigUtil.initPositionAdjustments(
this,
geomProto.preferredPositionAdjustments(this)
)
// tooltip list
tooltips = if (has(TOOLTIPS)) {
when (get(TOOLTIPS)) {
is Map<*, *> -> {
TooltipConfig(getMap(TOOLTIPS), constantsMap, explicitGroupingVarName).createTooltips()
}
NONE -> {
// not show tooltips
TooltipSpecification.withoutTooltip()
}
else -> {
error("Incorrect tooltips specification")
}
}
} else {
TooltipSpecification.defaultTooltip()
}
varBindings = LayerConfigUtil.createBindings(
combinedData,
aesMappings,
consumedAesSet,
clientSide
)
ownData = layerData
myCombinedData = combinedData
mySamplings = if (clientSide) {
null
} else {
LayerConfigUtil.initSampling(this, geomProto.preferredSampling())
}
}
private fun initGroupingVarName(data: DataFrame, mappingOptions: Map<*, *>): String? {
val groupBy = mappingOptions[Option.Mapping.GROUP]
var fieldName: String? = if (groupBy is String)
groupBy
else
null
if (fieldName == null && has(GEO_POSITIONS)) {
// 'default' group is important for 'geom_map'
val groupVar = variables(data)["group"]
if (groupVar != null) {
fieldName = groupVar.name
}
}
return fieldName
}
fun hasVarBinding(varName: String): Boolean {
for (binding in varBindings) {
if (binding.variable.name == varName) {
return true
}
}
return false
}
fun replaceOwnData(dataFrame: DataFrame?) {
checkState(!clientSide) // This class is immutable on client-side
require(dataFrame != null)
update(DATA, DataFrameUtil.toMap(dataFrame))
ownData = dataFrame
myOwnDataUpdated = true
}
fun hasExplicitGrouping(): Boolean {
return explicitGroupingVarName != null
}
fun isExplicitGrouping(varName: String): Boolean {
return explicitGroupingVarName != null && explicitGroupingVarName == varName
}
fun getVariableForAes(aes: Aes<*>): DataFrame.Variable? {
return varBindings.find { it.aes == aes }?.variable
}
fun getMapJoin(): Pair, List<*>>? {
if (!hasOwn(MAP_JOIN)) {
return null
}
val mapJoin = getList(MAP_JOIN)
require(mapJoin.size == 2) { "map_join require 2 parameters" }
val (dataVar, mapVar) = mapJoin
require(dataVar != null)
require(mapVar != null)
require(dataVar is List<*>) {
"Wrong map_join parameter type: should be a list of strings, but was ${dataVar::class.simpleName}"
}
require(mapVar is List<*>) {
"Wrong map_join parameter type: should be a list of string, but was ${mapVar::class.simpleName}"
}
return Pair(dataVar, mapVar)
}
private companion object {
private fun initDefaultOptions(
layerOptions: Map<*, *>,
geomProto: GeomProto
): Map {
checkArgument(
layerOptions.containsKey(GEOM) || layerOptions.containsKey(STAT),
"Either 'geom' or 'stat' must be specified."
)
val defaults = HashMap()
defaults.putAll(geomProto.defaultOptions())
var statName: String? = layerOptions[STAT] as String?
if (statName == null) {
statName = defaults[STAT] as String
}
return defaults + StatProto.defaultOptions(statName, geomProto.geomKind)
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy