diff --git a/.gitignore b/.gitignore index 4c927b00..76939f9e 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ ipyaladin/labextension/ package.json package-lock.json js/yarn.lock +js/.yarn # OS X .DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..c945db77 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Memo on sections + +* **Added** for new features. +* **Changed** for changes in existing functionality. +* **Deprecated** for soon-to-be removed features. +* **Removed** for now removed features. +* **Fixed** for any bug fixes. +* **Security** in case of vulnerabilities. + +## [0.2.2] + +### Added + +* the `height` parameter can now be called at instantiation to shape the ipyaladin widget +* there is now a right-click menu with the following options: + * take snapshot + * add + * new image layer + * new catalog layer + * load local file + * FITS Image + * FITS MOC + * VOTable + * What is this? + * HiPS2FITS cutout + * Select sources +* the attribute "show_simbad_pointer_control" can now be set to `True` at the instantiation of the widget + +### Fixed + +* compatible with JupyterLab4 diff --git a/README.md b/README.md index d49646e6..a7db7c18 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # ipyaladin -## Description - A bridge between Jupyter and Aladin Lite, enabling interactive sky visualization in IPython notebooks. - -![ipyaladin example](ipyaladin-screenshot.png) - With a couple of lines, you can display Aladin Lite, center it on the target of your choice, and overlay an Astropy table: -![ipyaladin example](ipyaladin-screencast.gif) +![ipyaladin example](assets/ipyaladin-screencast.gif) + +- [ipyaladin](#ipyaladin) + - [Examples](#examples) + - [Installation](#installation) + - [New features corner](#new-features-corner) + - [Development installation](#development-installation) ## Examples @@ -37,9 +38,13 @@ Additionny, for a jupyterlab usage you will need to: There is also a conda package that can be installed with: - conda install -c bmatthieu3 ipyaladin==0.2.1 + conda install -c bmatthieu3 ipyaladin==0.2.2 -## Development +## New features corner + +![new_features](assets/new_features.gif) + +## Development installation First, make sure you have installed jupyter on your python environnement: `pip install jupyter`. For a development installation [Node.js](https://nodejs.org) and [Yarn version 1](https://classic.yarnpkg.com/) are also required, @@ -47,11 +52,17 @@ For a development installation [Node.js](https://nodejs.org) and [Yarn version 1 git clone https://github.com/cds-astro/ipyaladin.git cd ipyaladin npm install yarn + cd js + npm install + cd .. pip install -e . + +For Jupyter Notebook, do + jupyter nbextension install --py --symlink --overwrite --sys-prefix ipyaladin jupyter nbextension enable --py --sys-prefix ipyaladin -When actively developing your extension for JupyterLab, you will need to run this command too: +For JupyterLab, you will need to run this command too: jupyter labextension develop --overwrite ipyaladin diff --git a/ipyaladin-screencast.gif b/assets/ipyaladin-screencast.gif similarity index 100% rename from ipyaladin-screencast.gif rename to assets/ipyaladin-screencast.gif diff --git a/ipyaladin-screenshot.png b/assets/ipyaladin-screenshot.png similarity index 100% rename from ipyaladin-screenshot.png rename to assets/ipyaladin-screenshot.png diff --git a/assets/new_features.gif b/assets/new_features.gif new file mode 100644 index 00000000..4217e6ba Binary files /dev/null and b/assets/new_features.gif differ diff --git a/conda-recipe/meta.yaml b/conda-recipe/meta.yaml index 1b308a03..a137425b 100644 --- a/conda-recipe/meta.yaml +++ b/conda-recipe/meta.yaml @@ -1,5 +1,5 @@ {% set name = "ipyaladin" %} -{% set version = "0.2.1" %} +{% set version = "0.2.2" %} package: name: "{{ name|lower }}" diff --git a/conda-recipe/readme b/conda-recipe/readme index 413b5e9e..a4337d4b 100644 --- a/conda-recipe/readme +++ b/conda-recipe/readme @@ -18,9 +18,9 @@ conda skeleton pypi --extra-specs jupyter-packaging ipyaladin // change the meta.yaml in ipyaladin/ cp post-link.* pre-unlink.* ipyaladin conda build ipyaladin --output-folder distrib -conda convert --platform all distrib/linux-64/ipyaladin-0.2.1-py39_0.tar.bz2 -o distrib +conda convert --platform all distrib/linux-64/ipyaladin-0.2.2-py39_0.tar.bz2 -o distrib anaconda login -anaconda upload distrib/osx-64/ipyaladin-0.2.1-py39_0.tar.bz2 -anaconda upload distrib/linux-64/ipyaladin-0.2.1-py39_0.tar.bz2 +anaconda upload distrib/osx-64/ipyaladin-0.2.2-py39_0.tar.bz2 +anaconda upload distrib/linux-64/ipyaladin-0.2.2-py39_0.tar.bz2 rm -r distrib ipyaladin \ No newline at end of file diff --git a/examples/1_Getting_Started.ipynb b/examples/1_Getting_Started.ipynb index 3198c1ed..38c33422 100644 --- a/examples/1_Getting_Started.ipynb +++ b/examples/1_Getting_Started.ipynb @@ -31,6 +31,7 @@ " target=\"galactic center\",\n", " coo_frame=\"galactic\",\n", " fov=40,\n", + " height=600\n", ")\n", "aladin" ] diff --git a/ipyaladin/_version.py b/ipyaladin/_version.py index 59223113..e4eeb1ca 100644 --- a/ipyaladin/_version.py +++ b/ipyaladin/_version.py @@ -1,4 +1,4 @@ # Module version -__version__ = '0.2.1' +__version__ = '0.2.2' -NPM_PACKAGE_RANGE='^0.2.1' +NPM_PACKAGE_RANGE='^0.2.2' diff --git a/ipyaladin/aladin_widget.py b/ipyaladin/aladin_widget.py index 1c9934d5..e69dca31 100644 --- a/ipyaladin/aladin_widget.py +++ b/ipyaladin/aladin_widget.py @@ -1,12 +1,63 @@ import ipywidgets as widgets from traitlets import (Float, Unicode, Bool, List, Dict, default) from ._version import NPM_PACKAGE_RANGE +import math # See js/lib/example.js for the frontend counterpart to this file. @widgets.register class Aladin(widgets.DOMWidget): - """An example widget.""" + """An instance of the Aladin widget. + + Adaptative attributes can be updated later. The other ones can only + be written when creating the widget instance, i.e. when calling Aladin() + + ... + Attributes + ---------- + + fov : float, default: 60 + The desired initial field of view, expressed in degrees. + adaptative + target : string, default: "0 +0" + The desired target. + adaptative + coo_frame : string, default: "J2000" + Reference frame. + adaptative + survey : string, default: "P/DSS2/color" + Name of the survey. + adaptative + ... + height : float, default: 400 + Height of the Aladin widget in pixels + reticle_size : float, default: 22 + Size of the reticle. + reticle_color : string, default: "rgb(178, 50, 178)" + The color of the reticle. + show_reticle : bool, default: True + Controls wether a reticle is present in the middle of the view + show_zoom_control : bool, default: True + show_fullscreen_control = bool, default: False + Wether the fullscreen button appears in the top right corner + Defaults to False because this does not work in retrolab. + Can safely be turned to True in Jupyterlab. + show_layers_control : bool, default: True + show_goto_control, bool, default: True + show_simbad_pointer_control : bool, default: True + Controls the quick search tool apparition on the left + side of the view. + show_share_control : bool, default: False + Controls the apparition of the share button in the bottom + right corner. This button opens a popup with a sharable link + to an Aladin previewer that starts with the actual state of the + view. + show_context_menu : bool, default: True + Controls wether a right click will open a menu. This menu is documented + here # TODO add link to documentation when it will exist. + + TODO: finish docstring + """ # Name of the widget view class in front-end _view_name = Unicode('AladinView').tag(sync=True) @@ -45,7 +96,9 @@ class Aladin(widgets.DOMWidget): show_fullscreen_control = Bool(False).tag(sync=True, o=True) show_layers_control = Bool(True).tag(sync=True, o=True) show_goto_control = Bool(True).tag(sync=True, o=True) + show_simbad_pointer_control = Bool(True).tag(sync=True, o=True) show_share_control = Bool(False).tag(sync=True, o=True) + show_context_menu = Bool(True).tag(sync=True, o=True) show_catalog = Bool(True).tag(sync=True, o=True) show_frame = Bool(True).tag(sync=True, o=True) show_coo_grid = Bool(False).tag(sync=True, o=True) @@ -55,6 +108,9 @@ class Aladin(widgets.DOMWidget): options = List(trait=Unicode).tag(sync=True) + # this sets the height of the widget + height = Float(400).tag(sync=True) + # the following values are used in the classe's functions # values used in the add_catalog_from_URL function @@ -159,11 +215,35 @@ def add_moc_from_dict(self, moc_dict, moc_options = {}): # 2 - It seems that the list.append() method does not work with traitlets, # the affectation of the columns must be done at once by using a buffer. def add_table(self, table): - """ load a VOTable -already accessible on the python side- into the widget - Args: - table: votable object""" + """ Load a table into the widget. + + Parameters + ---------- + table : astropy.table.table.QTable or astropy.table.table.Table + table that must contain coordinates information + + Examples + -------- + Cell 1: + >>> from ipyaladin import Aladin + >>> from astropy.table import QTable + >>> aladin = Aladin(fov=2, target='M1') + >>> aladin + Cell 2: + >>> ra = [83.63451584700, 83.61368056017, 83.58780251600] + >>> dec = [22.05652591227, 21.97517807639, 21.99277764451] + >>> name = ["Gaia EDR3 3403818589184411648", + "Gaia EDR3 3403817661471500416", + "Gaia EDR3 3403817936349408000", + ] + >>> table = QTable([ra, dec, name], + names=("ra", "dec", "name"), + meta={"name": "my sample table"}) + >>> aladin.add_table(table) + And the table should appear in the output of Cell 1! + """ - # theses library must be installed, and are used in votable operations + # this library must be installed, and is used in votable operations # http://www.astropy.org/ import astropy @@ -179,7 +259,13 @@ def add_table(self, table): if isinstance(item, bytes): row_data.append(item.decode('utf-8')) else: + if not isinstance(item, str): + # replace NaN by " ", this is a quick fix + # and should be questioned when doing a rework + # of this function + item = " " if math.isnan(item) else item row_data.append(item) + table_columns.append(row_data) self.table_columns = table_columns diff --git a/js/lib/jupyter-aladin.js b/js/lib/jupyter-aladin.js index a8a30f49..8d6966c7 100644 --- a/js/lib/jupyter-aladin.js +++ b/js/lib/jupyter-aladin.js @@ -2,7 +2,7 @@ import { DOMWidgetModel, DOMWidgetView } from '@jupyter-widgets/base'; // Allow us to use the DOMWidgetView base class for our models/views. // Additionnaly, this is where we put by default all the external libraries // fetched by using webpack (see webpack.config.js file). -var _ = require("underscore"); +let _ = require("underscore"); // The sole purpose of this module is to load the css stylesheet when the first instance // of the AladinLite widget const loadScript = (FILE_URL, async = true, type = "text/javascript") => { @@ -30,7 +30,7 @@ const loadScript = (FILE_URL, async = true, type = "text/javascript") => { } }); }; -var AladinLiteJS_Loader = loadScript("https://code.jquery.com/jquery-3.6.1.min.js") +let AladinLiteJS_Loader = loadScript("https://code.jquery.com/jquery-3.6.1.min.js") .then(() => { return loadScript("https://aladin.u-strasbg.fr/AladinLite/api/v3/latest/aladin.js") }) .then(async () => { await A.init; @@ -64,14 +64,14 @@ export class AladinModel extends DOMWidgetModel { _model_module : 'ipyaladin', _view_module : 'ipyaladin', - _model_module_version : '0.2.1', - _view_module_version : '0.2.1', + _model_module_version : '0.2.2', + _view_module_version : '0.2.2', }; } } -var idxView = 0; +let idxView = 0; export class AladinView extends DOMWidgetView { render() { // We load the aladin lite script @@ -92,14 +92,15 @@ export class AladinView extends DOMWidgetView { this.div = document.createElement('div'); this.div.id = 'aladin-lite-div' + parseInt(idxView); idxView += 1; - // TODO: should this style be somehow inherited from the widget Layout attribute? - this.div.setAttribute("style","width:100%;height:400px;"); + // creates the div section, height is fixed by the user or defaults to 400px + let height = this.model.get("height"); + this.div.setAttribute("style","width:100%;height:" + height + "px;"); // We get the options set on the python side and create an instance of the AladinLite object. - var aladin_options = {}; - var opt = this.model.get('options'); - for(var i = 0; i < opt.length; i++) { - aladin_options[this.convert_pyname_to_jsname(opt[i])] = this.model.get(opt[i]); + let aladin_options = {}; + let options = this.model.get('options'); + for(let option of options) { + aladin_options[this.convert_pyname_to_jsname(option)] = this.model.get(option); } // Observer triggered when this.el has been changed @@ -132,106 +133,107 @@ export class AladinView extends DOMWidgetView { } convert_pyname_to_jsname(pyname) { - var i, temp= pyname.split('_'); - for(i=1; i { + if(!this.fov_py){ + this.fov_js = true; // fov MUST be cast into float in order to be sent to the model - that.model.set('fov', parseFloat(fov.toFixed(5))); + this.model.set('fov', parseFloat(fov.toFixed(5))); // Note: touch function must be called after calling the model's set method - that.touch(); + this.touch(); } else { - that.fov_py = false; + this.fov_py = false; } }); - this.al.on('positionChanged', function(position) { - if(!that.target_py) { - that.target_js = true; - that.model.set('target', '' + position.ra.toFixed(6) + ' ' + position.dec.toFixed(6)); - that.touch(); + this.al.on('positionChanged', (position) => { + if(!this.target_py) { + this.target_js = true; + this.model.set('target', '' + position.ra.toFixed(6) + ' ' + position.dec.toFixed(6)); + this.touch(); } else { - that.target_py = false; + this.target_py = false; } }); } model_events() { - var that = this; // Model's class parameters listeners - this.listenTo(this.model, 'change:fov', function () { - if(!that.fov_js){ - that.fov_py= true; - that.al.setFoV(that.model.get('fov')); + this.listenTo(this.model, 'change:fov', () => { + if(!this.fov_js){ + this.fov_py= true; + this.al.setFoV(this.model.get('fov')); } else { - that.fov_js= false; + this.fov_js= false; } }, this); - this.listenTo(this.model, 'change:target', function () { - if(!that.target_js){ - that.target_py= true; - that.al.gotoObject(that.model.get('target')); + this.listenTo(this.model, 'change:target', () => { + if(!this.target_js){ + this.target_py= true; + this.al.gotoObject(this.model.get('target')); } else { - that.target_js= false; + this.target_js= false; } }, this); - this.listenTo(this.model, 'change:coo_frame', function () { - that.al.setFrame(that.model.get('coo_frame')); + this.listenTo(this.model, 'change:coo_frame', () => { + this.al.setFrame(this.model.get('coo_frame')); }, this); - this.listenTo(this.model, 'change:survey', function () { - that.al.setImageSurvey(that.model.get('survey')); + this.listenTo(this.model, 'change:height', () => { + let height = this.model.get('height'); + this.div.setAttribute("style","width:100%;height:" + height + "px;"); }, this); - this.listenTo(this.model, 'change:overlay_survey', function () { - that.al.setOverlayImageLayer(that.model.get('overlay_survey')); + this.listenTo(this.model, 'change:survey', () => { + this.al.setImageSurvey(this.model.get('survey')); }, this); - this.listenTo(this.model, 'change:overlay_survey_opacity', function () { - that.al.getOverlayImageLayer().setAlpha(that.model.get('overlay_survey_opacity')); + this.listenTo(this.model, 'change:overlay_survey', () => { + this.al.setOverlayImageLayer(this.model.get('overlay_survey')); + }, this); + this.listenTo(this.model, 'change:overlay_survey_opacity', () => { + this.al.getOverlayImageLayer().setAlpha(this.model.get('overlay_survey_opacity')); }, this); // Model's functions parameters listeners - this.listenTo(this.model, 'change:votable_from_URL_flag', function(){ - that.al.addCatalog(A.catalogFromURL(that.model.get('votable_URL'), that.model.get('votable_options'))); + this.listenTo(this.model, 'change:votable_from_URL_flag', () => { + this.al.addCatalog(A.catalogFromURL(this.model.get('votable_URL'), this.model.get('votable_options'))); }, this); - this.listenTo(this.model, 'change:moc_from_URL_flag', function(){ - that.al.addMOC(A.MOCFromURL(that.model.get('moc_URL'), that.model.get('moc_options'))); + this.listenTo(this.model, 'change:moc_from_URL_flag', () => { + this.al.addMOC(A.MOCFromURL(this.model.get('moc_URL'), this.model.get('moc_options'))); }, this); - this.listenTo(this.model, 'change:moc_from_dict_flag', function(){ - that.al.addMOC(A.MOCFromJSON(that.model.get('moc_dict'), that.model.get('moc_options'))); + this.listenTo(this.model, 'change:moc_from_dict_flag', () => { + this.al.addMOC(A.MOCFromJSON(this.model.get('moc_dict'), this.model.get('moc_options'))); }, this); - this.listenTo(this.model, 'change:table_flag', function(){ - var cat = A.catalog({onClick: 'showTable'}); - that.al.addCatalog(cat); - cat.addSourcesAsArray(that.model.get('table_keys'), that.model.get('table_columns')) + this.listenTo(this.model, 'change:table_flag', () => { + let cat = A.catalog({onClick: 'showTable'}); + this.al.addCatalog(cat); + cat.addSourcesAsArray(this.model.get('table_keys'), this.model.get('table_columns')) }, this); - this.listenTo(this.model, 'change:overlay_from_stcs_flag', function() { - var overlay = A.graphicOverlay(that.model.get('overlay_options')); - that.al.addOverlay(overlay); - overlay.addFootprints(A.footprintsFromSTCS(that.model.get('stc_string'))); + this.listenTo(this.model, 'change:overlay_from_stcs_flag', () => { + let overlay = A.graphicOverlay(this.model.get('overlay_options')); + this.al.addOverlay(overlay); + overlay.addFootprints(A.footprintsFromSTCS(this.model.get('stc_string'))); }, this); - this.listenTo(this.model, 'change:listener_flag', function(){ - var type= that.model.get('listener_type'); - that.al.on(type, function(object) { + this.listenTo(this.model, 'change:listener_flag', () => { + let type= this.model.get('listener_type'); + this.al.on(type, function(object) { if (type==='select') { - var sources = object; + let sources = object; // first, deselect previously selected sources - for (var k=0; k { + this.al.select(); }); - this.listenTo(this.model, 'change:thumbnail_flag', function(){ - that.al.exportAsPNG(); + this.listenTo(this.model, 'change:thumbnail_flag', () => { + this.al.exportAsPNG(); }); - this.listenTo(this.model, 'change:color_map_flag', function() { - const cmap = that.model.get('color_map_name'); - that.al.getBaseImageLayer().setColormap(cmap); + this.listenTo(this.model, 'change:color_map_flag', () => { + const cmap = this.model.get('color_map_name'); + this.al.getBaseImageLayer().setColormap(cmap); }); } } diff --git a/js/package.json b/js/package.json index abecba98..811dfc19 100644 --- a/js/package.json +++ b/js/package.json @@ -31,7 +31,7 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "devDependencies": { - "@jupyterlab/builder": "^3.0.0", + "@jupyterlab/builder": "^3.0.0 || ^4.0.0", "rimraf": "^2.6.1", "webpack": "^5" }, diff --git a/pyproject.toml b/pyproject.toml index 37d68ccd..eee6fa5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["jupyter_packaging~=0.7.9", "jupyterlab~=3.0", "setuptools>=40.8.0", "wheel"] +requires = ["jupyter_packaging>=0.7.9", "jupyterlab>=3.0", "setuptools>=40.8.0", "wheel", "traitlets>=5.9.0", "traitlets>=5.9.0"] build-backend = "setuptools.build_meta" \ No newline at end of file