Skip to content

Commit

Permalink
Legend title (#1109)
Browse files Browse the repository at this point in the history
* Add title parameter to guide_legend() and guide_colorbar()

* Merge guide parameters

* Fix test

* Add legend title demo. Update future_changes

* Cleaned up merge_dicts()

* Remove unnecessary scale name manipulations

* Fix link

* Refresh demo notebook

* Change parameter order

* Update demo notebook

* guide_legend()/guide_colorbar() require keyword arguments except of 'title'

* Simplify error message

* Simplify 'title' parameter description
  • Loading branch information
MKoroteev-HORIS committed Jun 12, 2024
1 parent 0f6ac94 commit b7bb4bc
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 13 deletions.
262 changes: 262 additions & 0 deletions docs/f-24e/legend_title.ipynb

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion future_changes.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
## [4.3.4] - 2024-mm-dd

### Added
- 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).

### Changed

- [**breaking change**] guide_legend()/guide_colorbar() require keyword arguments for 'nrow'/'barwidth' other parameters except 'title'.

### Fixed
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ class ColorBarOptions constructor(
val width: Double? = null,
val height: Double? = null,
val binCount: Int? = null,
title: String? = null,
isReverse: Boolean = false
) : GuideOptions(isReverse) {
) : GuideOptions(title, isReverse) {

override fun withReverse(reverse: Boolean): ColorBarOptions {
return ColorBarOptions(
width, height, binCount, isReverse = reverse
width, height, binCount, title, isReverse = reverse
)
}

override fun withTitle(title: String?): ColorBarOptions {
return ColorBarOptions(
width, height, binCount, title = title, isReverse
)
}

Expand All @@ -27,6 +34,7 @@ class ColorBarOptions constructor(
if (width != other.width) return false
if (height != other.height) return false
if (binCount != other.binCount) return false
if (title != other.title) return false

return true
}
Expand All @@ -35,6 +43,7 @@ class ColorBarOptions constructor(
var result = width?.hashCode() ?: 0
result = 31 * result + (height?.hashCode() ?: 0)
result = 31 * result + (binCount ?: 0)
result = 31 * result + (title?.hashCode() ?: 0)
return result
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
package org.jetbrains.letsPlot.core.plot.builder.assemble

abstract class GuideOptions(
val title: String?,
val isReverse: Boolean
) {
abstract fun withTitle(title: String?): GuideOptions
abstract fun withReverse(reverse: Boolean): GuideOptions

// // In Kotlin Native objects a frozen by default. Annotate with `ThreadLocal` to unfreeze.
Expand All @@ -16,8 +18,9 @@ abstract class GuideOptions(
// // - `isReverse` in the 'outer' class
// @ThreadLocal
companion object {
val NONE: GuideOptions = object : GuideOptions(false) {
override fun withReverse(reverse: Boolean): GuideOptions = this // Do nothing
val NONE: GuideOptions = object : GuideOptions(null, false) {
override fun withTitle(title: String?): GuideOptions = this
override fun withReverse(reverse: Boolean): GuideOptions = this // Do nothing
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ class LegendOptions constructor(
val colCount: Int? = null,
val rowCount: Int? = null,
val byRow: Boolean = false,
title: String? = null,
isReverse: Boolean = false
) : GuideOptions(isReverse) {
) : GuideOptions(title, isReverse) {
init {
require(colCount == null || colCount > 0) { "Invalid value: colCount=$colCount" }
require(rowCount == null || rowCount > 0) { "Invalid value: colCount=$rowCount" }
Expand All @@ -26,7 +27,13 @@ class LegendOptions constructor(

override fun withReverse(reverse: Boolean): LegendOptions {
return LegendOptions(
colCount, rowCount, byRow, isReverse = reverse
colCount, rowCount, byRow, title, isReverse = reverse
)
}

override fun withTitle(title: String?): LegendOptions {
return LegendOptions(
colCount, rowCount, byRow, title = title, isReverse
)
}

Expand All @@ -39,6 +46,7 @@ class LegendOptions constructor(
if (colCount != other.colCount) return false
if (rowCount != other.rowCount) return false
if (byRow != other.byRow) return false
if (title != other.title) return false

return true
}
Expand All @@ -47,6 +55,7 @@ class LegendOptions constructor(
var result = colCount ?: 0
result = 31 * result + (rowCount ?: 0)
result = 31 * result + byRow.hashCode()
result = 31 * result + (title?.hashCode() ?: 0)
return result
}

Expand All @@ -56,6 +65,7 @@ class LegendOptions constructor(
var colCount: Int? = null
var rowCount: Int? = null
var byRow = false
var title: String? = null
for (options in optionsList) {
if (options.byRow) {
byRow = true
Expand All @@ -66,8 +76,11 @@ class LegendOptions constructor(
if (options.hasRowCount()) {
rowCount = options.rowCount
}
if (options.title != null) {
title = options.title
}
}
return LegendOptions(colCount, rowCount, byRow)
return LegendOptions(colCount, rowCount, byRow, title)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,11 @@ internal object PlotAssemblerUtil {
}
}


val aesListByScaleName = LinkedHashMap<String, MutableList<Aes<*>>>()
val aesList = mappedRenderedAesToCreateGuides(layerInfo, guideOptionsMap)
for (aes in aesList) {
val scale = ctx.getScale(aes)
val scaleName = scale.name
val scaleName = guideOptionsMap[aes]?.title ?: scale.name

val colorBarOptions: ColorBarOptions? = guideOptionsMap[aes]?.let {
if (it is ColorBarOptions) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -675,6 +675,7 @@ object Option {
const val COLOR_BAR_GB = "colourbar"

const val REVERSE = "reverse"
const val TITLE = "title"

object Legend {
const val ROW_COUNT = "nrow"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ import org.jetbrains.letsPlot.core.spec.Option.Guide.Legend.COL_COUNT
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.Option.Guide.TITLE
import kotlin.math.max

abstract class GuideConfig private constructor(opts: Map<String, Any>) : OptionsAccessor(opts) {

fun createGuideOptions(): GuideOptions {
val options = createGuideOptionsIntern()
return options.withReverse(getBoolean(REVERSE))
return options
.withTitle(getString(TITLE))
.withReverse(getBoolean(REVERSE))
}

protected abstract fun createGuideOptionsIntern(): GuideOptions
Expand Down
15 changes: 15 additions & 0 deletions python-package/lets_plot/plot/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,11 @@ def __add__(self, other):
plot = plot.__add__(spec)
return plot

if other.kind == 'guides':
existing_guides_options = plot.props().get('guides', {})
plot.props()['guides'] = _merge_dicts_recursively(existing_guides_options, other.as_dict())
return plot

# add feature to properties
plot.props()[other.kind] = other
return plot
Expand Down Expand Up @@ -829,6 +834,16 @@ def _generate_data(size):
return PlotSpec(data='x' * size, mapping=None, scales=[], layers=[])


def _merge_dicts_recursively(d1, d2):
merged = d1.copy()
for key, value in d2.items():
if isinstance(value, dict) and isinstance(merged.get(key), dict):
merged[key] = _merge_dicts_recursively(merged[key], value)
else:
merged[key] = value
return merged


def _theme_dicts_merge(x, y):
"""
Simple values in `y` override values in `x`.
Expand Down
10 changes: 8 additions & 2 deletions python-package/lets_plot/plot/guide.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
__all__ = ['guide_legend', 'guide_colorbar', 'guides']


def guide_legend(nrow=None, ncol=None, byrow=None):
def guide_legend(title=None, *, nrow=None, ncol=None, byrow=None):
"""
Legend guide.
Parameters
----------
title : str
Title of guide.
nrow : int
Number of rows in legend's guide.
ncol : int
Expand Down Expand Up @@ -51,12 +53,14 @@ def guide_legend(nrow=None, ncol=None, byrow=None):
return _guide('legend', **locals())


def guide_colorbar(barwidth=None, barheight=None, nbin=None):
def guide_colorbar(title=None, *, barwidth=None, barheight=None, nbin=None):
"""
Continuous color bar guide.
Parameters
----------
title : str
Title of guide.
barwidth : float
Color bar width in px.
barheight : float
Expand Down Expand Up @@ -98,6 +102,8 @@ def guide_colorbar(barwidth=None, barheight=None, nbin=None):


def _guide(name, **kwargs):
if 'title' in kwargs and isinstance(kwargs['title'], int):
raise ValueError("Use keyword arguments for all other than 'title' parameters.")
return FeatureSpec('guide', name=name, **kwargs)


Expand Down
26 changes: 26 additions & 0 deletions python-package/test/plot/test_guides.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#
# Copyright (c) 2024. JetBrains s.r.o.
# 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.plot.guide import guide_legend, guide_colorbar


def test_two_guides():
spec = (gg.ggplot() + gg.guides(color=guide_legend(nrow=1)) + gg.guides(color=guide_legend(title="Title")))

as_dict = spec.as_dict()['guides']['color']
assert as_dict['nrow'] == 1
assert as_dict['title'] == "Title"


def test_shape_and_color_guides():
spec = (gg.ggplot() + gg.guides(shape=guide_legend(ncol=2, title="Shape title"))
+ gg.guides(color=guide_colorbar(nbin=8, title="Color title")))

as_dict = spec.as_dict()['guides']
assert as_dict['shape']['ncol'] == 2
assert as_dict['shape']['title'] == "Shape title"
assert as_dict['color']['nbin'] == 8
assert as_dict['color']['title'] == "Color title"

0 comments on commit b7bb4bc

Please sign in to comment.