diff --git a/astrowidgets/bqplot.py b/astrowidgets/bqplot.py index f4ac591..e8d1fdc 100644 --- a/astrowidgets/bqplot.py +++ b/astrowidgets/bqplot.py @@ -374,6 +374,9 @@ def __init__(self, *args, display_width=500, display_aspect_ratio=1): # to the viewer. self._viewport_change_source_is_gui = False + # Guards re-entrancy while we programmatically update the viewport. + self._updating_viewport = False + # Provide an Output widget to which prints can be directed for # debugging. self._print_out = ipw.Output() @@ -472,36 +475,49 @@ def _add_new_single_marker(self, x_mark, y_mark): # Update the viewport to match changes in the UI def _init_watch_image_changes(self): """ - Watch for changes to the image scale, which indicate the user - has either changed the zoom or has panned, and update the zoom_level. + Watch for changes to the image scales, which indicate the user + has either changed the zoom or has panned, and update the stored + viewport (center and field of view) to match. """ - def update_zoom_level(event): + def update_viewport(event): """ - Watch for changes in the zoom level from the viewer. + Watch for changes in the viewport (pan or zoom) from the viewer. """ - - old_width = self.get_viewport(sky_or_pixel='pixel', image_label=self._current_image_label)["fov"] new_width = self._astro_im.get_current_width() if new_width is None or self._updating_viewport: # There is no image yet, or this object is in the process - # of changing the zoom, so return + # of changing the viewport, so return return - # Do nothing if the zoom has not changed - if np.abs(new_width - old_width) > 1e-3: - # Let the zoom_level handler know the GUI itself - # generated this zoom change which means the GUI - # does not need to be updated. + current = self.get_viewport(sky_or_pixel='pixel', + image_label=self._current_image_label) + old_width = current["fov"] + old_center = current["center"] + new_center = self._astro_im.center + + width_changed = np.abs(new_width - old_width) > 1e-3 + center_changed = ( + old_center is None + or np.abs(new_center[0] - old_center[0]) > 1e-3 + or np.abs(new_center[1] - old_center[1]) > 1e-3 + ) + + # Do nothing if neither the zoom nor the pan has changed + if width_changed or center_changed: + # Let the viewport handler know the GUI itself generated this + # change, which means the GUI does not need to be updated. self._viewport_change_source_is_gui = True - self.set_viewport(fov=new_width) - - # Observe changes to the maximum of the x scale. Observing the y scale - # or the minimum instead of the maximum is also fine. - x_scale = self._astro_im._scales['x'] + self.set_viewport(center=new_center, fov=new_width) + # Observe changes to the maximum of both the x and y scales so that + # horizontal pan, vertical pan, and zoom are all detected. Observing + # the maximum (rather than the minimum) of each scale is sufficient + # because a pan shifts both ends of a scale. + # # If things seem laggy in the future, check whether throttling # the updates helps. - x_scale.observe(update_zoom_level, names='max') + for scale in (self._astro_im._scales['x'], self._astro_im._scales['y']): + scale.observe(update_viewport, names='max') def _interval_and_stretch(self, stretch=None, cuts=None): """ @@ -532,11 +548,16 @@ def _current_image_label(self): def set_stretch(self, value, image_label=None, **kwargs): super().set_stretch(value, image_label=image_label, **kwargs) - self._send_data(stretch=value) + # Changing the stretch only affects the color mapping, so leave the + # current viewport (zoom/pan) untouched. + self._send_data(stretch=value, reset_view=False) def set_cuts(self, value, image_label=None, **kwargs): super().set_cuts(value, image_label=image_label, **kwargs) - self._send_data(cuts=self.get_cuts(image_label=image_label)) + # Changing the cuts only affects the color mapping, so leave the + # current viewport (zoom/pan) untouched. + self._send_data(cuts=self.get_cuts(image_label=image_label), + reset_view=False) @property def viewer(self): diff --git a/astrowidgets/tests/test_widget_api_bqplot.py b/astrowidgets/tests/test_widget_api_bqplot.py index 9e800f7..baf23ff 100644 --- a/astrowidgets/tests/test_widget_api_bqplot.py +++ b/astrowidgets/tests/test_widget_api_bqplot.py @@ -2,7 +2,7 @@ from traitlets import TraitError -from astro_image_display_api import ImageAPITest +from astro_image_display_api.api_test import ImageAPITest from astro_image_display_api import ImageViewerInterface _ = pytest.importorskip("bqplot", @@ -29,3 +29,30 @@ def test_save(self, tmp_path): "a running browser.") def test_save_overwrite(self, tmp_path): pass + + def test_get_viewport_reflects_interactive_pan(self, data): + # Panning in the browser shifts the bqplot scales directly. Simulate + # that here and check that get_viewport reports the new center. + self.image.load_image(data) + self.image.set_viewport(center=(75, 50), fov=100) + + scales = self.image._astro_im._scales + + # Horizontal pan: shift the x scale. Set min before max so that the + # center is already correct when the 'max' observer fires. + dx = 20 + scales['x'].min += dx + scales['x'].max += dx + + vp = self.image.get_viewport(sky_or_pixel='pixel') + assert vp['center'][0] == pytest.approx(75 + dx) + assert vp['center'][1] == pytest.approx(50) + + # Vertical pan: shift the y scale. + dy = -15 + scales['y'].min += dy + scales['y'].max += dy + + vp = self.image.get_viewport(sky_or_pixel='pixel') + assert vp['center'][0] == pytest.approx(75 + dx) + assert vp['center'][1] == pytest.approx(50 + dy) diff --git a/example_notebooks/bqplot_widget.ipynb b/example_notebooks/bqplot_widget.ipynb new file mode 100644 index 0000000..3e77009 --- /dev/null +++ b/example_notebooks/bqplot_widget.ipynb @@ -0,0 +1,750 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Widget Example Using bqplot" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "See https://astrowidgets.readthedocs.io for additional details about the widget, including installation notes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "from astropy.visualization import AsymmetricPercentileInterval\n", + "from astrowidgets.bqplot import ImageWidget" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from ginga.misc.log import get_logger\n", + "\n", + "logger = get_logger('my viewer', log_stderr=True,\n", + " log_file=None, level=30)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w = ImageWidget()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For this example, we use an image from Astropy data repository and load it as `CCDData`. Feel free to modify `filename` to point to your desired image.\n", + "\n", + "Alternately, for local FITS file, you could load it like this instead:\n", + "```python\n", + "w.load_image(filename)\n", + "``` \n", + "Or if you wish to load a data array natively (without WCS):\n", + "```python\n", + "from astropy.io import fits\n", + "import numpy as np\n", + "with fits.open(filename, memmap=False) as pf:\n", + " arr = pf[numhdu].data.copy()\n", + "w.load_image(arr)\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "filename = 'http://data.astropy.org/photometry/spitzer_example_image.fits'\n", + "numhdu = 0\n", + "\n", + "# Loads NDData\n", + "# NOTE: memmap=False is needed for remote data on Windows.\n", + "# NOTE: Some file also requires unit to be explicitly set in CCDData.\n", + "from astropy.nddata import CCDData\n", + "ccd = CCDData.read(filename, hdu=numhdu, format='fits')\n", + "w.load_image(ccd)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A viewer will be shown after running the next cell.\n", + "In Jupyter Lab, you can split it out into a separate view by right-clicking on the viewer and then select\n", + "\"Create New View for Output\". Then, you can drag the new\n", + "\"Output View\" tab, say, to the right side of the workspace. Both viewers are connected to the same events." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Manipulating the widget with the API" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Use API methods for viewport control\n", + "\n", + "w.set_viewport(center=(0, 0))\n", + "# Note: zoom_level is not part of the API, use field of view instead\n", + "vport = w.get_viewport()\n", + "w.set_viewport(fov=vport['fov'] / 5) # Zoom in 5x by reducing FOV" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set a colormap\n", + "w.set_colormap('Greys_r')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get current cuts using API\n", + "print(\"Current cuts:\", w.get_cuts())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set cuts using API -- any astropy interval works\n", + "from astropy.visualization import AsymmetricPercentileInterval\n", + "w.set_cuts(AsymmetricPercentileInterval(1, 99.9))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get image data and access pixel values using API\n", + "img_data = w.get_image()\n", + "if hasattr(img_data, 'data'):\n", + " print(\"Pixel value at (240, 120):\", img_data.data[240, 120])\n", + " print(\"Data shape:\", img_data.data.shape)\n", + "else:\n", + " print(\"Pixel value at (240, 120):\", img_data[240, 120])\n", + " print(\"Data shape:\", img_data.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get cuts information using API\n", + "cuts = w.get_cuts()\n", + "if hasattr(cuts, 'upper_percentile'):\n", + " print(\"Upper percentile:\", cuts.upper_percentile)\n", + "else:\n", + " print(\"Current cuts:\", cuts)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The rest of the calls demonstrate how the widget API works. Comment/uncomment as needed. Feel free to experiment." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Programmatically center to (X, Y) on viewer using set_viewport\n", + "w.set_viewport(center=(1, 1))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: offset_by is not part of the astro-image-display-api\n", + "# Instead, get current center and offset from it\n", + "vport = w.get_viewport(sky_or_pixel='pixel')\n", + "current_center = vport['center']\n", + "if isinstance(current_center, tuple):\n", + " # Pixel coordinates\n", + " new_center = (current_center[0] + 100, current_center[1] + 100)\n", + " w.set_viewport(center=new_center)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from astropy.coordinates import SkyCoord\n", + "\n", + "# Change the values here if you are not using given\n", + "# example image.\n", + "ra_str = '01h13m23.193s'\n", + "dec_str = '+00d12m32.19s'\n", + "frame = 'galactic'\n", + "\n", + "# Programmatically center to SkyCoord on viewer using set_viewport\n", + "w.set_viewport(center=SkyCoord(ra_str, dec_str, frame=frame))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from astropy import units as u\n", + "import numpy as np\n", + "\n", + "# Change the values if needed.\n", + "deg_offset = 0.001 * u.deg\n", + "\n", + "# Note: offset_by with SkyCoord is not part of the astro-image-display-api\n", + "# Instead, get current center and offset from it\n", + "vport = w.get_viewport()\n", + "current_center = vport['center']\n", + "if hasattr(current_center, 'ra'):\n", + " # SkyCoord center - offset in RA/Dec\n", + " new_ra = current_center.ra + deg_offset / np.cos(current_center.dec)\n", + " new_dec = current_center.dec + deg_offset\n", + " new_center = SkyCoord(new_ra, new_dec, frame=current_center.frame)\n", + " w.set_viewport(center=new_center)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "current_center" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: zoom_level is not part of the astro-image-display-api\n", + "# Instead, show current field of view\n", + "vport = w.get_viewport()\n", + "print(\"Current field of view:\", vport['fov'])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: zoom() is not part of the astro-image-display-api\n", + "# Instead, set a specific field of view to zoom in/out\n", + "# Get current FOV and reduce it by half to zoom in 2x\n", + "vport = w.get_viewport()\n", + "current_fov = vport['fov']\n", + "if hasattr(current_fov, 'value'):\n", + " # Angular FOV with units\n", + " new_fov = current_fov / 2\n", + " w.set_viewport(fov=new_fov)\n", + "else:\n", + " # Pixel FOV \n", + " new_fov = current_fov / 2\n", + " w.set_viewport(fov=new_fov)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from astropy import units as u\n", + "w.set_viewport(fov=0.5 *u.degree)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Capture what viewer is showing and save RGB image.\n", + "w.save('test_bqplot.png', overwrite=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: stretch_options is not part of the astro-image-display-api\n", + "# The API uses astropy visualization stretch objects\n", + "from astropy.visualization import LinearStretch, LogStretch, SqrtStretch, PowerStretch\n", + "print(\"Available stretch types: LinearStretch, LogStretch, SqrtStretch, PowerStretch, etc.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get image stretch algorithm in use\n", + "print(w.get_stretch())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change the stretch using astropy visualization objects\n", + "from astropy.visualization import LogStretch\n", + "w.set_stretch(LogStretch())\n", + "print(w.get_stretch())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: autocut_options is not part of the astro-image-display-api\n", + "# The API uses astropy visualization interval objects\n", + "from astropy.visualization import MinMaxInterval, PercentileInterval, AsymmetricPercentileInterval\n", + "print(\"Available cuts types: MinMaxInterval, PercentileInterval, AsymmetricPercentileInterval, etc.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get image cut levels in use\n", + "print(w.get_cuts())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change the cuts by providing explicit low/high values\n", + "w.set_cuts((0, 100))\n", + "print(w.get_cuts())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Change the cuts with an astropy visualization interval object\n", + "from astropy.visualization import PercentileInterval\n", + "w.set_cuts(PercentileInterval(95))\n", + "print(w.get_cuts())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Marker/catalog interaction using the API" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Catalog/marker management using the astro-image-display-api" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a sample catalog to demonstrate catalog loading\n", + "import numpy as np\n", + "from astropy.table import Table\n", + "\n", + "# Create some sample marker positions\n", + "n_markers = 5\n", + "x_coords = np.random.uniform(50, 200, n_markers)\n", + "y_coords = np.random.uniform(50, 150, n_markers)\n", + "sample_catalog = Table([x_coords, y_coords], names=['x', 'y'])\n", + "\n", + "# Load catalog using the API\n", + "w.load_catalog(sample_catalog, catalog_label='sample')\n", + "print(\"Loaded catalog with\", len(sample_catalog), \"entries\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Get catalog data using the API\n", + "retrieved_catalog = w.get_catalog(catalog_label='sample')\n", + "\n", + "# Display the catalog nicely\n", + "print('{:^8s} {:^8s}'.format('X', 'Y'))\n", + "for row in retrieved_catalog:\n", + " print('{:8.2f} {:8.2f}'.format(row['x'], row['y']))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Remove catalog using the API\n", + "w.remove_catalog(catalog_label='sample')\n", + "print(\"Removed sample catalog\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Programmatic catalog loading and styling using the astro-image-display-api." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Create sample catalogs with different styles\n", + "# First 2 points as red circles, rest as cyan crosses\n", + "red_catalog = Table([x_coords[:2], y_coords[:2]], names=['x', 'y'])\n", + "w.load_catalog(red_catalog, catalog_label='red_circles')\n", + "w.set_catalog_style(catalog_label='red_circles', color='red', shape='circle', size=50, linewidth=2)\n", + "\n", + "# Rest as cyan crosses\n", + "cyan_catalog = Table([x_coords[2:], y_coords[2:]], names=['x', 'y'])\n", + "w.load_catalog(cyan_catalog, catalog_label='cyan_crosses')\n", + "w.set_catalog_style(catalog_label='cyan_crosses', color='cyan', shape='cross', size=20)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Clear existing catalogs and load new one with SkyCoord\n", + "try:\n", + " w.remove_catalog(catalog_label='red_circles')\n", + "except ValueError:\n", + " pass # Catalog doesn't exist\n", + "try:\n", + " w.remove_catalog(catalog_label='cyan_crosses')\n", + "except ValueError:\n", + " pass # Catalog doesn't exist\n", + "\n", + "# Create catalog with SkyCoord coordinates \n", + "from astropy.coordinates import SkyCoord\n", + "from astropy import units as u\n", + "\n", + "# Convert x,y to approximate RA/Dec for demonstration\n", + "ra_vals = (x_coords / 100 + 19.5) * u.deg # Approximate RA\n", + "dec_vals = (y_coords / 100 + 0.2) * u.deg # Approximate Dec\n", + "skycoord_table = Table([SkyCoord(ra_vals, dec_vals, frame=frame)], names=['coord'])\n", + "\n", + "w.load_catalog(skycoord_table, catalog_label='skycoord_markers', use_skycoord=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Clear all catalogs using the API\n", + "try:\n", + " w.remove_catalog(catalog_label='skycoord_markers')\n", + " print(\"Cleared all markers using catalog API\")\n", + "except ValueError:\n", + " print(\"No skycoord_markers catalog to remove\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Using widgets to drive image display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Generate random \"stars\" using the image from the API\n", + "import numpy as np\n", + "from astropy.table import Table\n", + "\n", + "# Maximum number of \"stars\" to generate randomly\n", + "max_stars = 1000\n", + "\n", + "# Number of pixels from edge to avoid\n", + "dpix = 20\n", + "\n", + "# Get image data using API\n", + "img_data = w.get_image()\n", + "if hasattr(img_data, 'shape'):\n", + " shape = img_data.shape\n", + "elif hasattr(img_data, 'data') and hasattr(img_data.data, 'shape'):\n", + " shape = img_data.data.shape\n", + "else:\n", + " # Fallback\n", + " shape = (256, 256)\n", + " \n", + "# Random \"stars\" generated \n", + "bad_locs = np.random.randint(\n", + " dpix, high=shape[1] - dpix, size=[max_stars, 2])\n", + "\n", + "# Only want those not near the edges\n", + "mask = ((dpix < bad_locs[:, 0]) &\n", + " (bad_locs[:, 0] < shape[0] - dpix) &\n", + " (dpix < bad_locs[:, 1]) &\n", + " (bad_locs[:, 1] < shape[1] - dpix))\n", + "locs = bad_locs[mask]\n", + "\n", + "# Put them in table\n", + "t = Table([locs[:, 1], locs[:, 0]], names=('x', 'y'))\n", + "print(f\"Generated {len(t)} random star positions\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load the \"stars\" as a catalog\n", + "w.load_catalog(t, catalog_label='stars')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following illustrates how to control number of markers displayed using interactive widget from `ipywidgets`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import ipywidgets as ipyw\n", + "\n", + "# Set catalog style and create function to control display\n", + "w.set_catalog_style(catalog_label='stars', color='red', shape='circle', size=10, linewidth=2)\n", + "output = ipyw.Output()\n", + "# Show the slider widget.\n", + "# Define a function to control marker display using catalog API\n", + "def show_circles(n):\n", + " \"\"\"Show subset of stars using catalog API.\"\"\"\n", + " # Remove existing catalog\n", + " try:\n", + " w.remove_catalog(catalog_label='stars')\n", + " except ValueError as exc:\n", + " raise exc # Catalog doesn't exist\n", + " \n", + " # Create subset catalog\n", + " t2show = t[:n]\n", + " w.load_catalog(t2show, catalog_label='stars')\n", + " w.set_catalog_style(catalog_label='stars', color='red', shape='circle', size=10, linewidth=2)\n", + " \n", + " with output:\n", + " print('Displaying {} markers...'.format(len(t2show)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We redisplay the image widget below above the slider. Note that the slider affects both this view of the image widget and the one near the top of the notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from IPython.display import display\n", + "\n", + "import ipywidgets as ipyw\n", + "from ipywidgets import interactive\n", + "\n", + "\n", + "# Show the slider widget.\n", + "slider = interactive(show_circles,\n", + " n=ipyw.IntSlider(min=0,max=len(t),step=1,value=0, continuous_update=False))\n", + "display(w, slider, output)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now, use the slider. The chosen `n` represents the first `n` \"stars\" being displayed." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Interactive features like click to center are astrowidgets-specific and not part of the astro-image-display-api" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following cell changes the visibility or position of the cursor info bar.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "w.cursor = 'top' # 'top', 'bottom', None\n", + "print(w.cursor)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: click_center is not part of the astro-image-display-api\n", + "# This feature is bqplot-specific and not available through the API\n", + "print(\"Click to center is a bqplot-specific feature not in the API\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Interactive marking is bqplot-specific\n", + "print(\"Interactive marking features are bqplot-specific\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Note: start_marking/is_marking is not part of the astro-image-display-api\n", + "# Interactive marking is bqplot-specific\n", + "print(\"Interactive marking is a bqplot-specific feature not in the API\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Interactive marking is not part of the astro-image-display-api\n", + "# The API uses catalog-based marking instead\n", + "print(\"Interactive marking is a bqplot-specific feature not in the API\")\n", + "print(\"Use catalog-based marking instead with load_catalog()\")" + ] + } + ], + "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.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/setup.cfg b/setup.cfg index 02ee833..629afc0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ zip_safe = False packages = find: install_requires = astropy - astro-image-display-api>=0.1.2 + astro-image-display-api>=0.2.1 ginga>=3.4 pillow ipywidgets>=8