{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Tooltip Layer Interaction\n", "\n", "This dev notebook is intended for manual tooltip testing.\n", "\n", "It covers three areas:\n", "- univariate layers, where tooltip visibility is driven by a single axis position;\n", "- bivariate layers, where tooltip visibility depends on both coordinates;\n", "- stacked bar cases, where a single visual column contains multiple tooltip-enabled objects.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "from lets_plot import *\n", "\n", "LetsPlot.setup_html()\n", "np.random.seed(42)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Univariate Layers\n", "\n", "For these plots, tooltip visibility is expected to be determined by one axis only.\n", "This is the main behavior to observe for `geom_line`, `geom_density`, `geom_bar`, `geom_histogram`, and similar layers.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 1. `geom_histogram` + `geom_density`\n", "\n", "What to test:\n", "- Hover at the same `x` position over both layers.\n", "- Check whether the histogram and density tooltips are chosen consistently when the layers overlap.\n", "- Check whether switching between layers depends only on horizontal movement.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "u_df = pd.DataFrame({\n", " 'x': np.concatenate([\n", " np.random.normal(loc=-0.8, scale=0.45, size=250),\n", " np.random.normal(loc=1.2, scale=0.65, size=250)\n", " ]),\n", " 'grp': ['A'] * 250 + ['B'] * 250\n", "})\n", "\n", "(ggplot(u_df, aes(x='x'))\n", " + ggtitle('Univariate: histogram + density')\n", " + geom_histogram(\n", " aes(fill='grp'),\n", " bins=18,\n", " alpha=.45,\n", " position='identity',\n", " tooltips=layer_tooltips().line('histogram @grp')\n", " )\n", " + geom_density(\n", " aes(color='grp'),\n", " size=1.2,\n", " tooltips=layer_tooltips().line('density @grp')\n", " ))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Two independent `geom_line` layers\n", "\n", "What to test:\n", "- Hover at the same `x` coordinate where both lines are present.\n", "- Check how the two univariate layers compete when they are close vertically but not equal in `y`.\n", "- Check whether tooltip selection still follows the univariate rule rather than nearest-point-in-2D behavior.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "line_x = np.arange(0, 10, 0.5)\n", "line_a = pd.DataFrame({\n", " 'x': line_x,\n", " 'y': 2.0 + np.sin(line_x),\n", " 'series': 'A'\n", "})\n", "line_b = pd.DataFrame({\n", " 'x': line_x,\n", " 'y': 2.35 + np.cos(line_x * 0.9),\n", " 'series': 'B'\n", "})\n", "\n", "(ggplot()\n", " + ggtitle('Univariate: two lines')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_a,\n", " color='#1f78b4',\n", " size=1.3,\n", " tooltips=layer_tooltips().line('line A')\n", " )\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_b,\n", " color='#e31a1c',\n", " size=1.3,\n", " tooltips=layer_tooltips().line('line B')\n", " ))\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "(ggplot()\n", " + ggtitle('Univariate: two independent lines')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_a,\n", " color='#1f78b4',\n", " size=1.3,\n", " tooltips=layer_tooltips().group('a').line('line A')\n", " )\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_b,\n", " color='#e31a1c',\n", " size=1.3,\n", " tooltips=layer_tooltips().group('b').line('line B')\n", " ))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. `geom_line` + `geom_point`\n", "\n", "What to test:\n", "- Hover on a point that lies directly on top of the line.\n", "- Check whether the point tooltip overrides the line tooltip when both layers are hit.\n", "- Move slightly left or right and confirm that the line tooltip remains available by `x` position.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "base_df = pd.DataFrame({\n", " 'x': np.arange(0, 9),\n", " 'y': [1.0, 1.4, 1.3, 2.0, 2.4, 2.1, 2.8, 2.6, 3.0]\n", "})\n", "hl_df = base_df.iloc[[2, 4, 6]].copy()\n", "hl_df['kind'] = ['p1', 'p2', 'p3']\n", "\n", "(ggplot(base_df, aes(x='x', y='y'))\n", " + ggtitle('Univariate: line + point')\n", " + geom_line(\n", " color='#33a02c',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('line')\n", " )\n", " + geom_point(\n", " data=hl_df,\n", " size=6,\n", " color='#ff7f00',\n", " tooltips=layer_tooltips().line('point @kind')\n", " ))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Bivariate Layers\n", "\n", "For these plots, tooltip visibility is expected to depend on both `x` and `y`.\n", "This is the behavior to observe for `geom_point`, `geom_polygon`, and similar layers.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. Two independent `geom_point` layers\n", "\n", "What to test:\n", "- Hover in dense regions where points from different layers are close to each other.\n", "- Check that tooltip selection follows actual 2D proximity rather than only matching the same `x`.\n", "- Check whether nearby points from different layers switch correctly as the cursor moves diagonally.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "point_a = pd.DataFrame({\n", " 'x': [1.0, 1.8, 2.8, 3.3, 4.0],\n", " 'y': [1.0, 2.2, 1.4, 2.8, 1.8],\n", " 'layer': ['A1', 'A2', 'A3', 'A4', 'A5']\n", "})\n", "point_b = pd.DataFrame({\n", " 'x': [1.3, 2.0, 2.9, 3.6, 4.1],\n", " 'y': [1.4, 2.0, 1.8, 2.5, 1.5],\n", " 'layer': ['B1', 'B2', 'B3', 'B4', 'B5']\n", "})\n", "\n", "(ggplot()\n", " + ggtitle('Bivariate: two independent point layers')\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_a,\n", " color='#6a3d9a',\n", " size=6,\n", " alpha=.8,\n", " tooltips=layer_tooltips().line('point A @layer')\n", " )\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_b,\n", " color='#b15928',\n", " size=6,\n", " alpha=.8,\n", " tooltips=layer_tooltips().line('point B @layer')\n", " ))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 5. `geom_polygon` + `geom_point`\n", "\n", "What to test:\n", "- Hover inside polygon areas away from vertices and points.\n", "- Hover on points placed inside the polygon and near its border.\n", "- Check how polygon and point tooltips interact when the cursor is inside the polygon but also close to a point.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "poly_df = pd.DataFrame({\n", " 'x': [0.6, 4.4, 4.0, 1.0],\n", " 'y': [0.7, 1.0, 4.0, 3.6],\n", " 'id': ['region'] * 4,\n", " 'name': ['region'] * 4\n", "})\n", "poly_points = pd.DataFrame({\n", " 'x': [1.2, 2.6, 3.5, 4.1],\n", " 'y': [1.4, 2.6, 1.7, 3.5],\n", " 'name': ['inner-1', 'inner-2', 'inner-3', 'edge-1']\n", "})\n", "\n", "(ggplot()\n", " + ggtitle('Bivariate: polygon + points')\n", " + geom_polygon(\n", " aes(x='x', y='y', group='id', fill='name'),\n", " data=poly_df,\n", " color='black',\n", " alpha=.35,\n", " tooltips=layer_tooltips().line('polygon @name')\n", " )\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=poly_points,\n", " color='#d95f02',\n", " size=6,\n", " tooltips=layer_tooltips().line('point @name')\n", " )\n", " + xlim(0, 5)\n", " + ylim(0, 4.5))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Plot Objects Handled Together\n", "\n", "This case is meant to check how multiple tooltip-enabled objects behave when they are rendered as one stacked column.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 6. `geom_bar(position='stack')` with multiple objects in one column\n", "\n", "What to test:\n", "- Hover the same `x` column at different `y` positions.\n", "- Check whether stacked objects remain individually addressable when several objects belong to the same visual column.\n", "- Pay special attention to duplicated `fill` values inside one column: visually they merge, but tooltip selection should still expose the underlying object.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stack_df = pd.DataFrame({\n", " 'x': ['A', 'A', 'A', 'A', 'B', 'B', 'B', 'B'],\n", " 'segment': ['low', 'low', 'high', 'high', 'low', 'low', 'high', 'high'],\n", " 'obj': ['A-low-1', 'A-low-2', 'A-high-1', 'A-high-2', 'B-low-1', 'B-low-2', 'B-high-1', 'B-high-2'],\n", " 'value': [1.0, 1.8, 1.2, 0.9, 1.4, 0.7, 1.6, 1.1]\n", "})\n", "\n", "(ggplot(stack_df, aes(x='x', y='value', fill='segment'))\n", " + ggtitle(\"Stacked bar: multiple objects in one column\")\n", " + geom_bar(\n", " stat='identity',\n", " position='stack',\n", " color='white',\n", " size=0.5,\n", " tooltips=layer_tooltips().line('@obj')\n", " ))\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Additional bar edge cases: negative and zero values\n", "\n", "What to test:\n", "- Hover negative bars below the baseline and check that they remain addressable across their full rendered height.\n", "- Hover the zero-height bar at the baseline and check whether it still exposes a tooltip object.\n", "- Compare vertical and horizontal orientations to confirm the same edge cases behave consistently after axis swap.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bar_edge_df = pd.DataFrame({\n", " 'cat': ['neg', 'zero', 'pos', 'neg2'],\n", " 'value': [-2.5, 0.0, 3.2, -1.4]\n", "})\n", "\n", "bar_edge_v = (\n", " ggplot(bar_edge_df)\n", " + ggtitle('Bars: negative and zero values, vertical')\n", " + geom_bar(\n", " aes(x='cat', y='value', fill='cat'),\n", " stat='identity',\n", " orientation='x',\n", " tooltips=layer_tooltips().line('bar @cat')\n", " )\n", ")\n", "\n", "bar_edge_h = (\n", " ggplot(bar_edge_df)\n", " + ggtitle('Bars: negative and zero values, horizontal')\n", " + geom_bar(\n", " aes(y='cat', x='value', fill='cat'),\n", " stat='identity',\n", " orientation='y',\n", " tooltips=layer_tooltips().line('bar @cat')\n", " )\n", ")\n", "\n", "gggrid([bar_edge_v, bar_edge_h])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Orientation-Switching Interval Geoms\n", "\n", "These are geoms that can be rendered in a vertical form using `x` + `ymin` / `ymax`\n", "or in a horizontal form using `y` + `xmin` / `xmax`.\n", "\n", "Tooltip testing here should confirm that hit detection follows the rendered interval and the active axis,\n", "not a hard-coded vertical-only or horizontal-only rule.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 7. `geom_errorbar`: vertical vs horizontal\n", "\n", "What to test:\n", "- Compare the same interval data rendered in vertical and horizontal orientation.\n", "- Check whether tooltip visibility follows the full rendered error bar, including whiskers.\n", "- Verify that switching orientation also switches the axis that primarily controls visibility.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "interval_df = pd.DataFrame({\n", " 'cat': ['A', 'B', 'C', 'D'],\n", " 'mid': [2.2, 3.4, 2.8, 4.0],\n", " 'low': [1.4, 2.7, 1.9, 3.1],\n", " 'high': [3.0, 4.2, 3.7, 4.9]\n", "})\n", "\n", "errorbar_v = (\n", " ggplot(interval_df)\n", " + ggtitle('geom_errorbar: vertical')\n", " + geom_errorbar(\n", " aes(x='cat', ymin='low', ymax='high'),\n", " width=.3,\n", " size=1.4,\n", " color='#1f78b4',\n", " tooltips=layer_tooltips().line('errorbar @cat')\n", " )\n", ")\n", "\n", "errorbar_h = (\n", " ggplot(interval_df)\n", " + ggtitle('geom_errorbar: horizontal')\n", " + geom_errorbar(\n", " aes(y='cat', xmin='low', xmax='high'),\n", " width=.3,\n", " size=1.4,\n", " color='#e31a1c',\n", " tooltips=layer_tooltips().line('errorbar @cat')\n", " )\n", ")\n", "\n", "gggrid([errorbar_v, errorbar_h])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 8. `geom_crossbar`, `geom_pointrange`, `geom_linerange`\n", "\n", "What to test:\n", "- Check geoms with and without a center value in both orientations.\n", "- For `geom_crossbar` and `geom_pointrange`, verify that the central mark is connected to the same tooltip object as the interval around it.\n", "- For `geom_linerange`, check whether hover behavior is consistent when only the span itself is rendered.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "center_df = pd.DataFrame({\n", " 'cat': ['A', 'B', 'C'],\n", " 'mid': [2.0, 3.3, 2.6],\n", " 'low': [1.2, 2.5, 1.7],\n", " 'high': [2.9, 4.1, 3.5]\n", "})\n", "\n", "crossbar_v = (\n", " ggplot(center_df)\n", " + ggtitle('crossbar: vertical')\n", " + geom_crossbar(\n", " aes(x='cat', y='mid', ymin='low', ymax='high', fill='cat'),\n", " color='black',\n", " alpha=.45,\n", " tooltips=layer_tooltips().line('crossbar @cat')\n", " )\n", ")\n", "\n", "crossbar_h = (\n", " ggplot(center_df)\n", " + ggtitle('crossbar: horizontal')\n", " + geom_crossbar(\n", " aes(y='cat', x='mid', xmin='low', xmax='high', fill='cat'),\n", " color='black',\n", " alpha=.45,\n", " tooltips=layer_tooltips().line('crossbar @cat')\n", " )\n", ")\n", "\n", "pointrange_v = (\n", " ggplot(center_df)\n", " + ggtitle('pointrange: vertical')\n", " + geom_pointrange(\n", " aes(x='cat', y='mid', ymin='low', ymax='high'),\n", " color='#33a02c',\n", " size=1.1,\n", " fatten=8,\n", " tooltips=layer_tooltips().line('pointrange @cat')\n", " )\n", ")\n", "\n", "pointrange_h = (\n", " ggplot(center_df)\n", " + ggtitle('pointrange: horizontal')\n", " + geom_pointrange(\n", " aes(y='cat', x='mid', xmin='low', xmax='high'),\n", " color='#ff7f00',\n", " size=1.1,\n", " fatten=8,\n", " tooltips=layer_tooltips().line('pointrange @cat')\n", " )\n", ")\n", "\n", "linerange_v = (\n", " ggplot(center_df)\n", " + ggtitle('linerange: vertical')\n", " + geom_linerange(\n", " aes(x='cat', ymin='low', ymax='high', color='cat'),\n", " size=3,\n", " tooltips=layer_tooltips().line('linerange @cat')\n", " )\n", ")\n", "\n", "linerange_h = (\n", " ggplot(center_df)\n", " + ggtitle('linerange: horizontal')\n", " + geom_linerange(\n", " aes(y='cat', xmin='low', xmax='high', color='cat'),\n", " size=3,\n", " tooltips=layer_tooltips().line('linerange @cat')\n", " )\n", ")\n", "\n", "gggrid([\n", " crossbar_v, crossbar_h,\n", " pointrange_v, pointrange_h,\n", " linerange_v, linerange_h\n", "], ncol=2)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 9. `geom_ribbon`: horizontal vs vertical ribbon\n", "\n", "What to test:\n", "- Check whether a ribbon behaves as univariate along its driving axis after orientation changes.\n", "- Hover near both interval boundaries and in the filled interior.\n", "- Verify that tooltip values are consistent for lower and upper bounds in both orientations.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "series = np.arange(1, 7)\n", "ribbon_df = pd.DataFrame({\n", " 'step': series,\n", " 'low': [1.0, 1.4, 1.8, 1.6, 2.0, 2.3],\n", " 'high': [2.2, 2.7, 3.1, 2.9, 3.3, 3.7]\n", "})\n", "\n", "ribbon_v = (\n", " ggplot(ribbon_df)\n", " + ggtitle('geom_ribbon: vertical')\n", " + geom_ribbon(\n", " aes(x='step', ymin='low', ymax='high'),\n", " fill='#a6cee3',\n", " color='#1f78b4',\n", " alpha=.6,\n", " tooltips=layer_tooltips().line('ribbon @step')\n", " )\n", ")\n", "\n", "ribbon_h = (\n", " ggplot(ribbon_df)\n", " + ggtitle('geom_ribbon: horizontal')\n", " + geom_ribbon(\n", " aes(y='step', xmin='low', xmax='high'),\n", " fill='#fdbf6f',\n", " color='#ff7f00',\n", " alpha=.6,\n", " tooltips=layer_tooltips().line('ribbon @step')\n", " )\n", ")\n", "\n", "gggrid([ribbon_v, ribbon_h])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Geoms With Explicit `orientation`\n", "\n", "These geoms expose an `orientation` parameter in the API.\n", "This section is meant to check that tooltip hit testing stays correct when orientation is forced explicitly instead of relying on automatic detection.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 10. `geom_bar` with explicit `orientation`\n", "\n", "What to test:\n", "- Compare vertical bars with `orientation='x'` and horizontal bars with `orientation='y'`.\n", "- Check whether tooltip visibility follows the axis implied by the forced orientation.\n", "- Check whether forcing orientation changes only rendering direction, not tooltip content or object identity.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "bar_df = pd.DataFrame({\n", " 'cat': ['A', 'B', 'C', 'D'],\n", " 'value': [3.0, 5.0, 2.5, 4.0]\n", "})\n", "\n", "bar_v = (\n", " ggplot(bar_df)\n", " + ggtitle(\"geom_bar: orientation='x'\")\n", " + geom_bar(\n", " aes(x='cat', y='value', fill='cat'),\n", " stat='identity',\n", " orientation='x',\n", " tooltips=layer_tooltips().line('bar @cat')\n", " )\n", ")\n", "\n", "bar_h = (\n", " ggplot(bar_df)\n", " + ggtitle(\"geom_bar: orientation='y'\")\n", " + geom_bar(\n", " aes(y='cat', x='value', fill='cat'),\n", " stat='identity',\n", " orientation='y',\n", " tooltips=layer_tooltips().line('bar @cat')\n", " )\n", ")\n", "\n", "gggrid([bar_v, bar_h])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 11. `geom_density` with explicit `orientation`\n", "\n", "What to test:\n", "- Compare density rendered along `x` versus along `y` with the same source values.\n", "- Check whether tooltip visibility follows the driving axis selected by explicit orientation.\n", "- Check whether filled interior and boundary line produce consistent tooltip behavior after orientation changes.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "values = np.concatenate([\n", " np.random.normal(loc=-0.7, scale=0.45, size=250),\n", " np.random.normal(loc=1.1, scale=0.6, size=250)\n", "])\n", "density_df = pd.DataFrame({'value': values})\n", "\n", "density_x = (\n", " ggplot(density_df)\n", " + ggtitle(\"geom_density: orientation='x'\")\n", " + geom_density(\n", " aes(x='value'),\n", " orientation='x',\n", " fill='#a6cee3',\n", " color='#1f78b4',\n", " alpha=.55,\n", " tooltips=layer_tooltips().line('density')\n", " )\n", ")\n", "\n", "density_y = (\n", " ggplot(density_df)\n", " + ggtitle(\"geom_density: orientation='y'\")\n", " + geom_density(\n", " aes(y='value'),\n", " orientation='y',\n", " fill='#fdbf6f',\n", " color='#ff7f00',\n", " alpha=.55,\n", " tooltips=layer_tooltips().line('density')\n", " )\n", ")\n", "\n", "gggrid([density_x, density_y])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 12. `geom_boxplot` with explicit `orientation`\n", "\n", "What to test:\n", "- Compare vertical and horizontal boxplots produced with explicit orientation.\n", "- Check tooltip behavior on whiskers, box body, and median line after forcing orientation.\n", "- Check whether hover selection stays stable near narrow whisker ends where orientation matters most.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "box_a = np.random.normal(loc=1.5, scale=0.35, size=120)\n", "box_b = np.random.normal(loc=2.4, scale=0.5, size=120)\n", "box_c = np.random.normal(loc=3.1, scale=0.4, size=120)\n", "box_df = pd.DataFrame({\n", " 'group': ['A'] * 120 + ['B'] * 120 + ['C'] * 120,\n", " 'value': np.concatenate([box_a, box_b, box_c])\n", "})\n", "\n", "boxplot_v = (\n", " ggplot(box_df)\n", " + ggtitle(\"geom_boxplot: orientation='x'\")\n", " + geom_boxplot(\n", " aes(x='group', y='value', fill='group'),\n", " orientation='x',\n", " tooltips=layer_tooltips().line('boxplot @group')\n", " )\n", ")\n", "\n", "boxplot_h = (\n", " ggplot(box_df)\n", " + ggtitle(\"geom_boxplot: orientation='y'\")\n", " + geom_boxplot(\n", " aes(y='group', x='value', fill='group'),\n", " orientation='y',\n", " tooltips=layer_tooltips().line('boxplot @group')\n", " )\n", ")\n", "\n", "gggrid([boxplot_v, boxplot_h])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 13. `geom_lollipop` with explicit `orientation`\n", "\n", "What to test:\n", "- Check both the stem and the point head in vertical and horizontal orientation.\n", "- Verify that the tooltip object stays unified across the full lollipop shape after forcing orientation.\n", "- Pay attention to hover near the intercept side versus the point side, where hit logic may differ.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "lollipop_df = pd.DataFrame({\n", " 'cat': ['A', 'B', 'C', 'D'],\n", " 'value': [1.6, 2.8, 2.1, 3.4]\n", "})\n", "\n", "lollipop_x = (\n", " ggplot(lollipop_df)\n", " + ggtitle(\"geom_lollipop: orientation='x'\")\n", " + geom_lollipop(\n", " aes(x='cat', y='value', color='cat'),\n", " orientation='x',\n", " size=1.2,\n", " tooltips=layer_tooltips().line('lollipop @cat')\n", " )\n", ")\n", "\n", "lollipop_y = (\n", " ggplot(lollipop_df)\n", " + ggtitle(\"geom_lollipop: orientation='y'\")\n", " + geom_lollipop(\n", " aes(y='cat', x='value', color='cat'),\n", " orientation='y',\n", " size=1.2,\n", " tooltips=layer_tooltips().line('lollipop @cat')\n", " )\n", ")\n", "\n", "gggrid([lollipop_x, lollipop_y])\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Common Real-Life Layer Combinations\n", "\n", "These are combinations that show up frequently in real plots.\n", "They should be tested without anchor first, then with anchored tooltips to exercise crosshair on the same patterns.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### `geom_smooth` without anchor: alone, with univariate, with bivariate, with both\n", "\n", "What to test:\n", "- Check `geom_smooth` alone as a baseline.\n", "- Compare `geom_smooth` with a univariate layer, with a bivariate layer, and with both layer types together.\n", "- Verify which layer wins selection when smooth competes with line and/or point layers at nearby positions.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "smooth_mix_df = pd.DataFrame({\n", " 'x': np.linspace(0, 8, 80)\n", "})\n", "smooth_mix_df['y'] = 1.3 + np.sin(smooth_mix_df['x']) + 0.18 * np.cos(smooth_mix_df['x'] * 2.6)\n", "\n", "line_df = pd.DataFrame({\n", " 'x': np.arange(0, 9),\n", " 'y': [0.9, 1.6, 1.2, 2.0, 1.7, 2.3, 2.0, 2.6, 2.2]\n", "})\n", "\n", "point_df = pd.DataFrame({\n", " 'x': [0.8, 2.0, 3.6, 5.2, 6.8],\n", " 'y': [1.9, 1.2, 2.4, 1.8, 2.7],\n", " 'id': ['p1', 'p2', 'p3', 'p4', 'p5']\n", "})\n", "\n", "smooth_only = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('smooth alone')\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth')\n", " )\n", ")\n", "\n", "smooth_univariate = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('smooth + univariate')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_df,\n", " color='#33a02c',\n", " size=1.2,\n", " tooltips=layer_tooltips().line('line')\n", " )\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth')\n", " )\n", ")\n", "\n", "smooth_bivariate = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('smooth + bivariate')\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_df,\n", " size=6,\n", " color='#ff7f00',\n", " tooltips=layer_tooltips().line('point @id')\n", " )\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth')\n", " )\n", ")\n", "\n", "smooth_both = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('smooth + univariate + bivariate')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_df,\n", " color='#33a02c',\n", " size=1.2,\n", " tooltips=layer_tooltips().line('line')\n", " )\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_df,\n", " size=6,\n", " color='#ff7f00',\n", " tooltips=layer_tooltips().line('point @id')\n", " )\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth')\n", " )\n", ")\n", "\n", "gggrid([\n", " smooth_only,\n", " smooth_univariate,\n", " smooth_bivariate,\n", " smooth_both\n", "], ncol=2)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Common real-life combinations without anchor\n", "\n", "What to test:\n", "- `geom_bar + geom_errorbar`: a common summary chart pattern.\n", "- `geom_boxplot + geom_jitter`: distribution summary plus raw points.\n", "- `geom_line + geom_ribbon`: line with uncertainty / confidence band.\n", "- Check which layer gets selected in overlap zones and near boundaries.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "summary_df = pd.DataFrame({\n", " 'cat': ['A', 'B', 'C', 'D'],\n", " 'value': [2.1, 3.4, 2.7, 4.0],\n", " 'low': [1.7, 2.9, 2.2, 3.4],\n", " 'high': [2.5, 3.9, 3.2, 4.6]\n", "})\n", "\n", "bar_error_plot = (\n", " ggplot(summary_df)\n", " + ggtitle('bar + errorbar')\n", " + geom_bar(\n", " aes(x='cat', y='value', fill='cat'),\n", " stat='identity',\n", " tooltips=layer_tooltips().line('bar @cat')\n", " )\n", " + geom_errorbar(\n", " aes(x='cat', ymin='low', ymax='high'),\n", " width=.22,\n", " size=1.2,\n", " color='black',\n", " tooltips=layer_tooltips().line('errorbar @cat')\n", " )\n", ")\n", "\n", "box_jitter_df = pd.DataFrame({\n", " 'group': ['A'] * 36 + ['B'] * 36 + ['C'] * 36,\n", " 'value': np.concatenate([\n", " np.random.normal(1.4, 0.28, 36),\n", " np.random.normal(2.2, 0.35, 36),\n", " np.random.normal(2.8, 0.30, 36)\n", " ])\n", "})\n", "\n", "box_jitter_plot = (\n", " ggplot(box_jitter_df)\n", " + ggtitle('boxplot + jitter')\n", " + geom_boxplot(\n", " aes(x='group', y='value', fill='group'),\n", " alpha=.45,\n", " tooltips=layer_tooltips().line('boxplot @group')\n", " )\n", " + geom_jitter(\n", " aes(x='group', y='value', color='group'),\n", " width=.12,\n", " size=3,\n", " alpha=.75,\n", " tooltips=layer_tooltips().line('point @group')\n", " )\n", ")\n", "\n", "band_line_df = pd.DataFrame({\n", " 'x': np.arange(1, 9),\n", " 'y': [1.2, 1.7, 1.5, 2.1, 2.0, 2.4, 2.3, 2.7],\n", " 'low': [0.8, 1.3, 1.1, 1.7, 1.6, 2.0, 1.9, 2.3],\n", " 'high': [1.6, 2.1, 1.9, 2.5, 2.4, 2.8, 2.7, 3.1]\n", "})\n", "\n", "line_ribbon_plot = (\n", " ggplot(band_line_df)\n", " + ggtitle('line + ribbon')\n", " + geom_ribbon(\n", " aes(x='x', ymin='low', ymax='high'),\n", " fill='#a6cee3',\n", " color='#1f78b4',\n", " alpha=.45,\n", " tooltips=layer_tooltips().line('ribbon')\n", " )\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " color='#e31a1c',\n", " size=1.3,\n", " tooltips=layer_tooltips().line('line')\n", " )\n", ")\n", "\n", "gggrid([\n", " bar_error_plot,\n", " box_jitter_plot,\n", " line_ribbon_plot\n", "], ncol=2)\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Anchored Variants\n", "\n", "These plots repeat the same combination patterns with anchored tooltips.\n", "Anchor is used here to exercise crosshair on top of the base interaction tests.\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Anchored versions of common combinations\n", "\n", "What to test:\n", "- Check that crosshair appears when tooltips are anchored.\n", "- Compare the anchored behavior with the non-anchored versions of the same combinations.\n", "- Verify that crosshair and selected tooltip stay synchronized when layers overlap or compete.\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "anch_hist_density = (\n", " ggplot(u_df, aes(x='x'))\n", " + ggtitle('anchored: histogram + density')\n", " + geom_histogram(\n", " aes(fill='grp'),\n", " bins=18,\n", " alpha=.45,\n", " position='identity',\n", " tooltips=layer_tooltips().line('histogram @grp').anchor('top_left')\n", " )\n", " + geom_density(\n", " aes(color='grp'),\n", " size=1.2,\n", " tooltips=layer_tooltips().line('density @grp').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_line_line = (\n", " ggplot()\n", " + ggtitle('anchored: line + line')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_a,\n", " color='#1f78b4',\n", " size=1.3,\n", " tooltips=layer_tooltips().line('line A').anchor('top_left')\n", " )\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_b,\n", " color='#e31a1c',\n", " size=1.3,\n", " tooltips=layer_tooltips().line('line B').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_line_point = (\n", " ggplot(base_df, aes(x='x', y='y'))\n", " + ggtitle('anchored: line + point')\n", " + geom_line(\n", " color='#33a02c',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('line').anchor('top_left')\n", " )\n", " + geom_point(\n", " data=hl_df,\n", " size=6,\n", " color='#ff7f00',\n", " tooltips=layer_tooltips().line('point @kind').anchor('bottom_right')\n", " )\n", ")\n", "\n", "anch_point_point = (\n", " ggplot()\n", " + ggtitle('anchored: point + point')\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_a,\n", " color='#6a3d9a',\n", " size=6,\n", " alpha=.8,\n", " tooltips=layer_tooltips().line('point A @layer').anchor('top_left')\n", " )\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_b,\n", " color='#b15928',\n", " size=6,\n", " alpha=.8,\n", " tooltips=layer_tooltips().line('point B @layer').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_polygon_point = (\n", " ggplot()\n", " + ggtitle('anchored: polygon + point')\n", " + geom_polygon(\n", " aes(x='x', y='y', group='id', fill='name'),\n", " data=poly_df,\n", " color='black',\n", " alpha=.35,\n", " tooltips=layer_tooltips().line('polygon @name').anchor('top_left')\n", " )\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=poly_points,\n", " color='#d95f02',\n", " size=6,\n", " tooltips=layer_tooltips().line('point @name').anchor('bottom_right')\n", " )\n", " + xlim(0, 5)\n", " + ylim(0, 4.5)\n", ")\n", "\n", "anch_stack = (\n", " ggplot(stack_df, aes(x='x', y='value', fill='segment'))\n", " + ggtitle('anchored: stacked bar')\n", " + geom_bar(\n", " stat='identity',\n", " position='stack',\n", " color='white',\n", " size=0.5,\n", " tooltips=layer_tooltips().line('@obj').anchor('top_center')\n", " )\n", ")\n", "\n", "anch_bar_error = (\n", " ggplot(summary_df)\n", " + ggtitle('anchored: bar + errorbar')\n", " + geom_bar(\n", " aes(x='cat', y='value', fill='cat'),\n", " stat='identity',\n", " tooltips=layer_tooltips().line('bar @cat').anchor('top_left')\n", " )\n", " + geom_errorbar(\n", " aes(x='cat', ymin='low', ymax='high'),\n", " width=.22,\n", " size=1.2,\n", " color='black',\n", " tooltips=layer_tooltips().line('errorbar @cat').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_box_jitter = (\n", " ggplot(box_jitter_df)\n", " + ggtitle('anchored: boxplot + jitter')\n", " + geom_boxplot(\n", " aes(x='group', y='value', fill='group'),\n", " alpha=.45,\n", " tooltips=layer_tooltips().line('boxplot @group').anchor('top_left')\n", " )\n", " + geom_jitter(\n", " aes(x='group', y='value', color='group'),\n", " width=.12,\n", " size=3,\n", " alpha=.75,\n", " tooltips=layer_tooltips().line('point @group').anchor('bottom_right')\n", " )\n", ")\n", "\n", "anch_line_ribbon = (\n", " ggplot(band_line_df)\n", " + ggtitle('anchored: line + ribbon')\n", " + geom_ribbon(\n", " aes(x='x', ymin='low', ymax='high'),\n", " fill='#a6cee3',\n", " color='#1f78b4',\n", " alpha=.45,\n", " tooltips=layer_tooltips().line('ribbon').anchor('top_left')\n", " )\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " color='#e31a1c',\n", " size=1.3,\n", " tooltips=layer_tooltips().line('line').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_smooth_only = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('anchored: smooth alone')\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_smooth_uni = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('anchored: smooth + univariate')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_df,\n", " color='#33a02c',\n", " size=1.2,\n", " tooltips=layer_tooltips().line('line').anchor('top_left')\n", " )\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_smooth_bi = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('anchored: smooth + bivariate')\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_df,\n", " size=6,\n", " color='#ff7f00',\n", " tooltips=layer_tooltips().line('point @id').anchor('bottom_right')\n", " )\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth').anchor('top_right')\n", " )\n", ")\n", "\n", "anch_smooth_both = (\n", " ggplot(smooth_mix_df, aes(x='x', y='y'))\n", " + ggtitle('anchored: smooth + univariate + bivariate')\n", " + geom_line(\n", " aes(x='x', y='y'),\n", " data=line_df,\n", " color='#33a02c',\n", " size=1.2,\n", " tooltips=layer_tooltips().line('line').anchor('top_left')\n", " )\n", " + geom_point(\n", " aes(x='x', y='y'),\n", " data=point_df,\n", " size=6,\n", " color='#ff7f00',\n", " tooltips=layer_tooltips().line('point @id').anchor('bottom_right')\n", " )\n", " + geom_smooth(\n", " method='loess',\n", " se=False,\n", " color='#1f78b4',\n", " size=1.4,\n", " tooltips=layer_tooltips().line('smooth').anchor('top_right')\n", " )\n", ")\n", "\n", "gggrid([\n", " anch_hist_density,\n", " anch_line_line,\n", " anch_line_point,\n", " anch_point_point,\n", " anch_polygon_point,\n", " anch_stack,\n", " anch_bar_error,\n", " anch_box_jitter,\n", " anch_line_ribbon,\n", " anch_smooth_only,\n", " anch_smooth_uni,\n", " anch_smooth_bi,\n", " anch_smooth_both\n", "], ncol=2)\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "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.12.11" } }, "nbformat": 4, "nbformat_minor": 4 }