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 {