Source code for eazy.visualization

"""
Scripts for more interactive visualization of SEDs, etc.
"""
import numpy as np
import astropy.io.fits as pyfits
import astropy.wcs as pywcs

from . import utils

__all__ = ['EazyExplorer']

CUTOUT_URL = "https://grizli-cutout.herokuapp.com/thumb?all_filters=True&size=4&scl=1.0&asinh=True&filters=f115w-clear,f277w-clear,f444w-clear&rgb_scl=1.5,0.74,1.3&pl=2&ra={ra}&dec={dec}"

EXTRA_SLIDER_KWS = {'column':'id', 'label':'ID',
                    'min':0, 'max':10, 'step':1,
                    'value':[1,5], 'checked':[],
                    }

[docs] class EazyExplorer(object): def __init__(self, photoz, zout, extra_zout_columns=[], selection=None, extra_plots={}, extra_slider_kws=EXTRA_SLIDER_KWS): """ Generating a tool for interactive visualization of `eazy` outputs with the `dash` + `plotly` libraries. Parameters ---------- photoz : `~eazy.photoz.PhotoZ` The main ``PhotoZ`` object. zout : `astropy.table.Table` The `zout` output table with galaxy parameters. At a minimum, it must have columns ``id``, ``z_phot``, ``z_spec``, ``z_phot_chi2``, ``nusefilt``, ``restU``, ``restV``, ``restJ``, ``sfr``, ``mass``, ``ra``, ``dec``. extra_zout_columns : list Additional columns from ``zout`` to copy to the app ``df`` `pandas.DataFrame` object. selection : array-like Selection array on `zout` for catalog subset, can be integer indices, or a boolean array with the same length as `zout` extra_plots : dict Extra scatter plot definitions following >>> extra_plots = {'PlotName': (xcol, ycol, xlabel, ylabel, xr, yr)} where ``xcol`` and ``ycol`` are `str` column names in `zout`, ``xlabel`` and ``ylabel`` are `str` used for the plot labels and ``xr`` and ``yr`` are the default plot range tuples. Only ``xcol`` and ``ycol`` are required, and the others are computed if not provided. Note that ``xcol`` and ``ycol`` are added automatically from `zout` if necessary and don't need to be specified in `extra_zout_columns`. """ import pandas as pd from astropy.table import Table try: import dash from dash import dcc, html import plotly.express as px except ImportError: print('Failed to import dash & plotly, so the interactive tool' 'won\'t work.\n' 'Install with `pip install "dash>=2.5.1"` and also ' '`pip install jupyter_dash` for running a server ' 'through jupyter') uv = -2.5*np.log10(zout['restU']/zout['restV']) vj = -2.5*np.log10(zout['restV']/zout['restJ']) ssfr = zout['sfr']/zout['mass'] for c in ['z_phot', 'z_spec']: if hasattr(zout[c], 'mask'): zout[c].fill_value = -0.1 def fill_masked(_data, fill_value=None): if hasattr(_data, 'mask'): if fill_value is None: return _data.filled() else: return _data.filled(fill_value) else: return _data self.extra_slider_kws = extra_slider_kws if 'flux_radius' in zout.colnames: self.extra_slider_kws = {'column':'flux_radius', 'label':'R50', 'min':0, 'max':20, 'step':0.2, 'value':[1,10], 'checked':[], } #df = pd.DataFrame() df = Table() df['id'] = fill_masked(zout['id']) df['nusefilt'] = fill_masked(zout['nusefilt']) df['uv'] = fill_masked(uv) df['vj'] = fill_masked(vj) df['ssfr'] = fill_masked(np.log10(ssfr)) df['mass'] = fill_masked(np.log10(zout['mass'])) df['z_phot'] = fill_masked(zout['z_phot']) df['z_spec'] = np.clip(fill_masked(zout['z_spec']), -0.1, 12) df['ra'] = fill_masked(photoz.RA) df['dec'] = fill_masked(photoz.DEC) df['chi2'] = fill_masked(zout['z_phot_chi2']/zout['nusefilt']) self.zp = photoz.zp*1 for c in extra_zout_columns: if c in zout.colnames: df[c] = fill_masked(zout[c]) col = self.extra_slider_kws['column'] if col in zout.colnames: if col not in df.columns: df[col] = fill_masked(zout[col]) else: if col not in df.columns: df[col] = df['id'] _red_ix = np.argmax(photoz.pivot*(photoz.pivot < 3.e4)) self.DEFAULT_FILTER = photoz.flux_columns[_red_ix] ZP = photoz.param['PRIOR_ABZP']*1. fmin = 10**(-0.4*(33-ZP)) fmax = 10**(-0.4*(12-ZP)) #print('flux limits', fmin, fmax) for i, f in enumerate(photoz.flux_columns): key = f'mag_{f}' df[key] = ZP - 2.5*np.log10(np.clip(fill_masked(photoz.cat[f]*self.zp[i]), fmin, fmax)) # if hasattr(photoz.cat[f], 'mask'): # # df[key] = ZP - 2.5*np.log10(np.clip(photoz.cat[f].filled(-99), # fmin, fmax)) # else: # df[key] = ZP - 2.5*np.log10(np.clip(photoz.cat[f], # fmin, fmax)) df['mag'] = df[f'mag_{self.DEFAULT_FILTER}'] df['mag1mag2'] = df['mag']*0 self.extra_plots = {} for k in extra_plots: _plot_args = extra_plots[k] xcol, ycol = _plot_args[:2] if xcol not in df.columns: df[xcol] = fill_masked(zout[xcol]) if ycol not in df.columns: df[ycol] = fill_masked(zout[ycol]) if len(_plot_args) == 2: self.extra_plots[k] = (xcol, ycol, xcol, ycol, np.nanmin(xcol), np.nanmax(ycol)) elif len(_plot_args) == 4: self.extra_plots[k] = (*_plot_args, np.nanmin(xcol), np.nanmax(ycol)) elif len(_plot_args) == 6: self.extra_plots[k] = _plot_args else: print(f'Expected 2,4 or 6 elements in extra_plots[{k}], ' f'found {len(_plot_args)}') if selection is not None: self.df = df[selection].to_pandas() else: self.df = df.to_pandas() self.zout = zout self.photoz = photoz self.ZMAX = photoz.zgrid.max() self.MAXNFILT = photoz.nusefilt.max() @property def ra_bounds(self): return (self.df['ra'].max(), self.df['ra'].min()) @property def dec_bounds(self): return (self.df['dec'].min(), self.df['dec'].max())
[docs] def get_filters_from_api(self): """ Query available HST/JWST filters from the heroku API """ import urllib import urllib.request import json import PIL.Image _ra = self.ra_bounds _dec = self.dec_bounds cosd = np.cos(np.mean(_dec)/180*np.pi) dx = np.diff(_ra)[0]*cosd dy = np.diff(_dec)[0] si = np.maximum(dx, dy)*3600 furl = 'https://grizli-cutout.herokuapp.com/overlap?' furl += f'ra={np.mean(_ra)}&dec={np.mean(_dec)}&size={si}' with open('/tmp/thumb.log','a') as fp: fp.write(furl+'\n') with urllib.request.urlopen(furl) as url: olap = json.loads(url.read().decode()) return olap
[docs] def make_dash_app(self, template='plotly_white', server_mode='external', port=8050, app=None, app_type='jupyter', plot_height=680, external_stylesheets=['https://codepen.io/chriddyp/pen/bWLwgP.css'], infer_proxy=False, slider_width=140, cutout_hdu=None, cutout_rgb=None, cutout_size=10, api_filters=None, api_size=2, api_args='', PLOT_TYPES=['zphot-zspec', 'Mag-redshift', 'Mag-color', 'redshift-color', 'Mass-redshift', 'UVJ', 'RA/Dec', 'UV-redshift', 'chi2-redshift'], get_content=False, fitsmap_url=None, cutout_url=CUTOUT_URL): """ Create a Plotly/Dash app for interactive exploration Parameters ---------- template : str `plotly` style `template <https://plotly.com/python/templates/#specifying-themes-in-graph-object-figures>`_. server_mode, port : str, int If not `None`, the app server is started with `app.run_server(mode=server_mode, port=port)`. app_type : str If ``jupyter`` then `app = jupyter_dash.JupyterDash()`, else `app = dash.Dash()` plot_height : int Height in pixels of the scatter and SED+P(z) plot windows. infer_proxy : bool Run `JupyterDash.infer_jupyter_proxy_config()`, before app initilization, e.g., for running on GoogleColab. Returns ------- app : object App object following `app_type`. """ import dash from dash import dcc, html, callback from dash.dependencies import Input, Output import plotly.express as px import matplotlib.pyplot as plt from urllib.parse import urlparse, parse_qsl, urlencode import astropy.wcs as pywcs self.template = template self.plot_height = plot_height self.slider_width = slider_width self.cutout_size = cutout_size pivots = {} for f, w in zip(self.photoz.flux_columns, self.photoz.pivot): pivots[f] = w if app is None: if app_type == 'dash': app = dash.Dash(__name__, external_stylesheets=external_stylesheets) else: from jupyter_dash import JupyterDash if infer_proxy: JupyterDash.infer_jupyter_proxy_config() app = JupyterDash(__name__, external_stylesheets=external_stylesheets) for _t in self.extra_plots: PLOT_TYPES.append(_t) COLOR_TYPES = ['z_phot', 'z_spec', 'mass', 'sSFR', 'chi2'] self.COLOR_TYPES = COLOR_TYPES self.PLOT_TYPES = PLOT_TYPES self.cutout_data = None self.cutout_wcs = None self.api_filters = api_filters self.fitsmap_url = fitsmap_url self.cutout_url = cutout_url #_title = f"{self.photoz.param['MAIN_OUTPUT_FILE']}" #_subhead = f"Nobj={self.photoz.NOBJ} Nfilt={self.photoz.NFILT}" _title = [html.Strong(self.photoz.param['MAIN_OUTPUT_FILE']), ' / N', html.Sub('obj'), f'={self.photoz.NOBJ}', ' / N', html.Sub('filt'), f'={self.photoz.NFILT}', ] slider_row_style={'width': '90%', 'float':'left', 'margin-left':'10px'} slider_container = {'width': f'{slider_width}px', 'margin-left':'-25px'} check_kwargs = dict(style={'text-align':'center', 'height':'14pt', 'margin-top':'-20px'}) # bool_options = {'has_zspec': 'z_spec > 0', # 'use': 'Use == 1'} if cutout_hdu is not None: self.cutout_wcs = pywcs.WCS(cutout_hdu.header, relax=True) if cutout_rgb is None: self.cutout_data = cutout_hdu.data else: self.cutout_data = np.flipud(plt.imread(cutout_rgb)) print('xxx', self.cutout_data.shape) cutout_div = html.Div([ dcc.Graph(id='cutout-figure', style={}) ], style={'right':'70px', 'width':'120px', 'height':'120px', 'border':'1px solid rgb(200,200,200)', 'top':'10px', 'position':'absolute'}) cutout_target = 'figure' elif api_filters is not None: cutout_div = html.Div([ dcc.Graph(id='cutout-figure', style={}) ], style={'right':'70px', 'width':'120px', 'height':'120px', 'border':'1px solid rgb(200,200,200)', 'top':'10px', 'position':'absolute'}) cutout_target = 'figure' self.cutout_data = None else: cutout_div = html.Div(id='cutout-figure', style={'left':'1px', 'width':'1px', 'height':'1px', 'bottom':'1px', 'position':'absolute'}) self.cutout_data = None cutout_target = 'children' ####### Main content content = [ # Selectors html.Div([ dcc.Location(id='url', refresh=False), html.Div([ html.Div(_title, id='title-bar', style={'float':'left', 'margin-top':'4pt'}), html.Div([ html.Div([dcc.Dropdown(id='plot-type', options=[{'label': i, 'value': i} for i in PLOT_TYPES], value=PLOT_TYPES[0], clearable=False, style={'width':'120px', 'margin-right':'5px', 'margin-left':'5px', 'font-size':'8pt'}), ], style={'float':'left'}), html.Div([dcc.Dropdown(id='color-type', options=[{'label': i, 'value': i} for i in COLOR_TYPES], value='sSFR', clearable=False, style={'width':'80px', 'margin-right':'5px', 'font-size':'8pt'}), ], style={'display':'inline-block', 'margin-left':'10px'}), ], style={'float':'right'}), ], style=slider_row_style), html.Div([ html.Div([dcc.Dropdown(id='mag-filter', options=[{'label': i, 'value': i} for i in self.photoz.flux_columns], value=self.DEFAULT_FILTER, style={'width': f'{slider_width-45}px', 'margin-right':'20px', 'font-size':'8pt'}, clearable=False), ], style={'float':'left'}), html.Div([ dcc.RangeSlider(id='mag-slider', min=12, max=32, step=0.2, value=[18, 27], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='mag-checked', options=[{'label':'AB mag', 'value':'checked'}], value=['checked'], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), # Mag2 for color html.Div([dcc.Dropdown(id='mag2-filter', options=[{'label': i, 'value': i} for i in self.photoz.flux_columns], value=self.photoz.flux_columns[0], style={'width': f'{slider_width-45}px', 'margin-right':'20px', 'font-size':'8pt'}, clearable=False), ], style={'float':'left'}), html.Div([ dcc.RangeSlider(id='color-slider', min=-2, max=5, step=0.1, value=[-1, 3], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='color-checked', options=[{'label':'Mag1 - Mag2', 'value':'checked'}], value=[], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), html.Div([ dcc.RangeSlider(id='nfilt-slider', min=1, max=self.MAXNFILT, step=1, value=[3, self.MAXNFILT], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='nfilt-checked', options=[{'label':'nfilt', 'value':'checked'}], value=['checked'], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), ], style=slider_row_style), html.Div([ html.Div([ dcc.RangeSlider(id='zphot-slider', min=-0.5, max=self.ZMAX, step=0.1, value=[0, self.ZMAX], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='zphot-checked', options=[{'label':'z_phot', 'value':'checked'}], value=['checked'], **check_kwargs), ], style=dict(float='left', **slider_container)), html.Div([ dcc.RangeSlider(id='zspec-slider', min=-0.5, max=self.ZMAX, step=0.1, value=[-0.5, 6.5], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='zspec-checked', options=[{'label':'z_spec', 'value':'checked'}], value=['checked'], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), html.Div([ dcc.RangeSlider(id='mass-slider', min=7, max=13, step=0.1, value=[8, 11.8], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='mass-checked', options=[{'label':'mass', 'value':'checked'}], value=['checked'], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), html.Div([ dcc.RangeSlider(id='chi2-slider', min=0, max=20, step=0.1, value=[0, 6], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='chi2-checked', options=[{'label':'chi2', 'value':'checked'}], value=[], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), # Customizable slider html.Div([ dcc.RangeSlider(id='extra-slider', min=self.extra_slider_kws['min'], max=self.extra_slider_kws['max'], step=self.extra_slider_kws['step'], value=self.extra_slider_kws['value'], updatemode='drag', tooltip={"placement":'left'}, marks=None), dcc.Checklist(id='extra-checked', options=[{'label':self.extra_slider_kws['label'], 'value':'checked'}], value=self.extra_slider_kws['checked'], **check_kwargs), ], style=dict(display='inline-block', **slider_container)), # Boolean dropdown # dcc.Dropdown(id='bool-checks', # options=[{'label': self.bool_options[k], # 'value': k} # for k in self.bool_options], # value=[], # multi=True, # style={'width':'100px', # 'display':'inline-block', # 'margin-left':'0px', # 'font-size':'8pt'}, # clearable=True), ], style=slider_row_style), ], style={'float':'left','width': '55%'}), # Object-level controls html.Div([ html.Div([ html.Div('ID / RA,Dec.', style={'float':'left', 'width':'100px', 'margin-top':'5pt'}), dcc.Input(id='id-input', type='text', style={'width':'120px', 'padding':'2px', 'display':'inline', 'font-size':'8pt'}), html.Div(children='', id='match-sep', style={'margin':'5pt', 'display':'inline', 'width':'50px', 'font-size':'8pt'}), dcc.RadioItems(id='sed-unit-selector', options=[{'label': i, 'value': i} for i in ['Fλ', 'Fν', 'νFν']], value='Fλ', labelStyle={'display':'inline', 'padding':'3px', }, style={'display':'inline', 'width':'130px'}) ], style={'width':'260pix', 'float':'left', 'margin-right':'20px'}), ]), html.Div([ # html.Div([ # ], style={'width':'120px', 'float':'left'}), html.Div(id='object-info', children='ID: ', style={'margin':'auto','margin-top':'10px', 'font-size':'10pt'}) ], style={'float':'right', 'width': '45%'}), # Plots html.Div([# Scatter plot dcc.Graph(id='sample-selection-scatter', hoverData={'points': [{'customdata': (self.df['id'][0], 1.0, -9.0)}]}, style={'width':'95%'}) ], style={'float':'left', 'height':'70%', 'width':'49%'}), html.Div([# SED dcc.Graph(id='object-sed-figure', style={'width':'95%'}) ], style={'float':'right', 'width':'49%', 'height':'70%'}), cutout_div ] if get_content: return content ##### Callback functions @app.callback( Output('url', 'search'), [Input('plot-type', 'value'), Input('color-type', 'value'), Input('mag-filter', 'value'), Input('mag-slider', 'value'), Input('mag2-filter', 'value'), Input('color-slider', 'value'), Input('mass-slider', 'value'), Input('chi2-slider', 'value'), Input('nfilt-slider', 'value'), Input('zphot-slider', 'value'), Input('zspec-slider', 'value'), Input('id-input', 'value')]) def update_url_state(plot_type, color_type, mag_filter, mag_range, mag2_filter, color_range, mass_range, chi2_range, nfilt_range, zphot_range, zspec_range, id_input): search = f'?plot_type={plot_type}&color_type={color_type}' search += f'&mag_filter={mag_filter}' search += f'&mag={mag_range[0]},{mag_range[1]}' search += f'&mag2_filter={mag2_filter}' search += f'&color={color_range[0]},{color_range[1]}' search += f'&mass={mass_range[0]},{mass_range[1]}' search += f'&chi2={chi2_range[0]},{chi2_range[1]}' search += f'&nfilt={nfilt_range[0]},{nfilt_range[1]}' search += f'&zphot={zphot_range[0]},{zphot_range[1]}' search += f'&zspec={zspec_range[0]},{zspec_range[1]}' if id_input is not None: search += f"&id={id_input.replace(' ', '%20')}" return search @app.callback([Output('plot-type', 'value'), Output('color-type', 'value'), Output('mag-filter', 'value'), Output('mag-slider', 'value'), Output('mag2-filter', 'value'), Output('color-slider', 'value'), Output('mass-slider', 'value'), Output('chi2-slider', 'value'), Output('nfilt-slider', 'value'), Output('zphot-slider', 'value'), Output('zspec-slider', 'value'), Output('id-input', 'value'), ],[ Input('url', 'search') ]) def set_state_from_url(search): plot_type = PLOT_TYPES[0] color_type = 'sSFR' mag_filter = self.DEFAULT_FILTER mag_range = [18, 27] mag2_filter = self.DEFAULT_FILTER color_range = [-0.5, 3] mass_range = [8, 11.6] chi2_range = [0, 4] nfilt_range = [1, self.MAXNFILT] zphot_range = [0, self.ZMAX] zspec_range = [-0.5, 6.5] id_input = None # if '?' not in href: if not search: return (plot_type, color_type, mag_filter, mag_range, mag2_filter, color_range, mass_range, chi2_range, nfilt_range, zphot_range, zspec_range, id_input) # search = href.split('?')[1] params = search.split('&') for p in params: if 'plot_type' in p: val = p.split('=')[1] if val in PLOT_TYPES: plot_type = val elif 'color_type' in p: val = p.split('=')[1] if val in COLOR_TYPES: color_type = val elif 'mag_filter' in p: val = p.split('=')[1] if val in self.photoz.flux_columns: mag_filter = val elif 'mag=' in p: try: vals = [float(v) for v in p.split('=')[1].split(',')] if len(vals) == 2: mag_range = vals except ValueError: pass elif 'mag2_filter' in p: val = p.split('=')[1] if val in self.photoz.flux_columns: mag2_filter = val elif 'color=' in p: try: vals = [float(v) for v in p.split('=')[1].split(',')] if len(vals) == 2: color_range = vals except ValueError: pass elif 'mass' in p: try: vals = [float(v) for v in p.split('=')[1].split(',')] if len(vals) == 2: mass_range = vals except ValueError: pass elif 'nfilt=' in p: try: vals = [int(v) for v in p.split('=')[1].split(',')] if len(vals) == 2: nfilt_range = vals except ValueError: pass elif 'zspec' in p: try: vals = [float(v) for v in p.split('=')[1].split(',')] if len(vals) == 2: zspec_range = vals except ValueError: pass elif 'zphot' in p: try: vals = [float(v) for v in p.split('=')[1].split(',')] if len(vals) == 2: zphot_range = vals except ValueError: pass elif 'id' in p: try: id_input = p.split('=')[1].replace('%20', ' ') except ValueError: id_input = None if not id_input: id_input = None return (plot_type, color_type, mag_filter, mag_range, mag2_filter, color_range, mass_range, chi2_range, nfilt_range, zphot_range, zspec_range, id_input) @app.callback( Output('nfilt-checked', 'label'), Input('sample-selection-scatter', 'selectedData')) def update_selected_data(selectedData): if selectedData is not None: #print('selectedData', len(selectedData['points'])) #print('y0', self.df['in_selectedData'].sum()) if len(selectedData['points']) == 1: # Reset self.df['in_selectedData'] = self.df['z_phot'] > 0 print('One point selected: reset selection!') elif len(selectedData['points']) > 0: selected_ids = [] for p in selectedData['points']: if "customdata" in p.keys(): selected_ids.append(p['customdata'][0]) #print(len(selected_ids), p) if len(selected_ids) == 1: # Reset self.df['in_selectedData'] = self.df['z_phot'] > 0 print('One point selected: reset selection!') elif len(selected_ids) > 0: self.df['in_selectedData'] = np.isin(self.df['id'], selected_ids) #print('y1', self.df['in_selectedData'].sum()) else: # print('selectedData None') self.df['in_selectedData'] = self.df['z_phot'] > 0 return 'nfilt' #f"N: {self.df['in_selectedData'].sum()}" @app.callback( Output('sample-selection-scatter', 'figure'), [Input('plot-type', 'value'), Input('color-type', 'value'), Input('mag-filter', 'value'), Input('mag-slider', 'value'), Input('mag-checked', 'value'), Input('mag2-filter', 'value'), Input('color-slider', 'value'), Input('color-checked', 'value'), Input('mass-slider', 'value'), Input('mass-checked', 'value'), Input('extra-slider', 'value'), Input('extra-checked', 'value'), Input('chi2-slider', 'value'), Input('chi2-checked', 'value'), Input('nfilt-slider', 'value'), Input('nfilt-checked', 'value'), Input('zphot-slider', 'value'), Input('zphot-checked', 'value'), Input('zspec-slider', 'value'), Input('zspec-checked', 'value'), Input('id-input', 'value') ]) def update_selection(plot_type, color_type, mag_filter, mag_range, mag_checked, mag2_filter, color_range, color_checked, mass_range, mass_checked, extra_range, extra_checked, chi2_range, chi2_checked, nfilt_range, nfilt_checked, zphot_range, zphot_checked, zspec_range, zspec_checked, id_input): """ Apply slider selections """ sel = np.isfinite(self.df['z_phot']) if 'checked' in zphot_checked: sel &= (self.df['z_phot'] > zphot_range[0]) sel &= (self.df['z_phot'] < zphot_range[1]) if 'checked' in zspec_checked: sel &= (self.df['z_spec'] > zspec_range[0]) sel &= (self.df['z_spec'] < zspec_range[1]) if 'checked' in mass_checked: sel &= (self.df['mass'] > mass_range[0]) sel &= (self.df['mass'] < mass_range[1]) if 'checked' in chi2_checked: sel &= (self.df['chi2'] >= chi2_range[0]) sel &= (self.df['chi2'] <= chi2_range[1]) if 'checked' in nfilt_checked: sel &= (self.df['nusefilt'] >= nfilt_range[0]) sel &= (self.df['nusefilt'] <= nfilt_range[1]) if 'checked' in extra_checked: _col = self.extra_slider_kws['column'] sel &= (self.df[_col] >= extra_range[0]) sel &= (self.df[_col] <= extra_range[1]) #print('redshift: ', sel.sum()) if mag_filter is None: mag_filter = self.DEFAULT_FILTER if mag2_filter is None: mag2_filter = self.DEFAULT_FILTER #self.self.df['mag'] = self.ABZP #self.self.df['mag'] -= 2.5*np.log10(self.photoz.cat[mag_filter]) mag_col = 'mag_'+mag_filter if 'checked' in mag_checked: sel &= (self.df[mag_col] > mag_range[0]) sel &= (self.df[mag_col] < mag_range[1]) mag2_col = 'mag_'+mag2_filter fblue, fred = get_sorted_mag_columns(mag_filter, mag2_filter) mag1mag2 = self.df['mag_'+fblue] - self.df['mag_'+fred] if 'checked' in color_checked: sel &= (mag1mag2 > color_range[0]) sel &= (mag1mag2 < color_range[1]) self.df['mag'] = self.df[mag_col] self.df['mag1mag2'] = mag1mag2 #print('mag: ', sel.sum()) if plot_type == 'zphot-zspec': sel &= self.df['z_spec'] > 0 #print('zspec: ', sel.sum()) if id_input is not None: id_i, dr_i = parse_id_input(id_input) if id_i is not None: self.df['is_selected'] = self.df['id'] == id_i sel |= self.df['is_selected'] else: self.df['is_selected'] = False else: self.df['is_selected'] = False xsel = self.df['in_selectedData'][sel] dff = self.df[sel] # Color-coding by color-type pulldown if color_type == 'z_phot': color_kwargs = dict(color=np.clip(dff['z_phot'][xsel], *zphot_range), color_continuous_scale='portland') elif color_type == 'z_spec': color_kwargs = dict(color=np.clip(dff['z_spec'][xsel], *zspec_range), color_continuous_scale='portland') elif color_type == 'mass': color_kwargs = dict(color=np.clip(dff['mass'][xsel], *mass_range), color_continuous_scale='magma_r') elif color_type == 'chi2': color_kwargs = dict(color=np.clip(dff['chi2'][xsel], *chi2_range), color_continuous_scale='viridis') else: color_kwargs = dict(color=np.clip(dff['ssfr'][xsel], -12., -8.), color_continuous_scale='portland_r') # Scatter plot plot_defs = {'Mass-redshift':['z_phot','mass', 'z<sub>phot</sub>', 'log Stellar mass', (-0.1, self.ZMAX), (7.5, 12.5)], 'Mag-redshift': ['z_phot','mag', 'z<sub>phot</sub>', f'AB mag ({mag_filter})', (-0.1, self.ZMAX), (18, 28)], 'Mag-color': ['mag','mag1mag2', f'AB mag ({mag_filter})', f'{fblue} - {fred}'.replace('_tot_1',''), (18, 28), (-0.5, 3)], 'redshift-color': ['z_phot','mag1mag2', f'z_phot', f'{fblue} - {fred}'.replace('_tot_1',''), (-0.1, self.ZMAX), (-0.5, 3)], 'RA/Dec': ['ra','dec', 'R.A.', 'Dec.', self.ra_bounds, self.dec_bounds], 'zphot-zspec': ['z_spec','z_phot', 'z<sub>spec</sub>', 'z<sub>phot</sub>', (0, 4.5), (0, 4.5)], 'UVJ': ['vj','uv', '(V-J)', '(U-V)', (-0.1, 2.5), (-0.1, 2.5)], 'UV-redshift': ['z_phot','uv', 'z<sub>phot</sub>', '(U-V)<sub>rest</sub>', (0, 4), (-0.1, 2.50)], 'chi2-redshift': ['z_phot','chi2', 'z<sub>phot</sub>', 'chi<sup>2</sup>', (0, 4), (0.1, 30)] } if plot_type in self.extra_plots: args = [*self.extra_plots[plot_type], {}, color_kwargs] elif plot_type in plot_defs: args = [*plot_defs[plot_type], {}, color_kwargs] else: args = [*plot_defs['zphot-zspec'], {}, color_kwargs] if args[0] == 'mag': args[2] = f'AB mag ({mag_filter})' if args[1] == 'mag': args[3] = f'AB mag ({mag_filter})' fig = update_sample_scatter(dff, *args) # Update ranges for some parameters if ('Mass' in plot_type) & ('checked' in mass_checked): fig.update_yaxes(range=mass_range) if ('Mag' in plot_type) & ('checked' in mag_checked): if args[0] == 'mag': fig.update_xaxes(range=mag_range) else: fig.update_yaxes(range=mag_range) if ('color' in plot_type) & ('checked' in color_checked): if args[0] == 'mag1mag2': fig.update_xaxes(range=color_range) else: fig.update_yaxes(range=color_range) if ('redshift' in plot_type) & ('checked' in zphot_checked): if args[0] == 'z_phot': fig.update_xaxes(range=zphot_range) else: fig.update_yaxes(range=zphot_range) if ('zspec' in plot_type) & ('checked' in zspec_checked): if args[0] == 'z_spec': fig.update_xaxes(range=zspec_range) else: fig.update_yaxes(range=zspec_range) return fig def update_sample_scatter(dff, xcol, ycol, x_label, y_label, x_range, y_range, extra, color_kwargs): """ Make scatter plot """ import plotly.graph_objects as go # print('update_sample_scatter xxx', xcol, len(dff[xcol])) is_sel = dff['in_selectedData'] fig = px.scatter(data_frame=dff[is_sel], x=xcol, y=ycol, custom_data=['id','z_phot','mass','ssfr','mag'], **color_kwargs) htempl = '(%{x:.2f}, %{y:.2f}) <br>' htempl += 'id: %{customdata[0]:0d} z_phot: %{customdata[1]:.2f}' htempl += '<br> mag: %{customdata[4]:.1f} ' htempl += 'mass: %{customdata[2]:.2f} ssfr: %{customdata[3]:.2f}' fig.update_traces(hovertemplate=htempl, opacity=0.7) if (~is_sel).sum() > 0: _xsel = go.Scatter(x=dff[~is_sel][xcol], y=dff[~is_sel][ycol], mode="markers", marker=dict(color='rgba(180,180,180,0.2)', size=5, symbol='circle'), hoverinfo='skip', ) fig.add_trace(_xsel) fig.data = fig.data[::-1] if dff['is_selected'].sum() > 0: dffs = dff[dff['is_selected']] _sel = go.Scatter(x=dffs[xcol], y=dffs[ycol], mode="markers+text", text=[f'{id}' for id in dffs['id']], textposition="bottom center", marker=dict(color='rgba(250,0,0,0.5)', size=20, symbol='circle-open')) fig.add_trace(_sel) fig.update_xaxes(range=x_range, title_text=x_label) fig.update_yaxes(range=y_range, title_text=y_label) fig.update_layout(template=template, autosize=True, showlegend=False, margin=dict(l=0,r=0,b=0,t=20,pad=0, autoexpand=True)) if plot_height is not None: fig.update_layout(height=plot_height) fig.update_traces(marker_showscale=False, selector=dict(type='scatter')) fig.update_coloraxes(showscale=False) if (xcol, ycol) == ('z_spec','z_phot'): _one2one = go.Scatter(x=[0, 8], y=[0,8], mode="lines", marker=dict(color='rgba(250,0,0,0.5)')) fig.add_trace(_one2one) fig.add_annotation(text=f'N = {len(dff)} / {len(self.df)}', xref="x domain", yref="y domain", x=0.98, y=0.05, showarrow=False) return fig def heroku_thumbnail(id_i): """ Thumbnail from grizli API """ import urllib import urllib.request import json import PIL.Image ix = np.where(self.df['id'] == id_i)[0][0] ri, di = self.df['ra'][ix], self.df['dec'][ix] turl = f'https://grizli-cutout.herokuapp.com/thumb?' turl += f'ra={ri}&dec={di}&size={api_size}' turl += f'{api_args}&filters={api_filters}' print(turl) # with open('/tmp/thumb.log','a') as fp: # fp.write(turl+'\n') req = urllib.request.urlopen(turl) thumb = np.array(PIL.Image.open(req)) return thumb def api_cutout_figure(id_i): """ Thumbnail from grizli API """ try: cutout = heroku_thumbnail(id_i) except: cutout = np.zeros((10, 10)) sh = cutout.shape fig = px.imshow(cutout, origin='upper') fig.update_coloraxes(showscale=False) fig.update_layout(width=120, height=120, margin=dict(l=0,r=0,b=0,t=0,pad=0, autoexpand=True)) fig.update_xaxes(range=(-0.5, sh[1]-0.5), visible=False, showticklabels=False) fig.update_yaxes(range=(-0.5, sh[0]-0.5), visible=False, showticklabels=False) return fig def hdu_cutout_figure(id_i): """ SED cutout """ ix = np.where(self.df['id'] == id_i)[0] ri, di = self.df['ra'][ix], self.df['dec'][ix] xi, yi = np.squeeze(self.cutout_wcs.all_world2pix([ri], [di], 0)) xp = int(np.round(xi)) yp = int(np.round(yi)) slx = slice(xp-cutout_size,xp+cutout_size+1) sly = slice(yp-cutout_size,yp+cutout_size+1) try: if self.cutout_data.ndim == 2: cutout = self.cutout_data[sly, slx] fig = px.imshow(cutout, color_continuous_scale='gray_r', origin='lower') else: cutout = self.cutout_data[sly, slx, :] fig = px.imshow(cutout, origin='lower') except: cutout = np.zeros((2*cutout_size, 2*cutout_size)) fig = px.imshow(cutout, color_continuous_scale='gray_r', origin='lower') fig.update_coloraxes(showscale=False) fig.update_layout(width=120, height=120, margin=dict(l=0,r=0,b=0,t=0,pad=0, autoexpand=True)) fig.update_xaxes(range=(0, 2*cutout_size), visible=False, showticklabels=False) fig.update_yaxes(range=(0, 2*cutout_size), visible=False, showticklabels=False) return fig def get_sorted_mag_columns(mag_filter, mag2_filter): """ """ if pivots[mag_filter] > pivots[mag2_filter]: return mag2_filter, mag_filter else: return mag_filter, mag2_filter def get_map_link(ra, dec): """ Return an html.A object with a link to a FITSMap or LegacySurvey map """ from dash import html if self.fitsmap_url is None: link = html.A('LegacySurvey', href=utils.show_legacysurvey(ra, dec, layer='ls-dr9')) else: href = self.fitsmap_url.format(ra=ra, dec=dec) link = html.A('FitsMap', href=href) return link def parse_id_input(id_input): """ Parse input as id or (ra dec) """ if id_input in ['None', None, '']: return None, None inp_split = id_input.replace(',',' ').split() if len(inp_split) == 1: return int(inp_split[0]), None ra, dec = np.cast[float](inp_split) cosd = np.cos(self.df['dec']/180*np.pi) dx = (self.df['ra'] - ra)*cosd dy = (self.df['dec'] - dec) dr = np.sqrt(dx**2+dy**2)*3600. imin = np.nanargmin(dr) return self.df['id'][imin], dr[imin] @app.callback([Output('object-sed-figure', 'figure'), Output('object-info', 'children'), Output('match-sep', 'children'), Output('cutout-figure', cutout_target)], [Input('sample-selection-scatter', 'hoverData'), Input('sed-unit-selector', 'value'), Input('id-input', 'value')]) def update_object_sed(hoverData, sed_unit, id_input): """ SED + p(z) plot """ id_i, dr_i = parse_id_input(id_input) if id_i is None: id_i = hoverData['points'][0]['customdata'][0] else: if id_i not in self.zout['id']: id_i = hoverData['points'][0]['customdata'][0] if dr_i is None: match_sep = '' else: match_sep = f'{dr_i:.1f}"' show_fnu = {'Fλ':0, 'Fν':1, 'νFν':2} layout_kwargs = dict(template=template, autosize=True, showlegend=False, margin=dict(l=0,r=0,b=0,t=20,pad=0, autoexpand=True)) fig = self.photoz.show_fit_plotly(id_i, show_fnu=show_fnu[sed_unit], vertical=True, panel_ratio=[0.6, 0.4], show=False, layout_kwargs=layout_kwargs) if plot_height is not None: fig.update_layout(height=plot_height) ix = self.df['id'] == id_i if ix.sum() == 0: object_info = 'ID: N/A' else: ix = np.where(ix)[0][0] ra, dec = self.df['ra'][ix], self.df['dec'][ix] object_info = [f'ID: {id_i} | α, δ = {ra:.6f} {dec:.6f} ', # ' | ', html.A('ESO', # href=utils.eso_query(ra, dec, # radius=1.0, # unit='s')), ' | ', html.A('CDS', href=utils.cds_query(ra, dec, radius=1.0, unit='s')), ' | ', html.A('Cutout', href=self.cutout_url.format(ra=ra, dec=dec)), ' | ', get_map_link(ra, dec), html.Br(), f"z_phot: {self.df['z_phot'][ix]:.3f} ", f" | z_spec: {self.df['z_spec'][ix]:.3f}", html.Br(), f"mag: {self.df['mag'][ix]:.2f} ", f" | mass: {self.df['mass'][ix]:.2f} ", f" | sSFR: {self.df['ssfr'][ix]:.2f}", html.Br()] if self.cutout_data is not None: cutout_fig = hdu_cutout_figure(id_i) elif api_filters is not None: cutout_fig = api_cutout_figure(id_i) else: cutout_fig = [''] return fig, object_info, match_sep, cutout_fig ####### App layout app.layout = html.Div(content) if server_mode is not None: app.run_server(mode=server_mode, port=port) return app