width
and height
Aesthetics¶Previously, the width
and height
aesthetics were limited to relative sizing based on data resolution, which sometimes made it difficult to maintain consistent visual dimensions across different data scales.
Now multiple geometries also support absolute unit specification for their width
and height
aesthetics via the new widthUnit
and heightUnit
parameters:
geomErrorBar()
geomCrossbar()
geomBoxplot()
geomTile()
geomHex()
Available Units:
"res"
(default): Value 1 corresponds to the resolution along the axis - the minimum distance between data points"identity"
: Value 1 corresponds to the distance from 0 to 1 on the axis"size"
: Value 1 corresponds to the diameter of a point of size 1"px"
: Value 1 corresponds to 1 pixel%useLatestDescriptors
%use dataframe
%use lets-plot
import kotlin.math.sqrt
LetsPlot.getInfo()
Lets-Plot Kotlin API v.4.10.0. Frontend: Notebook with dynamically loaded JS. Lets-Plot JS v.4.6.1.
val df = DataFrame.readCSV("https://github.com/JetBrains/lets-plot-docs/raw/refs/heads/master/data/mpg.csv")
.add("trans_type") { row ->
val trans: String? = row["trans"] as? String
trans?.split("(")?.firstOrNull()
}
val dataMap = df.toMap()
df.head()
DataFrame: rowsCount = 5, columnsCount = 13
untitled | manufacturer | model | displ | year | cyl | trans | drv | cty | hwy | fl | class | trans_type |
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | audi | a4 | 1.800000 | 1999 | 4 | auto(l5) | f | 18 | 29 | p | compact | auto |
2 | audi | a4 | 1.800000 | 1999 | 4 | manual(m5) | f | 21 | 29 | p | compact | manual |
3 | audi | a4 | 2.000000 | 2008 | 4 | manual(m6) | f | 20 | 31 | p | compact | manual |
4 | audi | a4 | 2.000000 | 2008 | 4 | auto(av) | f | 21 | 30 | p | compact | auto |
5 | audi | a4 | 2.800000 | 1999 | 6 | auto(l5) | f | 16 | 26 | p | compact | auto |
Suppose we have the following plot, but we are not satisfied that different facets have different whisker widths:
letsPlot(dataMap) +
geomErrorBar(stat = Stat.summary(), size = 1, width = .5)
{ x = asDiscrete("trans", order = 1); y = "hwy"; color = "trans_type" } +
facetGrid(x = "year", scales = "free_x")
We can make the widths uniform by setting the absolute unit of measurement for them. For example, in pixels:
letsPlot(dataMap) +
geomErrorBar(stat = Stat.summary(), size = 1,
width = 10, // <-- set width - 10 px
widthUnit = "px")
{ x = asDiscrete("trans", order = 1); y = "hwy"; color = "trans_type" } +
facetGrid(x = "year", scales = "free_x")
When using geomHex()
with the Stat.identity
, you must prepare the data yourself to fit into a hexagonal grid.
Let's assume you have prepared the following dataset:
fun getData(n: Int, m: Int, sizes: List<Int>, seed: Long): DataFrame<*> {
val rand = java.util.Random(seed)
fun generateFullDataset(): DataFrame<*> {
return dataFrameOf(
"x" to (0 until m).map { i -> (0 until n).map { j -> j + ((i % 2) / 2.0) } }.flatten(),
"y" to (0 until m).map { j -> (0 until n).map { i -> j } }.flatten(),
"v" to List(n * m) { rand.nextFloat() }
)
}
fun getRandomSample(df: DataFrame<*>, size: Int, reg: Boolean = false): DataFrame<*> {
val subDf = if (reg) {
df.filter { "y"<Int>() % 2 == 0 }.sortBy("x", "y")
} else {
df
}
val limit = min(subDf.rowsCount(), size)
val indices = (0 until subDf.rowsCount()).shuffled(java.util.Random(seed)).take(limit)
return subDf.filter { index() in indices }
}
return sizes.mapIndexed { i, size -> getRandomSample(generateFullDataset(), size, i == 0).add("g") { "group $i" } }.concat()
}
val df = getData(6, 5, listOf(8, 9, 7), seed = 0)
val dataMap = df.toMap()
df.head()
DataFrame: rowsCount = 5, columnsCount = 4
x | y | v | g |
---|---|---|---|
0.000000 | 0 | 0.730968 | group 0 |
0.000000 | 2 | 0.385189 | group 0 |
1.000000 | 2 | 0.613036 | group 0 |
1.000000 | 4 | 0.705175 | group 0 |
2.000000 | 2 | 0.984842 | group 0 |
The Lets-Plot tries to choose the sizes of the hexagons by itself, but in some situations the result may be unsatisfactory, as in the plot below. Namely in the facet with 'group 0', the hexagons are too large to fit into the grid:
letsPlot(dataMap) { x = "x"; y = "y"; fill = "v" } +
geomHex(stat = Stat.identity, size = .5) +
facetWrap(facets = "g", nrow = 1)
Since the data resolution varies across facets, simply resizing the hexagons
isn't sufficient.
By default, width
and height
use the 'res'
unit, which is relative to the resolution (minimum distance between hexagon centers) in each individual facet:
letsPlot(dataMap) { x = "x"; y = "y"; fill = "v" } +
geomHex(stat = Stat.identity, size = .5,
width = .5, height = .5) +
facetWrap(facets = "g", nrow = 1)
'identity'
units to express hexagon width
/height
in consistent X/Y-axis units.¶letsPlot(dataMap) { x = "x"; y = "y"; fill = "v" } +
geomHex(stat = Stat.identity, size = .5,
widthUnit = "identity", width = 1, // <-- Set width in data units
heightUnit = "identity", height = 2.0/sqrt(3.0) // <-- Set height in data units
) +
facetWrap(facets = "g", nrow = 1)
Note that when using "px"
or "size"
units the axis scales do not automatically expand to accommodate the dimensions of geometry.
fun getPlot(
width: Double,
height: Double,
widthUnit: String,
heightUnit: String
): org.jetbrains.letsPlot.intern.Plot {
val w = 7
val h = 5
val dataMap = mapOf(
"x" to listOf(-w, -w, w, w),
"y" to listOf(-h, h, -h, h),
"c" to listOf("a", "b", "c", "d")
)
return letsPlot(dataMap) { x = "x"; y = "y"; fill = "c" } +
geomTile(width = width, widthUnit = widthUnit,
height = height, heightUnit = heightUnit,
showLegend = false) +
geomPoint(shape = 21, fill = "white") +
coordFixed() +
ggtitle("width=$width, width_unit=\"$widthUnit\"\nheight=$height, height_unit=\"$heightUnit\"")
}
gggrid(listOf(
getPlot(.9, .9, "res", "res"),
getPlot(12.0, 8.0, "identity", "identity"),
getPlot(100.0, 70.0, "size", "size"),
getPlot(200.0, 150.0, "px", "px"),
), ncol = 2)