diff --git a/docs/dev/notebooks/LP-736-livemap.ipynb b/docs/dev/notebooks/LP-736-livemap.ipynb
new file mode 100644
index 00000000000..a80213cf33e
--- /dev/null
+++ b/docs/dev/notebooks/LP-736-livemap.ipynb
@@ -0,0 +1,178 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0394a107",
+ "metadata": {},
+ "source": [
+ "### Markers shape rotation of points on livemap"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "fd75f291",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "The geodata is provided by © OpenStreetMap contributors and is made available here under the Open Database License (ODbL).\n"
+ ]
+ }
+ ],
+ "source": [
+ "from lets_plot import *\n",
+ "from lets_plot.geo_data import *"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "b9ac5f61",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "LetsPlot.setup_html()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "cb8c8f82",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 3,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "data = {\"city\": [\"New York\", \"Los Angeles\", \"Chicago\"], \\\n",
+ " \"est_pop_2019\": [8_336_817, 3_979_576, 2_693_976], \\\n",
+ " \"angle\": [0, 45, 60]}\n",
+ "centroids = geocode_cities(data[\"city\"]).get_centroids()\n",
+ "ggplot() + geom_livemap() + \\\n",
+ " geom_point(aes(size=\"est_pop_2019\", angle=\"angle\"), color=\"red\", show_legend=False, \\\n",
+ " shape=13, data=data, map=centroids, map_join=\"city\", \\\n",
+ " tooltips=layer_tooltips().title(\"@city\").line(\"population|@est_pop_2019\"))"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.10.11"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/api/PointLayerBuilder.kt b/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/api/PointLayerBuilder.kt
index b4ea1a83f0a..784e8d1bedb 100644
--- a/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/api/PointLayerBuilder.kt
+++ b/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/api/PointLayerBuilder.kt
@@ -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
@@ -80,7 +81,7 @@ class PointEntityBuilder(
+IndexComponent(layerIndex!!, index!!)
}
+ RenderableComponent().apply {
- renderer = PointRenderer(shape)
+ renderer = PointRenderer(shape, angle)
}
+ChartElementComponent().apply {
sizeScalingRange = this@PointEntityBuilder.sizeScalingRange
diff --git a/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/chart/point/PointRenderer.kt b/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/chart/point/PointRenderer.kt
index f399fb5e003..cca96ec1359 100644
--- a/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/chart/point/PointRenderer.kt
+++ b/livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/livemap/chart/point/PointRenderer.kt
@@ -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
@@ -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 {
+ private val angle = toRadians(degreeAngle)
override fun render(entity: EcsEntity, ctx: Context2d, renderHelper: RenderHelper) {
val chartElement = entity.get()
@@ -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())
@@ -43,7 +47,12 @@ 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) {
+ val needToRotate = shape !in listOf(1, 10, 16, 19, 20, 21) && angle != 0.0
+ if (needToRotate) {
+ ctx.rotate(angle)
+ }
+
when (shape) {
0 -> square(ctx, radius)
1 -> circle(ctx, radius)
@@ -94,6 +103,10 @@ class PointRenderer(
25 -> triangle(ctx, radius, stroke, pointingUp = false)
else -> throw IllegalStateException("Unknown point shape")
}
+
+ if (needToRotate) {
+ ctx.rotate(-angle)
+ }
}
private fun circle(ctx: Context2d, r: Double) {
diff --git a/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/LayerConverter.kt b/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/LayerConverter.kt
index 56eb899da82..8911ad272a9 100644
--- a/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/LayerConverter.kt
+++ b/plot-livemap/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/livemap/LayerConverter.kt
@@ -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