{ "cells": [ { "cell_type": "markdown", "id": "issue-706-title", "metadata": {}, "source": [ "# Issue #706, \"scale_alpha: conflict of constant and mapped values of alpha aesthetic\"\n", "\n", "When a layer uses a constant numeric aesthetic (e.g. `geom_point(alpha=0.5)`) alongside another layer\n", "that maps the same aesthetic to data with a non-identity transform (e.g. `scale_alpha(trans='log10')`),\n", "the constant was incorrectly run through the transform, producing a wrong value. For alpha this often meant\n", "a negative value that was later clamped to 0." ] }, { "cell_type": "code", "execution_count": 1, "id": "issue-706-setup", "metadata": {}, "outputs": [ { "data": { "text/html": [ "\n", " \n", " \n", " " ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from lets_plot import *\n", "\n", "LetsPlot.setup_html()\n", "\n", "data = {\n", " 'x': [-3, -2, -1, 0, 1, 2, 3],\n", " 'y': [-3, -2, -1, 0, 1, 2, 3],\n", " # negative and zero values are outside log10 domain; only 1, 4, 9 survive\n", " 'v': [-9, -4, -1, 0, 1, 4, 9],\n", "}" ] }, { "cell_type": "markdown", "id": "issue-706-section-1", "metadata": {}, "source": [ "## 1. Original failing case: constant `alpha=0.5` + `scale_alpha(trans='log10')`\n", "\n", "Before the fix the large red points (Layer 1) were invisible (alpha clamped to 0).\n", "After the fix they should appear at 50% opacity.\n", "\n", "ggplot2 behavior: produces a warning about out-of-domain values but renders both layers." ] }, { "cell_type": "code", "execution_count": 2, "id": "issue-706-case1", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(size=10, alpha=0.5, color='red') # constant alpha - must stay 0.5\n", " + geom_point(aes(alpha='v'), color='black') # mapped alpha through log10\n", " + scale_alpha(trans='log10')\n", " + ggtitle('Original case: constant alpha=0.5 + log10 transform')\n", ")" ] }, { "cell_type": "markdown", "id": "issue-706-section-2", "metadata": {}, "source": [ "## 2. `trans='sqrt'` - wrong value before, correct after\n", "\n", "With `sqrt`, `sqrt(0.5) ~= 0.707` fell inside [0,1] so points were visible but at the wrong\n", "opacity. After the fix the large red points must appear at exactly 50% opacity.\n" ] }, { "cell_type": "code", "execution_count": 3, "id": "issue-706-case2", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(size=10, alpha=0.5, color='red')\n", " + geom_point(aes(alpha='v'), color='black')\n", " + scale_alpha(trans='sqrt')\n", " + ggtitle('Constant alpha=0.5 + sqrt transform (was ~0.71 before fix)')\n", ")" ] }, { "cell_type": "markdown", "id": "issue-706-section-3", "metadata": {}, "source": [ "## 3. `trans='reverse'` - negated constant before, correct after\n", "\n", "The `reverse` transform maps `v` to `-v`. With the old code the constant 0.5 became -0.5\n", "(clamped to 0). After the fix it stays 0.5.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "issue-706-case3", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data_pos = {\n", " 'x': [1, 2, 3, 4, 5],\n", " 'y': [1, 2, 3, 4, 5],\n", " 'v': [0.1, 0.3, 0.5, 0.7, 0.9],\n", "}\n", "\n", "(\n", " ggplot(data_pos, aes('x', 'y'))\n", " + geom_point(size=10, alpha=0.5, color='red')\n", " + geom_point(aes(alpha='v'), color='black')\n", " + scale_alpha(trans='reverse')\n", " + ggtitle('Constant alpha=0.5 + reverse transform')\n", ")" ] }, { "cell_type": "markdown", "id": "issue-706-section-4", "metadata": {}, "source": [ "## 4. Regression: constant alpha, no scale transform\n", "\n", "Without a transform the constant must pass through unchanged. Both layers visible." ] }, { "cell_type": "code", "execution_count": 5, "id": "issue-706-case4", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(size=10, alpha=0.5, color='red') # constant - must remain 0.5\n", " + geom_point(aes(alpha='v'), color='black') # mapped, identity transform\n", " + scale_alpha()\n", " + ggtitle('Regression: constant alpha + identity transform (no change expected)')\n", ")\n" ] }, { "cell_type": "markdown", "id": "issue-706-section-5", "metadata": {}, "source": [ "## 5. Regression: mapped alpha with `log10`, no constant layer\n", "\n", "This worked before and must continue to work. Only positive `v` values (1, 4, 9) are mapped;\n", "the rest render with the NA alpha." ] }, { "cell_type": "code", "execution_count": 6, "id": "issue-706-case5", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(aes(alpha='v'), color='red', size=8)\n", " + scale_alpha(trans='log10')\n", " + ggtitle('Regression: only mapped alpha + log10 (no constant layer)')\n", ")" ] }, { "cell_type": "markdown", "id": "issue-706-section-6", "metadata": {}, "source": [ "## 6. Positional constant on a log10 x-scale (must still be transformed)\n", "\n", "Unlike non-positional constants, a positional constant such as `xintercept=10` on a log10\n", "x-axis must be transformed (so it appears at the correct position on the log scale).\n", "Verify `geom_vline(xintercept=10)` appears at x=10 on the log10 x-axis." ] }, { "cell_type": "code", "execution_count": 7, "id": "issue-706-case6", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data_pos2 = {'x': [1, 2, 5, 10, 50, 100], 'y': [1, 2, 3, 4, 5, 6]}\n", "\n", "(\n", " ggplot(data_pos2, aes('x', 'y'))\n", " + geom_point(size=4)\n", " + geom_vline(xintercept=10, color='red', linetype='dashed') # must align with x=10 points\n", " + scale_x_log10()\n", " + ggtitle('Positional constant (xintercept=10) on log10 x-axis - must stay at x=10')\n", ")" ] }, { "cell_type": "markdown", "id": "issue-706-section-7", "metadata": {}, "source": [ "## 7. Two mapped layers sharing `scale_alpha(trans='log10')`\n", "\n", "Both layers use `aes(alpha=...)`. Neither has a constant. Both must render correctly." ] }, { "cell_type": "code", "execution_count": 8, "id": "issue-706-case7", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "\n", "data_a = {'x': [1, 2, 3, 4, 5], 'y': [2, 2, 2, 2, 2], 'v': [1, 2, 3, 4, 5]}\n", "data_b = {'x': [1, 2, 3, 4, 5], 'y': [4, 4, 4, 4, 4], 'w': [5, 4, 3, 2, 1]}\n", "\n", "(\n", " ggplot()\n", " + geom_point(aes('x', 'y', alpha='v'), data=data_a, color='steelblue', size=8)\n", " + geom_point(aes('x', 'y', alpha='w'), data=data_b, color='tomato', size=8)\n", " + scale_alpha(trans='log10')\n", " + ggtitle('Two mapped layers sharing scale_alpha(trans=log10)')\n", ")" ] }, { "cell_type": "markdown", "id": "issue-706-section-8", "metadata": {}, "source": [ "## 8. Constant alpha outside [0,1] with a log10-mapped layer\n", "\n", "A constant `alpha > 1` or `alpha < 0` is clamped by the renderer regardless of transforms.\n", "Verify no exception is thrown and the clamping is applied without the transform distorting things." ] }, { "cell_type": "code", "execution_count": 9, "id": "issue-706-case8a", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# alpha=1.5 -> clamped to 1.0 (fully opaque)\n", "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(size=10, alpha=1.5, color='red') # clamped to 1.0\n", " + geom_point(aes(alpha='v'), color='black')\n", " + scale_alpha(trans='log10')\n", " + ggtitle('Constant alpha=1.5 (clamped to 1.0) + log10 mapped layer')\n", ")\n" ] }, { "cell_type": "code", "execution_count": 10, "id": "issue-706-case8b", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# alpha=0.0 -> fully transparent (large points invisible by design)\n", "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(size=10, alpha=0.0, color='red') # fully transparent\n", " + geom_point(aes(alpha='v'), color='black')\n", " + scale_alpha(trans='log10')\n", " + ggtitle('Constant alpha=0.0 (fully transparent) + log10 mapped layer')\n", ")\n" ] }, { "cell_type": "code", "execution_count": 11, "id": "a3b1afeb-d5ff-4313-8cb3-28fe3a52b2c7", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# alpha is set via color -> fully transparent (large points invisible by design)\n", "(\n", " ggplot(data, aes('x', 'y'))\n", " + geom_point(size=10, color='red/0') # fully transparent\n", " + geom_point(aes(alpha='v'), color='black')\n", " + scale_alpha(trans='log10')\n", " + ggtitle('Constant alpha=0.0 (fully transparent) + log10 mapped layer')\n", ")\n" ] }, { "cell_type": "markdown", "id": "issue-706-section-9", "metadata": {}, "source": [ "## 9. Other numeric constants with a transform-only fallback\n", "\n", "Unlike alpha, some numeric constants can rely on a scale transform even when their own layer has no\n", "mapper for that aesthetic. A large size constant should still be reduced by a transformed size scale\n", "when another layer maps size.\n" ] }, { "cell_type": "code", "execution_count": 12, "id": "issue-706-case9", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data_sz = {\n", " 'x': [1, 2, 3, 4, 5],\n", " 'y': [2.5, 3, 3.5, 4, 4.5],\n", " 'v': [1, 2, 5, 10, 1e7],\n", "}\n", "\n", "(\n", " ggplot(data_sz, aes('x', 'y'))\n", " + geom_text(aes(label='v'), x=1, size=1e7, color='steelblue')\n", " + geom_point(aes(size='v'), x=2, color='tomato', alpha=0.5)\n", " + scale_size(trans='symlog')\n", " + ggtitle('Constant size=1e7 + scale_size(trans=symlog) - transform fallback is preserved')\n", ")\n" ] }, { "cell_type": "markdown", "id": "issue-706-section-10", "metadata": {}, "source": [ "## 10. Coordinate-derived constant: `slope` is positional already\n", "\n", "`slope` is not a scale-mapped visual property like alpha or size. It is treated as positional by\n", "`Aes.isPositional()`, so it keeps the positional constant path without a separate special case in\n", "`constantMapperOption`. " ] }, { "cell_type": "code", "execution_count": 13, "id": "issue-706-case10", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "data_slope = {\n", " 'x': [0, 1, 2, 3, 4, 5],\n", " 'y': [0, 1, 2, 3, 4, 5],\n", "}\n", "\n", "(\n", " ggplot(data_slope, aes('x', 'y'))\n", " + geom_point(size=5, color='black')\n", " + geom_abline(slope=1, intercept=0, color='red', size=1.2)\n", " + ggsize(760, 260)\n", " + ggtitle('Constant slope=1 keeps coordinate mapper in a wide plot')\n", ")\n" ] }, { "cell_type": "markdown", "id": "issue-706-section-11", "metadata": {}, "source": [ "## 11. Regression: huge constant text size must not leak into CSS\n", "\n", "The text layer has constant `size=1e7` and no size mapping, while the point layer maps `size` \n", "and defines `scale_size(trans='symlog')`. The rendered SVG should use the transformed size, \n", "not a raw `font-size:2.0E7px` declaration." ] }, { "cell_type": "code", "execution_count": 14, "id": "issue-706-case11", "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", " " ], "text/plain": [ "" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "values = [3.14 * 10**d for d in range(-7, 7)]\n", "(\n", " ggplot({'v': values}, aes(y='v'))\n", " + geom_point(aes(size='v'), x=0)\n", " + geom_text(aes(label='v'), x=1, size=1e7)\n", " + scale_x_continuous(limits=[-.5, 2])\n", " + scale_y_log10(limits=[10**-7, 10**7])\n", " + scale_size(trans='symlog')\n", " + ggsize(400, 600)\n", " + ggtitle('Regression: constant text size=1e7 + scale_size(trans=symlog)')\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.13.5" } }, "nbformat": 4, "nbformat_minor": 5 }