Skip to content

Commit

Permalink
Move bar/pie annotations building to geom.annotation package.
Browse files Browse the repository at this point in the history
  • Loading branch information
OLarionova-HORIS committed Mar 27, 2024
1 parent 944786c commit 0bcf2d1
Show file tree
Hide file tree
Showing 16 changed files with 622 additions and 577 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ package demo.plot.common.model
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.values.Font
import org.jetbrains.letsPlot.core.plot.base.annotations.Annotations
import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation
import org.jetbrains.letsPlot.core.plot.base.GeomContext
import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector
import org.jetbrains.letsPlot.core.plot.base.tooltip.NullGeomTargetCollector
Expand All @@ -24,7 +24,7 @@ import org.jetbrains.letsPlot.core.plot.base.PlotContext
class EmptyGeomContext : GeomContext {
override val flipped: Boolean = false
override val targetCollector: GeomTargetCollector = NullGeomTargetCollector()
override val annotations: Annotations? = null
override val annotation: Annotation? = null
override val backgroundColor: Color = Color.WHITE
override val plotContext: PlotContext? = null

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ package org.jetbrains.letsPlot.core.plot.base
import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle
import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.values.Color
import org.jetbrains.letsPlot.core.plot.base.annotations.Annotations
import org.jetbrains.letsPlot.core.plot.base.geom.annotation.Annotation
import org.jetbrains.letsPlot.core.plot.base.tooltip.GeomTargetCollector

interface GeomContext {
val flipped: Boolean
val targetCollector: GeomTargetCollector
val annotations: Annotations?
val annotation: Annotation?
val backgroundColor: Color
val plotContext: PlotContext? // ToDo: it's used to apply the same formatting to annotations as for tooltips, need refactoring

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,13 @@ 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.commons.intern.math.toDegrees
import org.jetbrains.letsPlot.core.commons.data.SeriesUtil.finiteOrNull
import org.jetbrains.letsPlot.core.plot.base.*
import org.jetbrains.letsPlot.core.plot.base.geom.util.AnnotationsUtil
import org.jetbrains.letsPlot.core.plot.base.geom.annotation.BarAnnotation
import org.jetbrains.letsPlot.core.plot.base.geom.util.RectangleTooltipHelper
import org.jetbrains.letsPlot.core.plot.base.geom.util.RectanglesHelper
import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot
import org.jetbrains.letsPlot.core.plot.base.render.svg.MultilineLabel
import org.jetbrains.letsPlot.core.plot.base.render.svg.Text
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgNode
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.atan2

open class BarGeom : GeomBase() {

Expand Down Expand Up @@ -50,273 +44,7 @@ open class BarGeom : GeomBase() {
rectangles.reverse() // TODO: why reverse?
rectangles.forEach(root::add)

ctx.annotations?.let {
if (coord.isLinear) {
buildAnnotations(root, helper, coord, ctx)
} else {
buildNonLinearAnnotations(root, helper, coord, ctx)
}
}
}

private fun buildNonLinearAnnotations(
root: SvgRoot,
rectanglesHelper: RectanglesHelper,
coord: CoordinateSystem,
ctx: GeomContext
) {
val annotations = ctx.annotations ?: return
val viewPort = overallAesBounds(ctx).let(coord::toClient) ?: return

rectanglesHelper.iterateRectangleGeometry { p, rect ->
val clientBarCenter = rectanglesHelper.toClient(rect.center, p) ?: return@iterateRectangleGeometry

val barBorder = with(rect.flipIf(ctx.flipped)) {
listOf(DoubleVector(left, bottom), DoubleVector(right, bottom))
}.mapNotNull {
rectanglesHelper.toClient(it.flipIf(ctx.flipped), p)
}

if (barBorder.size != 2) return@iterateRectangleGeometry

val v = barBorder[1].subtract(barBorder[0])
val angle = atan2(v.y, v.x).let {
when (abs(it)) {
in PI / 2..3 * PI / 2 -> PI - it
else -> 2 * PI - it
}
}
val text = annotations.getAnnotationText(p.index(), ctx.plotContext)
AnnotationsUtil.createLabelElement(
text,
clientBarCenter,
textParams = AnnotationsUtil.TextParams(
style = annotations.textStyle,
color = annotations.getTextColor(p.fill()),
hjust = "middle",
vjust = "center",
alpha = 0.0,
angle = toDegrees(angle)
),
geomContext = ctx,
boundsCenter = viewPort.center
)
.also(root::add)
}
}

private fun buildAnnotations(
root: SvgRoot,
rectanglesHelper: RectanglesHelper,
coord: CoordinateSystem,
ctx: GeomContext
) {
val annotations = ctx.annotations ?: return
val viewPort = overallAesBounds(ctx).let(coord::toClient) ?: return

val padding = annotations.textStyle.size / 2
val isHorizontallyOriented = ctx.flipped

val rectangles = mutableListOf<Triple<DataPointAesthetics, DoubleRectangle, Boolean>>()
rectanglesHelper.iterateRectangleGeometry { p, rect ->
val clientRect = rectanglesHelper.toClient(rect, p)
?.intersect(viewPort) // use the visible part of bar to place annotation on it
?: return@iterateRectangleGeometry

val isNegative = rect.dimension.y < 0
rectangles.add(Triple(p, clientRect, isNegative))
}

rectangles
.groupBy { (_, rect) ->
if (isHorizontallyOriented) rect.center.y else rect.center.x
}
.forEach { (_, bars) ->
val barsCount = bars.size
bars
.sortedBy { (_, rect) ->
if (isHorizontallyOriented) rect.center.x else rect.center.y
}
.forEachIndexed { index, (p, barRect, isNegative) ->
val text = annotations.getAnnotationText(p.index(), ctx.plotContext)
val textSize = AnnotationsUtil.textSizeGetter(annotations.textStyle, ctx).invoke(text, p)

val (hAlignment, textRect) = placeLabel(
barRect,
index,
barsCount,
textSize,
padding,
viewPort,
isHorizontallyOriented,
isNegative
)
?: return@forEachIndexed

val alpha: Double
val labelColor = when {
barRect.contains(textRect) -> {
alpha = 0.0
annotations.getTextColor(p.fill())
}
else -> {
alpha = 0.75
annotations.getTextColor()
}
}

var location = DoubleVector(
x = when (hAlignment) {
Text.HorizontalAnchor.LEFT -> textRect.left
Text.HorizontalAnchor.RIGHT -> textRect.right
Text.HorizontalAnchor.MIDDLE -> textRect.center.x
},
y = textRect.top
)

// separate label for each line
val labels = MultilineLabel.splitLines(text).map { line ->
AnnotationsUtil.createLabelElement(
line,
location,
textParams = AnnotationsUtil.TextParams(
style = annotations.textStyle,
color = labelColor,
hjust = hAlignment.toString().lowercase(),
vjust = "top",
fill = ctx.backgroundColor,
alpha = alpha
),
geomContext = ctx,
boundsCenter = viewPort.center
).also {
location = location.add(DoubleVector(0.0, annotations.textStyle.size))
}
}
labels.forEach(root::add)
}
}
}

internal fun DoubleRectangle.contains(other: DoubleRectangle): Boolean {
return other.xRange() in xRange() && other.yRange() in yRange()
}

private fun placeLabel(
barRect: DoubleRectangle,
index: Int,
barsCount: Int,
textSize: DoubleVector,
padding: Double,
viewPort: DoubleRectangle,
isHorizontallyOriented: Boolean,
isNegative: Boolean
): Pair<Text.HorizontalAnchor, DoubleRectangle>? {

val coordSelector: (DoubleVector) -> Double =
if (isHorizontallyOriented) DoubleVector::x else DoubleVector::y

var insideBar = when {
barsCount == 1 -> {
// use left (for horizontally orientated) or bottom (of the vertical bar)
if (isHorizontallyOriented) PlacementInsideBar.MIN else PlacementInsideBar.MAX
}
index == 0 -> PlacementInsideBar.MIN
index == barsCount - 1 -> PlacementInsideBar.MAX
else -> PlacementInsideBar.MIDDLE
}

fun place(placement: PlacementInsideBar): Pair<Text.HorizontalAnchor, DoubleRectangle> {
val textRect = createLabelRect(
hPlacement = if (isHorizontallyOriented) placement else PlacementInsideBar.MIDDLE,
vPlacement = if (isHorizontallyOriented) PlacementInsideBar.MIDDLE else placement,
barRect,
textSize,
padding
)
val hAlignment = if (isHorizontallyOriented) {
when (placement) {
PlacementInsideBar.MIN -> Text.HorizontalAnchor.LEFT
PlacementInsideBar.MAX -> Text.HorizontalAnchor.RIGHT
PlacementInsideBar.MIDDLE -> Text.HorizontalAnchor.MIDDLE
}
} else {
Text.HorizontalAnchor.MIDDLE
}
return hAlignment to textRect
}

var (hAlignment, textRect) = place(insideBar)
if (barRect.contains(textRect)) {
return hAlignment to textRect
} else if (index != 0 && index != barsCount - 1) {
return null
}

// try to move outside the bar

if (barsCount == 1) {
// move to the right (for horizontally orientated) or to the top (of the vertical bar)
insideBar = if (isHorizontallyOriented) PlacementInsideBar.MAX else PlacementInsideBar.MIN
if (isNegative) insideBar = insideBar.flip()
}

fun DoubleRectangle.moveTo(value: Double): DoubleRectangle {
val newOrigin =
if (isHorizontallyOriented) DoubleVector(value, origin.y) else DoubleVector(origin.x, value)
return DoubleRectangle(newOrigin, this.dimension)
}
textRect = if (insideBar == PlacementInsideBar.MAX) {
if (isHorizontallyOriented) hAlignment = Text.HorizontalAnchor.LEFT
val pos = coordSelector(barRect.origin) + coordSelector(barRect.dimension) + padding / 2
textRect.moveTo(pos)
} else {
if (isHorizontallyOriented) hAlignment = Text.HorizontalAnchor.RIGHT
val pos = coordSelector(barRect.origin) - coordSelector(textSize) - padding / 2
textRect.moveTo(pos)
}

if (viewPort.contains(textRect)) {
return hAlignment to textRect
} else if (coordSelector(textSize) + padding > coordSelector(barRect.dimension)) {
return null
}

// move it a little inward
return place(insideBar)
}

private enum class PlacementInsideBar {
MIN, MAX, MIDDLE;

fun flip() = when (this) {
MIN -> MAX
MAX -> MIN
MIDDLE -> MIDDLE
}
}

private fun createLabelRect(
hPlacement: PlacementInsideBar,
vPlacement: PlacementInsideBar,
barRect: DoubleRectangle,
textSize: DoubleVector,
padding: Double
): DoubleRectangle {

fun getCoord(coordSelector: (DoubleVector) -> Double, align: PlacementInsideBar): Double {
return when (align) {
PlacementInsideBar.MIN -> coordSelector(barRect.origin) + padding
PlacementInsideBar.MAX -> coordSelector(barRect.origin) + coordSelector(barRect.dimension) -
coordSelector(textSize) - padding

PlacementInsideBar.MIDDLE -> coordSelector(barRect.center) - coordSelector(textSize) / 2
}
}

val originX = getCoord(DoubleVector::x, hPlacement)
val originY = getCoord(DoubleVector::y, vPlacement)
return DoubleRectangle(originX, originY, textSize.x, textSize.y)
ctx.annotation?.let { BarAnnotation.build(root, helper, coord, ctx) }
}

companion object {
Expand Down
Loading

0 comments on commit 0bcf2d1

Please sign in to comment.