Skip to content

Commit

Permalink
Add shape rotation for points on livemap.
Browse files Browse the repository at this point in the history
  • Loading branch information
RYangazov committed May 23, 2024
1 parent 4242354 commit 2281fca
Show file tree
Hide file tree
Showing 6 changed files with 534 additions and 43 deletions.
484 changes: 478 additions & 6 deletions docs/dev/notebooks/rotate_points.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions future_changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
### Fixed
- Livemap: improve "tiles" documentation [[#1093](https://github.com/JetBrains/lets-plot/issues/1093)].
- Undesired vertical scroller when displaying `gggrid` in Jupyter notebook.
- GeoJson structure breaks if the ring start label occurs several times [[#1086](https://github.com/JetBrains/lets-plot/issues/1086)].
- `theme`: left margin doesn't work for the `plot_title` parameter [[#1101](https://github.com/JetBrains/lets-plot/issues/1101)].
- Support for marker rotation [[#736](https://github.com/JetBrains/lets-plot/issues/736)].
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class PointEntityBuilder(
var animation: Int = 0
var label: String = ""
var shape: Int = 1
var angle: Double = 0.0

fun build(nonInteractive: Boolean = false): EcsEntity {
val d = radius * 2.0
Expand All @@ -80,7 +81,7 @@ class PointEntityBuilder(
+IndexComponent(layerIndex!!, index!!)
}
+ RenderableComponent().apply {
renderer = PointRenderer(shape)
renderer = PointRenderer(shape, angle)
}
+ChartElementComponent().apply {
sizeScalingRange = this@PointEntityBuilder.sizeScalingRange
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package org.jetbrains.letsPlot.livemap.chart.point

import org.jetbrains.letsPlot.commons.intern.math.toRadians
import org.jetbrains.letsPlot.core.canvas.Context2d
import org.jetbrains.letsPlot.livemap.chart.ChartElementComponent
import org.jetbrains.letsPlot.livemap.chart.PointComponent
Expand All @@ -17,8 +18,10 @@ import kotlin.math.PI
import kotlin.math.sqrt

class PointRenderer(
private val shape: Int
private val shape: Int,
degreeAngle: Double
) : Renderer {
val angle: Double = toRadians(degreeAngle)

override fun render(entity: EcsEntity, ctx: Context2d, renderHelper: RenderHelper) {
val chartElement = entity.get<ChartElementComponent>()
Expand All @@ -31,7 +34,8 @@ class PointRenderer(
ctx = ctx,
radius = pointData.scaledRadius(chartElement.scalingSizeFactor),
stroke = chartElement.scaledStrokeWidth(),
shape = shape
shape = shape,
angle = angle
)
if (chartElement.fillColor != null) {
ctx.setFillStyle(chartElement.scaledFillColor())
Expand All @@ -43,55 +47,55 @@ class PointRenderer(
ctx.stroke()
}
}
private fun drawMarker(ctx: Context2d, radius: Double, stroke: Double, shape: Int) {
private fun drawMarker(ctx: Context2d, radius: Double, stroke: Double, shape: Int, angle: Double) {
when (shape) {
0 -> square(ctx, radius)
0 -> square(ctx, radius, angle)
1 -> circle(ctx, radius)
2 -> triangle(ctx, radius, stroke)
3 -> plus(ctx, radius)
4 -> cross(ctx, radius / sqrt(2.0))
5 -> diamond(ctx, radius)
6 -> triangle(ctx, radius, stroke, pointingUp = false)
2 -> triangle(ctx, radius, stroke, angle)
3 -> plus(ctx, radius, angle)
4 -> cross(ctx, radius / sqrt(2.0), angle)
5 -> diamond(ctx, radius, angle)
6 -> triangle(ctx, radius, stroke, angle, pointingUp = false)
7 -> {
square(ctx, radius)
cross(ctx, radius)
square(ctx, radius, angle)
cross(ctx, radius, angle)
}
8 -> {
plus(ctx, radius)
cross(ctx, radius / sqrt(2.0))
plus(ctx, radius, angle)
cross(ctx, radius / sqrt(2.0), angle)
}
9 -> {
diamond(ctx, radius)
plus(ctx, radius)
diamond(ctx, radius, angle)
plus(ctx, radius, angle)
}
10 -> {
circle(ctx, radius)
plus(ctx, radius)
plus(ctx, radius, angle)
}
11 -> {
triangle(ctx, radius, stroke, pointingUp = true, pinnedToCentroid = true)
triangle(ctx, radius, stroke, pointingUp = false, pinnedToCentroid = true)
triangle(ctx, radius, stroke, angle, pointingUp = true, pinnedToCentroid = true)
triangle(ctx, radius, stroke, angle, pointingUp = false, pinnedToCentroid = true)
}
12 -> {
square(ctx, radius)
plus(ctx, radius)
square(ctx, radius, angle)
plus(ctx, radius, angle)
}
13 -> {
circle(ctx, radius)
cross(ctx, radius / sqrt(2.0))
cross(ctx, radius / sqrt(2.0), angle)
}
14 -> squareTriangle(ctx, radius, stroke)
15 -> square(ctx, radius)
14 -> squareTriangle(ctx, radius, stroke, angle)
15 -> square(ctx, radius, angle)
16 -> circle(ctx, radius)
17 -> triangle(ctx, radius, 1.0)
18 -> diamond(ctx, radius)
17 -> triangle(ctx, radius, 1.0, angle)
18 -> diamond(ctx, radius, angle)
19 -> circle(ctx, radius)
20 -> circle(ctx, radius)
21 -> circle(ctx, radius)
22 -> square(ctx, radius)
23 -> diamond(ctx, radius)
24 -> triangle(ctx, radius, stroke)
25 -> triangle(ctx, radius, stroke, pointingUp = false)
22 -> square(ctx, radius, angle)
23 -> diamond(ctx, radius, angle)
24 -> triangle(ctx, radius, stroke, angle)
25 -> triangle(ctx, radius, stroke, angle, pointingUp = false)
else -> throw IllegalStateException("Unknown point shape")
}
}
Expand All @@ -100,15 +104,18 @@ class PointRenderer(
ctx.arc(0.0, 0.0, r, 0.0, 2 * PI)
}

private fun square(ctx: Context2d, r: Double) {
private fun square(ctx: Context2d, r: Double, angle: Double){
ctx.rotate(angle)
ctx.moveTo(-r, -r)
ctx.lineTo(r, -r)
ctx.lineTo(r, r)
ctx.lineTo(-r, r)
ctx.closePath()
ctx.rotate(-angle)
}

private fun squareTriangle(ctx: Context2d, r: Double, stroke: Double) {
private fun squareTriangle(ctx: Context2d, r: Double, stroke: Double, angle: Double) {
ctx.rotate(angle)
val outerSize = 2 * r + stroke
val triangleHeight = outerSize - stroke / 2 - sqrt(5.0) * stroke / 2
ctx.moveTo(-triangleHeight / 2, r)
Expand All @@ -119,9 +126,10 @@ class PointRenderer(
ctx.lineTo(r, -r)
ctx.lineTo(r, r)
ctx.closePath()
ctx.rotate(-angle)
}

private fun triangle(ctx: Context2d, r: Double, stroke: Double, pointingUp: Boolean = true, pinnedToCentroid: Boolean = false) {
private fun triangle(ctx: Context2d, r: Double, stroke: Double, angle: Double, pointingUp: Boolean = true, pinnedToCentroid: Boolean = false) {
val outerHeight = 2 * r + stroke
val height = outerHeight - 3.0 * stroke / 2.0
val side = 2.0 * height / sqrt(3.0)
Expand All @@ -135,34 +143,41 @@ class PointRenderer(
height / 6.0 + stroke / 4.0
else
0.0

ctx.rotate(angle)
ctx.moveTo(0.0, -pointingCoeff * (distanceToPeak + centroidOffset))
ctx.lineTo(side / 2.0, pointingCoeff * (distanceToBase - centroidOffset))
ctx.lineTo(-side / 2.0, pointingCoeff * (distanceToBase - centroidOffset))
ctx.lineTo(0.0, -pointingCoeff * (distanceToPeak + centroidOffset))
ctx.closePath()
ctx.rotate(-angle)
}

internal fun plus(ctx: Context2d, r: Double) {
internal fun plus(ctx: Context2d, r: Double, angle: Double) {
ctx.rotate(angle)
ctx.moveTo(0.0, -r)
ctx.lineTo(0.0, r)
ctx.moveTo(-r, 0.0)
ctx.lineTo(r, 0.0)
ctx.rotate(-angle)
}

private fun cross(ctx: Context2d, r: Double) {
private fun cross(ctx: Context2d, r: Double, angle: Double){
ctx.rotate(angle)
ctx.moveTo(-r, -r)
ctx.lineTo(r, r)
ctx.moveTo(-r, r)
ctx.lineTo(r, -r)
ctx.rotate(-angle)
}

private fun diamond(ctx: Context2d, r: Double) {
private fun diamond(ctx: Context2d, r: Double, angle: Double) {
ctx.rotate(angle)
ctx.moveTo(0.0, -r)
ctx.lineTo(r, 0.0)
ctx.lineTo(0.0, r)
ctx.lineTo(-r, 0.0)
ctx.closePath()
ctx.rotate(-angle)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ object LayerConverter {
label = it.label
animation = it.animation
shape = it.shape
angle = it.angle
radius = it.radius
fillColor = it.fillColor
strokeColor = it.strokeColor
Expand Down
1 change: 1 addition & 0 deletions python-package/lets_plot/plot/geom.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def geom_point(mapping=None, *, data=None, stat=None, position=None, show_legend
- color (colour) : color of the geometry. String in the following formats: RGB/RGBA (e.g. "rgb(0, 0, 255)"); HEX (e.g. "#0000FF"); color name (e.g. "red"); role name ("pen", "paper" or "brush").
- fill : fill color. Is applied only to the points of shapes having inner area. String in the following formats: RGB/RGBA (e.g. "rgb(0, 0, 255)"); HEX (e.g. "#0000FF"); color name (e.g. "red"); role name ("pen", "paper" or "brush").
- shape : shape of the point, an integer from 0 to 25.
- angle : rotation angle of the point shape, in degrees.
- size : size of the point.
- stroke : width of the shape border. Applied only to the shapes having border.
Expand Down

0 comments on commit 2281fca

Please sign in to comment.