diff --git a/docs/examples/jupyter-notebooks-dev/size_unit.ipynb b/docs/examples/jupyter-notebooks-dev/size_unit.ipynb
new file mode 100644
index 00000000000..db0103201fd
--- /dev/null
+++ b/docs/examples/jupyter-notebooks-dev/size_unit.ipynb
@@ -0,0 +1,492 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ " \n",
+ " \n",
+ " "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import numpy as np\n",
+ "from lets_plot import *\n",
+ "\n",
+ "LetsPlot.setup_html()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 25,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "data = {'x':[1,3,5], 'y':[0,0,0], 'z': [1,2,3]}"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 26,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 26,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# size_unit forces visible size of point with Aes.size == 1 to be equal to the unit of given scale.\n",
+ "ggplot(data) + geom_point(aes(x='x', y='y'), size = 1, size_unit='x')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 27,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 27,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# as previous, but with size = 2\n",
+ "ggplot(data) + geom_point(aes(x='x', y='y'), size = 2, size_unit='x')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 28,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# size = 0.4 , size_unit = 'y'\n",
+ "ggplot(data) + geom_point(aes(x='x', y='y'), size = 0.4, size_unit='y')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 30,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 30,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#with size and color mapped to z and scale_size_identity\n",
+ "ggplot(data) + geom_point(aes(x='x', y='y', size='z', color = 'z'), size_unit='x') + scale_size_identity()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 31,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "#as previous example, but without scale_size_identity\n",
+ "ggplot(data) + geom_point(aes(x='x', y='y', size='z', color='z'), size_unit='x')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 32,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# size_unit for text fits 5 symbol string to given axis unit \n",
+ "ggplot(data) + geom_text(aes(x='x', y='y', label='x'), size = 1, size_unit='x', label_format='.2f')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ " \n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 33,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# as previous, but size_unit = 'y' + angle = 90\n",
+ "ggplot(data) + geom_text(aes(x='x', y='y', label='x'), size = 1, size_unit='y', label_format='.2f', angle=90)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.7.6"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesScaling.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesScaling.kt
index 2470fadc7b7..f5a28e302e0 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesScaling.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AesScaling.kt
@@ -8,6 +8,8 @@ package jetbrains.datalore.plot.base.aes
import jetbrains.datalore.plot.base.DataPointAesthetics
object AesScaling {
+ const val UNIT_SHAPE_SIZE = 2.2
+
fun strokeWidth(p: DataPointAesthetics): Double {
// aes Units -> px
return p.size()!! * 2.0
@@ -15,7 +17,7 @@ object AesScaling {
fun circleDiameter(p: DataPointAesthetics): Double {
// aes Units -> px
- return p.size()!! * 2.2
+ return p.size()!! * UNIT_SHAPE_SIZE
}
fun circleDiameterSmaller(p: DataPointAesthetics): Double {
@@ -25,7 +27,7 @@ object AesScaling {
fun sizeFromCircleDiameter(diameter: Double): Double {
// px -> aes Units
- return diameter / 2.2
+ return diameter / UNIT_SHAPE_SIZE
}
fun textSize(p: DataPointAesthetics): Double {
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/PointGeom.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/PointGeom.kt
index 80150b5484d..129ffe467c3 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/PointGeom.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/PointGeom.kt
@@ -7,7 +7,12 @@ package jetbrains.datalore.plot.base.geom
import jetbrains.datalore.base.geometry.DoubleVector
import jetbrains.datalore.base.values.Color
-import jetbrains.datalore.plot.base.*
+import jetbrains.datalore.plot.base.Aesthetics
+import jetbrains.datalore.plot.base.CoordinateSystem
+import jetbrains.datalore.plot.base.DataPointAesthetics
+import jetbrains.datalore.plot.base.GeomContext
+import jetbrains.datalore.plot.base.PositionAdjustment
+import jetbrains.datalore.plot.base.aes.AesScaling
import jetbrains.datalore.plot.base.aes.AestheticsUtil
import jetbrains.datalore.plot.base.geom.util.GeomHelper
import jetbrains.datalore.plot.base.geom.util.HintColorUtil.fromColorValue
@@ -24,6 +29,7 @@ import jetbrains.datalore.vis.svg.slim.SvgSlimElements
open class PointGeom : GeomBase() {
var animation: Any? = null
+ var sizeUnit: String? = null
override val legendKeyElementFactory: LegendKeyElementFactory
get() = PointLegendKeyElementFactory()
@@ -40,6 +46,8 @@ open class PointGeom : GeomBase() {
val count = aesthetics.dataPointCount()
val slimGroup = SvgSlimElements.g(count)
+ val sizeUnitRatio = getSizeUnitRatio(ctx)
+
for (i in 0 until count) {
val p = aesthetics.dataPointAt(i)
val x = p.x()
@@ -49,16 +57,29 @@ open class PointGeom : GeomBase() {
val location = helper.toClient(DoubleVector(x!!, y!!), p)
val shape = p.shape()!!
- targetCollector.addPoint(i, location, shape.size(p) / 2,
+
+ targetCollector.addPoint(
+ i, location, sizeUnitRatio * shape.size(p) / 2,
tooltipParams(p)
)
- val o = PointShapeSvg.create(shape, location, p)
+ val o = PointShapeSvg.create(shape, location, p, sizeUnitRatio)
o.appendTo(slimGroup)
}
}
root.add(wrap(slimGroup))
}
+ private fun getSizeUnitRatio(ctx: GeomContext): Double {
+ return if (sizeUnit != null) {
+ val unitRes = ctx.getUnitResolution(GeomHelper.getSizeUnitAes(sizeUnit!!))
+ // TODO: Need refactoring: It's better to use NamedShape.FILLED_CIRCLE.size(1.0)
+ // but Shape.size() can't be used because it takes DataPointAesthetics as param
+ unitRes / AesScaling.UNIT_SHAPE_SIZE
+ } else {
+ 1.0
+ }
+ }
+
companion object {
const val HANDLES_GROUPS = false
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/TextGeom.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/TextGeom.kt
index 380440d205d..e216894fa69 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/TextGeom.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/TextGeom.kt
@@ -24,11 +24,18 @@ import jetbrains.datalore.plot.common.data.SeriesUtil
class TextGeom : GeomBase() {
var formatter: StringFormat? = null
var naValue = DEF_NA_VALUE
+ var sizeUnit: String? = null
override val legendKeyElementFactory: LegendKeyElementFactory
get() = TextLegendKeyElementFactory()
- override fun buildIntern(root: SvgRoot, aesthetics: Aesthetics, pos: PositionAdjustment, coord: CoordinateSystem, ctx: GeomContext) {
+ override fun buildIntern(
+ root: SvgRoot,
+ aesthetics: Aesthetics,
+ pos: PositionAdjustment,
+ coord: CoordinateSystem,
+ ctx: GeomContext
+ ) {
val helper = GeomHelper(pos, coord, ctx)
val targetCollector = getGeomTargetCollector(ctx)
for (p in aesthetics.dataPoints()) {
@@ -37,7 +44,7 @@ class TextGeom : GeomBase() {
val text = toString(p.label())
if (SeriesUtil.allFinite(x, y) && !Strings.isNullOrEmpty(text)) {
val label = TextLabel(text)
- GeomHelper.decorate(label, p)
+ GeomHelper.decorate(label, p, getSizeUnitRatio(ctx))
val loc = helper.toClient(x, y, p)
label.moveTo(loc)
@@ -55,6 +62,19 @@ class TextGeom : GeomBase() {
}
}
+ // This implementation is oversimplified.
+ // Current implementation works for label_format ='.2f'
+ // and values between -1.0 and 1.0.
+ private fun getSizeUnitRatio(ctx: GeomContext): Double {
+ return if ( sizeUnit != null) {
+ val textWidth = 6.0
+ val unitRes = ctx.getUnitResolution(GeomHelper.getSizeUnitAes(sizeUnit!!))
+ unitRes / textWidth
+ } else {
+ 1.0
+ }
+ }
+
private fun toString(label: Any?): String {
return when {
label == null -> naValue
diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/GeomHelper.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/GeomHelper.kt
index f72cd09e7df..fd2e81e48e2 100644
--- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/GeomHelper.kt
+++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/geom/util/GeomHelper.kt
@@ -8,6 +8,7 @@ package jetbrains.datalore.plot.base.geom.util
import jetbrains.datalore.base.gcommon.base.Strings
import jetbrains.datalore.base.geometry.DoubleRectangle
import jetbrains.datalore.base.geometry.DoubleVector
+import jetbrains.datalore.plot.base.Aes
import jetbrains.datalore.plot.base.CoordinateSystem
import jetbrains.datalore.plot.base.DataPointAesthetics
import jetbrains.datalore.plot.base.GeomContext
@@ -162,11 +163,11 @@ open class GeomHelper(private val myPos: PositionAdjustment, coord: CoordinateSy
"mono" to "monospace"
)
- fun decorate(label: TextLabel, p: DataPointAesthetics) {
+ fun decorate(label: TextLabel, p: DataPointAesthetics, scale: Double = 1.0) {
label.textColor().set(p.color())
label.textOpacity().set(p.alpha())
- label.setFontSize(AesScaling.textSize(p))
+ label.setFontSize(AesScaling.textSize(p) * scale)
// family
var family = p.family()
@@ -255,5 +256,13 @@ open class GeomHelper(private val myPos: PositionAdjustment, coord: CoordinateSy
shape.setStroke(stroke, strokeAlpha)
shape.setStrokeWidth(AesScaling.strokeWidth(p))
}
+
+ fun getSizeUnitAes(sizeUnitName: String): Aes {
+ return when (sizeUnitName.toLowerCase()) {
+ "x" -> Aes.X
+ "y" -> Aes.Y
+ else -> error("Size unit value must be either 'x' or 'y', but was $sizeUnitName.")
+ }
+ }
}
}
\ No newline at end of file
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt
index a798f2825dc..c8e35d902cd 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/GeomProtoClientSide.kt
@@ -24,6 +24,7 @@ import jetbrains.datalore.plot.config.Option.Geom.PointRange
import jetbrains.datalore.plot.config.Option.Geom.Segment
import jetbrains.datalore.plot.config.Option.Geom.Step
+
class GeomProtoClientSide(geomKind: GeomKind) : GeomProto(geomKind) {
private val preferredCoordinateSystem: CoordProvider? = when (geomKind) {
GeomKind.TILE,
@@ -115,9 +116,12 @@ class GeomProtoClientSide(geomKind: GeomKind) : GeomProto(geomKind) {
GeomKind.POINT -> return GeomProvider.point {
val geom = PointGeom()
+
if (opts.has(Point.ANIMATION)) {
geom.animation = opts[Point.ANIMATION]
}
+
+ geom.sizeUnit = opts.getString(Point.SIZE_UNIT)?.toLowerCase()
geom
}
@@ -148,6 +152,8 @@ class GeomProtoClientSide(geomKind: GeomKind) : GeomProto(geomKind) {
}
}
+ geom.sizeUnit = opts.getString(Text.SIZE_UNIT)?.toLowerCase()
+
geom
}
diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt
index d3b4816beb0..4d980eee01a 100644
--- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt
+++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/Option.kt
@@ -139,6 +139,7 @@ object Option {
object Point {
const val ANIMATION = "animation"
+ const val SIZE_UNIT = "size_unit"
}
object Image {
@@ -148,6 +149,7 @@ object Option {
object Text {
const val LABEL_FORMAT = "label_format"
const val NA_VALUE = "na_value"
+ const val SIZE_UNIT = "size_unit"
}
object LiveMap {
@@ -166,6 +168,11 @@ object Option {
const val GEOCODING = "geocoding"
const val DEV_PARAMS = "dev_params"
}
+
+ object SizeUnit {
+ const val X = "x"
+ const val Y = "y"
+ }
}
object Scale {