From 7a2383f431e723aa0337df34ffe002201b012f2b Mon Sep 17 00:00:00 2001 From: Olga Larionova <46743085+OLarionova-HORIS@users.noreply.github.com> Date: Tue, 2 May 2023 20:21:08 +0300 Subject: [PATCH] Fix calculation of XY-ranges for geom_errorbar (#770) * Fix XY-ranges calculation, use height/width expansion for errorbar depend on its representation. * Separate logic to identify renderedAes for errobar. * Remove unused import directive. * Code cleanup. * Fix requirements for the specified aesthetics for the errorbar. * Update demo notebook. --- docs/f-23b/horizontal_error_bars.ipynb | 300 +++++++++++++++--- .../plot/base/aes/AestheticsDefaults.kt | 4 - .../plot/builder/assemble/GeomLayerBuilder.kt | 26 +- 3 files changed, 286 insertions(+), 44 deletions(-) diff --git a/docs/f-23b/horizontal_error_bars.ipynb b/docs/f-23b/horizontal_error_bars.ipynb index 19d44875a00..a0b464f6205 100644 --- a/docs/f-23b/horizontal_error_bars.ipynb +++ b/docs/f-23b/horizontal_error_bars.ipynb @@ -4,10 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Horizontal error bars\n", + "# Horizontal error bars and vertical \"dodge\"\n", "\n", - "`geom_errorbar()` can be plotted horizontally by assigning `xmin` and `xmax` aesthetics.\n", - "The height of the error bar is spepicified by the `height`." + "`geom_errorbar()` can be plotted horizontally by assigning `y`,`xmin`,`xmax` aesthetics. The height of the error bar is defined by the `height`.\n", + "\n", + "New type of position adjustment `'dodgev'` is used to adjust the position by dodging overlaps to the side. Function `position_dodgev(height)` allows to set the dodge height.\n", + "\n" ] }, { @@ -22,7 +24,7 @@ " \n", " \n", @@ -38,26 +40,121 @@ "LetsPlot.setup_html()" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 1. Data Preparation" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ToothGrowth dataset describes the effect of Vitamin C on tooth growth in guinea pigs. Each animal received one of three dose levels of vitamin C (0.5, 1, and 2 mg/day) by one of two delivery methods: orange juice (OJ) or ascorbic acid (VC)." + ] + }, { "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
lensuppdose
04.2VC0.5
111.5VC0.5
27.3VC0.5
35.8VC0.5
46.4VC0.5
\n", + "
" + ], + "text/plain": [ + " len supp dose\n", + "0 4.2 VC 0.5\n", + "1 11.5 VC 0.5\n", + "2 7.3 VC 0.5\n", + "3 5.8 VC 0.5\n", + "4 6.4 VC 0.5" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "df = pd.read_csv(\"https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/ToothGrowth.csv\")\n", + "df.head()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, "source": [ - "data = dict(\n", - " supp = ['OJ', 'OJ', 'OJ', 'VC', 'VC', 'VC'],\n", - " dose = [0.5, 1.0, 2.0, 0.5, 1.0, 2.0],\n", - " length = [13.23, 22.70, 26.06, 7.98, 16.77, 26.14],\n", - " len_min = [11.83, 21.2, 24.50, 4.24, 15.26, 23.35],\n", - " len_max = [15.63, 24.9, 27.11, 10.72, 19.28, 28.93]\n", - ")" + "* len : Tooth length\n", + "* dose : Dose in milligrams (0.5, 1, 2)\n", + "* supp : Supplement type (VC or OJ)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "#### 1. Default Presentation" + "Let's calculate the mean value of tooth length in each group, minimum and maximum values, and use these information to plot error bars." ] }, { @@ -68,15 +165,145 @@ { "data": { "text/html": [ - "
\n", + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
suppdoselengthlen_minlen_max
0OJ0.513.238.221.5
1OJ1.022.7014.527.3
2OJ2.026.0622.430.9
3VC0.57.984.211.5
4VC1.016.7713.622.5
5VC2.026.1418.533.9
\n", + "
" + ], + "text/plain": [ + " supp dose length len_min len_max\n", + "0 OJ 0.5 13.23 8.2 21.5\n", + "1 OJ 1.0 22.70 14.5 27.3\n", + "2 OJ 2.0 26.06 22.4 30.9\n", + "3 VC 0.5 7.98 4.2 11.5\n", + "4 VC 1.0 16.77 13.6 22.5\n", + "5 VC 2.0 26.14 18.5 33.9" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np\n", + "\n", + "data = {}\n", + "\n", + "for supp_lvl in np.unique(df['supp']):\n", + " for dose_lvl in np.unique(df['dose']):\n", + " data_to_sum = df[(df['supp'] == supp_lvl) & (df['dose'] == dose_lvl)]\n", + "\n", + " mean = data_to_sum['len'].mean()\n", + " len_min = data_to_sum['len'].min()\n", + " len_max = data_to_sum['len'].max()\n", + "\n", + " data.setdefault('supp', []).append(supp_lvl)\n", + " data.setdefault('dose', []).append(dose_lvl)\n", + " data.setdefault('length', []).append(mean)\n", + " data.setdefault('len_min', []).append(len_min)\n", + " data.setdefault('len_max', []).append(len_max)\n", + " \n", + "pd.DataFrame(data) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### 2. Default Presentation" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -125,28 +352,29 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### 2. `position_dodgev()`\n", + "#### 3. With `position = 'dodgev'`\n", + "\n", "\n", "To fix errorbars overlapping, use `position_dodgev(height)` - to move them vertically." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -200,27 +428,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#### 3. Error-bars on bar plot" + "#### 4. Error-bars on bar plot" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 5, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } diff --git a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt index 67e092cc867..fd6ae421600 100644 --- a/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt +++ b/plot-base-portable/src/commonMain/kotlin/jetbrains/datalore/plot/base/aes/AestheticsDefaults.kt @@ -120,10 +120,6 @@ open class AestheticsDefaults { .update(Aes.COLOR, Color.BLACK) } - fun errorBarH(): AestheticsDefaults { - return errorBar() - } - fun crossBar(): AestheticsDefaults { return AestheticsDefaults() .update(Aes.WIDTH, 0.9) diff --git a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/GeomLayerBuilder.kt b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/GeomLayerBuilder.kt index fdc213800df..f05d3c63be1 100644 --- a/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/GeomLayerBuilder.kt +++ b/plot-builder-portable/src/commonMain/kotlin/jetbrains/datalore/plot/builder/assemble/GeomLayerBuilder.kt @@ -268,8 +268,8 @@ class GeomLayerBuilder( override val geomKind: GeomKind = geomProvider.geomKind override val aestheticsDefaults: AestheticsDefaults = geomProvider.aestheticsDefaults() + private val myConstantByAes: TypedKeyHashMap = TypedKeyHashMap() private val myRenderedAes: List> - private val myConstantByAes: TypedKeyHashMap override val legendKeyElementFactory: LegendKeyElementFactory get() = geom.legendKeyElementFactory @@ -278,13 +278,31 @@ class GeomLayerBuilder( get() = geom is LiveMapGeom init { - myRenderedAes = GeomMeta.renders(geomProvider.geomKind, colorByAes, fillByAes) - // constant value by aes (default + specified) - myConstantByAes = TypedKeyHashMap() for (key in constantByAes.keys()) { myConstantByAes.put(key, constantByAes[key]) } + + myRenderedAes = GeomMeta.renders(geomProvider.geomKind, colorByAes, fillByAes).let { allRenderedAes -> + if (geomKind == GeomKind.ERROR_BAR) { + // ToDo Need refactoring... + // This geometry supports a dual set of aesthetics (vertical and horizontal representation). + // Check that the settings are consistent + // and set the aesthetics needed for that geometry. + val definedAes = allRenderedAes.filter { aes -> hasBinding(aes) || hasConstant(aes) } + val isVertical = setOf(Aes.YMIN, Aes.YMAX).all { aes -> aes in definedAes } + val isHorizontal = setOf(Aes.XMIN, Aes.XMAX).all { aes -> aes in definedAes } + require(!(isVertical && isHorizontal)) { + "Either ymin, ymax or xmin, xmax must be specified for the errorbar." + } + allRenderedAes - when (isVertical) { + true -> setOf(Aes.Y, Aes.XMIN, Aes.XMAX, Aes.HEIGHT) + false -> setOf(Aes.X, Aes.YMIN, Aes.YMAX, Aes.WIDTH) + } + } else { + allRenderedAes + } + } } override fun renderedAes(): List> {