All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
commonMain.org.jetbrains.letsPlot.intern.ToSpecConverters.kt Maven / Gradle / Ivy
/*
* Copyright (c) 2021. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/
package org.jetbrains.letsPlot.intern
import org.jetbrains.letsPlot.Figure
import org.jetbrains.letsPlot.GGBunch
import org.jetbrains.letsPlot.MappingMeta
import org.jetbrains.letsPlot.core.spec.Option
import org.jetbrains.letsPlot.core.spec.Option.Meta.DATA_META
import org.jetbrains.letsPlot.core.spec.Option.Meta.KIND
import org.jetbrains.letsPlot.core.spec.Option.Meta.Kind.PLOT
import org.jetbrains.letsPlot.core.spec.Option.Meta.MappingAnnotation
import org.jetbrains.letsPlot.core.spec.Option.Meta.SeriesAnnotation
import org.jetbrains.letsPlot.core.spec.Option.Scale.AES
import org.jetbrains.letsPlot.core.spec.Option.Scale.BREAKS
import org.jetbrains.letsPlot.core.spec.Option.Scale.CONTINUOUS_TRANSFORM
import org.jetbrains.letsPlot.core.spec.Option.Scale.EXPAND
import org.jetbrains.letsPlot.core.spec.Option.Scale.FORMAT
import org.jetbrains.letsPlot.core.spec.Option.Scale.GUIDE
import org.jetbrains.letsPlot.core.spec.Option.Scale.LABELS
import org.jetbrains.letsPlot.core.spec.Option.Scale.LABLIM
import org.jetbrains.letsPlot.core.spec.Option.Scale.LIMITS
import org.jetbrains.letsPlot.core.spec.Option.Scale.NAME
import org.jetbrains.letsPlot.core.spec.Option.Scale.NA_VALUE
import org.jetbrains.letsPlot.core.spec.Option.Scale.POSITION
import org.jetbrains.letsPlot.core.spec.provideMap
import org.jetbrains.letsPlot.intern.figure.SubPlotsFigure
import org.jetbrains.letsPlot.intern.layer.WithSpatialParameters
import org.jetbrains.letsPlot.intern.standardizing.JvmStandardizing
import org.jetbrains.letsPlot.intern.standardizing.MapStandardizing
import org.jetbrains.letsPlot.intern.standardizing.SeriesStandardizing.asList
import org.jetbrains.letsPlot.intern.standardizing.SeriesStandardizing.isListy
import org.jetbrains.letsPlot.intern.standardizing.SeriesStandardizing.toList
import org.jetbrains.letsPlot.spatial.CRSCode.isWGS84Code
import org.jetbrains.letsPlot.spatial.GeometryFormat
import org.jetbrains.letsPlot.spatial.SpatialDataset
import kotlin.reflect.KClass
fun Figure.toSpec(): MutableMap {
return when (this) {
is Plot -> this.toSpec()
is SubPlotsFigure -> this.toSpec()
is GGBunch -> this.toSpec()
else -> throw IllegalArgumentException("Unsupported figure type ${this::class.simpleName}")
}
}
fun Plot.toSpec(): MutableMap {
val spec = HashMap()
val plot = this
spec[KIND] = PLOT
plot.data?.let {
// SpatialDataset is not allowed in 'ggplot(data=..)' or 'lets_plot(data=..)'
val data = if (plot.data is SpatialDataset) {
HashMap(plot.data) // convert to a regular Map.
} else {
plot.data
}
spec[Option.PlotBase.DATA] = asPlotData(data)
val dataMeta = createDataMeta(data, plot.mapping.map)
if (dataMeta.isNotEmpty()) {
spec[DATA_META] = dataMeta
}
}
spec[Option.PlotBase.MAPPING] = asMappingData(plot.mapping.map)
spec[Option.Plot.LAYERS] = plot.layers().map(Layer::toSpec)
spec[Option.Plot.SCALES] = plot.scales().flatMap(Scale::toSpec)
// Width of plot in percents of the available in frontend width.
plot.widthScale?.let { spec["widthScale"] = it }
val features = plot.otherFeatures()
val themeOptionList = features.filter { it.kind == Option.Plot.THEME }
val metaInfoOptionList = features.filter { it.kind == Option.Plot.METAINFO }
val guidesOptionList = features.filter { it.kind == Option.Plot.GUIDES }
// Merge themes
ThemeOptionsUtil.toSpec(themeOptionList)?.let {
spec[Option.Plot.THEME] = it
}
metaInfoOptionList.forEach { metaInfoOptions ->
val l = spec.getOrPut(Option.Plot.METAINFO_LIST) { ArrayList>() }
@Suppress("UNCHECKED_CAST")
(l as MutableList>).add(metaInfoOptions.toSpec())
}
// Merge guides
OptionsUtil.toSpec(guidesOptionList)?.let {
spec[Option.Plot.GUIDES] = it
}
@Suppress("ConvertArgumentToSet")
(features - themeOptionList - metaInfoOptionList - guidesOptionList).forEach {
spec[it.kind] = it.toSpec()
}
return spec
}
fun Layer.toSpec(): MutableMap {
val spec = HashMap()
data?.let {
val data = beforeAsPlotData(data)
spec[Option.PlotBase.DATA] = asPlotData(data)
}
// val allMappings = (mapping + geom.mapping + stat.mapping).map
val allMappings = mapping.map
spec[Option.PlotBase.MAPPING] = asMappingData(allMappings)
val dataMeta = createDataMeta(data, allMappings)
if (dataMeta.isNotEmpty()) {
spec[DATA_META] = dataMeta
}
spec[Option.Layer.GEOM] = geom.kind.optionName()
spec[Option.Layer.STAT] = stat.kind.optionName()
position?.let {
spec[Option.Layer.POS] =
if (it.parameters.isEmpty()) {
it.kind.optionName()
} else {
OptionsMap(
"pos",
it.kind.optionName(),
it.parameters.map
).toSpec()
}
}
sampling?.let {
spec[Option.Layer.SAMPLING] =
if (it.isNone) "none"
else it.mapping.map
}
tooltips?.let {
spec[Option.Layer.TOOLTIPS] = it.options
}
labels?.let {
spec[Option.Layer.ANNOTATIONS] = it.options
}
orientation?.let {
spec[Option.Layer.ORIENTATION] = it
}
// parameters 'map', 'mapJoin'
if (this is WithSpatialParameters) {
map?.run {
if (useCRS != "provided") {
this.crs?.let {
require(isWGS84Code(it)) {
"Geometry must use WGS84 coordinate reference system but was: $it."
}
}
}
useCRS?.let { spec[Option.Layer.USE_CRS] = it }
spec[Option.Geom.Choropleth.GEO_POSITIONS] = this
spec[Option.Meta.MAP_DATA_META] = createGeoDataframeAnnotation(this)
mapJoin?.let {
val (first, second) = it
when (first) {
is String -> require(second is String) { "'mapJoin': both members must be either Strings or Lists." }
is List<*> -> {
require(second is List<*>) { "'mapJoin': both members must be either Strings or Lists." }
require(first.isNotEmpty()) { "'mapJoin': 'first' should not be empty." }
require(first.size == second.size) { "'mapJoin': members must have equal size." }
}
}
spec[Option.Layer.MAP_JOIN] = listOf(
when (first) {
is String -> listOf(first)
else -> first
},
when (second) {
is String -> listOf(second)
else -> second
}
)
}
}
}
val allParameters = parameters.map
spec.putAll(allParameters)
if (!showLegend) {
spec[Option.Layer.SHOW_LEGEND] = false
}
inheritAes?.let {
spec[Option.Layer.INHERIT_AES] = it
}
manualKey?.let {
spec[Option.Layer.MANUAL_KEY] = it
}
return spec
}
private fun Layer.beforeAsPlotData(rawData: Map<*, *>): Map<*, *> {
if (rawData is SpatialDataset) {
return when (this) {
is WithSpatialParameters -> if (this.map == null) {
// No "map" parameter -> keep the Spatial dataset.
rawData
} else {
// Has "map" parameter -> convert "data" to a regular Map.
HashMap(rawData)
}
else -> HashMap(rawData) // convert "data" to a regular Map.
}
}
return rawData
}
@Suppress("UNCHECKED_CAST")
fun Map.filterNonNullValues(): Map {
return filter { it.value != null } as Map
}
fun Scale.toSpec(): List> {
val spec = HashMap()
name?.let { spec[NAME] = name }
breaks?.let { spec[BREAKS] = toList(breaks, BREAKS) }
labels?.let { spec[LABELS] = labels }
lablim?.let { spec[LABLIM] = lablim }
limits?.let { spec[LIMITS] = toList(limits, LIMITS) }
expand?.let { spec[EXPAND] = expand }
naValue?.let { spec[NA_VALUE] = naValue }
guide?.let { spec[GUIDE] = guide }
trans?.let { spec[CONTINUOUS_TRANSFORM] = trans }
format?.let { spec[FORMAT] = format }
position?.let { spec[POSITION] = position }
spec.putAll(otherOptions.map)
return aesthetic.map {
mapOf(AES to it.name) + spec
}
}
fun OptionsMap.toSpec(): MutableMap {
return HashMap(
MapStandardizing.standardize(options)
)
}
internal fun asPlotData(rawData: Map<*, *>): Map> {
val standardisedData = HashMap>()
for ((rawKey, rawValue) in rawData) {
val key = rawKey.toString()
standardisedData[key] = toList(rawValue!!, key)
}
return standardisedData
}
private fun asMappingData(rawMapping: Map): Map {
val mapping = rawMapping.toMutableMap()
return mapping.mapValues { (_, value) ->
when (value) {
is MappingMeta -> value.variable
else -> value
}
}
}
private fun createDataMeta(data: Map<*, *>?, mappingSpec: Map): Map {
val spatialDataMeta: Map = if (data is SpatialDataset) {
createGeoDataframeAnnotation(data)
} else {
emptyMap()
}
// VarName to Type
val dataTypeByVar: MutableMap = mutableMapOf()
// VarName to Dict[Aes, MappingMeta]
val mappingMetaByVar: MutableMap> = mutableMapOf()
// Aes to VarName
val regularMapping: MutableMap = mutableMapOf()
if (mappingSpec.isNotEmpty()) {
mappingSpec.forEach { (aes, spec) ->
when (spec) {
is String -> regularMapping[aes] = spec
is MappingMeta -> {
regularMapping[aes] = spec.variable
mappingMetaByVar.provideMap(spec.variable)[aes] = spec
dataTypeByVar[spec.variable] = SeriesAnnotation.Types.UNKNOWN
}
is Collection<*> -> {} // no variable name, can't use inferred type
else -> throw IllegalArgumentException("Unsupported mapping spec: $spec")
}
}
}
dataTypeByVar += inferType(data)
// fill series annotations
val seriesAnnotations = mutableMapOf>()
dataTypeByVar.forEach { (varName, dataType) ->
val seriesAnnotation = mutableMapOf()
if (dataType != SeriesAnnotation.Types.UNKNOWN) {
seriesAnnotation[SeriesAnnotation.TYPE] = dataType
}
if (varName in mappingMetaByVar) {
val levels = mappingMetaByVar[varName]?.values?.mapNotNull(MappingMeta::levels)?.lastOrNull()
if (levels != null) {
seriesAnnotation[SeriesAnnotation.FACTOR_LEVELS] = levels
}
}
if (SeriesAnnotation.FACTOR_LEVELS in seriesAnnotation && varName in mappingMetaByVar) {
val order = mappingMetaByVar[varName]!!.values.mapNotNull(MappingMeta::order).lastOrNull()
if (order != null) {
seriesAnnotation[SeriesAnnotation.ORDER] = order
}
}
if (seriesAnnotation.isNotEmpty()) {
seriesAnnotation[SeriesAnnotation.COLUMN] = varName
seriesAnnotations[varName] = seriesAnnotation
}
}
// fill mapping annotations
val mappingAnnotations = mappingMetaByVar.flatMap { (varName, varMeta) ->
varMeta.mapNotNull { (aes, mappingMeta) ->
if (mappingMeta.annotation != "as_discrete") {
return@mapNotNull null
}
if (seriesAnnotations[varName]?.contains(SeriesAnnotation.FACTOR_LEVELS) == true) {
// Don't duplicate ordering options - store them in mappingAnnotation only if they are not in seriesAnnotations
return@mapNotNull null
}
val mappingAnnotation = mutableMapOf(
MappingAnnotation.AES to aes,
MappingAnnotation.ANNOTATION to "as_discrete",
MappingAnnotation.PARAMETERS to mutableMapOf(
MappingAnnotation.LABEL to mappingMeta.label
)
)
mappingMeta.levels?.let {
mappingAnnotation[SeriesAnnotation.FACTOR_LEVELS] = it
}
mappingMeta.orderBy?.let {
mappingAnnotation.provideMap(MappingAnnotation.PARAMETERS)[MappingAnnotation.ORDER_BY] = it
}
mappingMeta.order?.let {
mappingAnnotation.provideMap(MappingAnnotation.PARAMETERS)[MappingAnnotation.ORDER] = it
}
mappingAnnotation
}
}
val dataMeta = mutableMapOf()
if (seriesAnnotations.isNotEmpty()) {
dataMeta[SeriesAnnotation.TAG] = seriesAnnotations.values.toList()
}
if (mappingAnnotations.isNotEmpty()) {
dataMeta[MappingAnnotation.TAG] = mappingAnnotations
}
return spatialDataMeta + dataMeta
}
private fun inferType(data: Any?): Map {
if (data == null) {
return emptyMap()
}
return if (data is Map<*, *>) {
data
.entries
.associate { (key, values) -> key.toString() to inferSeriesType(values) }
} else {
emptyMap()
}
}
private fun inferSeriesType(data: Any?): String {
if (data == null) {
return SeriesAnnotation.Types.UNKNOWN
}
if (!isListy(data)) {
return SeriesAnnotation.Types.UNKNOWN
}
val l = asList(data).filterNotNull()
if (l.isEmpty()) {
return SeriesAnnotation.Types.UNKNOWN
}
val types = l.fold(mutableSetOf>()) { acc, value -> acc.apply { acc + value::class } }
if (types.size > 1) {
return SeriesAnnotation.Types.UNKNOWN
}
// types.size == 1 means all elements are of the same type, so we can take any (let's take the first one)
val value = l.first()
return if (JvmStandardizing.isDateTimeJvm(value)) {
SeriesAnnotation.Types.DATE_TIME
} else {
when (value) {
is Byte -> SeriesAnnotation.Types.INTEGER
is Short -> SeriesAnnotation.Types.INTEGER
is Int -> SeriesAnnotation.Types.INTEGER
is Long -> SeriesAnnotation.Types.INTEGER
is Double -> SeriesAnnotation.Types.FLOATING
is Float -> SeriesAnnotation.Types.FLOATING
is String -> SeriesAnnotation.Types.STRING
is Boolean -> SeriesAnnotation.Types.BOOLEAN
else -> SeriesAnnotation.Types.UNKNOWN
}
}
}
private fun createGeoDataframeAnnotation(data: SpatialDataset): Map {
require(data.geometryFormat == GeometryFormat.GEOJSON) { "Only GEOJSON geometry format is supported." }
return mapOf(
"geodataframe" to mapOf(
"geometry" to data.geometryKey
)
)
}
//private fun mergeThemeOptions(m0: Map, m1: Map): Map {
// val overlappingKeys = m0.keys.intersect(m1.keys)
// val keysToMerge = overlappingKeys.filter {
// m0[it] is Map<*, *> && m1[it] is Map<*, *>
// }
// val m2 = keysToMerge.map {
// it to (m0[it] as Map<*, *> + m1[it] as Map<*, *>)
// }.toMap()
// return m0 + m1 + m2
//}