Skip to content

Commit

Permalink
coord_polar: axis, ticks, labels, xlim/ylim (#979)
Browse files Browse the repository at this point in the history
* Make expand additive

* coord_polar: reduce number of ticks for v axis

* coord_polar: replace absolute expand with relative

* coord_polar: v axis ticks alignment

* coord_polar: hardcoded anchor for ticks

* coord_polar: more ticks for circle axis

* coord_polar: fix horizontal axis ticks order, fix axis circle positioned above ticks

* coord_polar: fix horizontal axis first and last ticks overlapping

* coord_polar: update notebook

* coord_polar: replace thetaFromX with flipped

* coord_polar: temp fix for axis, need proper fix for the domain flip

* coord_polar: properly handle theta=y in PolarCoordinateSystem

* coord_polar: handle theta=y mostly in PolarCoordinateSystem. Investigate bug with stack bars with theta=y not rendered properly

* coord_polar: fixed breaks and ticks. The only not working case is a Y breaks with start parameter

* coord_polar: fixed breaks and ticks with start parameter

* coord_polar: minor code cleanup

* coord_polar: fix domain for discrete angle scale

* coord_polar: add xlim/ylim for radar plot / lollipop
  • Loading branch information
IKupriyanov-HORIS committed Jan 10, 2024
1 parent 5c7618b commit c0956bc
Show file tree
Hide file tree
Showing 22 changed files with 2,747 additions and 1,153 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ class DoubleRectangle(val origin: DoubleVector, val dimension: DoubleVector) {
)
}

fun flipIf(flipped: Boolean): DoubleRectangle {
return if (flipped) flip() else this
}

fun union(rect: DoubleRectangle): DoubleRectangle {
val newOrigin = origin.min(rect.origin)
val corner = origin.add(dimension)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package org.jetbrains.letsPlot.commons.geometry
import kotlin.math.*

class DoubleVector(val x: Double, val y: Double) {
constructor(x: Number, y: Number) : this(x.toDouble(), y.toDouble())

operator fun component1(): Double = x
operator fun component2(): Double = y

Expand Down Expand Up @@ -63,6 +65,10 @@ class DoubleVector(val x: Double, val y: Double) {
return DoubleVector(y, x)
}

fun flipIf(flipped: Boolean): DoubleVector {
return if (flipped) flip() else this
}

override fun equals(other: Any?): Boolean {
if (other !is DoubleVector) {
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ open class PolygonWithCoordMapDemo : SimpleDemoBase() {
.alpha(constant(0.5))
.build()
val coord = CoordProviders.map().let {
val adjustedDomain = it.adjustDomain(DoubleRectangle(domainX, domainY))
val adjustedDomain = it.adjustDomain(DoubleRectangle(domainX, domainY), false)
it.createCoordinateSystem(
adjustedDomain = adjustedDomain,
clientSize = demoInnerSize
Expand Down
3,186 changes: 2,188 additions & 998 deletions docs/dev/notebooks/coord_polar.ipynb

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import org.jetbrains.letsPlot.core.commons.data.SeriesUtil.pickAtIndices
import org.jetbrains.letsPlot.core.plot.base.CoordinateSystem
import org.jetbrains.letsPlot.core.plot.base.scale.ScaleBreaks
import org.jetbrains.letsPlot.core.plot.base.theme.AxisTheme
import org.jetbrains.letsPlot.core.plot.builder.coord.PolarCoordinateSystem
import org.jetbrains.letsPlot.core.plot.builder.guide.AxisComponent
import org.jetbrains.letsPlot.core.plot.builder.guide.Orientation
import org.jetbrains.letsPlot.core.plot.builder.layout.PlotLabelSpecFactory
import org.jetbrains.letsPlot.core.plot.builder.presentation.LabelSpec
import kotlin.math.PI
import kotlin.math.abs

object AxisUtil {
Expand All @@ -42,7 +44,8 @@ object AxisUtil {
val label = pair.first
val labelOffset = tickLabelBaseOffset.add(labelAdjustments.additionalOffset(i))

if (labelsMap.haveSpace(br, label, labelOffset)) {
val loc = if (orientation.isHorizontal) br.x else br.y
if (labelsMap.haveSpace(loc, label, labelOffset)) {
return@mapIndexedNotNull i
} else {
return@mapIndexedNotNull null
Expand All @@ -53,9 +56,7 @@ object AxisUtil {
majorClientBreaks.indices.toList()
}

val visibleMajorClientBreaks = pickAtIndices(majorClientBreaks, visibleBreaks)
val visibleMajorLabels = pickAtIndices(scaleBreaks.labels, visibleBreaks)

val visibleMajorDomainBreak = pickAtIndices(scaleBreaks.transformedValues, visibleBreaks)
val visibleMinorDomainBreak = if (visibleMajorDomainBreak.size > 1) {
val step = (visibleMajorDomainBreak[1] - visibleMajorDomainBreak[0])
Expand All @@ -65,25 +66,41 @@ object AxisUtil {
emptyList()
}

val visibleMinorClientBreaks = toClient(visibleMinorDomainBreak, domain, coord, flipAxis, orientation.isHorizontal)
val visibleMajorClientBreaks = pickAtIndices(majorClientBreaks, visibleBreaks)
.map { checkNotNull(it) { "Nulls are not allowed. Properly clean and sync breaks, grids and labels." } }

val visibleMinorClientBreaks =
toClient(visibleMinorDomainBreak, domain, coord, flipAxis, orientation.isHorizontal)
.map { checkNotNull(it) { "Nulls are not allowed. Properly clean and sync breaks, grids and labels." } }

val majorGrid = buildGrid(visibleMajorDomainBreak, domain, coord, flipAxis, orientation.isHorizontal)
val minorGrid = buildGrid(visibleMinorDomainBreak, domain, coord, flipAxis, orientation.isHorizontal)

// For coord_polar squash first and last labels into one to avoid overlapping.
val labels = if (visibleMajorClientBreaks.first().subtract(visibleMajorClientBreaks.last()).length() <= 3.0) {
val labels = visibleMajorLabels.toMutableList()
labels[labels.lastIndex] = "${labels[labels.lastIndex]}/${labels[0]}"
labels[0] = ""
labels
} else {
visibleMajorLabels
}

return AxisComponent.BreaksData(
majorBreaks = visibleMajorClientBreaks.filterNotNull(),
minorBreaks = visibleMinorClientBreaks.filterNotNull(),
majorLabels = visibleMajorLabels,
majorBreaks = visibleMajorClientBreaks,
minorBreaks = visibleMinorClientBreaks,
majorLabels = labels,
majorGrid = majorGrid,
minorGrid = minorGrid
)
}

private fun toClient(v: DoubleVector, coordinateSystem: CoordinateSystem, flipAxis: Boolean): DoubleVector? {
return finiteOrNull(coordinateSystem.toClient(if (flipAxis) v.flip() else v))
return finiteOrNull(coordinateSystem.toClient(v.flipIf(flipAxis)))
}

private fun toClient(v: DoubleRectangle, coordinateSystem: CoordinateSystem, flipAxis: Boolean): DoubleRectangle? {
return coordinateSystem.toClient(if (flipAxis) v.flip() else v)
return coordinateSystem.toClient(v.flipIf(flipAxis))
}

private fun toClient(
Expand All @@ -92,20 +109,34 @@ object AxisUtil {
coordinateSystem: CoordinateSystem,
flipAxis: Boolean,
horizontal: Boolean
): List<Double?> {
return breaks.map { breakValue ->
val breakCoord = when (horizontal) {
true -> DoubleVector(breakValue, domain.yRange().lowerEnd)
false -> DoubleVector(domain.xRange().lowerEnd, breakValue)
): List<DoubleVector?> {
return if (coordinateSystem is PolarCoordinateSystem) {

val startAnglePercent = (coordinateSystem.startAngle % (2 * PI)) / (2 * PI)
val startAngleOffset = domain.xRange().length * startAnglePercent
val verticalAngleValue = (domain.xRange().lowerEnd - startAngleOffset).let {
when { // non-normalized domain value
it < domain.xRange().lowerEnd -> it + domain.xRange().length
it > domain.xRange().upperEnd -> it - domain.xRange().length
else -> it
}
}

val breakClientCoord = toClient(breakCoord, coordinateSystem, flipAxis) ?: return@map null

when (horizontal) {
true -> breakClientCoord.x
false -> breakClientCoord.y
breaks.map { breakValue ->
when (horizontal) {
true -> DoubleVector(breakValue, domain.yRange().upperEnd)
false -> DoubleVector(verticalAngleValue, breakValue)
}
}
} else {
breaks.map { breakValue ->
when (horizontal) {
true -> DoubleVector(breakValue, domain.yRange().upperEnd)
false -> DoubleVector(domain.xRange().lowerEnd, breakValue)
}
}
}
.map { toClient(it, coordinateSystem, flipAxis) ?: return@map null }
}

private fun buildGrid(
Expand Down Expand Up @@ -198,11 +229,7 @@ object AxisUtil {
labelOffset: DoubleVector
): DoubleRectangle {
val labelNormalSize = labelSpec.dimensions(label)
val wh = if (isVertical(rotationDegree)) {
labelNormalSize.flip()
} else {
labelNormalSize
}
val wh = labelNormalSize.flipIf(isVertical(rotationDegree))
val origin = if (horizontalAxis) DoubleVector(loc, 0.0) else DoubleVector(0.0, loc)
return DoubleRectangle(origin, wh)
.subtract(wh.mul(0.5)) // labels use central adjustments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.jetbrains.letsPlot.core.plot.builder.GeomLayer
import org.jetbrains.letsPlot.core.plot.builder.MarginalLayerUtil
import org.jetbrains.letsPlot.core.plot.builder.PlotSvgComponent
import org.jetbrains.letsPlot.core.plot.builder.coord.CoordProvider
import org.jetbrains.letsPlot.core.plot.builder.coord.PolarCoordProvider
import org.jetbrains.letsPlot.core.plot.builder.frame.BogusFrameOfReferenceProvider
import org.jetbrains.letsPlot.core.plot.builder.frame.SquareFrameOfReferenceProvider
import org.jetbrains.letsPlot.core.plot.builder.layout.GeomMarginsLayout
Expand Down Expand Up @@ -97,8 +98,9 @@ class PlotAssembler constructor(

val flipAxis = coordProvider.flipped

val (hAxisPosition, vAxisPosition) = when (flipAxis) {
true -> yAxisPosition.flip() to xAxisPosition.flip()
val (hAxisPosition, vAxisPosition) = when {
coordProvider is PolarCoordProvider -> AxisPosition.BOTTOM to AxisPosition.LEFT
flipAxis -> yAxisPosition.flip() to xAxisPosition.flip()
else -> xAxisPosition to yAxisPosition
}

Expand Down Expand Up @@ -245,7 +247,7 @@ class PlotAssembler constructor(

// Create frame of reference provider for each tile.
return domainsXYByTile.map { (xDomain, yDomain) ->
val adjustedDomain = coordProvider.adjustDomain(DoubleRectangle(xDomain, yDomain))
val adjustedDomain = coordProvider.adjustDomain(DoubleRectangle(xDomain, yDomain), hScaleProto.isContinuous)
SquareFrameOfReferenceProvider(
hScaleProto, vScaleProto,
adjustedDomain,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface CoordProvider {
/**
* Reshape and flip the domain if necessary.
*/
fun adjustDomain(domain: DoubleRectangle): DoubleRectangle
fun adjustDomain(domain: DoubleRectangle, isHScaleContinuous: Boolean): DoubleRectangle

fun adjustGeomSize(
hDomain: DoubleSpan,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import org.jetbrains.letsPlot.commons.interval.DoubleSpan
import org.jetbrains.letsPlot.core.plot.base.coord.CoordinatesMapper

internal abstract class CoordProviderBase(
private val xLim: DoubleSpan?,
private val yLim: DoubleSpan?,
protected val xLim: DoubleSpan?,
protected val yLim: DoubleSpan?,
override val flipped: Boolean,
protected val projection: Projection = identity(),
) : CoordProvider {
Expand All @@ -29,7 +29,7 @@ internal abstract class CoordProviderBase(
/**
* Reshape and flip the domain if necessary.
*/
override fun adjustDomain(domain: DoubleRectangle): DoubleRectangle {
override fun adjustDomain(domain: DoubleRectangle, isHScaleContinuous: Boolean): DoubleRectangle {
val validDomain = domain.let {
val withLims = DoubleRectangle(
xLim ?: domain.xRange(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ object CoordProviders {
)
}

fun polar(thetaFromX: Boolean, start: Double, clockwise: Boolean): CoordProvider {
return PolarCoordProvider(thetaFromX, start, clockwise)
fun polar(
xLim: DoubleSpan? = null,
yLim: DoubleSpan? = null,
flipped: Boolean,
start: Double,
clockwise: Boolean
): CoordProvider {
return PolarCoordProvider(xLim, yLim, flipped, start, clockwise)
}
}
Loading

0 comments on commit c0956bc

Please sign in to comment.