{ "cells": [ { "cell_type": "markdown", "id": "c6ba76d5-7d0e-44b6-9524-f55b2bd4aa30", "metadata": {}, "source": [ "# Collecting Guides in `gggrid()`\n", "\n", "The `guides` parameter controls how legends and colorbars are handled when arranging plots in a grid.\n", "\n", "**`guides='auto'`** (default) \n", "Keep guides in subplots by default. However, if this grid is nested inside another grid that uses `guides='collect'`, pass the guides up for collection at that higher level.\n", "\n", "**`guides='collect'`** \n", "Collect all guides (legends and colorbars) from subplots and place them alongside the grid figure, automatically removing duplicates.\n", "\n", "**`guides='keep'`** \n", "Keep guides in their original subplots. No collection occurs at this level, even if an outer grid requests collection.\n", "\n", "#### Duplicate Detection\n", "\n", "Guides are compared by their visual appearance:\n", "\n", "**For legends:** \n", "Two legends are considered duplicates if they have identical \n", "- title\n", "- labels\n", "- all aesthetic values (colors, shapes, sizes, line types, etc.)\n", "\n", "**For colorbars:** \n", "Two colorbars are considered duplicates if they have identical \n", "- title\n", "- domain limits\n", "- breaks (tick positions)\n", "- color gradient\n", "\n", "Note: Colorbars from different data ranges typically have different limits and will not merge without manual harmonization." ] }, { "cell_type": "code", "execution_count": 1, "id": "945e991b-fb1f-4085-ae55-bad8302ad79a", "metadata": {}, "outputs": [], "source": [ "from lets_plot import *\n", "import pandas as pd" ] }, { "cell_type": "code", "execution_count": 2, "id": "157872ee-59fa-4f4d-b8a9-61cbae831485", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", "
\n", " \n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "LetsPlot.setup_html()" ] }, { "cell_type": "code", "execution_count": 3, "id": "a7b44990-4707-4880-b1f6-6b24c8ca8b94", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "(53940, 10)\n", "(1000, 10)\n" ] }, { "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", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
caratcutcolorclaritydepthtablepricexyz
00.24IdealGVVS162.156.05593.974.002.47
10.58Very GoodFVVS260.057.022015.445.423.26
20.40IdealEVVS262.155.012384.764.742.95
30.43PremiumEVVS260.857.013044.924.892.98
41.55IdealESI262.355.069017.447.374.61
\n", "
" ], "text/plain": [ " carat cut color clarity depth table price x y z\n", "0 0.24 Ideal G VVS1 62.1 56.0 559 3.97 4.00 2.47\n", "1 0.58 Very Good F VVS2 60.0 57.0 2201 5.44 5.42 3.26\n", "2 0.40 Ideal E VVS2 62.1 55.0 1238 4.76 4.74 2.95\n", "3 0.43 Premium E VVS2 60.8 57.0 1304 4.92 4.89 2.98\n", "4 1.55 Ideal E SI2 62.3 55.0 6901 7.44 7.37 4.61" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "diamonds = pd.read_csv(\"https://raw.githubusercontent.com/JetBrains/lets-plot-docs/refs/heads/master/data/diamonds.csv\")\n", "print(diamonds.shape)\n", "diamonds = diamonds.sample(1_000, random_state=42).reset_index(drop=True)\n", "print(diamonds.shape)\n", "diamonds.head()" ] }, { "cell_type": "markdown", "id": "c1226de6-3bb7-44a2-b1ed-d9612a452140", "metadata": {}, "source": [ "#### 1. Collecting Legends" ] }, { "cell_type": "code", "execution_count": 4, "id": "e6615cd0-42d8-41ca-94fc-257871f4dd9b", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\n", "# Create three plots that share the same color aesthetic ('clarity')\n", "clarity_p = ggplot(diamonds, aes(y='price', color='clarity')) + scale_color_hue() + theme_classic()\n", "\n", "clarity_p0 = clarity_p + geom_point(aes(x='carat'))\n", "clarity_p1 = clarity_p + geom_point(aes(x='depth'))\n", "clarity_p2 = clarity_p + geom_point(aes(x='color'))\n", "\n", "# By default, each plot keeps its own legend.\n", "gggrid([\n", " clarity_p0, \n", " clarity_p1,\n", " clarity_p2\n", "])" ] }, { "cell_type": "code", "execution_count": 5, "id": "0f4a82bd-4588-4901-b6f1-66f1d54ad964", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Now collect the legends into a single shared legend.\n", "\n", "# Note: \n", "# All 3 legends are visually identical (same title, labels, and aesthetic values), \n", "# so duplicates are removed and only one legend is shown.\n", "\n", "gggrid([\n", " clarity_p0, \n", " clarity_p1 + theme(axis_title_y='blank'),\n", " clarity_p2 + theme(axis_title_y='blank')\n", " ],\n", " \n", " guides='collect' # <-- collect legends from subplots\n", " \n", ") + theme(legend_position='bottom') # <-- also adjust the legend position " ] }, { "cell_type": "markdown", "id": "116aa3dd-00e7-4544-b0fa-67ea962a52f4", "metadata": {}, "source": [ "#### 2. Collecting Colorbars" ] }, { "cell_type": "code", "execution_count": 6, "id": "0f080ae8-0494-449b-aed4-0f3866badb4f", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ideal_diamonds = diamonds[diamonds['cut'] == 'Ideal']\n", "fair_diamonds = diamonds[diamonds['cut'] == 'Fair']\n", "\n", "price_p = (ggplot(mapping=aes('carat', 'depth', color='price')) \n", " + scale_color_viridis() \n", " + theme_grey()\n", " + theme(plot_title=element_text(hjust=0.5))\n", " )\n", "price_p0 = price_p + geom_point(data=ideal_diamonds, size=6) + ggtitle('Ideal Cut')\n", "price_p1 = price_p + geom_point(data=fair_diamonds, size=6) + ggtitle('Fair Cut')\n", "\n", "# Arrange two plots in a grid with guides='collect'.\n", "\n", "# Note: \n", "# The colorbars have different domain limits and breaks (due to different data ranges),\n", "# so both are retained as separate colorbars.\n", "\n", "gggrid([\n", " price_p0, \n", " price_p1, \n", "], \n", " sharex=True, sharey=True,\n", " guides='collect'\n", ")" ] }, { "cell_type": "code", "execution_count": 7, "id": "f8f818b6-b41f-48e9-8b51-7dfbf6708333", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Apply the same color scale limits to both subplots.\n", "# Now both colorbars are visually identical, so the duplicate is removed.\n", "\n", "price_min = min(ideal_diamonds['price'].min(), fair_diamonds['price'].min())\n", "price_max = max(ideal_diamonds['price'].max(), fair_diamonds['price'].max())\n", "\n", "price_lims = scale_color_viridis(limits=[price_min, price_max])\n", "gggrid([\n", " price_p0 + price_lims, \n", " price_p1 + price_lims, \n", "], \n", " sharex=True, sharey=True,\n", " guides='collect'\n", ")" ] }, { "cell_type": "markdown", "id": "57a9846e-fc89-4d88-ae92-49c7c900ddcc", "metadata": {}, "source": [ "#### 3. Collecting Guides in Nested Grids" ] }, { "cell_type": "code", "execution_count": 8, "id": "23519eab-1527-4fb7-a2e2-1a4e6e81c40c", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create nested grids and collect all guides at the top level.\n", "\n", "clarity_grid = gggrid([\n", " clarity_p0, \n", " clarity_p1, \n", " clarity_p2\n", "])\n", "price_grid = gggrid([\n", " price_p0 + price_lims, \n", " price_p1 + price_lims, \n", "])\n", "\n", "\n", "gggrid([\n", " clarity_grid, \n", " price_grid\n", "],\n", " ncol=1,\n", " guides='collect' # <-- collects from all nested grids\n", " \n", ") + theme(legend_position='bottom') + ggsize(800, 600)\n" ] }, { "cell_type": "code", "execution_count": 9, "id": "993e08c8-330b-4c10-8bda-54fb866ebe85", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# A nested grid can override this behavior by collecting guides at its own level,\n", "# keeping them separate from guides collected by the upper level.\n", "\n", "clarity_grid_1 = gggrid([\n", " clarity_p0, \n", " clarity_p1, \n", " clarity_p2\n", " ], \n", " \n", " guides='collect' # <-- collect 'clarity' legends at this level\n", ")\n", "\n", "gggrid([\n", " clarity_grid_1, \n", " price_grid\n", "],\n", " ncol=1,\n", " guides='collect'\n", " \n", ") + theme(legend_position='bottom') + ggsize(800, 600)\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.9.23" } }, "nbformat": 4, "nbformat_minor": 5 }