Skip to content

Commit

Permalink
Fix lims for FixedCoordProvider (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
IKupriyanov-HORIS committed Feb 12, 2020
1 parent bdebd9f commit 6111e54
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ class Pair<FirstT, SecondT>(val first: FirstT, val second: SecondT) {
override fun toString(): String {
return "[$first, $second]"
}

operator fun component1() = first
operator fun component2() = second
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,19 +54,14 @@ internal abstract class CoordProviderBase(
displaySize: DoubleVector
): Pair<ClosedRange<Double>, ClosedRange<Double>> {
return adjustDomainsImpl(xDomain, yDomain, displaySize)
.let { Pair(
xLim ?: it.first,
yLim ?: it.second
)
}
}

protected open fun adjustDomainsImpl(
xDomain: ClosedRange<Double>,
yDomain: ClosedRange<Double>,
displaySize: DoubleVector
): Pair<ClosedRange<Double>, ClosedRange<Double>> {
return Pair(xDomain, yDomain)
return Pair(xLim ?: xDomain, yLim ?: yDomain)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.values.Pair
import jetbrains.datalore.plot.common.data.SeriesUtil
import jetbrains.datalore.plot.common.data.SeriesUtil.span

/**
* A fixed scale coordinate system forces a specified ratio between the physical representation of data units on the axes.
Expand All @@ -27,17 +28,6 @@ internal open class FixedRatioCoordProvider(
yDomain: ClosedRange<Double>,
displaySize: DoubleVector
): Pair<ClosedRange<Double>, ClosedRange<Double>> {
@Suppress("NAME_SHADOWING")
var xDomain = xDomain
@Suppress("NAME_SHADOWING")
var yDomain = yDomain

val spanX = SeriesUtil.span(xDomain)
val spanY = SeriesUtil.span(yDomain)
if (spanX < SeriesUtil.TINY || spanY < SeriesUtil.TINY) {
return Pair(xDomain, yDomain) // don't touch
}

// fit the data into the display
var displayW = displaySize.x
var displayH = displaySize.y
Expand All @@ -52,19 +42,61 @@ internal open class FixedRatioCoordProvider(
displayH *= 1 / myRatio
}

val ratioX = spanX / displayW
val ratioY = spanY / displayH
if (
listOf(xLim, yLim).all { it == null } ||
listOf(xLim, yLim).all { it != null }
) {
@Suppress("NAME_SHADOWING")
var xDomain = xDomain
@Suppress("NAME_SHADOWING")
var yDomain = yDomain

// Take bigger ratio and apply to ortogonal domain (axis) so that
// ratio: (data range) / (axis length) is the same for both X and Y.
if (ratioX > ratioY) {
val spanAdjusted = displayH * ratioX
yDomain = SeriesUtil.expand(yDomain, spanAdjusted)
} else {
val spanAdjusted = displayW * ratioY
xDomain = SeriesUtil.expand(xDomain, spanAdjusted)
if (listOf(xLim, yLim).all { it != null }) {
xDomain = xLim!!
yDomain = yLim!!
}

val spanX = span(xDomain)
val spanY = span(yDomain)
if (spanX < SeriesUtil.TINY || spanY < SeriesUtil.TINY) {
return Pair(xDomain, yDomain) // don't touch
}

val ratioX = spanX / displayW
val ratioY = spanY / displayH

// Take bigger ratio and apply to ortogonal domain (axis) so that
// ratio: (data range) / (axis length) is the same for both X and Y.
if (ratioX > ratioY) {
val spanAdjusted = displayH * ratioX
yDomain = SeriesUtil.expand(yDomain, spanAdjusted)
} else {
val spanAdjusted = displayW * ratioY
xDomain = SeriesUtil.expand(xDomain, spanAdjusted)
}

return Pair(xDomain, yDomain)
}

fun limitOrth(orig: ClosedRange<Double>, lim: ClosedRange<Double>, orth: ClosedRange<Double>):ClosedRange<Double> {
val scale = span(orig) / span(orth)
val lowerExpand = (orig.lowerEndpoint() - lim.lowerEndpoint()) * scale
val upperExpand = (lim.upperEndpoint() - orig.upperEndpoint()) * scale
return SeriesUtil.expand(orth, lowerExpand, upperExpand)
}

return Pair(xDomain, yDomain)
if (xLim != null) {
val newSpan = displayH * (span(xLim) / displayW)
val yLim = limitOrth(xDomain, xLim, yDomain)
return Pair(xLim, SeriesUtil.expand(yLim, newSpan))

} else if (yLim != null) {
val newSpan = displayW * (span(yLim) / displayH)
val xLim = limitOrth(yDomain, yLim, xDomain)
return Pair(SeriesUtil.expand(xLim, newSpan), yLim)

} else {
error("Impossible")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal class CoordCartesianTest : jetbrains.datalore.plot.builder.coord.CoordT

@Test
fun adjustDomains() {
val dataBounds = dataBounds!!
val dataBounds = dataBounds
// domains not changed
tryAdjustDomains(2.0,
PROVIDER, dataBounds.xRange(), dataBounds.yRange())
Expand Down
121 changes: 116 additions & 5 deletions plot-builder/src/jvmTest/kotlin/plot/builder/coord/CoordFixedTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,122 @@

package jetbrains.datalore.plot.builder.coord

import jetbrains.datalore.base.gcommon.collect.ClosedRange
import jetbrains.datalore.base.gcommon.collect.ClosedRange.Companion.closed
import jetbrains.datalore.base.geometry.DoubleRectangle
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.values.Pair
import kotlin.math.abs
import kotlin.math.min
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.fail

internal class CoordFixedTest : jetbrains.datalore.plot.builder.coord.CoordTestBase() {

@BeforeTest
fun setUp() {
dataBounds = DoubleRectangle(DoubleVector.ZERO,
DATA_SPAN
)
dataBounds = DoubleRectangle(DoubleVector.ZERO, DATA_SPAN)
}

@Test
fun limits() {
fun squareCoord(xLim: ClosedRange<Double>? = null, yLim: ClosedRange<Double>? = null): Pair<ClosedRange<Double>, ClosedRange<Double>> {
val dataBounds = DoubleRectangle(DoubleVector.ZERO, DoubleVector(25.0, 25.0))
val screenSize = DoubleVector(800.0, 600.0)
val (xDomain, yDomain) = CoordProviders
.fixed(1.0, xLim, yLim)
.adjustDomains(dataBounds.xRange(), dataBounds.yRange(), screenSize)
return Pair(xDomain, yDomain)
}

// xLim
run {

// xLim in range
squareCoord(xLim = closed(1.0, 4.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(1.0, 4.0), xDomain)
assertEquals(closed(1.375, 3.625), yDomain)
}

// xLim wider than range
squareCoord(xLim = closed(-3.0, 4.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(-3.0, 4.0), xDomain)
assertEquals(closed(-2.125, 3.125), yDomain)
}

// xLim out of range
squareCoord(xLim = closed(-9.0, -4.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(-9.0, -4.0), xDomain)
assertEquals(closed(-8.375, -4.625), yDomain)
}

}

// yLim
run {
// yLim in range
squareCoord(yLim = closed(1.0, 4.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(0.5, 4.5), xDomain)
assertEquals(closed(1.0, 4.0), yDomain)
}

// yLim wider than range
squareCoord(yLim = closed(-3.0, 6.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(-4.5, 7.5), xDomain)
assertEquals(closed(-3.0, 6.0), yDomain)
}

// yLim out of range
squareCoord(yLim = closed(-9.0, -6.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(-9.5, -5.5), xDomain)
assertEquals(closed(-9.0, -6.0), yDomain)
}
}


// xLim && yLim
run {
// intersecting, wider yLim wins
squareCoord(xLim = closed(10.0, 15.0), yLim = closed(9.0, 16.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(7.833, 17.166), xDomain)
assertEquals(closed(9.0, 16.0), yDomain)
}

// intersecting, wider xLim wins
squareCoord(xLim = closed(9.0, 16.0), yLim = closed(12.0, 14.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(9.0, 16.0), xDomain)
assertEquals(closed(10.375, 15.625), yDomain)
}

// non-intersecting, wider xLim wins
squareCoord(xLim = closed(9.0, 16.0), yLim = closed(19.0, 22.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(9.0, 16.0), xDomain)
assertEquals(closed(17.875, 23.125), yDomain)
}

// non-intersecting, wider yLim wins
squareCoord(xLim = closed(15.0, 16.0), yLim = closed(18.0, 22.0))
.let { (xDomain, yDomain) ->
assertEquals(closed(12.833, 18.166), xDomain)
assertEquals(closed(18.0, 22.0), yDomain)
}
}
}

@Test
fun adjustDomains() {
// fixed ratio == 1 (equal X and Y)
val dataBounds = dataBounds!!
val dataBounds = dataBounds
val rangeX = dataBounds.xRange()
val rangeY = dataBounds.yRange()

Expand Down Expand Up @@ -104,7 +202,20 @@ internal class CoordFixedTest : jetbrains.datalore.plot.builder.coord.CoordTestB

private fun shortSideOfDisplay(ratio: Double): Double {
val displaySize = unitDisplaySize(ratio)
return Math.min(displaySize.x, displaySize.y)
return min(displaySize.x, displaySize.y)
}

private fun ClosedRange<Double>.equals(other: ClosedRange<Double>, epsilon: Double = 0.00001): Boolean {
fun doubleEquals(expected: Double, actual: Double, epsilon: Double) = abs(expected - actual) < epsilon

return doubleEquals(lowerEndpoint(), other.lowerEndpoint(), epsilon) &&
doubleEquals(upperEndpoint(), other.upperEndpoint(), epsilon)
}

private fun assertEquals(expected: ClosedRange<Double>, actual: ClosedRange<Double>, epsilon: Double = 0.001) {
if (!expected.equals(actual, epsilon)) {
fail("$expected != $actual")
}
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package jetbrains.datalore.plot.builder.coord

import jetbrains.datalore.base.geometry.DoubleRectangle
import jetbrains.datalore.base.geometry.DoubleVector
import kotlin.math.min
import kotlin.test.BeforeTest
import kotlin.test.Test

Expand All @@ -24,7 +25,7 @@ internal class CoordMapTest : jetbrains.datalore.plot.builder.coord.CoordTestBas
// Coord Map keeps fixed ratio == 1 (equal X and Y)
val dataBounds = dataBounds
tryAdjustDomains(2.0,
PROVIDER, dataBounds!!.xRange(),
PROVIDER, dataBounds.xRange(),
expand(dataBounds.yRange(), 2.0)
)
tryAdjustDomains(0.5,
Expand All @@ -51,7 +52,7 @@ internal class CoordMapTest : jetbrains.datalore.plot.builder.coord.CoordTestBas

private fun shortSideOfDisplay(ratio: Double): Double {
val displaySize = unitDisplaySize(ratio)
return Math.min(displaySize.x, displaySize.y)
return min(displaySize.x, displaySize.y)
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import kotlin.test.assertEquals

internal open class CoordTestBase {

var dataBounds: DoubleRectangle? = null
lateinit var dataBounds: DoubleRectangle

/**
* ratio - ratio between height and width of the display (ratio = h / w)
*/
fun tryAdjustDomains(ratio: Double, provider: CoordProvider, expectedX: ClosedRange<Double>, expectedY: ClosedRange<Double>) {

val dataBounds = this.dataBounds
val domainX = dataBounds!!.xRange()
val domainX = dataBounds.xRange()
val domainY = dataBounds.yRange()
val displaySize = unitDisplaySize(ratio)

Expand All @@ -40,7 +40,7 @@ internal open class CoordTestBase {
fun tryApplyScales(ratio: Double, provider: CoordProvider, expectedMin: DoubleVector, expectedMax: DoubleVector, accuracy: DoubleVector) {

val dataBounds = this.dataBounds
var domainX = dataBounds!!.xRange()
var domainX = dataBounds.xRange()
var domainY = dataBounds.yRange()
val displaySize = unitDisplaySize(ratio)
val domains = provider.adjustDomains(domainX, domainY, displaySize)
Expand Down

0 comments on commit 6111e54

Please sign in to comment.