diff --git a/docs/examples/images/tooltips_8.png b/docs/examples/images/tooltips_8.png new file mode 100644 index 00000000000..bf13852aa93 Binary files /dev/null and b/docs/examples/images/tooltips_8.png differ diff --git a/docs/examples/images/tooltips_9.png b/docs/examples/images/tooltips_9.png new file mode 100644 index 00000000000..cb39e47bcea Binary files /dev/null and b/docs/examples/images/tooltips_9.png differ diff --git a/docs/examples/jupyter-notebooks/tooltip_config.ipynb b/docs/examples/jupyter-notebooks/tooltip_config.ipynb index ececdd06e4f..38686e2f741 100644 --- a/docs/examples/jupyter-notebooks/tooltip_config.ipynb +++ b/docs/examples/jupyter-notebooks/tooltip_config.ipynb @@ -39,36 +39,15 @@ "data": { "text/html": [ "\n", - "
\n", " \n", + " \n", " " ] }, @@ -115,7 +94,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 4, @@ -184,7 +163,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 5, @@ -254,7 +233,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 6, @@ -331,7 +310,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 7, @@ -407,7 +386,7 @@ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, "execution_count": 8, @@ -503,7 +482,171 @@ { "data": { "text/html": [ - "
\n", + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# List of variables to place in a multiline tooltip with the default formatting\n", + "p + geom_point(shape=21, \n", + " color='white',\n", + " tooltips=layer_tooltips(['manufacturer', 'model', 'class', 'year'])) " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Define the format for the variable from the list and specify an additional line\n", + "p + geom_point(shape=21, \n", + " color='white',\n", + " tooltips=layer_tooltips(['manufacturer', 'model', 'class', 'drv'])\n", + " .format('drv', '{}wd')\n", + " .line('cty/hwy [mpg]|@cty/@hwy')\n", + " ) " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -605,13 +748,13 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -679,13 +822,13 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 11, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -768,13 +911,13 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 12, + "execution_count": 14, "metadata": {}, "output_type": "execute_result" } @@ -861,19 +1004,19 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 13, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -953,7 +1096,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 16, "metadata": {}, "outputs": [], "source": [ @@ -963,13 +1106,13 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 15, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -1030,13 +1173,13 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 16, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1107,13 +1250,13 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
\n", + "
\n", " " ], "text/plain": [ - "" + "" ] }, - "execution_count": 17, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -1202,9 +1345,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.9" + "version": "3.7.10" } }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/tooltips.md b/docs/tooltips.md new file mode 100644 index 00000000000..1fb8840f5b7 --- /dev/null +++ b/docs/tooltips.md @@ -0,0 +1,46 @@ +# Tooltip Customization + +- [Tooltip variable list](#variables) + - [Examples](#example-variables) + +------ + + +The following functions set lines, define formatting of the tooltip, its location and width: +``` +tooltips=layer_tooltips(variables) + .format(field, format) + .line(template) + .anchor(position) + .min_width(value) +``` + + +### Tooltip variable list: `layer_tooltips(variables = ['varName1', ... , 'varNameN'] )` +The `variables` parameter defines a list of variable names, which values will be placed line by line in the general tooltip. +If formatting is specified for a variable from this list (with the `format` function), it will be applied. +Otherwise, the default formatting is used. +Additional tooltip lines can be specified using the `line` functions. + + + +### Examples + +Set list of variables to place them in a multiline tooltip with the default formatting: + +``` +ggplot(mpg) + geom_point(aes(x='displ', y='cty', fill='drv', size='hwy'), shape=21, color='black', + tooltips=layer_tooltips(['manufacturer', 'model', 'class', 'drv'])) +``` +![](examples/images/tooltips_8.png) + + +Define the format for the variable from the list and specify an additional line: +``` +ggplot(mpg) + geom_point(aes(x='displ', y='cty', fill='drv', size='hwy'), shape=21, color='black', + tooltips=layer_tooltips(['manufacturer', 'model', 'class', 'drv']) + .format('drv', '{}wd') + .line('cty/hwy [mpg]|@cty/@hwy')) +``` + +![](examples/images/tooltips_9.png) diff --git a/future_changes.md b/future_changes.md index aaf060d6128..e50e3567770 100644 --- a/future_changes.md +++ b/future_changes.md @@ -2,6 +2,10 @@ ### Added +- In tooltip customization API: + - `layer_tooltips(variables)` - the new parameter `variables` defines a list of variable names, which values will be placed line by line in the general tooltip. + See: [Tooltip Customization](https://github.com/JetBrains/lets-plot/blob/master/docs/tooltips.md). + ### Changed ### Fixed 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 3e910ee32a0..ca5d2eb2d5a 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 @@ -85,6 +85,7 @@ object Option { const val SHOW_LEGEND = "show_legend" const val TOOLTIPS = "tooltips" const val TOOLTIP_LINES = "tooltip_lines" + const val TOOLTIP_VARIABLES = "tooltip_variables" const val TOOLTIP_FORMATS = "tooltip_formats" const val TOOLTIP_ANCHOR = "tooltip_anchor" const val TOOLTIP_MIN_WIDTH = "tooltip_min_width" diff --git a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/TooltipConfig.kt b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/TooltipConfig.kt index 015056f60fd..28a4bb0328a 100644 --- a/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/TooltipConfig.kt +++ b/plot-config-portable/src/commonMain/kotlin/jetbrains/datalore/plot/config/TooltipConfig.kt @@ -30,13 +30,15 @@ class TooltipConfig( } else { null }, - tooltipFormats = getList(Option.Layer.TOOLTIP_FORMATS) + tooltipFormats = getList(Option.Layer.TOOLTIP_FORMATS), + tooltipVariables = getStringList(Option.Layer.TOOLTIP_VARIABLES) ).parse() } private inner class TooltipConfigParseHelper( private val tooltipLines: List?, - tooltipFormats: List<*> + tooltipFormats: List<*>, + tooltipVariables: List ) { // Key is Pair: + private val myValueSources: MutableMap, ValueSource> = prepareFormats(tooltipFormats) @@ -44,11 +46,22 @@ class TooltipConfig( createValueSource(fieldName = field.first, isAes = field.second, format = format) }.toMutableMap() + // Create tooltip lines from the given variable list + private val myLinesForVariableList = tooltipVariables.map { variableName -> + val valueSource = getValueSource(VARIABLE_NAME_PREFIX + variableName) + TooltipLine.defaultLineForValueSource(valueSource) + } + internal fun parse(): TooltipSpecification { val lines = tooltipLines?.map(::parseLine) + val allTooltipLines = when { + lines != null -> myLinesForVariableList + lines + myLinesForVariableList.isNotEmpty() -> myLinesForVariableList + else -> null + } return TooltipSpecification( myValueSources.map { it.value }, - lines, + allTooltipLines, TooltipSpecification.TooltipProperties( anchor = readAnchor(), minWidth = readMinWidth(), diff --git a/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt b/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt index bd3ac70b3d0..1e30b343d1a 100644 --- a/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt +++ b/plot-config/src/jvmTest/kotlin/plot/config/TooltipConfigTest.kt @@ -14,6 +14,7 @@ import jetbrains.datalore.plot.config.Option.Layer.GEOM import jetbrains.datalore.plot.config.Option.Layer.TOOLTIPS import jetbrains.datalore.plot.config.Option.Layer.TOOLTIP_FORMATS import jetbrains.datalore.plot.config.Option.Layer.TOOLTIP_LINES +import jetbrains.datalore.plot.config.Option.Layer.TOOLTIP_VARIABLES import jetbrains.datalore.plot.config.Option.Meta.KIND import jetbrains.datalore.plot.config.Option.Plot.LAYERS import jetbrains.datalore.plot.config.Option.PlotBase.DATA @@ -44,7 +45,6 @@ class TooltipConfigTest { Aes.SHAPE.name to "class" ) - @Test fun defaultTooltips() { val geomLayer = buildGeomPointLayer(data, mapping, tooltips = null) @@ -517,6 +517,82 @@ class TooltipConfigTest { ) } + @Test + fun `variables list to place in a tooltip with default formatting`() { + val tooltipConfig = mapOf( + TOOLTIP_VARIABLES to listOf( + "model name", + "class", + "displ", + "hwy", + "origin" + ) + ) + val geomLayer = buildGeomPointLayer(data, mapping, tooltips = tooltipConfig) + val expectedLines = listOf( + "model name: dodge", + "class: suv", + "displ: 1.6", + "hwy: 160.0", + "origin: US" + + ) + val lines = getGeneralTooltipStrings(geomLayer) + assertTooltipStrings(expectedLines, lines) + } + + @Test + fun `variables list should use the defined formatting`() { + val tooltipConfig = mapOf( + TOOLTIP_VARIABLES to listOf( + "model name", + "class", + "displ", + "hwy", + "origin" + ), + TOOLTIP_FORMATS to listOf( + mapOf( + FIELD to "hwy", + FORMAT to "{.2f} mpg" + ) + ) + ) + val geomLayer = buildGeomPointLayer(data, mapping, tooltips = tooltipConfig) + val expectedLines = listOf( + "model name: dodge", + "class: suv", + "displ: 1.6", + "hwy: 160.00 mpg", + "origin: US" + ) + val lines = getGeneralTooltipStrings(geomLayer) + assertTooltipStrings(expectedLines, lines) + } + + @Test + fun `tooltip lines should be formed by variables list and line functions`() { + val tooltipConfig = mapOf( + TOOLTIP_VARIABLES to listOf( + "model name", + "class", + "displ" + ), + TOOLTIP_LINES to listOf( + "@|@hwy mpg" + ) + ) + val geomLayer = buildGeomPointLayer(data, mapping, tooltips = tooltipConfig) + val expectedLines = listOf( + "model name: dodge", + "class: suv", + "displ: 1.6", + "hwy: 160.0 mpg" + ) + val lines = getGeneralTooltipStrings(geomLayer) + assertTooltipStrings(expectedLines, lines) + } + companion object { private fun buildGeomPointLayer( diff --git a/python-package/lets_plot/plot/tooltip.py b/python-package/lets_plot/plot/tooltip.py index 839e1043916..e23993d7d8f 100644 --- a/python-package/lets_plot/plot/tooltip.py +++ b/python-package/lets_plot/plot/tooltip.py @@ -3,7 +3,7 @@ # Use of this source code is governed by the MIT license that can be found in the LICENSE file. from .core import FeatureSpec -from typing import List, Dict +from typing import List # # Tooltips @@ -68,13 +68,20 @@ class layer_tooltips(FeatureSpec): """ - def __init__(self): - """Initialize self.""" + def __init__(self, variables: List[str] = None): + """ + Initialize self. + + :param variables: List of strings + Variable names to place in the general tooltip with default formatting. + """ + self._tooltip_formats: List = [] self._tooltip_lines: List = None self._tooltip_anchor = None self._tooltip_min_width = None self._tooltip_color = None + self._tooltip_variables = variables super().__init__('tooltips', name=None) def as_dict(self): @@ -107,6 +114,7 @@ def as_dict(self): d['tooltip_anchor'] = self._tooltip_anchor d['tooltip_min_width'] = self._tooltip_min_width d['tooltip_color'] = self._tooltip_color + d['tooltip_variables'] = self._tooltip_variables return d def format(self, field=None, format=None):