commonMain.jetbrains.datalore.plot.config.LineSpecConfigParseHelper.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of plot-config-portable-js Show documentation
Show all versions of plot-config-portable-js Show documentation
The Let-Plot Kotlin API depends on this artifact.
/*
* Copyright (c) 2022. 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.stringFormat.StringFormat
import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.builder.tooltip.LinesContentSpecification
import jetbrains.datalore.plot.builder.tooltip.LinesContentSpecification.Companion.LineSpec
import jetbrains.datalore.plot.builder.VarBinding
import jetbrains.datalore.plot.builder.tooltip.ConstantValue
import jetbrains.datalore.plot.builder.tooltip.DataFrameValue
import jetbrains.datalore.plot.builder.tooltip.MappingValue
import jetbrains.datalore.plot.builder.tooltip.ValueSource
open class LineSpecConfigParser(
opts: Map,
private val constantsMap: Map, Any>,
private val groupingVarName: String?,
private val varBindings: List,
) : OptionsAccessor(opts) {
fun create(): LinesContentSpecification {
return LineSpecConfigParserHelper(
lines = if (has(Option.LinesSpec.LINES)) {
getStringList(Option.LinesSpec.LINES)
} else {
null
},
formats = getList(Option.LinesSpec.FORMATS),
variables = getStringList(Option.LinesSpec.VARIABLES),
titleLine = getString(Option.LinesSpec.TITLE)
).parse()
}
internal inner class LineSpecConfigParserHelper(
private val lines: List?,
formats: List<*>,
variables: List,
private val titleLine: String?
) {
private val myValueSources: MutableMap = prepareFormats(formats)
.let { specifiedFormats ->
val valueSources = specifiedFormats.mapValues { (field, format) ->
createValueSource(fieldName = field.name, isAes = field.isAes, format = format)
}
// the specified format for the variable should be applied also to the aes (if it doesn't have its own format())
val aesValueSources = mutableMapOf()
specifiedFormats.map { (field, format) ->
aesValueSources += getAesValueSourceForVariable(field, format, valueSources)
}
valueSources + aesValueSources
}.toMutableMap()
// Create lines from the given variable list
private val myLinesForVariableList: List = variables.map { variableName ->
val valueSource = getValueSource(varField(variableName))
LineSpec.defaultLineForValueSource(valueSource)
}
internal fun parse(): LinesContentSpecification {
val allLines = parseLines()
val title = titleLine?.let(::parseLine)
return LinesContentSpecification(myValueSources.map { it.value }, allLines, title)
}
private fun parseLines(): List? {
val lines = lines?.map(::parseLine)
return when {
lines != null -> myLinesForVariableList + lines
myLinesForVariableList.isNotEmpty() -> myLinesForVariableList
else -> null
}
}
private fun parseLine(line: String): LineSpec {
val label = detachLabel(line)
val valueString = line.substringAfter(LABEL_SEPARATOR)
val fieldsInPattern = mutableListOf()
val pattern: String = SOURCE_RE_PATTERN.replace(valueString) {
if (it.value == "\\$AES_NAME_PREFIX" || it.value == "\\$VARIABLE_NAME_PREFIX") {
// it is a part of the text (not of the name)
it.value.removePrefix("\\")
} else {
fieldsInPattern += getValueSource(it.value)
StringFormat.valueInLinePattern()
}
}
return LineSpec(
label,
pattern,
fieldsInPattern
)
}
private fun createValueSource(fieldName: String, isAes: Boolean, format: String? = null): ValueSource {
return when {
isAes && fieldName == Option.Mapping.GROUP -> {
requireNotNull(groupingVarName) { "Variable name for 'group' is not specified" }
DataFrameValue(groupingVarName, format)
}
isAes -> {
val aes = Option.Mapping.toAes(fieldName)
when (val constant = constantsMap[aes]) {
null -> MappingValue(aes, format = format)
else -> ConstantValue(aes, constant, format)
}
}
else -> {
DataFrameValue(fieldName, format)
}
}
}
private fun prepareFormats(formats: List<*>): Map {
val allFormats = mutableMapOf()
formats.forEach { lineFormat ->
require(lineFormat is Map<*, *>) { "Wrong 'format' arguments" }
require(lineFormat.has(Option.LinesSpec.Format.FIELD) && lineFormat.has(Option.LinesSpec.Format.FORMAT)) {
"Invalid 'format' arguments: 'field' and 'format' are expected"
}
val field = lineFormat[Option.LinesSpec.Format.FIELD] as String
val format = lineFormat[Option.LinesSpec.Format.FORMAT] as String
if (field.startsWith(AES_NAME_PREFIX)) {
val positionals = when (field.removePrefix(AES_NAME_PREFIX)) {
"X" -> Aes.values().filter(Aes.Companion::isPositionalX)
"Y" -> Aes.values().filter(Aes.Companion::isPositionalY)
else -> {
// it is aes name
val aesField = aesField(field.removePrefix(AES_NAME_PREFIX))
allFormats[aesField] = format
emptyList()
}
}
positionals.forEach { aes ->
val aesField = aesField(aes.name)
if (aesField !in allFormats)
allFormats[aesField] = format
}
} else {
val varField = varField(detachVariableName(field))
allFormats[varField] = format
}
}
return allFormats
}
private fun getAesValueSourceForVariable(
field: Field,
format: String?,
valueSources: Map
): Map {
if (field.isAes) {
return emptyMap()
}
return varBindings
.filter { it.variable.name == field.name }
.map(VarBinding::aes).associate { aes ->
val aesField = aesField(aes.name)
if (aesField in valueSources)
aesField to valueSources[aesField]!!
else
aesField to createValueSource(
fieldName = aes.name,
isAes = true,
format = format
)
}
}
private fun getValueSource(field: Field): ValueSource {
if (field !in myValueSources) {
// If format() is not specified for the variable, use the aes formatting
val aesValueSources =
getAesValueSourceForVariable(field, format = null, valueSources = myValueSources)
// Choose the specified before or use the first aes
val specifiedBefore = (aesValueSources
.filter { it.key in myValueSources }
.takeIf { it.isNotEmpty() }
?: aesValueSources)
.toList()
.minByOrNull { (aesField, _) -> aesField.name }
?.second
myValueSources[field] =
specifiedBefore ?: createValueSource(fieldName = field.name, isAes = field.isAes)
}
return myValueSources[field]!!
}
private fun getValueSource(fieldString: String): ValueSource {
val field = when {
fieldString.startsWith(AES_NAME_PREFIX) -> {
aesField(fieldString.removePrefix(AES_NAME_PREFIX))
}
fieldString.startsWith(VARIABLE_NAME_PREFIX) -> {
varField(detachVariableName(fieldString))
}
else -> error("Unknown type of the field with name = \"$fieldString\"")
}
return getValueSource(field)
}
private fun detachVariableName(field: String) =
field.removePrefix(VARIABLE_NAME_PREFIX).removeSurrounding("{", "}")
private fun detachLabel(line: String): String? {
return if (LABEL_SEPARATOR in line) {
line.substringBefore(LABEL_SEPARATOR).trim()
} else {
null
}
}
private fun aesField(aesName: String) = Field(aesName, true)
private fun varField(varName: String) = Field(varName, false)
}
private data class Field(val name: String, val isAes: Boolean)
companion object {
private const val AES_NAME_PREFIX = "^"
private const val VARIABLE_NAME_PREFIX = "@"
private const val LABEL_SEPARATOR = "|"
// escaping ('\^', '\@') or aes name ('^aesName') or variable name ('@varName', '@{var name with spaces}', '@..stat_var..')
private val SOURCE_RE_PATTERN = Regex("""(?:\\\^|\\@)|(\^\w+)|@(([\w^@]+)|(\{(.*?)\})|\.{2}\w+\.{2})""")
}
}