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

commonMain.org.jetbrains.letsPlot.intern.ToSpecConverters.kt Maven / Gradle / Ivy

There is a newer version: 4.9.2
Show newest version
/*
 * 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
//}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy