From 226e4211ab0760bd1e5bed29ccdc226ea286b4c4 Mon Sep 17 00:00:00 2001 From: Igor Alshannikov Date: Mon, 9 Sep 2024 18:01:43 -0400 Subject: [PATCH] Fix #820 : Add expand_limits --- docs/f-24g/expand_limits.ipynb | 761 ++++++++++++++++++ future_changes.md | 4 + .../letsPlot/core/plot/builder/VarBinding.kt | 3 + .../letsPlot/core/spec/PlotConfigUtil.kt | 23 +- python-package/lets_plot/plot/__init__.py | 4 +- .../lets_plot/plot/expand_limits_.py | 48 ++ python-package/lets_plot/plot/label.py | 4 +- 7 files changed, 834 insertions(+), 13 deletions(-) create mode 100644 docs/f-24g/expand_limits.ipynb create mode 100644 python-package/lets_plot/plot/expand_limits_.py diff --git a/docs/f-24g/expand_limits.ipynb b/docs/f-24g/expand_limits.ipynb new file mode 100644 index 00000000000..3c285326417 --- /dev/null +++ b/docs/f-24g/expand_limits.ipynb @@ -0,0 +1,761 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9a1e235f-7006-4656-8e0f-f57da158c04d", + "metadata": {}, + "source": [ + "# Expanding Plot Limits with `expand_limits()`\n", + "\n", + "When creating visualizations, you might occasionally need to adjust your plot boundaries to encompass specific data points or values. This is where the `expand_limits()` function comes in handy. It allows you to extend the plot's scales to include particular values, ensuring they're visible in your visualization.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "984c7118-bef0-4362-bee5-b4b500e62d65", + "metadata": {}, + "outputs": [], + "source": [ + "from lets_plot import *\n", + "import pandas as pd\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "aaf39f46-4153-4ad4-98ea-15a5e8c1262c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "LetsPlot.setup_html()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "4678d72d-fc0d-4d51-b6c6-3dead0bd5ad1", + "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", + "
miles per gallonnumber of cylindersengine displacement (cu. inches)engine horsepowervehicle weight (lbs.)time to accelerate (sec.)model yearorigin of carvehicle name
018.08307.0130350412.070USchevrolet chevelle malibu
115.08350.0165369311.570USbuick skylark 320
218.08318.0150343611.070USplymouth satellite
\n", + "
" + ], + "text/plain": [ + " miles per gallon number of cylinders engine displacement (cu. inches) \\\n", + "0 18.0 8 307.0 \n", + "1 15.0 8 350.0 \n", + "2 18.0 8 318.0 \n", + "\n", + " engine horsepower vehicle weight (lbs.) time to accelerate (sec.) \\\n", + "0 130 3504 12.0 \n", + "1 165 3693 11.5 \n", + "2 150 3436 11.0 \n", + "\n", + " model year origin of car vehicle name \n", + "0 70 US chevrolet chevelle malibu \n", + "1 70 US buick skylark 320 \n", + "2 70 US plymouth satellite " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mpg = pd.read_csv('https://raw.githubusercontent.com/JetBrains/lets-plot-docs/master/data/mpg2.csv')\n", + "mpg.head(3)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8891c0dd-1f00-4291-86fc-82c015a0bb33", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p = ggplot(mpg, aes(\"miles per gallon\", \"vehicle weight (lbs.)\")) + geom_point() \n", + "p" + ] + }, + { + "cell_type": "markdown", + "id": "5bc1e328-338c-4423-a1fa-9298c062489b", + "metadata": {}, + "source": [ + "#### Expand Lower Limit Along the x-axis" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "2cf4112a-aa7b-4c58-bafa-cfb520de3667", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p + expand_limits(x=0)" + ] + }, + { + "cell_type": "markdown", + "id": "69516526-82fc-4e4c-bb47-c60ec9a692da", + "metadata": {}, + "source": [ + "#### Expand Limits Along the y-axis " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "0c32bee6-2b08-462f-9600-2340f5e4a3e6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p + expand_limits(y=[1000, 9000])" + ] + }, + { + "cell_type": "markdown", + "id": "d97e894e-eea4-4d9c-a271-e86465acb8e7", + "metadata": {}, + "source": [ + "#### Expand Lower Limits Along Both Axes" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5a3ba04a-5ccc-402c-a03b-91910674291e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p + expand_limits(x = 0, y = 0)" + ] + }, + { + "cell_type": "markdown", + "id": "ff4fff16-66d0-449f-b7cf-78211dc65bd8", + "metadata": {}, + "source": [ + "#### Expanding Color-scale Limits" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6719668e-0753-45a1-8e18-abc912656d0c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + " " + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Add color mapping\n", + "p1 = p + aes(color=\"number of cylinders\")\n", + "\n", + "gggrid([\n", + " p1, \n", + " # Expand the color-scale limits.\n", + " p1 + expand_limits(color=range(2, 11, 2))\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 e26c37e1892..336be43590e 100644 --- a/future_changes.md +++ b/future_changes.md @@ -3,6 +3,10 @@ ### Added - `geom_blank()` [[#831](https://github.com/JetBrains/lets-plot/issues/831)]. +- `expand_limits()` [[#820](https://github.com/JetBrains/lets-plot/issues/820)]. + + See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24g/expand_limits.ipynb). + - `base` parameter in `waterfall_plot()` [[#1159](https://github.com/JetBrains/lets-plot/issues/1159)]: See [example notebook](https://nbviewer.org/github/JetBrains/lets-plot/blob/master/docs/f-24g/waterfall_plot_base.ipynb). diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/VarBinding.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/VarBinding.kt index 3967c530078..9a1a4bbc5de 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/VarBinding.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/VarBinding.kt @@ -25,6 +25,9 @@ class VarBinding( // Generally, we can't use VarBinding as a key in a hashmap. // As result, PlotConfigUtil.associateVarBindingsWithData() fails when building Map // because several (..count.. -> color) bindings become 1 entry in the map. + // + // See commit: https://github.com/JetBrains/lets-plot/commit/e492dac808ec4fc12be13337f782f358044f19ec#diff-c6bd11010baad2b31ea05c0a3482f76ebb8f6bac1e3d8303cb6ed3ad28cb04adR16 + // if (variable != other.variable) return false if (aes != other.aes) return false diff --git a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/PlotConfigUtil.kt b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/PlotConfigUtil.kt index 55c3f9c3187..0b1ddbdd586 100644 --- a/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/PlotConfigUtil.kt +++ b/plot-stem/src/commonMain/kotlin/org/jetbrains/letsPlot/core/spec/PlotConfigUtil.kt @@ -149,17 +149,20 @@ internal object PlotConfigUtil { } internal fun defaultScaleName(aes: Aes<*>, variablesByMappedAes: Map, List>): String { - return if (variablesByMappedAes.containsKey(aes)) { - val variables = variablesByMappedAes.getValue(aes) - val labels = variables.map(DataFrame.Variable::label).distinct() - if (labels.size > 1 && (aes == Aes.X || aes == Aes.Y)) { - // Don't show multiple labels on X,Y axis. - aes.name - } else { - labels.joinToString() - } - } else { + val variables = variablesByMappedAes[aes] ?: emptyList() + val labels = variables.map(DataFrame.Variable::label) + .distinct() + // Give preference to more descriptive labels. + // This also prevents overriding axis/legend title by the `expand_limits()` function. + .filter { it != aes.name } + + return if (labels.isEmpty()) { aes.name + } else if (labels.size > 1 && (aes == Aes.X || aes == Aes.Y)) { + // Don't show multiple labels on X,Y axis. + aes.name + } else { + labels.joinToString() } } diff --git a/python-package/lets_plot/plot/__init__.py b/python-package/lets_plot/plot/__init__.py index 934d6ac2869..d75fe2786a5 100644 --- a/python-package/lets_plot/plot/__init__.py +++ b/python-package/lets_plot/plot/__init__.py @@ -5,6 +5,7 @@ from .annotation import * from .coord import * from .core import * +from .expand_limits_ import * from .facet import * from .font_features import * from .geom import * @@ -56,5 +57,6 @@ marginal_layer.__all__ + font_features.__all__ + gggrid_.__all__ + - ggtb_.__all__ + ggtb_.__all__ + + expand_limits_.__all__ ) diff --git a/python-package/lets_plot/plot/expand_limits_.py b/python-package/lets_plot/plot/expand_limits_.py new file mode 100644 index 00000000000..ae7a214eb5f --- /dev/null +++ b/python-package/lets_plot/plot/expand_limits_.py @@ -0,0 +1,48 @@ +# 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. +from .core import aes +from .geom import geom_blank + +__all__ = ['expand_limits'] + +def expand_limits(*, x=None, y=None, size=None, color=None, fill=None, alpha=None, shape=None): + """ + Expand the plot limits to include additional data values. + + This function extends the plot boundaries to encompass new data points, + whether a single value or multiple values are provided. It acts as a + thin wrapper around geom_blank(). + + Parameters + ---------- + x, y, size, color, fill, alpha, shape : Any, list, tuple or range + List of name-value pairs specifying the value (or values) that should be included in each scale. + These parameters extend the corresponding plot dimensions or aesthetic scales. + + Returns + ------- + FeatureSpec + A result of the `geom_blank()` call. + + Examples + -------- + + """ + params = locals() + + def standardize(value): + if value is None: + return [None] + elif isinstance(value, (list, tuple, range)): + return list(value) + else: + return [value] + + standardized = {k: standardize(v) for k, v in params.items()} + + max_length = max(len(v) for v in standardized.values()) + raw_data = {k: v + [None] * (max_length - len(v)) for k, v in standardized.items()} + + # remove all undefined but keep x and y even if undefined. + filtered_data = {k: v for k, v in raw_data.items() if k in ['x', 'y'] or not all(e is None for e in v)} + return geom_blank(mapping=aes(**filtered_data)) diff --git a/python-package/lets_plot/plot/label.py b/python-package/lets_plot/plot/label.py index 00dc9d0f965..48af3418fa9 100644 --- a/python-package/lets_plot/plot/label.py +++ b/python-package/lets_plot/plot/label.py @@ -82,12 +82,12 @@ def xlab(label): def ylab(label): """ - Add label to the y axis. + Add label to the y-axis. Parameters ---------- label : str - The text for the y axis label. + The text for the y-axis label. Returns -------