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