Skip to content

Commit

Permalink
Boxplot refactored (#198)
Browse files Browse the repository at this point in the history
* `geomBoxplot()` as two layers.

* Add `statBoxplot()`.
  • Loading branch information
OLarionova-HORIS committed Aug 4, 2023
1 parent 4dfa98a commit f6a66eb
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package frontendContextDemo.scripts
import frontendContextDemo.ScriptInBatikContext
import org.jetbrains.letsPlot.geom.geomBoxplot
import org.jetbrains.letsPlot.geom.geomJitter
import org.jetbrains.letsPlot.label.ggtitle
import org.jetbrains.letsPlot.letsPlot
import org.jetbrains.letsPlot.stat.statBoxplot
import kotlin.math.abs
Expand All @@ -31,10 +32,17 @@ object Boxplot {
val p = letsPlot(data) { x = "cat"; y = "val" }

(p + geomJitter()).show()
(p + geomBoxplot(outlierColor = "red")).show()
(p + geomBoxplot(outlierColor = "red", outlierSize = 2, outlierShape = 21)).show()
(p + geomBoxplot(outlierColor = "red", varWidth = true)).show()
(p + statBoxplot(outlierColor = "red", varWidth = true, fatten = 2, color = "dark-magenta")).show()
(p + geomBoxplot(whiskerWidth = 0.5)).show()

// Y-orientation
(letsPlot(data) { y = "cat"; x = "val" } + ggtitle("Boxplot Y-orientation") +
geomBoxplot(
orientation = "y",
outlierColor = "red", outlierSize = 2, outlierShape = 21
)).show()
}
}
}
5 changes: 0 additions & 5 deletions plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/Geom.kt
Original file line number Diff line number Diff line change
Expand Up @@ -428,11 +428,6 @@ object Geom {
override val upper: Number? = null,
override val ymin: Number? = null,
override val ymax: Number? = null,
override val outlierColor: Any? = null,
override val outlierFill: Any? = null,
override val outlierShape: Any? = null,
override val outlierSize: Number? = null,
override val outlierStroke: Number? = null,
override val fatten: Number? = null,
override val whiskerWidth: Number? = null,
override val alpha: Number? = null,
Expand Down
13 changes: 13 additions & 0 deletions plot-api/src/commonMain/kotlin/org/jetbrains/letsPlot/Stat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ object Stat {
override val parameters = this.seal()
}

@Suppress("ClassName")
class boxplotOutlier(
@Suppress("SpellCheckingInspection")
override val coef: Number? = null,
mapping: BoxplotStatMapping.() -> Unit = {}
) : BoxplotOutlierStatParameters,
StatOptions(
StatKind.BOXPLOT_OUTLIER,
mapping = BoxplotStatMapping().apply(mapping).seal()
) {
override val parameters = this.seal()
}

@Suppress("ClassName")
class bin2D(
override val bins: Pair<Int, Int>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ package org.jetbrains.letsPlot.geom

import org.jetbrains.letsPlot.Geom.boxplot
import org.jetbrains.letsPlot.Stat
import org.jetbrains.letsPlot.intern.Options
import org.jetbrains.letsPlot.intern.Layer
import org.jetbrains.letsPlot.intern.*
import org.jetbrains.letsPlot.intern.layer.PosOptions
import org.jetbrains.letsPlot.intern.layer.SamplingOptions
import org.jetbrains.letsPlot.intern.layer.StatOptions
Expand All @@ -22,7 +21,6 @@ import org.jetbrains.letsPlot.intern.layer.stat.BoxplotStatParameters
import org.jetbrains.letsPlot.pos.positionDodge
import org.jetbrains.letsPlot.tooltips.TooltipOptions

@Suppress("ClassName")
/**
* Displays the distribution of data based on a five number summary ("minimum", first quartile (Q1),
* median, third quartile (Q3), and "maximum"), and "outlying" points individually.
Expand All @@ -46,6 +44,7 @@ import org.jetbrains.letsPlot.tooltips.TooltipOptions
* Specifies appearance, style and content.
* @param orientation Specifies the axis that the layer's stat and geom should run along, default = "x".
* Possible values: "x", "y".
* @param outlierAlpha Default transparency aesthetic for outliers.
* @param outlierColor Color aesthetic for outliers.
* @param outlierFill Fill aesthetic for outliers.
* @param outlierShape Shape aesthetic for outliers.
Expand Down Expand Up @@ -94,7 +93,86 @@ import org.jetbrains.letsPlot.tooltips.TooltipOptions
* Aesthetic mappings describe the way that variables in the data are
* mapped to plot "aesthetics".
*/
class geomBoxplot(
fun geomBoxplot(
data: Map<*, *>? = null,
stat: StatOptions = Stat.boxplot(),
position: PosOptions = positionDodge(),
showLegend: Boolean = true,
sampling: SamplingOptions? = null,
tooltips: TooltipOptions? = null,
orientation: String? = null,
x: Number? = null,
y: Number? = null,
lower: Number? = null,
middle: Number? = null,
upper: Number? = null,
ymin: Number? = null,
ymax: Number? = null,
alpha: Number? = null,
color: Any? = null,
fill: Any? = null,
size: Number? = null,
stroke: Number? = null,
linetype: Any? = null,
shape: Any? = null,
width: Any? = null,
weight: Any? = null,
outlierAlpha: Number? = null,
outlierColor: Any? = null,
outlierFill: Any? = null,
outlierShape: Any? = null,
outlierSize: Number? = null,
outlierStroke: Number? = null,
fatten: Number? = null,
whiskerWidth: Number? = null,
varWidth: Boolean? = null,
@Suppress("SpellCheckingInspection")
coef: Number? = null,
colorBy: String? = null,
fillBy: String? = null,
mapping: OptionsCapsule .() -> Unit = {}
): FeatureList {
val layers = mutableListOf<Layer>()

layers += geomBoxplotInternal(
data,
stat,
position,
showLegend,
sampling,
tooltips,
orientation,
x, y, lower, middle, upper, ymin, ymax, alpha, color, fill, size, linetype, shape, width, weight, fatten,
whiskerWidth, varWidth, coef,
colorBy, fillBy,
mapping
)

if (stat.kind == StatKind.BOXPLOT) {
val outlierFatten = 4.0
layers += geomPoint(
data = data,
stat = Stat.boxplotOutlier(),
position = position,
showLegend = false,
sampling = null,
orientation = orientation,
x = x, y = y,
alpha = outlierAlpha ?: alpha,
color = outlierColor ?: color,
fill = outlierFill ?: fill,
shape = outlierShape ?: shape,
size = (outlierSize ?: size)?.let { it.toDouble() * outlierFatten },
stroke = outlierStroke ?: stroke,
colorBy = colorBy, fillBy = fillBy,
mapping = mapping
)
}
return FeatureList(layers)
}

@Suppress("ClassName")
private class geomBoxplotInternal(
data: Map<*, *>? = null,
stat: StatOptions = Stat.boxplot(),
position: PosOptions = positionDodge(),
Expand All @@ -117,11 +195,6 @@ class geomBoxplot(
override val shape: Any? = null,
override val width: Any? = null,
override val weight: Any? = null,
override val outlierColor: Any? = null,
override val outlierFill: Any? = null,
override val outlierShape: Any? = null,
override val outlierSize: Number? = null,
override val outlierStroke: Number? = null,
override val fatten: Number? = null,
override val whiskerWidth: Number? = null,
override val varWidth: Boolean? = null,
Expand All @@ -130,7 +203,6 @@ class geomBoxplot(
override val colorBy: String? = null,
override val fillBy: String? = null,
mapping: BoxplotMapping .() -> Unit = {}

) : BoxplotAesthetics,
BoxplotParameters,
BoxplotStatAesthetics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import org.jetbrains.letsPlot.tooltips.TooltipOptions
* For more info see [sampling.md](https://github.com/JetBrains/lets-plot-kotlin/blob/master/docs/sampling.md).
* @param tooltips Result of the call to the `layerTooltips()` function.
* Specifies appearance, style and content.
* @param orientation Specifies the axis that the layer's stat and geom should run along, default = "x".
* Possible values: "x", "y".
* @param map Data-structure containing series of planar shapes and, optionally, associates data series (for example:
* names of States and their boundaries).
*
Expand Down Expand Up @@ -92,6 +94,7 @@ class geomPoint(
showLegend: Boolean = true,
sampling: SamplingOptions? = null,
tooltips: TooltipOptions? = null,
orientation: String? = null,
override val map: SpatialDataset? = null,
override val mapJoin: Pair<Any, Any>? = null,
override val useCRS: String? = null,
Expand Down Expand Up @@ -121,7 +124,8 @@ class geomPoint(
position = position,
showLegend = showLegend,
sampling = sampling,
tooltips = tooltips
tooltips = tooltips,
orientation = orientation
) {
override fun seal() = super<PointAesthetics>.seal() +
super<WithSizeUnitOption>.seal() +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import org.jetbrains.letsPlot.intern.layer.MarginalLayer
fun ggmarginal(
sides: String,
size: Any? = null,
layer: Layer
layer: Feature
): Feature {

require(sides.isNotBlank()) { SIDES_ARG_ERROR }
Expand All @@ -51,6 +51,13 @@ fun ggmarginal(
else -> throw IllegalArgumentException("Invalid 'size' type: ${size::class.simpleName}. Expected: number, list or pair.")
} + List<Any?>(4) { null }

if (layer is FeatureList) {
return FeatureList(
layer.elements.map { sublayer -> ggmarginal(sides, size = size, layer = sublayer) }
)
}

require(layer is Layer) { "Invalid 'layer' type: ${layer::class.simpleName}" }

var result: Feature = DummyFeature
for ((i, side) in sides.withIndex()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum class StatKind {
BIN,
BIN2D,
BOXPLOT,
BOXPLOT_OUTLIER,
CONTOUR,
CONTOURF,
SMOOTH,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,10 @@ import org.jetbrains.letsPlot.intern.Options
import org.jetbrains.letsPlot.intern.OptionsCapsule

interface BoxplotParameters : OptionsCapsule {
val outlierColor: Any?
val outlierFill: Any?
val outlierShape: Any?
val outlierSize: Number?
val outlierStroke: Number?
val fatten: Number?
val whiskerWidth: Number?

override fun seal() = Options.of(
// ToDo: 'BoxplotOutlier' was recently removed.
// Option.Geom.BoxplotOutlier.COLOR to outlierColor,
// Option.Geom.BoxplotOutlier.FILL to outlierFill,
// Option.Geom.BoxplotOutlier.SHAPE to outlierShape,
// Option.Geom.BoxplotOutlier.SIZE to outlierSize,
// Option.Geom.BoxplotOutlier.STROKE to outlierStroke,
Option.Geom.Boxplot.FATTEN to fatten,
Option.Geom.Boxplot.WHISKER_WIDTH to whiskerWidth
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright (c) 2023. 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.layer.stat

import org.jetbrains.letsPlot.core.spec.Option
import org.jetbrains.letsPlot.intern.Options
import org.jetbrains.letsPlot.intern.OptionsCapsule

interface BoxplotOutlierStatParameters : OptionsCapsule {
@Suppress("SpellCheckingInspection")
val coef: Number? // Whisker IQR ratio

override fun seal() = Options.of(
Option.Stat.Boxplot.COEF to coef
)
}
Loading

0 comments on commit f6a66eb

Please sign in to comment.