This notebook is inspired by an example Radar chart with ggradar.
%useLatestDescriptors
%use dataframe
%use lets-plot
fun rescaleInGroupDataFrame(df: DataFrame<*>, valueCol: String, groupCol: String, rescaledCol: String? = null): DataFrame<*> {
val rescaledColName = if (rescaledCol == null) {
"rescaled_${valueCol}"
} else {
rescaledCol
}
fun rescaleSubDataFrame(subDf: DataFrame<*>): DataFrame<*> {
val minValue = subDf.minByOrNull(valueCol)?.let { it[valueCol] } as Double
val maxValue = subDf.maxByOrNull(valueCol)?.let { it[valueCol] } as Double
return subDf.add(rescaledColName) { (valueCol<Double>() - minValue) / (maxValue - minValue) }
}
return df.select(groupCol).distinct().map { v -> rescaleSubDataFrame(df.filter { groupCol<String>() == v[groupCol] }) }.concat()
}
fun getData(): DataFrame<*> {
val df = DataFrame.readCSV("https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/penguins.csv")
.dropNulls()
.rename("bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g")
.into("avg. bill length", "avg. bill depth", "avg. flipper length", "avg. body mass")
.groupBy("species")
.mean()
.gather("avg. bill length", "avg. bill depth", "avg. flipper length", "avg. body mass")
.into("variable", "value")
.update { "value"<Double>() }
.where { "variable"<String>() == "avg. body mass" }
.with { it / 1000 }
.add("units") {
when ("variable"<String>()) {
"avg. body mass" -> "kg"
else -> "mm"
}
}
return rescaleInGroupDataFrame(df, "value", "variable")
.add("rescaled_value_pct") { floor(100 * "rescaled_value"<Double>()) }
.convert { "rescaled_value_pct"<Double>() }.to<Int>()
.sortBy("species")
}
val df = getData()
df.head(4)
DataFrame: rowsCount = 4, columnsCount = 6
species | variable | value | units | rescaled_value | rescaled_value_pct |
---|---|---|---|---|---|
Adelie | avg. bill length | 38.823973 | mm | 0.000000 | 0 |
Adelie | avg. bill depth | 18.347260 | mm | 0.978584 | 97 |
Adelie | avg. flipper length | 190.102740 | mm | 0.000000 | 0 |
Adelie | avg. body mass | 3.706164 | kg | 0.000000 | 0 |
val fontFamily = "roboto"
val axisColor = "lightgray"
val axisTextData = mapOf(
"x" to List(3) { "avg. bill length" },
"y" to listOf(0, 50, 100),
"text" to listOf("0%", "50%", "100%")
)
val penguinsColors = mapOf(
"Adelie" to "#ff5a5f",
"Chinstrap" to "#ffb400",
"Gentoo" to "#007a87"
)
val penguinsTooltips = layerTooltips().title("@species").line("@variable (@units): @value").format("@value", ".2~f")
val penguinsTheme = theme(
text = elementText(family = fontFamily, size = 18),
plotTitle = elementText(size = 28, hjust = .5, face = "bold"),
axisTitle = "blank", axisTextY = "blank", axisLineX = "blank",
axisTicks = elementLine(color = axisColor),
panelGrid = elementLine(color = axisColor),
panelInset = Pair(0, 100),
tooltip = elementRect(),
axisTooltip = "blank",
).legendPosition(1, 0).legendJustification(1, 0)
letsPlot(df.toMap()) +
geomArea(position = positionIdentity, flat = true,
size = 2.5, colorBy = "paint_a", fillBy = "paint_a", alpha = .2)
{ x = "variable"; y = "rescaled_value_pct"; paint_a = "species" } +
geomPoint(size = 6, colorBy = "paint_a", tooltips = penguinsTooltips)
{ x = "variable"; y = "rescaled_value_pct"; paint_a = "species" } +
geomText(data = axisTextData, hjust = 1, fontface = "bold", family = fontFamily, size = 10)
{ x = "x"; y = "y"; label = "text" } +
scaleXDiscrete() +
scaleManual("paint_a", name = "", values = penguinsColors) +
coordPolar(ylim = Pair(-15, 100)) +
ggsize(800, 600) +
ggtitle("Penguins species") +
penguinsTheme + flavorSolarizedLight()