Skip to content

Commit

Permalink
LP-1040 and LP-1041 (#1052)
Browse files Browse the repository at this point in the history
  • Loading branch information
IKupriyanov-HORIS authored Mar 21, 2024
1 parent a1c1066 commit c82651f
Show file tree
Hide file tree
Showing 25 changed files with 604 additions and 540 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package org.jetbrains.letsPlot.commons.geometry

import org.jetbrains.letsPlot.commons.intern.math.lineSlope
import org.jetbrains.letsPlot.commons.intern.math.toRadians
import kotlin.math.*


// https://svn.r-project.org/R/trunk/src/library/grid/R/curve.R
fun curve(
start: DoubleVector,
end: DoubleVector,
curvature: Double,
angle: Double,
ncp: Int
): List<DoubleVector> {
val controlPoints = calcControlPoints(start, end, curvature, angle, ncp)
return listOf(start) + controlPoints + listOf(end)
}

private fun calcControlPoints(
start: DoubleVector,
end: DoubleVector,
curvature: Double,
angle: Double,
ncp: Int
): List<DoubleVector> {
// straight line
if (curvature == 0.0 || abs(angle) !in 1.0..179.0) {
return emptyList()
}

val mid = start.add(end).mul(0.5)
val d = end.subtract(start)

val rAngle = toRadians(angle)
val corner = mid.add(
start.subtract(mid).rotate(rAngle)
)

// Calculate angle to rotate region by to align it with x/y axes
val beta = -atan(lineSlope(start, corner))

// Rotate end point about start point to align region with x/y axes
val new = start.add(
d.rotate(beta)
)

// Calculate x-scale factor to make region "square"
val scaleX = lineSlope(start, new)

// Calculate the origin in the "square" region
// (for rotating start point to produce control points)
// (depends on 'curvature')
// 'origin' calculated from 'curvature'
val ratio = 2 * (sin(atan(curvature)).pow(2))
val origin = curvature - curvature / ratio

val ps = DoubleVector(start.x * scaleX, start.y)
val oxy = calcOrigin(
ps = ps,
pe = DoubleVector(new.x * scaleX, new.y),
origin
)

// Direction of rotation
val dir = sign(curvature)

// Angle of rotation depends on location of origin
val maxTheta = PI + sign(origin * dir) * 2 * atan(abs(origin))

val theta = (0 until (ncp + 2))
.map { it * dir * maxTheta / (ncp + 1) }
.drop(1)
.dropLast(1)

// May have BOTH multiple end points AND multiple
// control points to generate (per set of end points)
// Generate consecutive sets of control points by performing
// matrix multiplication

val indices = List(theta.size) { index -> index }

val p = ps.subtract(oxy)
val cp = indices.map { index ->
oxy.add(
p.rotate(theta[index])
)
}
// Reverse transformations (scaling and rotation) to
// produce control points in the original space
.map {
DoubleVector(
it.x / scaleX,
it.y
)
}

return indices.map { index ->
start.add(
cp[index].subtract(start).rotate(-beta)
)
}
}

private fun calcOrigin(
ps: DoubleVector,
pe: DoubleVector,
origin: Double
): DoubleVector {

val mid = ps.add(pe).mul(0.5)
val d = pe.subtract(ps)
val slope = lineSlope(ps, pe)

val oSlope = -1 / slope

// The origin is a point somewhere along the line between
// the end points, rotated by 90 (or -90) degrees
// Two special cases:
// If slope is non-finite then the end points lie on a vertical line, so
// the origin lies along a horizontal line (oSlope = 0)
// If oSlope is non-finite then the end points lie on a horizontal line,
// so the origin lies along a vertical line (oSlope = Inf)
val tmpOX = when {
!slope.isFinite() -> 0.0
!oSlope.isFinite() -> origin * d.x / 2
else -> origin * d.x / 2
}

val tmpOY = when {
!slope.isFinite() -> origin * d.y / 2
!oSlope.isFinite() -> 0.0
else -> origin * d.y / 2
}

return DoubleVector(mid.x + tmpOY, mid.y - tmpOX)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2024. JetBrains s.r.o.
* Use of this source code is governed by the MIT license that can be found in the LICENSE file.
*/

package org.jetbrains.letsPlot.commons.geometry

import org.jetbrains.letsPlot.commons.intern.math.distance
import org.jetbrains.letsPlot.commons.intern.math.distance2
import org.jetbrains.letsPlot.commons.intern.math.pointOnLine

private fun pad(lineString: List<DoubleVector>, padding: Double): Pair<Int, DoubleVector>? {
if (lineString.size < 2) {
return null
}

val padding2 = padding * padding
val indexOutsidePadding = lineString.indexOfFirst { distance2(lineString.first(), it) >= padding2 }
if (indexOutsidePadding < 1) { // not found or first points already satisfy the padding
return null
}

val adjustedStartPoint = run {
val insidePadding = lineString[indexOutsidePadding - 1]
val outsidePadding = lineString[indexOutsidePadding]
val overPadding = distance(lineString.first(), outsidePadding) - padding

pointOnLine(outsidePadding, insidePadding, overPadding)
}

return indexOutsidePadding to adjustedStartPoint
}

private fun padStart(lineString: List<DoubleVector>, padding: Double): List<DoubleVector> {
val (index, adjustedStartPoint) = pad(lineString, padding) ?: return lineString
return listOf(adjustedStartPoint) + lineString.subList(index, lineString.size)
}

private fun padEnd(lineString: List<DoubleVector>, padding: Double): List<DoubleVector> {
val (index, adjustedEndPoint) = pad(lineString.asReversed(), padding) ?: return lineString
return lineString.subList(0, lineString.size - index) + adjustedEndPoint
}

fun padLineString(
lineString: List<DoubleVector>,
startPadding: Double,
endPadding: Double
): List<DoubleVector> {
val startPadded = padStart(lineString, startPadding)
return padEnd(startPadded, endPadding)
}
2 changes: 2 additions & 0 deletions future_changes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## [4.3.1] - 2024-mm-dd

### Added
- Parameter `min_tail_length` for `arrow()` function [[#1040](https://github.com/JetBrains/lets-plot/issues/1040)].

### Changed
Expand All @@ -9,4 +10,5 @@
- livemap: when release the mouse button from outside the map, it gets stuck in panning mode [[#1044](https://github.com/JetBrains/lets-plot/issues/1044)].
- Incorrect 'plot_background' area (with empty space capture) [[#918](https://github.com/JetBrains/lets-plot/issues/918)].
- Support arrow() in geom_spoke() [[#986](https://github.com/JetBrains/lets-plot/issues/986)].
- arrow on curve sometimes looks weird [[#1041](https://github.com/JetBrains/lets-plot/issues/1041)].
- Livemap: `vjust` implemented incorrectly [[#1051](https://github.com/JetBrains/lets-plot/issues/1051)].
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,20 @@ object AesScaling {
return p.size()!! * 2
}

private fun targetSize(p: DataPointAesthetics, atStart: Boolean): Double {
// px -> aes Units
val sizeAes = if (atStart) DataPointAesthetics::sizeStart else DataPointAesthetics::sizeEnd
val strokeAes = if (atStart) DataPointAesthetics::strokeStart else DataPointAesthetics::strokeEnd
return circleDiameter(p, sizeAes) / 2 + pointStrokeWidth(p, strokeAes)
}

fun targetStartSize(p: DataPointAesthetics): Double {
// px -> aes Units
return targetSize(p, true)
}

fun targetEndSize(p: DataPointAesthetics): Double {
// px -> aes Units
return targetSize(p, false)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,8 @@ class ABLineGeom : GeomBase() {

if (lineEnds.size == 2) {
val it = lineEnds.iterator()
val line = helper.createLine(it.next(), it.next(), p)
if (line != null) {
lines.add(line)
}
val (svg) = helper.createLine(it.next(), it.next(), p) ?: continue
lines.add(svg)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,15 @@ class BoxplotGeom : GeomBase() {
DoubleVector(x, hinge),
DoubleVector(x, fence),
p
)!!
)!!.first
)
// fence line
lines.add(
elementHelper.createLine(
DoubleVector(x - halfFenceWidth, fence),
DoubleVector(x + halfFenceWidth, fence),
p
)!!
)!!.first
)
}

Expand All @@ -97,15 +97,15 @@ class BoxplotGeom : GeomBase() {
DoubleVector(x, hinge),
DoubleVector(x, fence),
p
)!!
)!!.first
)
// fence line
lines.add(
elementHelper.createLine(
DoubleVector(x - halfFenceWidth, fence),
DoubleVector(x + halfFenceWidth, fence),
p
)!!
)!!.first
)

lines.forEach { root.add(it) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ class CrossBarGeom(
val middle = p[yAes]!!
val width = p[sizeAes]!! * ctx.getResolution(xAes)

val line = elementHelper.createLine(
val (line) = elementHelper.createLine(
afterRotation(DoubleVector(x - width / 2, middle)),
afterRotation(DoubleVector(x + width / 2, middle)),
p
)!!
) ?: continue

// TODO: use strokeScale in createLine() function
// adjust thickness
Expand Down
Loading

0 comments on commit c82651f

Please sign in to comment.