Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LP-1040 and LP-1041 #1052

Merged
merged 9 commits into from
Mar 21, 2024
Next Next commit
WIP
  • Loading branch information
IKupriyanov-HORIS committed Mar 15, 2024
commit 94c656c88839d506e43ce2914eb24b06518805de
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,10 @@ object AesScaling {
return p.size()!! * 2
}

fun targetSize(p: DataPointAesthetics, atStart: Boolean): Double {
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)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.jetbrains.letsPlot.core.plot.base.*
import org.jetbrains.letsPlot.core.plot.base.aes.AesScaling
import org.jetbrains.letsPlot.core.plot.base.geom.util.ArrowSpec
import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomHelper
import org.jetbrains.letsPlot.core.plot.base.geom.util.GeomUtil.toLocation
import org.jetbrains.letsPlot.core.plot.base.geom.util.TargetCollectorHelper
import org.jetbrains.letsPlot.core.plot.base.render.LegendKeyElementFactory
import org.jetbrains.letsPlot.core.plot.base.render.SvgRoot
Expand Down Expand Up @@ -49,7 +50,9 @@ class SegmentGeom : GeomBase() {
.setResamplingEnabled(!coord.isLinear && !flat)

for (p in aesthetics.dataPoints()) {
val segmentGeometry = createSegmentGeometry(p, svgHelper) ?: continue
val start = p.toLocation(Aes.X, Aes.Y) ?: continue
val end = p.toLocation(Aes.XEND, Aes.YEND) ?: continue
val segmentGeometry = svgHelper.createLineGeometry(start, end, p) ?: continue

// Apply padding to segment geometry based on the target size, spacer and arrow spec
val startPadding = padding(p, arrowSpec, spacer, atStart = true)
Expand All @@ -69,18 +72,6 @@ class SegmentGeom : GeomBase() {
}
}

private fun createSegmentGeometry(p: DataPointAesthetics, geomHelper: GeomHelper.SvgElementHelper): List<DoubleVector>? {
val x = finiteOrNull(p.x()) ?: return null
val y = finiteOrNull(p.y()) ?: return null
val xend = finiteOrNull(p.xend()) ?: return null
val yend = finiteOrNull(p.yend()) ?: return null

val start = DoubleVector(x, y)
val end = DoubleVector(xend, yend)

return geomHelper.createLineGeometry(start, end, p)
}

companion object {
const val HANDLES_GROUPS = false

Expand Down Expand Up @@ -125,19 +116,13 @@ class SegmentGeom : GeomBase() {
return padEnd(startPadded, endPadding)
}

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

fun padding(
p: DataPointAesthetics,
arrowSpec: ArrowSpec?,
spacer: Double,
atStart: Boolean
): Double {
val targetSize = targetSize(p, atStart)
val targetSize = AesScaling.targetSize(p, atStart)

val miterOffset = arrowSpec?.let {
val hasArrow = if (atStart) arrowSpec.isOnFirstEnd else arrowSpec.isOnLastEnd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,27 @@
package org.jetbrains.letsPlot.core.plot.base.geom.util

import org.jetbrains.letsPlot.commons.geometry.DoubleVector
import org.jetbrains.letsPlot.commons.intern.math.distance
import org.jetbrains.letsPlot.commons.values.Color
import org.jetbrains.letsPlot.core.plot.base.Aes
import org.jetbrains.letsPlot.core.plot.base.DataPointAesthetics
import org.jetbrains.letsPlot.core.plot.base.aes.AesScaling
import org.jetbrains.letsPlot.core.plot.base.render.linetype.NamedLineType
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgPathDataBuilder
import org.jetbrains.letsPlot.datamodel.svg.dom.SvgPathElement
import kotlin.math.abs
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.*

/**
* @param angle The angle of the arrow head in radians (smaller numbers produce narrower, pointier arrows).
* Essentially describes the width of the arrow head.
* @param length The length of the arrow head (px).
*/
class ArrowSpec(val angle: Double, val length: Double, val end: End, val type: Type) {
class ArrowSpec(
val angle: Double,
val length: Double,
val end: End,
val type: Type
) {

val isOnFirstEnd: Boolean
get() = end == End.FIRST || end == End.BOTH
Expand Down Expand Up @@ -74,7 +77,7 @@ class ArrowSpec(val angle: Double, val length: Double, val end: End, val type: T

val arrowAes = arrowSpec.toArrowAes(p)

val arrow = createElement(polarAngle, end.x, end.y, arrowSpec)
val arrow = createElement(polarAngle, end, arrowSpec, listOf(start, end))
val strokeScaler = AesScaling::strokeWidth
GeomHelper.decorate(arrow, arrowAes, applyAlphaToAll = true, strokeScaler, filled = arrowSpec.type == Type.CLOSED)
// Use 'stroke-miterlimit' attribute to avoid the bevelled corner
Expand All @@ -86,22 +89,56 @@ class ArrowSpec(val angle: Double, val length: Double, val end: End, val type: T
/**
* @param polarAngle Angle between X-axis and the arrowed vector.
*/
private fun createElement(polarAngle: Double, x: Double, y: Double, arrowSpec: ArrowSpec): SvgPathElement {
val xs = with(arrowSpec) { doubleArrayOf(x - length * cos(polarAngle - angle), x, x - length * cos(polarAngle + angle)) }
val ys = with(arrowSpec) { doubleArrayOf(y - length * sin(polarAngle - angle), y, y - length * sin(polarAngle + angle)) }
private fun createElement(polarAngle: Double, tipPoint: DoubleVector, arrowSpec: ArrowSpec, geometry: List<DoubleVector>): SvgPathElement {
val headLength = when {
geometry.size == 2 -> min(arrowSpec.length, distance(geometry[0], geometry[1]))
else -> arrowSpec.length
}

val b = SvgPathDataBuilder(true)
.moveTo(xs[0], ys[0])
if (true) {
val side = tipPoint.subtract(DoubleVector(headLength, 0))
val headSide1 = side.rotateAround(tipPoint, polarAngle - arrowSpec.angle)
val headSide2 = side.rotateAround(tipPoint, polarAngle + arrowSpec.angle)

for (i in 1..2) {
b.lineTo(xs[i], ys[i], true)
}
val b = SvgPathDataBuilder(true)
.moveTo(headSide1)
.lineTo(tipPoint)
.lineTo(headSide2)

if (arrowSpec.type == Type.CLOSED) {
b.closePath()
}
if (arrowSpec.type == Type.CLOSED) {
b.closePath()
}

return SvgPathElement(b.build())
} else {
val xs = with(arrowSpec) {
doubleArrayOf(
tipPoint.x - headLength * cos(polarAngle - angle),
tipPoint.x,
tipPoint.x - headLength * cos(polarAngle + angle)
)
}
val ys = with(arrowSpec) {
doubleArrayOf(
tipPoint.y - headLength * sin(polarAngle - angle),
tipPoint.y,
tipPoint.y - headLength * sin(polarAngle + angle)
)
}

return SvgPathElement(b.build())
val b = SvgPathDataBuilder(true)
.moveTo(xs[0], ys[0])

for (i in 1..2) {
b.lineTo(xs[i], ys[i], true)
}

if (arrowSpec.type == Type.CLOSED) {
b.closePath()
}

return SvgPathElement(b.build())
}
}

private fun ArrowSpec.toArrowAes(p: DataPointAesthetics): DataPointAesthetics {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import org.jetbrains.letsPlot.core.plot.base.DataPointAesthetics


object GeomUtil {
fun DataPointAesthetics.toLocation(xAes: Aes<Double>, yAes: Aes<Double>): DoubleVector? {
return toLocationOrNull(get(xAes), get(yAes))
}

val TO_LOCATION_X_Y = { p: DataPointAesthetics ->
toLocationOrNull(
p.x(),
Expand Down