Skip to content

Commit

Permalink
Horizontal orientation by assigning y, xmin, xmax aesthetics of geoms: (
Browse files Browse the repository at this point in the history
  • Loading branch information
RYangazov committed Sep 27, 2023
1 parent add3571 commit d03961c
Show file tree
Hide file tree
Showing 13 changed files with 966 additions and 279 deletions.
430 changes: 430 additions & 0 deletions docs/f-23e/horizontal_geoms.ipynb

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions future_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

### Added

- Horizontal orientation by assigning y, xmin, xmax aesthetics of geoms:
- `geom_errorbar()`;
- `geom_crossbar()`;
- `geom_pointrange()`;
- `geom_linerange()`;
- `geom_ribbon()`.

See: [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-23e/horizontal_geoms.ipynb).

### Changed

- [BREAKING] `stat_summary()` and `stat_summary_bin` no longer supports computing of additional variables through the specifying of mappings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,13 @@ object GeomMeta {
)

GeomKind.CROSS_BAR -> listOf(
Aes.X,
Aes.YMIN, Aes.YMAX, Aes.Y,
Aes.X, Aes.Y,
// vertical representation
Aes.YMIN, Aes.YMAX,
Aes.WIDTH,
// horizontal
Aes.XMIN, Aes.XMAX,
Aes.HEIGHT,

Aes.ALPHA,
Aes.COLOR,
Expand All @@ -170,8 +174,13 @@ object GeomMeta {
)

GeomKind.LINE_RANGE -> listOf(
// vertical representation
Aes.X,
Aes.YMIN, Aes.YMAX,
// horizontal
Aes.Y,
Aes.XMIN, Aes.XMAX,

Aes.ALPHA,
Aes.COLOR,
Aes.LINETYPE,
Expand All @@ -180,7 +189,11 @@ object GeomMeta {

GeomKind.POINT_RANGE -> listOf(
Aes.X, Aes.Y,
// vertical representation
Aes.YMIN, Aes.YMAX,
// horizontal
Aes.XMIN, Aes.XMAX,

Aes.ALPHA,
Aes.COLOR,
Aes.FILL,
Expand Down Expand Up @@ -286,8 +299,13 @@ object GeomMeta {
)

GeomKind.RIBBON -> listOf(
//vertical representation
Aes.X,
Aes.YMIN, Aes.YMAX,
//horizontal
Aes.Y,
Aes.XMIN, Aes.XMAX,

Aes.SIZE,
Aes.LINETYPE,
Aes.COLOR,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,34 @@ package org.jetbrains.letsPlot.core.plot.base.geom
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.core.plot.base.*
import org.jetbrains.letsPlot.core.plot.base.geom.util.BarTooltipHelper
import org.jetbrains.letsPlot.core.plot.base.geom.util.BoxHelper
import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomHelper
import org.jetbrains.letsPlot.core.plot.base.geom.util.HintColorUtil
import org.jetbrains.letsPlot.core.plot.base.geom.util.*
import org.jetbrains.letsPlot.core.plot.base.render.LegendKeyElementFactory
import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot

class CrossBarGeom : GeomBase() {
class CrossBarGeom(private val isVertical: Boolean) : GeomBase() {
private val flipHelper = FlippableGeomHelper(isVertical)
var fattenMidline: Double = 2.5

override val legendKeyElementFactory: LegendKeyElementFactory
get() = LEGEND_FACTORY

override val wontRender: List<Aes<*>>
get() {
return listOf(Aes.XMIN, Aes.XMAX).map(::afterRotation)
}

private fun afterRotation(aes: Aes<Double>): Aes<Double> {
return flipHelper.getEffectiveAes(aes)
}

private fun afterRotation(rectangle: DoubleRectangle): DoubleRectangle {
return flipHelper.flip(rectangle)
}

private fun afterRotation(vector: DoubleVector): DoubleVector {
return flipHelper.flip(vector)
}

override fun buildIntern(
root: SvgRoot,
aesthetics: Aesthetics,
Expand All @@ -33,58 +48,95 @@ class CrossBarGeom : GeomBase() {
root, aesthetics, pos, coord, ctx,
rectFactory = clientRectByDataPoint(ctx, geomHelper, isHintRect = false)
)
BoxHelper.buildMidlines(root, aesthetics, middleAesthetic = Aes.Y, ctx, geomHelper, fatten = fattenMidline)
BarTooltipHelper.collectRectangleTargets(
listOf(Aes.YMAX, Aes.YMIN),
buildMidlines(root, aesthetics, ctx, geomHelper, fatten = fattenMidline)
// tooltip
flipHelper.buildHints(
listOf(Aes.YMIN, Aes.YMAX).map(::afterRotation),
aesthetics, pos, coord, ctx,
clientRectByDataPoint(ctx, geomHelper, isHintRect = true),
{ HintColorUtil.colorWithAlpha(it) }
)
}

companion object {
const val HANDLES_GROUPS = false

private val LEGEND_FACTORY = BoxHelper.legendFactory(false)
private fun clientRectByDataPoint(
ctx: GeomContext,
geomHelper: GeomHelper,
isHintRect: Boolean
): (DataPointAesthetics) -> DoubleRectangle? {
return { p ->
val xAes = afterRotation(Aes.X)
val yAes = afterRotation(Aes.Y)
val minAes = afterRotation(Aes.YMIN)
val maxAes = afterRotation(Aes.YMAX)
val sizeAes = Aes.WIDTH // do not flip as height is not defined for CrossBarGeom
val rect = if (!isHintRect &&
p.defined(xAes) &&
p.defined(minAes) &&
p.defined(maxAes) &&
p.defined(sizeAes)
) {
val x = p[xAes]!!
val ymin = p[minAes]!!
val ymax = p[maxAes]!!
val width = p[sizeAes]!! * ctx.getResolution(xAes)
val origin = DoubleVector(x - width / 2, ymin)
val dimensions = DoubleVector(width, ymax - ymin)
DoubleRectangle(origin, dimensions)
} else if (isHintRect &&
p.defined(xAes) &&
p.defined(yAes) &&
p.defined(sizeAes)
) {
val x = p[xAes]!!
val y = p[yAes]!!
val width = p[sizeAes]!! * ctx.getResolution(xAes)
val origin = DoubleVector(x - width / 2, y)
val dimensions = DoubleVector(width, 0.0)
DoubleRectangle(origin, dimensions)
} else {
null
}
rect?.let { geomHelper.toClient(afterRotation(it), p) }
}
}

private fun clientRectByDataPoint(
ctx: GeomContext,
geomHelper: GeomHelper,
isHintRect: Boolean
): (DataPointAesthetics) -> DoubleRectangle? {
return { p ->
val rect = if (!isHintRect &&
p.defined(Aes.X) &&
p.defined(Aes.YMIN) &&
p.defined(Aes.YMAX) &&
p.defined(Aes.WIDTH)
) {
val x = p.x()!!
val ymin = p.ymin()!!
val ymax = p.ymax()!!
val width = p.width()!! * ctx.getResolution(Aes.X)
private fun buildMidlines(
root: SvgRoot,
aesthetics: Aesthetics,
ctx: GeomContext,
geomHelper: GeomHelper,
fatten: Double
) {
val elementHelper = geomHelper.createSvgElementHelper()
val xAes = afterRotation(Aes.X)
val yAes = afterRotation(Aes.Y)
val sizeAes = Aes.WIDTH // do not flip as height is not defined for CrossBarGeom
for (p in GeomUtil.withDefined(
aesthetics.dataPoints(),
xAes,
yAes,
sizeAes
)) {
val x = p[xAes]!!
val middle = p[yAes]!!
val width = p[sizeAes]!! * ctx.getResolution(xAes)

val origin = DoubleVector(x - width / 2, ymin)
val dimensions = DoubleVector(width, ymax - ymin)
DoubleRectangle(origin, dimensions)
} else if (isHintRect &&
p.defined(Aes.X) &&
p.defined(Aes.Y) &&
p.defined(Aes.WIDTH)
) {
val x = p.x()!!
val y = p.y()!!
val width = p.width()!! * ctx.getResolution(Aes.X)
val line = elementHelper.createLine(
afterRotation(DoubleVector(x - width / 2, middle)),
afterRotation(DoubleVector(x + width / 2, middle)),
p
)!!

val origin = DoubleVector(x - width / 2, y)
val dimensions = DoubleVector(width, 0.0)
DoubleRectangle(origin, dimensions)
} else {
null
}
// adjust thickness
val thickness = line.strokeWidth().get()!!
line.strokeWidth().set(thickness * fatten)

rect?.let { geomHelper.toClient(it, p) }
}
root.add(line)
}
}

companion object {
const val HANDLES_GROUPS = false
private val LEGEND_FACTORY = BoxHelper.legendFactory(false)
}
}
Loading

0 comments on commit d03961c

Please sign in to comment.