Source code for prospect.viewer.widgets
# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
=======================
prospect.viewer.widgets
=======================
Class containing bokeh widgets needed for the viewer (except for VI widgets)
"""
import numpy as np
from bokeh.models import CustomJS, ColumnDataSource
from bokeh.models.widgets import (
Slider, Button, TextInput, RadioButtonGroup, TableColumn,
DataTable, CheckboxButtonGroup, CheckboxGroup, Select, Div)
from ..utilities import get_resources
[docs]def _metadata_table(table_keys, viewer_cds, table_width=500, shortcds_name='shortcds', selectable=False):
""" Returns bokeh's (ColumnDataSource, DataTable) needed to display a set of metadata given by table_keys.
"""
special_cell_width = { 'TARGETID':150, 'MORPHTYPE':70, 'SPECTYPE':70, 'SUBTYPE':60,
'Z':50, 'ZERR':50, 'Z_ERR':50, 'ZWARN':50, 'ZWARNING':50, 'DELTACHI2':70 }
for x in viewer_cds.phot_bands:
special_cell_width['mag_'+x] = 40
special_cell_title = { 'DELTACHI2': 'Δχ2(N+1/N)' }
table_columns = []
cdsdata = dict()
for key in table_keys:
if key in special_cell_width.keys():
cell_width = special_cell_width[key]
else:
cell_width = table_width//len(table_keys)
if key in special_cell_title.keys():
cell_title = special_cell_title[key]
else:
cell_title = key
if ('mag_' in key) or ('EXPTIME' in key):
cdsdata[key] = [ "{:.2f}".format(viewer_cds.cds_metadata.data[key][0]) ]
elif 'CHI2' in key:
cdsdata[key] = [ "{:.1f}".format(viewer_cds.cds_metadata.data[key][0]) ]
elif key in ['Z', 'ZERR', 'Z_ERR']:
cdsdata[key] = [ "{:.4f}".format(viewer_cds.cds_metadata.data[key][0]) ]
else:
cdsdata[key] = [ viewer_cds.cds_metadata.data[key][0] ]
table_columns.append( TableColumn(field=key, title=cell_title, width=cell_width) )
shortcds = ColumnDataSource(cdsdata, name=shortcds_name)
# In order to be able to copy-paste the metadata in browser,
# the combination selectable=True, editable=True is needed:
editable = True if selectable else False
output_table = DataTable(source = shortcds, columns=table_columns,
index_position=None, selectable=selectable, editable=editable, width=table_width)
output_table.height = 2 * output_table.row_height
return (shortcds, output_table)
[docs]class ViewerWidgets(object):
"""
Encapsulates Bokeh widgets, and related callbacks, that are part of prospect's GUI.
Except for VI widgets
"""
def __init__(self, plots, nspec):
self.js_files = get_resources('js')
self.navigation_button_width = 30
self.z_button_width = 30
self.plot_widget_width = (plots.plot_width+(plots.plot_height//2))//2 - 40 # used for widgets scaling
#-----
#- Ispectrumslider and smoothing widgets
# Ispectrumslider's value controls which spectrum is displayed
# These two widgets call update_plot(), later defined
slider_end = nspec-1 if nspec > 1 else 0.5 # Slider cannot have start=end
slidertitle = 'Spectrum number (0 to '+str(nspec-1)+')'
self.ispectrumslider = Slider(start=0, end=slider_end, value=0, step=1, title=slidertitle)
self.smootherslider = Slider(start=0, end=26, value=0, step=1.0, title='Gaussian Sigma Smooth')
self.coaddcam_buttons = None
self.model_select = None
#- Small CDS to contain informations on widgets/plots status
self.cds_widgetinfos = ColumnDataSource({'oii_save_xmin': [plots.fig.x_range.start],
'oii_save_xmax': [plots.fig.x_range.end],
'oii_save_nsmooth': [self.smootherslider.value],
'waveframe_active': [0],
'z_input_value': [0]
})
def add_navigation(self, nspec):
#-----
#- Navigation buttons
self.prev_button = Button(label="<", width=self.navigation_button_width)
self.next_button = Button(label=">", width=self.navigation_button_width)
self.prev_callback = CustomJS(
args=dict(ispectrumslider=self.ispectrumslider),
code="""
if(ispectrumslider.value>0 && ispectrumslider.end>=1) {
ispectrumslider.value--
}
""")
self.next_callback = CustomJS(
args = dict(ispectrumslider=self.ispectrumslider, nspec=nspec),
code = """
if(ispectrumslider.value<nspec-1 && ispectrumslider.end>=1) {
ispectrumslider.value++
}
""")
self.prev_button.js_on_event('button_click', self.prev_callback)
self.next_button.js_on_event('button_click', self.next_callback)
#- Input spectrum number
self.ispec_input = TextInput(value=str(self.ispectrumslider.value), width=50)
self.ispec_input_callback = CustomJS(
args = dict(ispec_input=self.ispec_input, ispectrumslider=self.ispectrumslider, nspec=nspec),
code = """
var i_spec = parseInt(ispec_input.value) ;
if (Number.isInteger(i_spec) && i_spec>=0 && i_spec<nspec) {
// Avoid recursive call
if (i_spec != ispectrumslider.value) {
ispectrumslider.value = i_spec ;
}
}
""")
self.ispec_input.js_on_change('value', self.ispec_input_callback)
def add_resetrange(self, viewer_cds, plots):
#-----
#- Axis reset button (superseeds the default bokeh "reset"
self.reset_plotrange_button = Button(label="Reset X-Y range", button_type="default")
reset_plotrange_code = self.js_files["adapt_plotrange.js"] + self.js_files["reset_plotrange.js"]
self.reset_plotrange_callback = CustomJS(
args = dict(fig = plots.fig,
xmin = plots.xmin,
xmax = plots.xmax,
spectra = viewer_cds.cds_spectra,
widgetinfos = self.cds_widgetinfos),
code = reset_plotrange_code)
self.reset_plotrange_button.js_on_event('button_click', self.reset_plotrange_callback)
def add_redshift_widgets(self, z, viewer_cds, plots):
## TODO handle "z" (same issue as viewerplots TBD)
#-----
#- Redshift / wavelength scale widgets
z1 = np.floor(z*100)/100
dz = z-z1
self.zslider = Slider(start=-0.1, end=5.0, value=z1, step=0.01, title='Redshift rough tuning')
self.dzslider = Slider(start=0.0, end=0.0099, value=dz, step=0.0001, title='Redshift fine-tuning')
self.zslider.format = "0[.]00" # default bokeh value, for record
self.dzslider.format = "0[.]0000"
self.z_input = TextInput(value="{:.4f}".format(z), title="Redshift value:")
self.cds_widgetinfos.data['z_input_value'][0] = self.z_input.value
#- Observer vs. Rest frame wavelengths
self.waveframe_buttons = RadioButtonGroup(
labels=["Obs", "Rest"], active=0)
self.zslider_callback = CustomJS(
args=dict(zslider=self.zslider, dzslider=self.dzslider, z_input=self.z_input),
code="""
// Protect against 1) recursive call with z_input callback;
// 2) out-of-range zslider values (should never happen in principle)
var z1 = Math.floor(parseFloat(z_input.value)*100) / 100
if ( (Math.abs(zslider.value-z1) >= 0.01) &&
(zslider.value >= -0.1) && (zslider.value <= 5.0) ){
var new_z = zslider.value + dzslider.value
z_input.value = new_z.toFixed(4)
}
""")
self.dzslider_callback = CustomJS(
args=dict(zslider=self.zslider, dzslider=self.dzslider, z_input=self.z_input),
code="""
var z = parseFloat(z_input.value)
var z1 = Math.floor(z) / 100
var z2 = z-z1
if ( (Math.abs(dzslider.value-z2) >= 0.0001) &&
(dzslider.value >= 0.0) && (dzslider.value <= 0.0099) ){
var new_z = zslider.value + dzslider.value
z_input.value = new_z.toFixed(4)
}
""")
self.zslider.js_on_change('value', self.zslider_callback)
self.dzslider.js_on_change('value', self.dzslider_callback)
self.z_minus_button = Button(label="<", width=self.z_button_width)
self.z_plus_button = Button(label=">", width=self.z_button_width)
self.z_minus_callback = CustomJS(
args=dict(z_input=self.z_input),
code="""
var z = parseFloat(z_input.value)
if(z >= -0.09) {
z -= 0.01
z_input.value = z.toFixed(4)
}
""")
self.z_plus_callback = CustomJS(
args=dict(z_input=self.z_input),
code="""
var z = parseFloat(z_input.value)
if(z <= 4.99) {
z += 0.01
z_input.value = z.toFixed(4)
}
""")
self.z_minus_button.js_on_event('button_click', self.z_minus_callback)
self.z_plus_button.js_on_event('button_click', self.z_plus_callback)
if 'Z' in viewer_cds.cds_metadata.data.keys():
self.zreset_button = Button(label='Reset to z_pipe')
self.zreset_callback = CustomJS(
args=dict(z_input=self.z_input, metadata=viewer_cds.cds_metadata, ispectrumslider=self.ispectrumslider),
code="""
var z = metadata.data['Z'][ispectrumslider.value]
z_input.value = z.toFixed(4)
""")
self.zreset_button.js_on_event('button_click', self.zreset_callback)
else:
self.zreset_button = Div(text="(z_pipe not available)")
z_input_args = dict(spectra = viewer_cds.cds_spectra,
coaddcam_spec = viewer_cds.cds_coaddcam_spec,
model = viewer_cds.cds_model,
othermodel = viewer_cds.cds_othermodel,
metadata = viewer_cds.cds_metadata,
widgetinfos = self.cds_widgetinfos,
ispectrumslider = self.ispectrumslider,
zslider = self.zslider,
dzslider = self.dzslider,
z_input = self.z_input,
line_data = viewer_cds.cds_spectral_lines,
lines = plots.speclines,
line_labels = plots.specline_labels,
zlines = plots.zoom_speclines,
zline_labels = plots.zoom_specline_labels,
overlap_waves = plots.overlap_waves,
overlap_bands = plots.overlap_bands,
fig = plots.fig)
self.z_input_callback = CustomJS(
args = z_input_args,
code = self.js_files["shift_wave.js"] + self.js_files["change_redshift.js"]
)
self.z_input.js_on_change('value', self.z_input_callback)
waveframe_args = z_input_args
self.waveframe_callback = CustomJS(
args = waveframe_args,
code = self.js_files["shift_wave.js"] + self.js_files["change_waveframe.js"])
self.waveframe_buttons.js_on_click(self.waveframe_callback)
def add_oii_widgets(self, plots):
#------
#- Zoom on the OII doublet
# TODO? optimize smoothing for autozoom (current value: 0)
self.oii_zoom_button = Button(label="OII-zoom", button_type="default")
self.oii_zoom_callback = CustomJS(
args = dict(fig=plots.fig, smootherslider=self.smootherslider,
widgetinfos=self.cds_widgetinfos),
code = """
// Save previous setting (for the "Undo" button)
widgetinfos.data['oii_save_xmin'][0] = fig.x_range.start
widgetinfos.data['oii_save_xmax'][0] = fig.x_range.end
widgetinfos.data['oii_save_nsmooth'][0] = smootherslider.value
// Center on the middle of the redshifted OII doublet (vaccum)
var central_wave = 3728.48;
if (widgetinfos.data['waveframe_active'][0] == 0) {
var z = parseFloat(widgetinfos.data['z_input_value'][0])
central_wave *= (1+z)
}
fig.x_range.start = central_wave - 100
fig.x_range.end = central_wave + 100
// No smoothing (this implies a call to update_plot)
smootherslider.value = 0
""")
self.oii_zoom_button.js_on_event('button_click', self.oii_zoom_callback)
self.oii_undo_button = Button(label="Undo OII-zoom", button_type="default")
self.oii_undo_callback = CustomJS(
args = dict(fig=plots.fig, smootherslider=self.smootherslider, widgetinfos=self.cds_widgetinfos),
code = """
fig.x_range.start = widgetinfos.data['oii_save_xmin'][0]
fig.x_range.end = widgetinfos.data['oii_save_xmax'][0]
smootherslider.value = widgetinfos.data['oii_save_nsmooth'][0]
""")
self.oii_undo_button.js_on_event('button_click', self.oii_undo_callback)
def add_coaddcam(self, plots):
#-----
#- Highlight individual-arm or camera-coadded spectra
coaddcam_labels = ["Camera-coadded", "Single-arm"]
self.coaddcam_buttons = RadioButtonGroup(labels=coaddcam_labels, active=0)
self.coaddcam_callback = CustomJS(
args = dict(coaddcam_buttons = self.coaddcam_buttons,
list_lines=[plots.data_lines, plots.noise_lines,
plots.zoom_data_lines, plots.zoom_noise_lines],
alpha_discrete = plots.alpha_discrete,
overlap_bands = plots.overlap_bands,
alpha_overlapband = plots.alpha_overlapband),
code="""
var n_lines = list_lines[0].length
for (var i=0; i<n_lines; i++) {
var new_alpha = 1
if (coaddcam_buttons.active == 0 && i<n_lines-1) new_alpha = alpha_discrete
if (coaddcam_buttons.active == 1 && i==n_lines-1) new_alpha = alpha_discrete
for (var j=0; j<list_lines.length; j++) {
list_lines[j][i].glyph.line_alpha = new_alpha
}
}
var new_alpha = 0
if (coaddcam_buttons.active == 0) new_alpha = alpha_overlapband
for (var j=0; j<overlap_bands.length; j++) {
overlap_bands[j].fill_alpha = new_alpha
}
"""
)
self.coaddcam_buttons.js_on_click(self.coaddcam_callback)
[docs] def add_metadata_tables(self, viewer_cds, show_zcat=True,
top_metadata=['TARGETID', 'EXPID', 'COADD_NUMEXP', 'COADD_EXPTIME']):
""" Display object-related informations
top_metadata: metadata to be highlighted in table_a
Note: "short" CDS, with a single row, are used to fill these bokeh tables.
When changing object, js code modifies these short CDS so that tables are updated.
"""
#- Sorted list of potential metadata:
metadata_to_check = [ ('mag_'+x) for x in viewer_cds.phot_bands ]
metadata_to_check += ['MORPHTYPE', 'TARGETID', 'HPXPIXEL', 'TILEID', 'COADD_NUMEXP', 'COADD_EXPTIME', 'COADD_NUMNIGHT',
'COADD_NUMTILE', 'NIGHT', 'EXPID', 'FIBER', 'CAMERA']
table_keys = []
for key in metadata_to_check:
if key in viewer_cds.cds_metadata.data.keys():
table_keys.append(key)
if 'NUM_'+key in viewer_cds.cds_metadata.data.keys():
for prefix in ['FIRST','LAST','NUM']:
table_keys.append(prefix+'_'+key)
if key in top_metadata:
top_metadata.append(prefix+'_'+key)
#- Table a: "top metadata"
table_a_keys = [ x for x in table_keys if x in top_metadata ]
self.shortcds_table_a, self.table_a = _metadata_table(table_a_keys, viewer_cds, table_width=600,
shortcds_name='shortcds_table_a', selectable=True)
#- Table b: Targeting information
self.shortcds_table_b, self.table_b = _metadata_table(['Targeting masks'], viewer_cds, table_width=self.plot_widget_width,
shortcds_name='shortcds_table_b', selectable=True)
#- Table(s) c/d : Other information (imaging, etc.)
remaining_keys = [ x for x in table_keys if x not in top_metadata ]
if len(remaining_keys) > 7:
table_c_keys = remaining_keys[0:len(remaining_keys)//2]
table_d_keys = remaining_keys[len(remaining_keys)//2:]
else:
table_c_keys = remaining_keys
table_d_keys = None
self.shortcds_table_c, self.table_c = _metadata_table(table_c_keys, viewer_cds, table_width=self.plot_widget_width,
shortcds_name='shortcds_table_c', selectable=False)
if table_d_keys is None:
self.shortcds_table_d, self.table_d = None, None
else:
self.shortcds_table_d, self.table_d = _metadata_table(table_d_keys, viewer_cds, table_width=self.plot_widget_width,
shortcds_name='shortcds_table_d', selectable=False)
#- Table z: redshift fitting information
if show_zcat:
if viewer_cds.dict_rrdetails is not None: # "Detailled" info for Nth best fits
fit_results = viewer_cds.dict_rrdetails
# Case of DeltaChi2 : compute it from Chi2s
# The "DeltaChi2" in redrock files is between best fits for a given (spectype,subtype)
# Here we want to display DeltaChi2 independently of (spectype,subtype)
# Convention: DeltaChi2 = -1 for the last fit.
chi2s = fit_results['CHI2'][0]
full_deltachi2s = np.zeros(len(chi2s))-1
full_deltachi2s[:-1] = chi2s[1:]-chi2s[:-1]
cdsdata = dict(Nfit = np.arange(1,len(chi2s)+1),
SPECTYPE = fit_results['SPECTYPE'][0], # [0:num_best_fits] (if we want to restrict... TODO?)
SUBTYPE = fit_results['SUBTYPE'][0],
Z = [ "{:.4f}".format(x) for x in fit_results['Z'][0] ],
ZERR = [ "{:.4f}".format(x) for x in fit_results['ZERR'][0] ],
ZWARN = fit_results['ZWARN'][0],
CHI2 = [ "{:.1f}".format(x) for x in fit_results['CHI2'][0] ],
DELTACHI2 = [ "{:.1f}".format(x) for x in full_deltachi2s ])
self.shortcds_table_z = ColumnDataSource(cdsdata, name='shortcds_table_z')
columns_table_z = [ TableColumn(field=x, title=t, width=w) for x,t,w in [ ('Nfit','Nfit',5), ('SPECTYPE','SPECTYPE',70), ('SUBTYPE','SUBTYPE',60), ('Z','Z',50) , ('ZERR','ZERR',50), ('ZWARN','ZWARN',50), ('DELTACHI2','Δχ2(N+1/N)',70)] ]
self.table_z = DataTable(source=self.shortcds_table_z, columns=columns_table_z,
selectable=False, index_position=None, width=self.plot_widget_width)
self.table_z.height = 3 * self.table_z.row_height
else :
self.shortcds_table_z, self.table_z = _metadata_table(viewer_cds.zcat_keys, viewer_cds,
table_width=self.plot_widget_width, shortcds_name='shortcds_table_z', selectable=False)
else :
self.table_z = Div(text="Not available ")
self.shortcds_table_z = None
def add_specline_toggles(self, viewer_cds, plots):
#-----
#- Toggle lines
self.speclines_button_group = CheckboxButtonGroup(
labels=["Emission lines", "Absorption lines"], active=[0, 1])
self.majorline_checkbox = CheckboxGroup(
labels=['Show only major lines'], active=[0])
self.speclines_callback = CustomJS(
args = dict(line_data = viewer_cds.cds_spectral_lines,
lines = plots.speclines,
line_labels = plots.specline_labels,
zlines = plots.zoom_speclines,
zline_labels = plots.zoom_specline_labels,
lines_button_group = self.speclines_button_group,
majorline_checkbox = self.majorline_checkbox),
code="""
var show_emission = false
var show_absorption = false
if (lines_button_group.active.indexOf(0) >= 0) { // index 0=Emission in active list
show_emission = true
}
if (lines_button_group.active.indexOf(1) >= 0) { // index 1=Absorption in active list
show_absorption = true
}
for(var i=0; i<lines.length; i++) {
if ( !(line_data.data['major'][i]) && (majorline_checkbox.active.indexOf(0)>=0) ) {
lines[i].visible = false
line_labels[i].visible = false
zlines[i].visible = false
zline_labels[i].visible = false
} else if (line_data.data['emission'][i]) {
lines[i].visible = show_emission
line_labels[i].visible = show_emission
zlines[i].visible = show_emission
zline_labels[i].visible = show_emission
} else {
lines[i].visible = show_absorption
line_labels[i].visible = show_absorption
zlines[i].visible = show_absorption
zline_labels[i].visible = show_absorption
}
}
"""
)
self.speclines_button_group.js_on_click(self.speclines_callback)
self.majorline_checkbox.js_on_click(self.speclines_callback)
def add_model_select(self, viewer_cds, num_approx_fits):
#------
#- Select secondary model to display
model_options = []
if viewer_cds.cds_model is not None:
model_options = ['Best fit']
if viewer_cds.cds_model_2ndfit is not None:
model_options.append('2nd best fit')
if num_approx_fits is not None and viewer_cds.dict_rrdetails is not None:
# NB approx fits are computed from coefs in detailled redrock file
for i in range(1,1+num_approx_fits) :
ith = 'th'
if i==1 : ith='st'
if i==2 : ith='nd'
if i==3 : ith='rd'
model_options.append(str(i)+ith+' fit (approx)')
if viewer_cds.dict_std_templates is not None:
std_template_labels = [x[5:] for x in viewer_cds.dict_std_templates.keys() if x[:5]=='wave_']
for std_template in std_template_labels:
model_options.append('STD '+std_template)
self.model_select = Select(value=model_options[0],
title="Other model (dashed curve):",
options=model_options)
model_select_code = self.js_files["interp_grid.js"] + self.js_files["smooth_data.js"] + self.js_files["select_model.js"]
self.model_select_callback = CustomJS(
args = dict(ispectrumslider = self.ispectrumslider,
model_select = self.model_select,
fit_templates = viewer_cds.dict_fit_templates,
cds_othermodel = viewer_cds.cds_othermodel,
cds_model_2ndfit = viewer_cds.cds_model_2ndfit,
cds_model = viewer_cds.cds_model,
rrdetails = viewer_cds.dict_rrdetails,
std_templates = viewer_cds.dict_std_templates,
median_spectra = viewer_cds.cds_median_spectra,
smootherslider = self.smootherslider,
z_input = self.z_input,
cds_metadata = viewer_cds.cds_metadata),
code = model_select_code)
self.model_select.js_on_change('value', self.model_select_callback)
def add_update_plot_callback(self, viewer_cds, plots, vi_widgets):
#-----
#- Main js code to update plots
update_plot_code = (self.js_files["adapt_plotrange.js"] + self.js_files["interp_grid.js"] +
self.js_files["smooth_data.js"] + self.js_files["coadd_brz_cameras.js"] +
self.js_files["update_plot.js"])
self.update_plot_callback = CustomJS(
args = dict(
spectra = viewer_cds.cds_spectra,
coaddcam_spec = viewer_cds.cds_coaddcam_spec,
model = viewer_cds.cds_model,
othermodel = viewer_cds.cds_othermodel,
model_2ndfit = viewer_cds.cds_model_2ndfit,
metadata = viewer_cds.cds_metadata,
rrdetails = viewer_cds.dict_rrdetails,
shortcds_table_z = self.shortcds_table_z,
shortcds_table_a = self.shortcds_table_a,
shortcds_table_b = self.shortcds_table_b,
shortcds_table_c = self.shortcds_table_c,
shortcds_table_d = self.shortcds_table_d,
ispectrumslider = self.ispectrumslider,
ispec_input = self.ispec_input,
smootherslider = self.smootherslider,
z_input = self.z_input,
widgetinfos = self.cds_widgetinfos,
fig = plots.fig,
xrange = [plots.xmin, plots.xmax],
imfig_source = plots.imfig_source,
crosshair_source = plots.crosshair_source,
imfig_urls = plots.imfig_urls,
model_select = self.model_select,
vi_comment_input = vi_widgets.vi_comment_input,
vi_std_comment_select = vi_widgets.vi_std_comment_select,
vi_name_input = vi_widgets.vi_name_input,
vi_quality_input = vi_widgets.vi_quality_input,
vi_quality_labels = vi_widgets.vi_quality_labels,
vi_issue_input = vi_widgets.vi_issue_input,
vi_z_input = vi_widgets.vi_z_input,
vi_category_select = vi_widgets.vi_category_select,
vi_issue_slabels = vi_widgets.vi_issue_slabels
),
code = update_plot_code
)
self.smootherslider.js_on_change('value', self.update_plot_callback)
self.ispectrumslider.js_on_change('value', self.update_plot_callback)