Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'levels' parameter to as_discrete() #957

Merged
merged 21 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4a7f510
`as_discrete(levels)` generates 'data_meta/series_annotation'.
OLarionova-HORIS Nov 17, 2023
d8a3b44
Fix creating of 'factorLevelsByVar'-map .
OLarionova-HORIS Nov 17, 2023
89f09b2
Fix `applyTransform` to avoid error 'value is not in the domain'.
OLarionova-HORIS Nov 17, 2023
11452cf
Add examples.
OLarionova-HORIS Nov 17, 2023
4c36cad
Add tests.
OLarionova-HORIS Nov 22, 2023
4c74e31
Correct specified 'factor_levels' according to actual data and append…
OLarionova-HORIS Nov 23, 2023
d40eb00
Update test notebook.
OLarionova-HORIS Nov 23, 2023
a747f90
Fix description of 'levels' parameter.
OLarionova-HORIS Nov 24, 2023
122f46b
Minor.
OLarionova-HORIS Nov 24, 2023
d1cd7bf
Make variables specified in 'series_annotations' with levels discrete.
OLarionova-HORIS Nov 28, 2023
b53782b
Skip creation of 'mapping_annotations' for variable with specified 'f…
OLarionova-HORIS Nov 29, 2023
79180cf
Few improvements.
OLarionova-HORIS Nov 29, 2023
0c2037d
Refactor code.
OLarionova-HORIS Nov 30, 2023
83a4488
Add order to series_annotations.
OLarionova-HORIS Nov 30, 2023
4353f2a
Minor refactoring.
OLarionova-HORIS Nov 30, 2023
1407676
Refactor python code.
OLarionova-HORIS Dec 1, 2023
9173754
Minor refactoring (python).
OLarionova-HORIS Dec 1, 2023
4b6c988
Add ordering for levels in example.
OLarionova-HORIS Dec 1, 2023
3ff0510
Add example with facets to notebook.
OLarionova-HORIS Dec 5, 2023
35f55fb
Add demo notebook. Update future_changes.md.
OLarionova-HORIS Dec 5, 2023
01d497d
Improve notebook.
OLarionova-HORIS Dec 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Skip creation of 'mapping_annotations' for variable with specified 'f…
…actor_levels'.
  • Loading branch information
OLarionova-HORIS committed Dec 5, 2023
commit b53782b5bc2551019f70a813599a502b60316993
Original file line number Diff line number Diff line change
Expand Up @@ -249,18 +249,6 @@ class AsDiscrete {
'column': 'value',
'factor_levels': [4,1,2]
}
],
'mapping_annotations': [
{
'aes': 'x',
'annotation': 'as_discrete',
'parameters': {'label': 'Name'}
},
{
'aes': 'fill',
'annotation': 'as_discrete',
'parameters': {'label': 'Value'}
}
]
}
}
Expand Down
155 changes: 12 additions & 143 deletions docs/dev/notebooks/as_discrete_levels.ipynb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,6 @@ internal object DataConfigUtil {
asDiscreteAesSet: Set<String>,
orderOptions: List<OrderOptionUtil.OrderOption>,
aggregateOperation: (List<Double?>) -> Double?,
combinedMappingOptions: Map<String, String>,
clientSide: Boolean
): DataFrame {

Expand Down Expand Up @@ -182,18 +181,8 @@ internal object DataConfigUtil {
val orderSpecs = OrderOptionUtil.createOrderSpecs(orderOptions, variables, varBindings, aggregateOperation)

val factorLevelsByVar = DataMetaUtil.getFactorLevelsByVariable(ownDataMeta)
.flatMap { (varName, levels) ->
val variable = variables.find { it.name == varName }
val mappedVariables = combinedMappingOptions
.filterValues { it == varName }
.keys
.mapNotNull { aesName -> varBindings.find { it.aes.name == aesName }?.variable }
val variablesWithLevels = (mappedVariables + variable)
.filterNotNull()
variablesWithLevels.map { it to levels }
}
.toMap()

.mapKeys { (varName, _) -> variables.find { it.name == varName } }
.filterNotNullKeys()
this
.addOrderSpecs(orderSpecs)
.addFactorLevels(factorLevelsByVar)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,6 @@ class LayerConfig(
asDiscreteAesSet = combinedDiscreteMappings.keys,
orderOptions = orderOptions,
aggregateOperation = aggregateOperation,
combinedMappingOptions = consumedAesMappings,
clientSide = clientSide
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

package org.jetbrains.letsPlot.core.spec.config

import demoAndTestShared.parsePlotSpec
import org.jetbrains.letsPlot.core.plot.base.data.DataFrameUtil
import org.jetbrains.letsPlot.core.spec.config.AsDiscreteTest.*
import org.jetbrains.letsPlot.core.spec.front.PlotConfigFrontend
Expand All @@ -14,185 +13,127 @@ import kotlin.test.assertEquals

class DataMetaFactorLevelsTest {

private fun makePlotSpec(
seriesAnnotations: String?,
mappingAnnotations: String?,
dataStorage: Storage = Storage.LAYER,
mappingStorage: Storage = Storage.LAYER,
data: String = myData,
mapping: String = myMapping,
): String {
val dataSpec = "\'data\': $data,"
val mappingSpec = "\'mapping\': $mapping,"
val annotations = listOfNotNull(seriesAnnotations, mappingAnnotations).joinToString()
val annotation = """'data_meta': { $annotations },"""
return """{
'kind': 'plot',
${dataSpec.takeIf { dataStorage == Storage.PLOT } ?: ""}
${annotation.takeIf { mappingStorage == Storage.PLOT } ?: ""}
${mappingSpec.takeIf { mappingStorage == Storage.PLOT } ?: ""}
'layers': [
{
${dataSpec.takeIf { dataStorage == Storage.LAYER } ?: ""}
${annotation.takeIf { mappingStorage == Storage.LAYER } ?: ""}
${mappingSpec.takeIf { mappingStorage == Storage.LAYER } ?: ""}
'geom': 'bar', 'stat': 'identity'
}
]
}""".trimIndent()
}

private fun mappingAnnotationsSpec(aesList: List<String>): String {
val asDiscreteAnnotationsSpec = aesList.joinToString { aes ->
"""{
'aes': '$aes',
'annotation': 'as_discrete'
}""".trimIndent()
}
return "'mapping_annotations': [$asDiscreteAnnotationsSpec]"
}

private fun seriesAnnotationsSpec(varListWithLevels: Map<String, List<Any>>): String {
val seriesAnnotations = varListWithLevels.toList().joinToString { (variable, factorLevels) ->
val factorStringList = factorLevels.joinToString { if (it is String) "\'$it\'" else "$it" }
"""{
'column': '$variable',
'factor_levels': [ $factorStringList ]
}""".trimIndent()
}
return "'series_annotations': [$seriesAnnotations]"
}

private val myData = """{
'name': ['a', 'b', 'c', 'd'],
'c': [1, 2, 3, 4]
'name': ['b', 'c', 'a', 'd'],
'c': [4, 1, 3, 2]
}""".trimIndent()

private val myMapping = "{'x': 'name', 'y': 'c', 'fill': 'c'}"
private val myMapping = "{'x': 'name', 'y': 'c'}"

@Test
fun default() {
val spec = makePlotSpec(
seriesAnnotations = null,
mappingAnnotations = null
)
transformToClientPlotConfig(spec)
.assertDistinctValues("name", listOf("a", "b", "c", "d"))
.assertDistinctValues("c", listOf(1.0, 2.0, 3.0, 4.0))
}

@Test
fun setupAsDiscreteAnnotations() {
val spec = makePlotSpec(
seriesAnnotations = null,
mappingAnnotations = mappingAnnotationsSpec(listOf("x", "fill"))
)
transformToClientPlotConfig(spec)
.assertDistinctValues("name", listOf("a", "b", "c", "d"))
.assertDistinctValues("c", listOf(1.0, 2.0, 3.0, 4.0))
// + 'as_discrete' variables:
.assertDistinctValues("x.name", listOf("a", "b", "c", "d"))
.assertDistinctValues("fill.c", listOf(1.0, 2.0, 3.0, 4.0))
}

@Test
fun setupSeriesAnnotations() {
val spec = makePlotSpec(
seriesAnnotations = seriesAnnotationsSpec(
mapOf(
"name" to listOf("c", "b", "a"),
"c" to listOf(2.0, 3.0, 1.0)
)
),
mappingAnnotations = null
)
val spec = makePlotSpec(seriesAnnotations = null)
transformToClientPlotConfig(spec)
.assertVariable("name", isDiscrete = true)
.assertVariable("c", isDiscrete = true) // specified levels (in series_annotations) make variable discrete
.assertDistinctValues("name", listOf("c", "b", "a", "d"))
.assertDistinctValues("c", listOf(2.0, 3.0, 1.0, 4.0))
.assertDistinctValues("name", listOf("b", "c", "a", "d"))
.assertDistinctValues("c", listOf(4.0, 1.0, 3.0, 2.0))
}

private fun checkWithMappingAndSeriesAnnotations(
private fun withSeriesAnnotations(
dataStorage: Storage,
mappingStorage: Storage
) {
val spec = makePlotSpec(
seriesAnnotations = seriesAnnotationsSpec(
seriesAnnotations = withFactorLevels(
mapOf(
"name" to listOf("c", "b", "a"),
"c" to listOf(2.0, 3.0, 1.0)
"name" to listOf("c", "b", "a", "d"),
"c" to listOf(2.0, 3.0, 1.0, 4.0)
)
),
mappingAnnotations = mappingAnnotationsSpec(listOf("x", "fill")),
dataStorage,
mappingStorage
)
transformToClientPlotConfig(spec)
.assertVariable("name", isDiscrete = true)
.assertVariable("x.name", isDiscrete = true)
.assertDistinctValues("name", listOf("c", "b", "a", "d"))
.assertDistinctValues("x.name", listOf("c", "b", "a", "d"))

.assertVariable("c", isDiscrete = true)
.assertVariable("fill.c", isDiscrete = true)
.assertDistinctValues("name", listOf("c", "b", "a", "d"))
.assertDistinctValues("c", listOf(2.0, 3.0, 1.0, 4.0))
.assertDistinctValues("fill.c", listOf(2.0, 3.0, 1.0, 4.0))
}

// settings in plot/layer spec

@Test
fun plot_LayerDataMapping() {
checkWithMappingAndSeriesAnnotations(
withSeriesAnnotations(
dataStorage = Storage.LAYER,
mappingStorage = Storage.LAYER
)
}

@Test
fun plotDataMapping_Layer() {
checkWithMappingAndSeriesAnnotations(
withSeriesAnnotations(
dataStorage = Storage.PLOT,
mappingStorage = Storage.PLOT
)
}

@Test
fun plotData_LayerMapping() {
checkWithMappingAndSeriesAnnotations(
withSeriesAnnotations(
dataStorage = Storage.PLOT,
mappingStorage = Storage.LAYER
)
}

@Test
fun plotMapping_LayerData() {
checkWithMappingAndSeriesAnnotations(
withSeriesAnnotations(
dataStorage = Storage.LAYER,
mappingStorage = Storage.PLOT
)
}

@Test
fun test_series_annotations() {
val spec = """{
'kind': 'plot',
'data': {'name': ['a', 'b', 'c']},
'mapping': {'x': 'name' },
'data_meta': {
'series_annotations': [
{
'column': 'name',
'factor_levels': ['a', 'c', 'b']
}
]
},
'layers': [ { 'geom': 'bar' } ]
}""".trimIndent()

transformToClientPlotConfig(parsePlotSpec(spec))
.assertDistinctValues("name", listOf("a", "c", "b"))
fun `should extend levels with missing values`() {
val spec = makePlotSpec(
seriesAnnotations = withFactorLevels(
mapOf(
"name" to listOf("c", "a"),
"c" to listOf(2.0, 3.0)
)
)
)

transformToClientPlotConfig(spec)
.assertDistinctValues("name", listOf("c", "a", "b", "d"))
.assertDistinctValues("c", listOf(2.0, 3.0, 4.0, 1.0))
}

private fun makePlotSpec(
seriesAnnotations: String?,
dataStorage: Storage = Storage.LAYER,
mappingStorage: Storage = Storage.LAYER,
data: String = myData,
mapping: String = myMapping,
): String {
val dataSpec = "\'data\': $data,"
val mappingSpec = "\'mapping\': $mapping,"
val annotation = seriesAnnotations?.let { """'data_meta': { $seriesAnnotations },""" } ?: ""
return """{
'kind': 'plot',
${dataSpec.takeIf { dataStorage == Storage.PLOT } ?: ""}
${annotation.takeIf { mappingStorage == Storage.PLOT } ?: ""}
${mappingSpec.takeIf { mappingStorage == Storage.PLOT } ?: ""}
'layers': [
{
${dataSpec.takeIf { dataStorage == Storage.LAYER } ?: ""}
${annotation.takeIf { mappingStorage == Storage.LAYER } ?: ""}
${mappingSpec.takeIf { mappingStorage == Storage.LAYER } ?: ""}
'geom': 'bar', 'stat': 'identity'
}
]
}""".trimIndent()
}

private fun withFactorLevels(varListWithLevels: Map<String, List<Any>>): String {
val seriesAnnotations = varListWithLevels.toList().joinToString { (variable, factorLevels) ->
val factorStringList = factorLevels.joinToString { if (it is String) "\'$it\'" else "$it" }
"""{
'column': '$variable',
'factor_levels': [ $factorStringList ]
}""".trimIndent()
}
return "'series_annotations': [$seriesAnnotations]"
}

companion object {
Expand Down
27 changes: 18 additions & 9 deletions python-package/lets_plot/plot/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,33 @@ def as_annotated_data(raw_data: Any, raw_mapping: Any) -> Tuple:
# series annotations
series_meta = []

if raw_mapping is not None:
for aesthetic, variable in raw_mapping.as_dict().items():
if aesthetic == 'name': # ignore FeatureSpec.name property
continue
categorical_variables = []
# todo add pandas Categorical

if raw_mapping is not None:
# series annotations
for variable in raw_mapping.as_dict().values():
if isinstance(variable, MappingMeta):
if variable.levels is not None:
categorical_variables.append(variable.variable)
series_meta.append({
'column': variable.variable,
'factor_levels': variable.levels
})

# mapping_annotations
for aesthetic, variable in raw_mapping.as_dict().items():
if aesthetic == 'name': # ignore FeatureSpec.name property
continue

if isinstance(variable, MappingMeta):
mapping[aesthetic] = variable.variable
mapping_meta.append({
'aes': aesthetic,
'annotation': variable.annotation,
'parameters': variable.parameters
})
if variable.variable not in categorical_variables:
mapping_meta.append({
'aes': aesthetic,
'annotation': variable.annotation,
'parameters': variable.parameters
})
else:
mapping[aesthetic] = variable

Expand Down
Loading