From d861a6329371145e7295190ab2e750be5d1edb9a Mon Sep 17 00:00:00 2001 From: Olga Larionova Date: Tue, 14 May 2024 18:40:17 +0300 Subject: [PATCH 1/8] `override_aes` in `guide_legend()`. --- .../plot/builder/assemble/LegendAssembler.kt | 6 +++ .../plot/builder/assemble/LegendOptions.kt | 7 +++- .../builder/assemble/PlotAssemblerUtil.kt | 11 +++++ .../jetbrains/letsPlot/core/spec/Option.kt | 1 + .../letsPlot/core/spec/config/GuideConfig.kt | 42 +++++++++++++++---- .../core/spec/front/PlotConfigFrontend.kt | 3 +- .../core/spec/front/PlotConfigFrontendUtil.kt | 9 ++-- python-package/lets_plot/plot/guide.py | 4 +- 8 files changed, 69 insertions(+), 14 deletions(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt index 3adfb46f5e3..e1a6d9a7e9b 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt @@ -38,6 +38,7 @@ class LegendAssembler( fun addLayer( keyFactory: LegendKeyElementFactory, aesList: List>, + overrideAesValues: Map, Any>, constantByAes: Map, Any>, aestheticsDefaults: AestheticsDefaults, colorByAes: Aes, @@ -50,6 +51,7 @@ class LegendAssembler( LegendLayer( keyFactory, aesList, + overrideAesValues, constantByAes, aestheticsDefaults, scaleMappers, @@ -121,6 +123,7 @@ class LegendAssembler( private class LegendLayer( val keyElementFactory: LegendKeyElementFactory, val aesList: List>, + overrideAesValues: Map, Any>, constantByAes: Map, Any>, aestheticsDefaults: AestheticsDefaults, scaleMappers: Map, ScaleMapper<*>>, @@ -150,6 +153,9 @@ class LegendAssembler( val labels = scaleBreaks.labels for ((label, aesValue) in labels.zip(aesValues)) { aesValuesByLabel.getOrPut(label) { HashMap() }[aes] = aesValue + overrideAesValues.forEach { (aesToOverride, v) -> + aesValuesByLabel.getOrPut(label) { HashMap() }[aesToOverride] = v + } } } diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt index 39378526bec..3d61ccd5be3 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt @@ -5,10 +5,13 @@ package org.jetbrains.letsPlot.core.plot.builder.assemble +import org.jetbrains.letsPlot.core.plot.base.Aes + class LegendOptions constructor( val colCount: Int? = null, val rowCount: Int? = null, val byRow: Boolean = false, + val overrideAesValues: Map, Any>? = null, isReverse: Boolean = false ) : GuideOptions(isReverse) { init { @@ -26,7 +29,7 @@ class LegendOptions constructor( override fun withReverse(reverse: Boolean): LegendOptions { return LegendOptions( - colCount, rowCount, byRow, isReverse = reverse + colCount, rowCount, byRow, overrideAesValues, isReverse = reverse ) } @@ -39,6 +42,7 @@ class LegendOptions constructor( if (colCount != other.colCount) return false if (rowCount != other.rowCount) return false if (byRow != other.byRow) return false + if (overrideAesValues != other.overrideAesValues) return false return true } @@ -47,6 +51,7 @@ class LegendOptions constructor( var result = colCount ?: 0 result = 31 * result + (rowCount ?: 0) result = 31 * result + byRow.hashCode() + result = 31 * result + (overrideAesValues?.hashCode() ?: 0) return result } diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt index e44525b294e..f056410faa1 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt @@ -117,9 +117,20 @@ internal object PlotAssemblerUtil { val aesListForScaleName = aesListByScaleName.getValue(scaleName) val legendKeyFactory = layerInfo.legendKeyElementFactory val aestheticsDefaults = layerInfo.aestheticsDefaults + + val overrideAesValues = mutableMapOf, Any>() + aesListForScaleName.forEach { aes -> + val overrideOptionsForAes = (guideOptionsMap[aes] as? LegendOptions)?.overrideAesValues + if (!overrideOptionsForAes.isNullOrEmpty()) { + // ToDo Need to check for conflicting settings? + overrideAesValues += overrideOptionsForAes + } + } + legendAssembler.addLayer( legendKeyFactory, aesListForScaleName, + overrideAesValues, layerConstantByAes, aestheticsDefaults, layerInfo.colorByAes, diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt index 1d4016f06ec..15411fc259b 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/Option.kt @@ -678,6 +678,7 @@ object Option { const val ROW_COUNT = "nrow" const val COL_COUNT = "ncol" const val BY_ROW = "byrow" + const val OVERRIDE_AES = "override_aes" } object ColorBar { diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/config/GuideConfig.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/config/GuideConfig.kt index 50e1942d217..5b81bceae39 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/config/GuideConfig.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/config/GuideConfig.kt @@ -5,9 +5,11 @@ package org.jetbrains.letsPlot.core.spec.config +import org.jetbrains.letsPlot.core.plot.base.Aes import org.jetbrains.letsPlot.core.plot.builder.assemble.ColorBarOptions import org.jetbrains.letsPlot.core.plot.builder.assemble.GuideOptions import org.jetbrains.letsPlot.core.plot.builder.assemble.LegendOptions +import org.jetbrains.letsPlot.core.spec.Option import org.jetbrains.letsPlot.core.spec.Option.Guide.COLOR_BAR import org.jetbrains.letsPlot.core.spec.Option.Guide.COLOR_BAR_GB import org.jetbrains.letsPlot.core.spec.Option.Guide.ColorBar.BIN_COUNT @@ -16,41 +18,67 @@ import org.jetbrains.letsPlot.core.spec.Option.Guide.ColorBar.WIDTH import org.jetbrains.letsPlot.core.spec.Option.Guide.LEGEND import org.jetbrains.letsPlot.core.spec.Option.Guide.Legend.BY_ROW import org.jetbrains.letsPlot.core.spec.Option.Guide.Legend.COL_COUNT +import org.jetbrains.letsPlot.core.spec.Option.Guide.Legend.OVERRIDE_AES import org.jetbrains.letsPlot.core.spec.Option.Guide.Legend.ROW_COUNT import org.jetbrains.letsPlot.core.spec.Option.Guide.NONE import org.jetbrains.letsPlot.core.spec.Option.Guide.REVERSE +import org.jetbrains.letsPlot.core.spec.conversion.AesOptionConversion import kotlin.math.max abstract class GuideConfig private constructor(opts: Map) : OptionsAccessor(opts) { - fun createGuideOptions(): GuideOptions { - val options = createGuideOptionsIntern() + fun createGuideOptions(aopConversion: AesOptionConversion): GuideOptions { + val options = createGuideOptionsIntern(aopConversion) return options.withReverse(getBoolean(REVERSE)) } - protected abstract fun createGuideOptionsIntern(): GuideOptions + protected abstract fun createGuideOptionsIntern(aopConversion: AesOptionConversion): GuideOptions private class GuideNoneConfig internal constructor() : GuideConfig(emptyMap()) { - override fun createGuideOptionsIntern(): GuideOptions { + override fun createGuideOptionsIntern(aopConversion: AesOptionConversion): GuideOptions { return GuideOptions.NONE } } private class LegendConfig internal constructor(opts: Map) : GuideConfig(opts) { - override fun createGuideOptionsIntern(): GuideOptions { + override fun createGuideOptionsIntern(aopConversion: AesOptionConversion): GuideOptions { return LegendOptions( colCount = getDouble(COL_COUNT)?.toInt()?.let { max(1, it) }, rowCount = getDouble(ROW_COUNT)?.toInt()?.let { max(1, it) }, - byRow = getBoolean(BY_ROW) + byRow = getBoolean(BY_ROW), + overrideAesValues = initValues( + OptionsAccessor(getMap(OVERRIDE_AES)), + aopConversion + ) ) } + + private fun initValues( + layerOptions: OptionsAccessor, + aopConversion: AesOptionConversion + ): Map, Any> { + val result = HashMap, Any>() + Option.Mapping.REAL_AES_OPTION_NAMES + .filter(layerOptions::has) + .associateWith(Option.Mapping::toAes) + .forEach { (option, aes) -> + val optionValue = layerOptions.getSafe(option) + val value = if (optionValue is List<*>) { + optionValue.map { aopConversion.apply(aes, it) } + } else { + aopConversion.apply(aes, optionValue) + } ?: throw IllegalArgumentException("Can't convert to '$option' value: $optionValue") + result[aes] = value + } + return result + } } private class ColorBarConfig(opts: Map) : GuideConfig(opts) { - override fun createGuideOptionsIntern(): GuideOptions { + override fun createGuideOptionsIntern(aopConversion: AesOptionConversion): GuideOptions { return ColorBarOptions( width = getDouble(WIDTH), height = getDouble(HEIGHT), diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontend.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontend.kt index 44cd28f89fe..28f82115a70 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontend.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontend.kt @@ -32,7 +32,8 @@ class PlotConfigFrontend private constructor( internal val yAxisPosition: AxisPosition init { - guideOptionsMap = createGuideOptionsMap(this.scaleConfigs) + createGuideOptionsMap(getMap(GUIDES)) + guideOptionsMap = createGuideOptionsMap(this.scaleConfigs, aopConversion) + + createGuideOptionsMap(getMap(GUIDES), aopConversion) xAxisPosition = scaleProviderByAes.getValue(Aes.X).axisPosition yAxisPosition = scaleProviderByAes.getValue(Aes.Y).axisPosition diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontendUtil.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontendUtil.kt index d2282b940e8..29b8153a86d 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontendUtil.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/front/PlotConfigFrontendUtil.kt @@ -22,25 +22,26 @@ import org.jetbrains.letsPlot.core.spec.config.CoordConfig import org.jetbrains.letsPlot.core.spec.config.GuideConfig import org.jetbrains.letsPlot.core.spec.config.PlotConfigTransforms import org.jetbrains.letsPlot.core.spec.config.ScaleConfig +import org.jetbrains.letsPlot.core.spec.conversion.AesOptionConversion import org.jetbrains.letsPlot.core.spec.front.tiles.PlotTilesConfig object PlotConfigFrontendUtil { - internal fun createGuideOptionsMap(scaleConfigs: List>): Map, GuideOptions> { + internal fun createGuideOptionsMap(scaleConfigs: List>, aopConversion: AesOptionConversion): Map, GuideOptions> { val guideOptionsByAes = HashMap, GuideOptions>() for (scaleConfig in scaleConfigs) { if (scaleConfig.hasGuideOptions()) { - val guideOptions = scaleConfig.getGuideOptions().createGuideOptions() + val guideOptions = scaleConfig.getGuideOptions().createGuideOptions(aopConversion) guideOptionsByAes[scaleConfig.aes] = guideOptions } } return guideOptionsByAes } - internal fun createGuideOptionsMap(guideOptionsList: Map): Map, GuideOptions> { + internal fun createGuideOptionsMap(guideOptionsList: Map, aopConversion: AesOptionConversion): Map, GuideOptions> { val guideOptionsByAes = HashMap, GuideOptions>() for ((key, value) in guideOptionsList) { val aes = Option.Mapping.toAes(key) - guideOptionsByAes[aes] = GuideConfig.create(value).createGuideOptions() + guideOptionsByAes[aes] = GuideConfig.create(value).createGuideOptions(aopConversion) } return guideOptionsByAes } diff --git a/python-package/lets_plot/plot/guide.py b/python-package/lets_plot/plot/guide.py index 798d8fb90f4..b04b5afd1e1 100644 --- a/python-package/lets_plot/plot/guide.py +++ b/python-package/lets_plot/plot/guide.py @@ -7,7 +7,7 @@ __all__ = ['guide_legend', 'guide_colorbar', 'guides'] -def guide_legend(nrow=None, ncol=None, byrow=None): +def guide_legend(nrow=None, ncol=None, byrow=None, override_aes=None): """ Legend guide. @@ -19,6 +19,8 @@ def guide_legend(nrow=None, ncol=None, byrow=None): Number of columns in legend's guide. byrow : bool, default=True Type of output: by row, or by column. + override_aes : list + A list of aesthetic parameters that will override the default legend appearance. Returns ------- From bf564c4da106f4c6503ea1e5bbc06fe7ee36ec29 Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Wed, 12 Jun 2024 18:33:14 +0300 Subject: [PATCH 2/8] Support dict in override_aes for legend --- .../core/plot/builder/assemble/LegendAssembler.kt | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt index e1a6d9a7e9b..9b45b425dd8 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt @@ -151,10 +151,19 @@ class LegendAssembler( scaleMappers.getValue(aes)(it) as Any // Don't expect nulls. } val labels = scaleBreaks.labels - for ((label, aesValue) in labels.zip(aesValues)) { - aesValuesByLabel.getOrPut(label) { HashMap() }[aes] = aesValue + labels.zip(aesValues).forEachIndexed { index, (label, aesValue) -> + val labelMap = aesValuesByLabel.getOrPut(label) { HashMap() } + labelMap[aes] = aesValue + overrideAesValues.forEach { (aesToOverride, v) -> - aesValuesByLabel.getOrPut(label) { HashMap() }[aesToOverride] = v + val newAesValue = if (v is List<*>) { + v.getOrElse(index) { v.last() } + } else { + v + } + newAesValue?.let { + labelMap[aesToOverride] = it + } } } } From 0a768dc6f9a918627e1508fa73b9cdb999205341 Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Thu, 13 Jun 2024 17:52:25 +0300 Subject: [PATCH 3/8] Fix merging --- .../letsPlot/core/plot/builder/assemble/LegendOptions.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt index c7c4576e916..59252a8405c 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendOptions.kt @@ -36,7 +36,7 @@ class LegendOptions constructor( override fun withTitle(title: String?): LegendOptions { return LegendOptions( - colCount, rowCount, byRow, title = title, isReverse + colCount, rowCount, byRow, title = title, overrideAesValues, isReverse ) } From 940e0b55e9b15ed95588fbc26d5c0a709244c12a Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Sun, 16 Jun 2024 21:31:25 +0300 Subject: [PATCH 4/8] Add a test for override_aes --- python-package/test/plot/test_guides.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/python-package/test/plot/test_guides.py b/python-package/test/plot/test_guides.py index 7c5ef5c98e9..a150c1770e8 100644 --- a/python-package/test/plot/test_guides.py +++ b/python-package/test/plot/test_guides.py @@ -3,6 +3,7 @@ # Use of this source code is governed by the MIT license that can be found in the LICENSE file. # import lets_plot as gg +from lets_plot import geom_point from lets_plot.plot.guide import guide_legend, guide_colorbar @@ -24,3 +25,11 @@ def test_shape_and_color_guides(): assert as_dict['color']['nbin'] == 8 assert as_dict['color']['title'] == "Color title" + +def test_override_aes(): + spec = (gg.ggplot() + gg.guides(color=guide_legend(override_aes=dict(color=['grey'], size=10)))) + + as_dict = spec.as_dict()['guides']['color']['override_aes'] + assert as_dict['color'][0] == 'grey' + assert as_dict['size'] == 10 + From 34802c1bea4f74bc4a0d469e647de8019a318dff Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Sun, 16 Jun 2024 21:39:17 +0300 Subject: [PATCH 5/8] Add override_aes demo. Update future_changes.md --- docs/f-24e/legend_override_aes.ipynb | 323 +++++++++++++++++++++++++++ future_changes.md | 5 +- 2 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 docs/f-24e/legend_override_aes.ipynb diff --git a/docs/f-24e/legend_override_aes.ipynb b/docs/f-24e/legend_override_aes.ipynb new file mode 100644 index 00000000000..5826427ed85 --- /dev/null +++ b/docs/f-24e/legend_override_aes.ipynb @@ -0,0 +1,323 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "36032680-91bb-4226-bb44-a9d7a01d5d42", + "metadata": {}, + "source": [ + "## Legend override_aes\n", + "Now aesthetic parameters of legend keys can be changed without affecting thr rest of the plot. " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "96f6596f", + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "from lets_plot import *" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "09704322", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "LetsPlot.setup_html()" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "7ccad20f", + "metadata": {}, + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Unnamed: 0manufacturermodeldisplyearcyltransdrvctyhwyflclass
01audia41.819994auto(l5)f1829pcompact
12audia41.819994manual(m5)f2129pcompact
23audia42.020084manual(m6)f2031pcompact
\n", + "
" + ], + "text/plain": [ + " Unnamed: 0 manufacturer model displ year cyl trans drv cty hwy \\\n", + "0 1 audi a4 1.8 1999 4 auto(l5) f 18 29 \n", + "1 2 audi a4 1.8 1999 4 manual(m5) f 21 29 \n", + "2 3 audi a4 2.0 2008 4 manual(m6) f 20 31 \n", + "\n", + " fl class \n", + "0 p compact \n", + "1 p compact \n", + "2 p compact " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mpg = pd.read_csv (\"https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/mpg.csv\")\n", + "mpg.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4bddfeb1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Default legend\n", + "\n", + "p = ggplot(mpg, aes('displ', 'hwy', color='drv')) + \\\n", + " geom_point(size=4, alpha=0.2, stroke=0)\n", + "p" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "8fae39e2-c897-4570-9a32-3c00b6300584", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# override_aes\n", + "\n", + "p + guides(color=guide_legend(override_aes={'alpha': 0.3, 'size': 8}))\n" + ] + } + ], + "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.8.19" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/future_changes.md b/future_changes.md index 0c272e74b1c..bcf486492eb 100644 --- a/future_changes.md +++ b/future_changes.md @@ -1,9 +1,12 @@ ## [4.3.4] - 2024-mm-dd ### Added -- Legend title in guide_legend() and guide_colorbar(). +- Legend title in `guide_legend()` and `guide_colorbar()`. See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/legend_title.ipynb). +- Parameter `override_aes` in `guide_legend()`. + See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/legend_override_aes.ipynb). + ### Changed - [**breaking change**] guide_legend()/guide_colorbar() require keyword arguments for 'nrow'/'barwidth' other parameters except 'title'. From 946ac5c3d2a9fb32404c1519f59d0199eca31ab0 Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Mon, 17 Jun 2024 13:25:20 +0300 Subject: [PATCH 6/8] Fix wording --- future_changes.md | 2 +- python-package/test/plot/test_guides.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/future_changes.md b/future_changes.md index bcf486492eb..a971e2bd62c 100644 --- a/future_changes.md +++ b/future_changes.md @@ -8,6 +8,6 @@ See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24e/legend_override_aes.ipynb). ### Changed -- [**breaking change**] guide_legend()/guide_colorbar() require keyword arguments for 'nrow'/'barwidth' other parameters except 'title'. +- [**breaking change**] `guide_legend()`/`guide_colorbar()` require keyword arguments for `nrow`/`barwidth` other parameters except `title`. ### Fixed diff --git a/python-package/test/plot/test_guides.py b/python-package/test/plot/test_guides.py index a150c1770e8..a0c62f06e08 100644 --- a/python-package/test/plot/test_guides.py +++ b/python-package/test/plot/test_guides.py @@ -27,9 +27,9 @@ def test_shape_and_color_guides(): def test_override_aes(): - spec = (gg.ggplot() + gg.guides(color=guide_legend(override_aes=dict(color=['grey'], size=10)))) + spec = (gg.ggplot() + gg.guides(color=guide_legend(override_aes=dict(color=['red'], size=10)))) as_dict = spec.as_dict()['guides']['color']['override_aes'] - assert as_dict['color'][0] == 'grey' + assert as_dict['color'][0] == 'red' assert as_dict['size'] == 10 From 2f13718279dff9dbacf8044d8585e0ae1f693e92 Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Mon, 17 Jun 2024 16:48:13 +0300 Subject: [PATCH 7/8] Fix wording --- docs/f-24e/legend_override_aes.ipynb | 14 +++++++------- python-package/lets_plot/plot/guide.py | 6 ++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/f-24e/legend_override_aes.ipynb b/docs/f-24e/legend_override_aes.ipynb index 5826427ed85..91934cee24a 100644 --- a/docs/f-24e/legend_override_aes.ipynb +++ b/docs/f-24e/legend_override_aes.ipynb @@ -6,7 +6,7 @@ "metadata": {}, "source": [ "## Legend override_aes\n", - "Now aesthetic parameters of legend keys can be changed without affecting thr rest of the plot. " + "Now aesthetic parameters of legend keys can be changed without affecting the rest of the plot. " ] }, { @@ -170,7 +170,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -235,7 +235,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 5, diff --git a/python-package/lets_plot/plot/guide.py b/python-package/lets_plot/plot/guide.py index 824adf113da..4e668312e95 100644 --- a/python-package/lets_plot/plot/guide.py +++ b/python-package/lets_plot/plot/guide.py @@ -21,8 +21,10 @@ def guide_legend(title=None, *, nrow=None, ncol=None, byrow=None, override_aes=N Number of columns in legend's guide. byrow : bool, default=True Type of output: by row, or by column. - override_aes : list - A list of aesthetic parameters that will override the default legend appearance. + override_aes : dict + Dictionary that maps aesthetic parameters to new values, overriding the default legend appearance. + Each value can be a constant applied to all keys or a list that changes particular keys. + Returns ------- From 44287db6892116e9f2f869303c88c2d00af4eb4c Mon Sep 17 00:00:00 2001 From: Mikhail Koroteev Date: Tue, 18 Jun 2024 14:04:12 +0300 Subject: [PATCH 8/8] Refactoring legend --- .../plot/builder/assemble/LegendAssembler.kt | 23 ++++--------------- .../builder/assemble/PlotAssemblerUtil.kt | 17 +++++++------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt index 9b45b425dd8..cad500a0d5c 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/LegendAssembler.kt @@ -78,27 +78,14 @@ class LegendAssembler( } } - val legendBreaks = ArrayList() - for (legendBreak in legendBreaksByLabel.values) { - if (legendBreak.isEmpty) { - continue - } - legendBreaks.add(legendBreak) - } - - + val legendBreaks = legendBreaksByLabel.values.filterNot { it.isEmpty } if (legendBreaks.isEmpty()) { return LegendBoxInfo.EMPTY } - // legend options - val legendOptionsList = ArrayList() - for (legendLayer in legendLayers) { - val aesList = legendLayer.aesList - for (aes in aesList) { - if (guideOptionsMap[aes] is LegendOptions) { - legendOptionsList.add(guideOptionsMap[aes] as LegendOptions) - } + val legendOptionsList = legendLayers.flatMap { legendLayer -> + legendLayer.aesList.mapNotNull { aes -> + guideOptionsMap[aes] as? LegendOptions } } @@ -157,7 +144,7 @@ class LegendAssembler( overrideAesValues.forEach { (aesToOverride, v) -> val newAesValue = if (v is List<*>) { - v.getOrElse(index) { v.last() } + v.getOrElse(index) { v.lastOrNull() } } else { v } diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt index b362496f4ff..33e169bb020 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/assemble/PlotAssemblerUtil.kt @@ -117,19 +117,18 @@ internal object PlotAssemblerUtil { val legendKeyFactory = layerInfo.legendKeyElementFactory val aestheticsDefaults = layerInfo.aestheticsDefaults - val overrideAesValues = mutableMapOf, Any>() - aesListForScaleName.forEach { aes -> - val overrideOptionsForAes = (guideOptionsMap[aes] as? LegendOptions)?.overrideAesValues - if (!overrideOptionsForAes.isNullOrEmpty()) { - // ToDo Need to check for conflicting settings? - overrideAesValues += overrideOptionsForAes - } - } + val allOverrideAesValues = + guideOptionsMap + .filter { (aes, _) -> aes in aesListForScaleName } + .values + .filterIsInstance() + .map{it.overrideAesValues.orEmpty()} + .fold(mapOf, Any>(), { acc, overrideAesValues -> acc + overrideAesValues }) legendAssembler.addLayer( legendKeyFactory, aesListForScaleName, - overrideAesValues, + allOverrideAesValues, layerConstantByAes, aestheticsDefaults, layerInfo.colorByAes,