diff --git a/_static/img/common_errors.png b/_static/img/common_errors.png new file mode 100644 index 0000000..f886861 Binary files /dev/null and b/_static/img/common_errors.png differ diff --git a/tutorials/common_errors.ipynb b/tutorials/common_errors.ipynb new file mode 100644 index 0000000..44cd8b1 --- /dev/null +++ b/tutorials/common_errors.ipynb @@ -0,0 +1,527 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dd17a4bc", + "metadata": {}, + "source": [ + "# Common errors and what they mean\n", + "\n", + "`spatialdata-plot` validates its inputs up front and fails with an actionable message rather than deep\n", + "inside matplotlib or datashader. This notebook collects the errors you are most likely to hit, shows\n", + "what triggers each, and gives the one-line fix. Each cell deliberately raises and catches the error so\n", + "you can read the exact message.\n", + "\n", + "We use the synthetic `blobs` dataset throughout." + ] + }, + { + "cell_type": "markdown", + "id": "1079e5ef", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "38a9546a", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:32:58.684716Z", + "iopub.status.busy": "2026-08-19T03:32:58.684535Z", + "iopub.status.idle": "2026-08-19T03:33:04.321850Z", + "shell.execute_reply": "2026-08-19T03:33:04.321265Z" + } + }, + "outputs": [ + { + "data": { + "text/plain": [ + "SpatialData object\n", + "├── Images\n", + "│ ├── 'blobs_image': DataArray[cyx] (3, 512, 512)\n", + "│ └── 'blobs_multiscale_image': DataTree[cyx] (3, 512, 512), (3, 256, 256), (3, 128, 128)\n", + "├── Labels\n", + "│ ├── 'blobs_labels': DataArray[yx] (512, 512)\n", + "│ └── 'blobs_multiscale_labels': DataTree[yx] (512, 512), (256, 256), (128, 128)\n", + "├── Points\n", + "│ └── 'blobs_points': DataFrame with shape: (, 4) (2D points)\n", + "├── Shapes\n", + "│ ├── 'blobs_circles': GeoDataFrame shape: (5, 2) (2D shapes)\n", + "│ ├── 'blobs_multipolygons': GeoDataFrame shape: (2, 1) (2D shapes)\n", + "│ └── 'blobs_polygons': GeoDataFrame shape: (5, 1) (2D shapes)\n", + "└── Tables\n", + " └── 'table': AnnData (26, 3)\n", + "with coordinate systems:\n", + " ▸ 'global', with elements:\n", + " blobs_image (Images), blobs_multiscale_image (Images), blobs_labels (Labels), blobs_multiscale_labels (Labels), blobs_points (Points), blobs_circles (Shapes), blobs_multipolygons (Shapes), blobs_polygons (Shapes)" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "import numpy as np # noqa: F401\n", + "import spatialdata as sd\n", + "import spatialdata_plot # noqa: F401 # registers the .pl accessor\n", + "from matplotlib.colors import Normalize\n", + "from spatialdata_plot import PercentileNormalize\n", + "\n", + "sdata = sd.datasets.blobs()\n", + "sdata" + ] + }, + { + "cell_type": "markdown", + "id": "03f14f09", + "metadata": {}, + "source": [ + "## 0. `AttributeError: ... object has no attribute 'pl'`\n", + "\n", + "The `.pl` accessor is only attached when you `import spatialdata_plot`. Forget that import and every\n", + "`sdata.pl.…` call raises `AttributeError` — the first wall most newcomers hit. The `Setup` cell above\n", + "already does the import; the cell below runs a *fresh* interpreter without it to show the message." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "d550e432", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:04.323735Z", + "iopub.status.busy": "2026-08-19T03:33:04.323350Z", + "iopub.status.idle": "2026-08-19T03:33:08.844352Z", + "shell.execute_reply": "2026-08-19T03:33:08.843536Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "AttributeError: 'SpatialData' object has no attribute 'pl'\n" + ] + } + ], + "source": [ + "import os\n", + "import subprocess\n", + "import sys\n", + "\n", + "# A fresh interpreter that never imports spatialdata_plot, so `.pl` is unregistered.\n", + "# PYTHON_COLORS=0 keeps the captured traceback free of ANSI colour codes.\n", + "snippet = \"import spatialdata as sd; sd.datasets.blobs().pl.render_shapes('blobs_circles')\"\n", + "result = subprocess.run(\n", + " [sys.executable, \"-c\", snippet],\n", + " capture_output=True,\n", + " text=True,\n", + " env={**os.environ, \"PYTHON_COLORS\": \"0\", \"NO_COLOR\": \"1\"},\n", + ")\n", + "print(result.stderr.strip().splitlines()[-1])" + ] + }, + { + "cell_type": "markdown", + "id": "5ec56751", + "metadata": {}, + "source": [ + "## 1. Element not found\n", + "\n", + "A typo in the element name raises a `KeyError` naming the element it looked for. Check\n", + "`sdata` for the exact key." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "12b055e9", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:08.846271Z", + "iopub.status.busy": "2026-08-19T03:33:08.846138Z", + "iopub.status.idle": "2026-08-19T03:33:08.849074Z", + "shell.execute_reply": "2026-08-19T03:33:08.848642Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KeyError: \"Could not find element with name 'blobs_circle'\"\n" + ] + } + ], + "source": [ + "try:\n", + " sdata.pl.render_shapes(\"blobs_circle\").pl.show() # missing trailing 's'\n", + "except KeyError as e:\n", + " print(\"KeyError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "cf6fc7d0", + "metadata": {}, + "source": [ + "## 2. Colouring by a column with no annotating table\n", + "\n", + "To colour an element by a column, that column must live on the element or on a table annotating it.\n", + "Passing a name that is neither a valid colour nor an available column raises, naming the element and\n", + "the column." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "86745998", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:08.850507Z", + "iopub.status.busy": "2026-08-19T03:33:08.850389Z", + "iopub.status.idle": "2026-08-19T03:33:08.852860Z", + "shell.execute_reply": "2026-08-19T03:33:08.852432Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "KeyError: \"Element 'blobs_circles' has no annotating tables. Cannot use column 'gene_x' for coloring. Please ensure the element is annotated by at least one table.\"\n" + ] + } + ], + "source": [ + "try:\n", + " sdata.pl.render_shapes(\"blobs_circles\", color=\"gene_x\").pl.show()\n", + "except KeyError as e:\n", + " print(\"KeyError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "9c53205c", + "metadata": {}, + "source": [ + "## 3. Ambiguous colour/column name\n", + "\n", + "If a `color` string is **both** a valid matplotlib colour name and a column in the element or its\n", + "annotating table, `spatialdata-plot` cannot tell which you meant and raises. Disambiguate with a hex\n", + "string or an RGB(A) tuple, or rename the column. Here we deliberately add a column called `red` to\n", + "force the clash." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "dac9e6a7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:08.854274Z", + "iopub.status.busy": "2026-08-19T03:33:08.854151Z", + "iopub.status.idle": "2026-08-19T03:33:08.857544Z", + "shell.execute_reply": "2026-08-19T03:33:08.857049Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError: `color='red'` is ambiguous: it is a valid matplotlib color name AND a column name in element 'blobs_circles'. Disambiguate by either passing an unambiguous color form (hex string like '#ffa500' or an RGB(A) tuple), or by renaming the column.\n" + ] + } + ], + "source": [ + "circles = sdata[\"blobs_circles\"]\n", + "circles[\"red\"] = circles[\"radius\"] # a column whose name is also a colour\n", + "try:\n", + " sdata.pl.render_shapes(\"blobs_circles\", color=\"red\").pl.show()\n", + "except ValueError as e:\n", + " print(\"ValueError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "555854a5", + "metadata": {}, + "source": [ + "## 4. Invalid image channel\n", + "\n", + "Selecting a channel that does not exist raises a `ValueError` that lists the valid channels — helpful\n", + "when you are unsure how a multichannel image is indexed (see the *Multichannel & fluorescence images*\n", + "tutorial)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "b1164416", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:08.858839Z", + "iopub.status.busy": "2026-08-19T03:33:08.858735Z", + "iopub.status.idle": "2026-08-19T03:33:08.861474Z", + "shell.execute_reply": "2026-08-19T03:33:08.861008Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError: Invalid channel(s): DAPI. Valid choices are: [0 1 2]\n" + ] + } + ], + "source": [ + "try:\n", + " sdata.pl.render_images(\"blobs_image\", channel=\"DAPI\").pl.show()\n", + "except ValueError as e:\n", + " print(\"ValueError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "bac6ece4", + "metadata": {}, + "source": [ + "## 5. A per-channel `norm` list of the wrong length\n", + "\n", + "When you pass a list of norms for an image, its length must match the number of channels you are\n", + "rendering (see the *Normalization and contrast* tutorial)." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "50f8a28b", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:08.862734Z", + "iopub.status.busy": "2026-08-19T03:33:08.862642Z", + "iopub.status.idle": "2026-08-19T03:33:08.865037Z", + "shell.execute_reply": "2026-08-19T03:33:08.864515Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError: Length of 'norm' list (2) must match the number of channels (3).\n" + ] + } + ], + "source": [ + "try:\n", + " sdata.pl.render_images(\"blobs_image\", channel=[0, 1, 2], norm=[Normalize()] * 2).pl.show()\n", + "except ValueError as e:\n", + " print(\"ValueError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "4d06836d", + "metadata": {}, + "source": [ + "## 6. `grayscale` needs exactly three channels\n", + "\n", + "`grayscale=True` collapses a three-channel selection into one intensity, so it requires exactly three\n", + "channels." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "b77803c7", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:08.866284Z", + "iopub.status.busy": "2026-08-19T03:33:08.866186Z", + "iopub.status.idle": "2026-08-19T03:33:09.025021Z", + "shell.execute_reply": "2026-08-19T03:33:09.024560Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "ValueError: grayscale=True requires exactly 3 channels, got 1. Select 3 channels via the 'channel' parameter.\n" + ] + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAosAAAHrCAYAAACn9tfQAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQAAIEdJREFUeJzt3QeQVeX9+OF3AUVEiiglIBYsECBWsKBGReyNoNiCaKIzsaFGzc9BHesoMdYUoxHbaAQVC4om2HvF3hsqolEsICugSLn/ec/8d2dX+cIuLrsLPs/Mnd172Jd74Oze+9n3lFtWKpVKCQAAFqDJghYCAIBYBABgocwsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAADUbSzOnTs3ffzxx2nWrFm1GvPFF1+k+fPnL85DAgDQ2GPxs88+S6effnrq1q1b6tq1a7rttttqNG7EiBGpXbt2aa211krt27dPI0eOXNz1BQCgscbiPffck8rKytJTTz1V4zGjR49OZ511Vho7dmyaMWNGuuyyy9Lhhx+eHnroocVZXwAA6lHZ4r43dI7G66+/Pg0ZMmShX7fVVlul1VdfPY0aNapy2TbbbJM6dOiQxowZszgPDQDAsnCCSz4+8bnnnktbbrllteVbb711evbZZ5fkQwMAUAeapSXom2++SbNnz06rrrpqteX5fj7ZJZLH5FvV6Jw6dWpaZZVVihlNAAB+LO8wzv3VuXPn1KRJk8YfixUrmc+ErmrOnDmpadOmCz0h5swzz1ySqwYAsMyaPHlyWm211Rp/LLZq1Sq1bt26OIu6qilTpqQuXbqE44YPH56OP/74yvvTp08vjnvM//D89wEA8GPl5eXFFWtyg9WVZktiJfNZz3n6M/v1r3+d7rvvvnTCCSdUfs348eOL5ZHmzZsXtx/KoSgWAQAWri4P26vVzux8HGG+GHe+ZdOmTSs+zx8rXHTRRalnz56V908++eT0wAMPpHPPPTe9+uqrRTR+8MEH1eIRAIDGqVaxOGHChLT55psXt7wb+bzzzis+z8cYVsgzf1V3MW+xxRbp7rvvLmYXBw4cmF5//fUiHrt37163/xIAABrPdRbrU9613aZNm+LYRbuhAQDqr5mW6HUWAQBYuolFAABCYhEAgJBYBAAgJBYBAAiJRQAAQmIRAICQWAQAICQWAQAIiUUAAEJiEQCAkFgEACAkFgEACIlFAABCYhEAgJBYBAAgJBYBAAiJRQAAQmIRAICQWAQAICQWAQAIiUUAAEJiEQCAkFgEACAkFgEACIlFAABCYhEAgJBYBAAgJBYBAAiJRQAAQmIRAICQWAQAICQWAQAIiUUAAEJiEQCAkFgEACAkFgEACIlFAABCYhEAgJBYBAAgJBYBAAiJRQAAQmIRAICQWAQAICQWAQAIiUUAAEJiEQCAkFgEACAkFgEACIlFAABCYhEAgJBYBAAgJBYBAAiJRQAAQmIRAICQWAQAICQWAQAIiUUAAEJiEQCAkFgEACAkFgEACIlFAABCYhEAgJBYBAAgJBYBAAiJRQAAQmIRAACxCABA7ZlZBAAgJBYBAAiJRQAAQmIRAICQWAQAICQWAQAIiUUAAEJiEQCAkFgEACDULNXS9OnT0w033JAmTZqU1l133fTb3/42tWjRYqFjJk6cmO644470+eefp86dO6e99947denSpbYPDQBAY55ZzLG38cYbp3//+99phRVWSH//+99Tv3790syZM8Mxd955Z+rRo0d68cUXU9u2bdODDz6Y1llnnfTEE0/UxfoDALAElZVKpVJNv/iYY45J99xzT3rllVdS8+bN07Rp09J6662Xjj/++DR8+PAFjhkwYEBq06ZNuvXWWyuX5cDs1q1bEZ01UV5eXvwdeVazdevWNV1dAICflfIl0Ey1mlnMu5IHDx5chGK28sorpz322CONHTs2HNOhQ4c0Y8aMyvvz589Ps2bNSp06dfop6w0AQD2ocSzOnj07ffTRR2nttdeutjzff/fdd8NxF110UWrZsmXaZptt0hFHHJG22GKLtNFGG6XTTjttoY+Vy7jqDQCARhyLeTYwa9WqVbXleYqz4s8WJJ8I89JLLxUntnTt2jV17NgxPfvss+nTTz8Nx4wYMaKYQq245XEAADTiWMyzg2VlZenrr7+utjwft/jDgKxq6NChaaeddkqjR49OJ598cnHCSz7O8fDDDw/H5OMf8772itvkyZNrupoAADRELC6//PLFWcxvvfVWteX5fs+ePRc4Zt68eem9995Lffv2rba8T58+6c033wwfKx8TmWcsq94AAKh/tTrBZd9990033XRTMZuY5Rm/u+66q1heYfz48ZVnRjdt2rS4bM69995b+ef55Ov7778/9erVq+7+FQAANPylc/JZzdtvv3366quv0pZbbpkeeOCB1Lt37zRu3Li03HLLFV9zxhlnpEsuuaRyd/XDDz+cBg0alLp3757WX3/99Mwzz6QpU6YUUbnBBhvU6HFdOgcAoGGaqVaxmM2dO7e41mI+Mzq/g0uOx3wsY4Wnn366uAB3PvO5Qg7HHI05EvM7t/Tv3z+tuOKKNX5MsQgAsJTEYkMQiwAAS8FFuQEA+HkRiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgBQd7F43XXXpfXWWy81b9489e7dO40bN26RYz755JM0dOjQtOqqq6b27dunY489Ns2cObO2Dw0AQGOOxfHjx6dDDz00nXrqqemzzz5Lv//979OgQYPS888/H46ZOnVq2nLLLdP06dPTc889lz788MPUo0ePdN9999XF+gMAsASVlUqlUk2/eMCAAal169bptttuq1zWt2/fIv6uv/76BY75v//7v+LP3n///dSiRYvFWsny8vLUpk2bIjjz4wMAUD/NVOOZxdyUTz/9dNp2222rLd9+++3Tk08+GY4bO3Zs+s1vfrPYoQgAQMOpcSx+8803xXGG+ZjDqjp06FDsko588MEHqV27dmnXXXctgnGNNdZIJ5xwwkKPWZw9e3ZRxlVvAAAshWdD5xnHsrKyhf75+eefnw455JD05ZdfpltuuSWNGTOmOMklMmLEiGIKteLWtWvXn7qaAAAsyVhs1apVatmyZfriiy+qLc/3O3bsGI7r1KlT2mmnndK+++5bjM/HOB533HFFNEaGDx9e7GuvuE2ePLmmqwkAQEPEYp493HzzzdNDDz1UbfmDDz6Y+vXrF47LZ0IvaLaxSZP4ofNlefJBmVVvAAA08t3QJ554YrrrrruKay1OmzYtXXzxxenFF18sZgornHHGGalt27bVxuTL5Nx8881p1qxZacKECemvf/1rOuCAA+r2XwIAQMPG4s4775yuuuqqdPbZZxe7l/Pnt956a9pkk03CMXm3c97lfM455xQnuuTd0UOGDEkXXnhhXaw/AACN5TqLDcV1FgEAGvl1FgEA+PkRiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAgFgEAqD0ziwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABAqFmqpTlz5qT//ve/adKkSWnddddNO+64Y2rSpGbN+b///S+NGjUqde/ePe2xxx61fWgAABpzLH7zzTepf//+afr06WnrrbdO559/furRo0e666670vLLL7/QsfPnz08HHnhgev7559NOO+0kFgEAlrVYHDFiRJoyZUp65ZVXUtu2bdMnn3ySevbsma644op09NFHL3Ts2WefnVZZZZW0zTbb/NR1BgCgMR6zOGbMmLTffvsVoZh16dIl7b777unmm29e6LjHHnssXX311WnkyJE/bW0BAGicM4vff/99mjhxYnG8YVV5N/S9994bjps6dWoaMmRIuuqqq1K7du1q9FizZ88ubhXKy8trupoAADTEzOLMmTNTqVSqnFWskO/nYxkjv/vd79I+++yTBgwYUKvd3W3atKm8de3atcZjAQBogFhcccUVFzjLl092admy5QLH3HfffWn8+PHFjOIFF1xQ3N5///30zjvvFJ9HM4bDhw8v/t6K2+TJk2v3rwIAoH53Qzdv3jytscYaxa7oqvL9fAmdBencuXMaNmxYmjZtWuWyvHt53rx56bPPPis+Ro+VbwAANKyyUt63XEPHHntscY3FfDb0CiusUByPuN5666U//elP6aSTTiq+5sknn0wvvPBCeHZ0PiEmj73llltqvJJ5BjLvjs6zjK1bt67xOACAn5PyJdBMtTob+tRTTy2ul5gvf3PKKacUH/NsY9UwzCe75K8DAOBndp3F9u3bpxdffLF4F5aPPvoonXDCCWn//fcvZgor9OvXL9y9nA0aNCg1a1brN44BAKCx74ZuKHZDAwAsBbuhAQD4eRGLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCACAWAQAoPbMLAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEDdxeJ9992X+vfvn9Zee+208847p2eeeWahX//xxx+nE044IfXp0ydtsMEG6bDDDksffvhhbR8WAIDGHotPPfVU2m233dKAAQPS2LFjU69evYpwfPvtt8MxgwcPTquttlq6/PLL07XXXps+//zztNVWW6Uvv/yyLtYfAIAlqKxUKpVq+sV77rln+v7779P48eMrl/Xu3Tv169cvXXHFFQscM2/evNS0adPK+zNmzEht27YtwnHIkCE1etzy8vLUpk2bNH369NS6deuari4AwM9K+RJoplrNLD7yyCNphx12qLYs74p+9NFHwzFVQzGbM2dOyn263HLL1XZdAQCoZ81q+oXffPNNUaudOnWqtrxjx47pk08+qfEDnnrqqWnllVdOO+64Y/g1s2fPLm4V8uMCAFD/ajyzOH/+/OJjs2bV+zLPEOZdzTVx6aWXppEjR6ZRo0YVwRgZMWJEMYVacevatWtNVxMAgIaIxVatWqXmzZv/6MSUfL99+/aLHJ+PaTz++OPTzTffvNBZxWz48OHFvvaK2+TJk2u6mgAANEQsNmnSpLj8zRNPPFFt+WOPPZY23XTThY698sor07Bhw9Lo0aPTwIEDF/lYOUrzQZlVbwAA1L9aneBy1FFHpdtvvz09+OCDxf0xY8akxx9/PB155JGVX3PRRRcVl9SpcM011xTjcigOGjSoLtcdAIDGcoJLdsABBxQX1M6zg/kYxjwDeNlll6Xtttuu2skoVU94yaFYVlaWjjnmmOJWIe+SzjcAAJaR6yxWmDt3bpo2bVpq167djy6Nk2MxX0uxc+fOxf0cjgt6iNrsXnadRQCAhmmmZos1qFmz8KSWH0Zgly5dFn/tAABYut4bGgCAnw+xCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhMQiAAAhsQgAQEgsAgAQEosAAITEIgAAIbEIAEBILAIAEBKLAACExCIAACGxCABASCwCABASiwAAhJqlxfDKK6+kSZMmpXXXXTf16NFjiY0BAGApmln8/vvv08CBA9N2222XLrnkkrTpppumQw89NJVKpTodAwDAUjizmGPviSeeSC+//HJabbXV0uuvv5769OmTtt1223TQQQfV2RgAAJbCmcXrr78+7bfffkX0Zb169Uq77LJLsbwuxwAAsJTNLM6dOze9+eabadiwYdWWr7/++unyyy+vszHZ7Nmzi1uF6dOnFx/Ly8truroAAD875f+/lerycL8ax+KMGTPSvHnz0sorr1xt+SqrrJK+/vrrOhuTjRgxIp155pk/Wt61a9eari4AwM/WV199ldq0aVO/sdi8efPi46xZs34UhCussEKdjcmGDx+ejj/++Mr7OSzXWGON9NFHH9XZP5zG8dtP/gVg8uTJqXXr1g29OtQB23TZZLsum2zXZdP06dPT6quvntq1a1dnf2eNY7FFixapU6dORbBVle9369atzsZURGZFaFaVQ1FULHvyNrVdly226bLJdl022a7LpiZN6u5S2rX6m/KJKbfddluaP39+cf+7775L48aNK5ZXeOONN9Kdd95ZqzEAADROtYrF0047LX388cdp7733TiNHjky77bZbatasWbVdxjfffHMaOnRorcYAALAMxOKaa66ZXnjhheIdWB5++OG09dZbpwkTJhQnrFTo2bNn2muvvWo1ZlHyLunTTz99gbumWXrZrsse23TZZLsum2zXZVPzJdBMZSVvpQIAQKDujn4EAGCZIxYBAAiJRQAAfvp1Fpek/NZ+TzzxRHGx7k033bS4NuOSGEP9yhdTz9uoadOmaauttkorrbTSIse8/fbb6Z133km/+MUv0sYbb1yn14mibuSLqOeT1vI7M/Xr16+4ukFN5ctq5Z/dwYMH2xyNzGuvvZbefffd4g0Q8s9eTeRt+dRTTxUf8/dCq1atlvh6UnP5knVPP/10+vzzz9OvfvWrtPbaay9yTH4Tjeeeey5Nmzat+F7YcMMN/Zc3MvlUk4ceeqjYrvvuu2+NXifzZQsff/zx9O2336bNN988tW/fvtYP2qDefffd0pprrlnq3r176de//nVpxRVXLF111VV1Pob6dc8995TatGlT2myzzUobbbRRaZVVVik9/vjj4de/9tprpS222KLYprvvvntpjTXWKPXu3bs0ceLEel1vFu6CCy4otWjRotS/f//iZ7Bnz56lTz75pEb/bVdeeWVp+eWXLzVv3tx/cyMyb9680tChQ4uf1x133LG06qqrlnbbbbfSd999t8if8Y4dO5Y23HDD0l577VXq0aNH6cknn6y39Wbhpk6dWurbt2+pS5cupQEDBhSvk6eccspCxzzwwAPFc3XepnvuuWepQ4cO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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "try:\n", + " sdata.pl.render_images(\"blobs_image\", channel=[0], grayscale=True).pl.show()\n", + "except ValueError as e:\n", + " print(\"ValueError:\", e)" + ] + }, + { + "cell_type": "markdown", + "id": "52c83cb4", + "metadata": {}, + "source": [ + "## 7. Invalid `PercentileNormalize` bounds\n", + "\n", + "Percentile bounds are validated at construction time: they must satisfy `0 <= pmin < pmax <= 100`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c5293c63", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:09.026804Z", + "iopub.status.busy": "2026-08-19T03:33:09.026686Z", + "iopub.status.idle": "2026-08-19T03:33:09.029442Z", + "shell.execute_reply": "2026-08-19T03:33:09.028925Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "PercentileNormalize(50, 50) -> Require 0 <= pmin < pmax <= 100, got pmin=50, pmax=50.\n", + "PercentileNormalize(90, 10) -> Require 0 <= pmin < pmax <= 100, got pmin=90, pmax=10.\n", + "PercentileNormalize(-1, 50) -> Require 0 <= pmin < pmax <= 100, got pmin=-1, pmax=50.\n", + "PercentileNormalize(0, 101) -> Require 0 <= pmin < pmax <= 100, got pmin=0, pmax=101.\n" + ] + } + ], + "source": [ + "bad_bounds = [\n", + " (50, 50), # pmin == pmax (must be strictly increasing)\n", + " (90, 10), # pmin > pmax\n", + " (-1, 50), # pmin < 0\n", + " (0, 101), # pmax > 100\n", + "]\n", + "for bad in bad_bounds:\n", + " try:\n", + " PercentileNormalize(*bad)\n", + " except ValueError as e:\n", + " print(f\"PercentileNormalize{bad} -> {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c21a9c5c", + "metadata": {}, + "source": [ + "## Summary\n", + "\n", + "`spatialdata-plot` fails fast with messages that name the offending element, column, channel, or\n", + "bound:\n", + "\n", + "- **`AttributeError: ... has no attribute 'pl'`** — you forgot `import spatialdata_plot`.\n", + "- **Element not found** — check the key in `sdata`.\n", + "- **No column to colour by** — put the value on the element or its annotating table.\n", + "- **Ambiguous colour/column name** — pass a hex/RGB(A) colour, or rename the column.\n", + "- **Invalid channel** — the message lists the valid channels.\n", + "- **Wrong-length `norm` list** — one norm per rendered channel.\n", + "- **`grayscale` needs three channels** — select exactly three.\n", + "- **Invalid percentile bounds** — `0 <= pmin < pmax <= 100`.\n", + "\n", + "When a plot fails, read the message first — it usually names the fix." + ] + }, + { + "cell_type": "markdown", + "id": "d3106cc6", + "metadata": {}, + "source": [ + "## For reproducibility" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "384e30e6", + "metadata": { + "execution": { + "iopub.execute_input": "2026-08-19T03:33:09.030827Z", + "iopub.status.busy": "2026-08-19T03:33:09.030712Z", + "iopub.status.idle": "2026-08-19T03:33:09.058691Z", + "shell.execute_reply": "2026-08-19T03:33:09.058073Z" + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Python implementation: CPython\n", + "Python version : 3.14.6\n", + "IPython version : 9.14.1\n", + "\n", + "spatialdata : 0.7.3\n", + "spatialdata_plot: 0.4.1\n", + "matplotlib : 3.11.0\n", + "numpy : 2.4.6\n", + "\n", + "Compiler : Clang 20.1.8 \n", + "OS : Darwin\n", + "Release : 25.2.0\n", + "Machine : arm64\n", + "Processor : arm\n", + "CPU cores : 8\n", + "Architecture: 64bit\n", + "\n" + ] + } + ], + "source": [ + "# ruff: noqa: F401, F811, I001, E402\n", + "# fmt: off\n", + "import warnings\n", + "import spatialdata_plot\n", + "\n", + "%load_ext watermark\n", + "# fmt: on\n", + "\n", + "%watermark -v -m -p spatialdata,spatialdata_plot,matplotlib,numpy" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "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.14.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/index.md b/tutorials/index.md index a16c1bb..e496d38 100644 --- a/tutorials/index.md +++ b/tutorials/index.md @@ -54,6 +54,16 @@ Add a physical scalebar with `scalebar_dx`, choose units, and style placement, colour, length and fonts through `scalebar_params`. ::: +:::{grid-item-card} Common errors and what they mean +:link: /notebooks/tutorials/common_errors +:link-type: doc +:img-top: /notebooks/_static/img/common_errors.png + +A troubleshooting reference: the errors you are most likely to hit — +missing elements, invalid channels, wrong-length `norm` lists — with what +triggers each and the one-line fix. +::: + :::: @@ -66,4 +76,5 @@ color_and_palette multi_panel_color performance scalebars +common_errors ```