checkbox_button_group = CheckboxButtonGroup( labels=["Option 1", "Option 2", "Option 3"], active=[0, 1]) checkbox_button_group.on_click( lambda value: print('checkbox_button_group: %s' % value)) checkbox_button_group.js_on_click( CustomJS( code= "console.log('checkbox_button_group: ' + this.active, this.toString())" )) radio_button_group = RadioButtonGroup( labels=["Option 1", "Option 2", "Option 3"], active=0) radio_button_group.on_click( lambda value: print('radio_button_group: %s' % value)) radio_button_group.js_on_click( CustomJS( code= "console.log('radio_button_group: ' + this.active, this.toString())")) widgetBox = WidgetBox(children=[ button, button_disabled, toggle_inactive, toggle_active, dropdown, dropdown_disabled, #dropdown_split, checkbox_group, radio_group, checkbox_button_group, radio_button_group, ])
dropdown_disabled.js_on_click(CustomJS(code="console.log('dropdown_disabled: ' + this.value, this.toString())")) #dropdown_split = Dropdown(label="Split button", button_type="danger", menu=menu, default_value="default") #dropdown_split.js_on_click(CustomJS(code="console.log('dropdown_split: ' + this.value, this.toString())")) checkbox_group = CheckboxGroup(labels=["Option 1", "Option 2", "Option 3"], active=[0, 1]) checkbox_group.js_on_click(CustomJS(code="console.log('checkbox_group: ' + this.active, this.toString())")) radio_group = RadioGroup(labels=["Option 1", "Option 2", "Option 3"], active=0) radio_group.js_on_click(CustomJS(code="console.log('radio_group: ' + this.active, this.toString())")) checkbox_button_group = CheckboxButtonGroup(labels=["Option 1", "Option 2", "Option 3"], active=[0, 1]) checkbox_button_group.js_on_click(CustomJS(code="console.log('checkbox_button_group: ' + this.active, this.toString())")) radio_button_group = RadioButtonGroup(labels=["Option 1", "Option 2", "Option 3"], active=0) radio_button_group.js_on_click(CustomJS(code="console.log('radio_button_group: ' + this.active, this.toString())")) widget_box = WidgetBox(children=[ button, button_disabled, toggle_inactive, toggle_active, dropdown, dropdown_disabled, #dropdown_split, checkbox_group, radio_group, checkbox_button_group, radio_button_group, ]) doc = Document() doc.add_root(widget_box) if __name__ == "__main__": doc.validate() filename = "buttons.html"
class ViewerVIWidgets(object): """ Encapsulates Bokeh widgets, and related callbacks, used for VI """ def __init__(self, title, viewer_cds): self.vi_quality_labels = [ x["label"] for x in vi_flags if x["type"]=="quality" ] self.vi_issue_labels = [ x["label"] for x in vi_flags if x["type"]=="issue" ] self.vi_issue_slabels = [ x["shortlabel"] for x in vi_flags if x["type"]=="issue" ] self.js_files = get_resources('js') self.title = title self.vi_countdown_toggle = None #- List of fields to be recorded in output csv file, contains for each field: # [ field name (in VI file header), associated variable in viewer_cds.cds_metadata ] self.output_file_fields = [] for file_field in vi_file_fields: if file_field[1] in viewer_cds.cds_metadata.data.keys() : self.output_file_fields.append([file_field[0], file_field[1]]) def add_filename(self, username='******'): #- VI file name default_vi_filename = "desi-vi_"+self.title default_vi_filename += ("_"+username) default_vi_filename += ".csv" self.vi_filename_input = TextInput(value=default_vi_filename, title="VI file name:") def add_vi_issues(self, viewer_cds, widgets): #- Optional VI flags (issues) self.vi_issue_input = CheckboxGroup(labels=self.vi_issue_labels, active=[]) vi_issue_code = self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] vi_issue_code += """ var issues = [] for (var i=0; i<vi_issue_labels.length; i++) { if (vi_issue_input.active.indexOf(i) >= 0) issues.push(vi_issue_slabels[i]) } if (issues.length > 0) { cds_metadata.data['VI_issue_flag'][ifiberslider.value] = ( issues.join('') ) } else { cds_metadata.data['VI_issue_flag'][ifiberslider.value] = " " } autosave_vi_localStorage(output_file_fields, cds_metadata.data, title) cds_metadata.change.emit() """ self.vi_issue_callback = CustomJS( args=dict(cds_metadata = viewer_cds.cds_metadata, ifiberslider = widgets.ifiberslider, vi_issue_input = self.vi_issue_input, vi_issue_labels = self.vi_issue_labels, vi_issue_slabels = self.vi_issue_slabels, title = self.title, output_file_fields = self.output_file_fields), code = vi_issue_code ) self.vi_issue_input.js_on_click(self.vi_issue_callback) def add_vi_z(self, viewer_cds, widgets): ## TODO: z_tovi behaviour if with_vi_widget=False ..? #- Optional VI information on redshift self.vi_z_input = TextInput(value='', title="VI redshift:") vi_z_code = self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] vi_z_code += """ cds_metadata.data['VI_z'][ifiberslider.value]=vi_z_input.value autosave_vi_localStorage(output_file_fields, cds_metadata.data, title) cds_metadata.change.emit() """ self.vi_z_callback = CustomJS( args=dict(cds_metadata = viewer_cds.cds_metadata, ifiberslider = widgets.ifiberslider, vi_z_input = self.vi_z_input, title = self.title, output_file_fields=self.output_file_fields), code = vi_z_code ) self.vi_z_input.js_on_change('value', self.vi_z_callback) # Copy z value from redshift slider to VI self.z_tovi_button = Button(label='Copy z to VI') self.z_tovi_callback = CustomJS( args=dict(z_input=widgets.z_input, vi_z_input=self.vi_z_input), code=""" vi_z_input.value = z_input.value """) self.z_tovi_button.js_on_event('button_click', self.z_tovi_callback) def add_vi_spectype(self, viewer_cds, widgets): #- Optional VI information on spectral type self.vi_category_select = Select(value=' ', title="VI spectype:", options=([' '] + vi_spectypes)) # The default value set to ' ' as setting value='' does not seem to work well with Select. vi_category_code = self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] vi_category_code += """ if (vi_category_select.value == ' ') { cds_metadata.data['VI_spectype'][ifiberslider.value]='' } else { cds_metadata.data['VI_spectype'][ifiberslider.value]=vi_category_select.value } autosave_vi_localStorage(output_file_fields, cds_metadata.data, title) cds_metadata.change.emit() """ self.vi_category_callback = CustomJS( args=dict(cds_metadata=viewer_cds.cds_metadata, ifiberslider = widgets.ifiberslider, vi_category_select=self.vi_category_select, title=self.title, output_file_fields=self.output_file_fields), code=vi_category_code ) self.vi_category_select.js_on_change('value', self.vi_category_callback) def add_vi_comment(self, viewer_cds, widgets): #- Optional VI comment self.vi_comment_input = TextInput(value='', title="VI comment (see guidelines):") vi_comment_code = self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] vi_comment_code += """ var stored_comment = (vi_comment_input.value).replace(/./g, function(char){ if ( char==',' ) { return ';' } else if ( char.charCodeAt(0)<=127 ) { return char } else { var char_list = ['Å','α','β','γ','δ','λ'] var char_replace = ['Angstrom','alpha','beta','gamma','delta','lambda'] for (var i=0; i<char_list.length; i++) { if ( char==char_list[i] ) return char_replace[i] } return '?' } }) cds_metadata.data['VI_comment'][ifiberslider.value] = stored_comment autosave_vi_localStorage(output_file_fields, cds_metadata.data, title) cds_metadata.change.emit() """ self.vi_comment_callback = CustomJS( args=dict(cds_metadata = viewer_cds.cds_metadata, ifiberslider = widgets.ifiberslider, vi_comment_input = self.vi_comment_input, title=self.title, output_file_fields=self.output_file_fields), code=vi_comment_code ) self.vi_comment_input.js_on_change('value',self.vi_comment_callback) #- List of "standard" VI comment self.vi_std_comment_select = Select(value=" ", title="Standard comment:", options=([' '] + vi_std_comments)) vi_std_comment_code = """ if (vi_std_comment_select.value != ' ') { if (vi_comment_input.value != '') { vi_comment_input.value = vi_comment_input.value + " " + vi_std_comment_select.value } else { vi_comment_input.value = vi_std_comment_select.value } } """ self.vi_std_comment_callback = CustomJS( args = dict(vi_std_comment_select = self.vi_std_comment_select, vi_comment_input = self.vi_comment_input), code = vi_std_comment_code ) self.vi_std_comment_select.js_on_change('value', self.vi_std_comment_callback) def add_vi_quality(self, viewer_cds, widgets): #- Main VI quality widget self.vi_quality_input = RadioButtonGroup(labels=self.vi_quality_labels) vi_quality_code = self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] vi_quality_code += """ if ( vi_quality_input.active >= 0 ) { cds_metadata.data['VI_quality_flag'][ifiberslider.value] = vi_quality_labels[vi_quality_input.active] } else { cds_metadata.data['VI_quality_flag'][ifiberslider.value] = "-1" } autosave_vi_localStorage(output_file_fields, cds_metadata.data, title) cds_metadata.change.emit() """ self.vi_quality_callback = CustomJS( args = dict(cds_metadata = viewer_cds.cds_metadata, vi_quality_input = self.vi_quality_input, vi_quality_labels = self.vi_quality_labels, ifiberslider = widgets.ifiberslider, title=self.title, output_file_fields = self.output_file_fields), code=vi_quality_code ) self.vi_quality_input.js_on_click(self.vi_quality_callback) def add_vi_scanner(self, viewer_cds): #- VI scanner name self.vi_name_input = TextInput(value=(viewer_cds.cds_metadata.data['VI_scanner'][0]).strip(), title="Your name (3-letter acronym):") vi_name_code = self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] vi_name_code += """ for (var i=0; i<(cds_metadata.data['VI_scanner']).length; i++) { cds_metadata.data['VI_scanner'][i]=vi_name_input.value } var newname = vi_filename_input.value var name_chunks = newname.split("_") newname = ( name_chunks.slice(0,name_chunks.length-1).join("_") ) + ("_"+vi_name_input.value+".csv") vi_filename_input.value = newname autosave_vi_localStorage(output_file_fields, cds_metadata.data, title) """ self.vi_name_callback = CustomJS( args = dict(cds_metadata = viewer_cds.cds_metadata, vi_name_input = self.vi_name_input, vi_filename_input = self.vi_filename_input, title=self.title, output_file_fields = self.output_file_fields), code=vi_name_code ) self.vi_name_input.js_on_change('value', self.vi_name_callback) def add_guidelines(self): #- Guidelines for VI flags vi_guideline_txt = "<B> VI guidelines </B>" vi_guideline_txt += "<BR /> <B> Classification flags: </B>" for flag in vi_flags : if flag['type'] == 'quality' : vi_guideline_txt += ("<BR />  [ "+flag['label']+" ] "+flag['description']) vi_guideline_txt += "<BR /> <B> Optional indications: </B>" for flag in vi_flags : if flag['type'] == 'issue' : vi_guideline_txt += ( "<BR />  [ " + flag['label'] + " (" + flag['shortlabel'] + ") ] " + flag['description'] ) vi_guideline_txt += "<BR /> <B> Comments: </B> <BR /> 100 characters max, avoid commas (automatically replaced by semi-columns), ASCII only." self.vi_guideline_div = Div(text=vi_guideline_txt) def add_vi_storage(self, viewer_cds, widgets): #- Save VI info to CSV file self.save_vi_button = Button(label="Download VI", button_type="success") save_vi_code = self.js_files["FileSaver.js"] + self.js_files["CSVtoArray.js"] + self.js_files["save_vi.js"] save_vi_code += """ download_vi_file(output_file_fields, cds_metadata.data, vi_filename_input.value) """ self.save_vi_callback = CustomJS( args = dict(cds_metadata = viewer_cds.cds_metadata, output_file_fields = self.output_file_fields, vi_filename_input = self.vi_filename_input), code=save_vi_code ) self.save_vi_button.js_on_event('button_click', self.save_vi_callback) #- Recover auto-saved VI data in browser self.recover_vi_button = Button(label="Recover auto-saved VI", button_type="default") recover_vi_code = self.js_files["CSVtoArray.js"] + self.js_files["recover_autosave_vi.js"] self.recover_vi_callback = CustomJS( args = dict(title=self.title, output_file_fields=self.output_file_fields, cds_metadata = viewer_cds.cds_metadata, ifiber = widgets.ifiberslider.value, vi_comment_input = self.vi_comment_input, vi_name_input=self.vi_name_input, vi_quality_input=self.vi_quality_input, vi_issue_input=self.vi_issue_input, vi_issue_slabels=self.vi_issue_slabels, vi_quality_labels=self.vi_quality_labels), code = recover_vi_code ) self.recover_vi_button.js_on_event('button_click', self.recover_vi_callback) #- Clear all auto-saved VI self.clear_vi_button = Button(label="Clear all auto-saved VI", button_type="default") self.clear_vi_callback = CustomJS( args = dict(), code = """ localStorage.clear() """ ) self.clear_vi_button.js_on_event('button_click', self.clear_vi_callback) def add_vi_table(self, viewer_cds): #- Show VI in a table vi_table_columns = [ TableColumn(field="VI_quality_flag", title="Flag", width=40), TableColumn(field="VI_issue_flag", title="Opt.", width=50), TableColumn(field="VI_z", title="VI z", width=50), TableColumn(field="VI_spectype", title="VI spectype", width=150), TableColumn(field="VI_comment", title="VI comment", width=200) ] self.vi_table = DataTable(source=viewer_cds.cds_metadata, columns=vi_table_columns, width=500) self.vi_table.height = 10 * self.vi_table.row_height def add_countdown(self, vi_countdown): #- VI countdown assert vi_countdown > 0 self.vi_countdown_callback = CustomJS(args=dict(vi_countdown=vi_countdown), code=""" if ( (cb_obj.label).includes('Start') ) { // Callback doesn't do anything after countdown started var countDownDate = new Date().getTime() + (1000 * 60 * vi_countdown); var countDownLoop = setInterval(function(){ var now = new Date().getTime(); var distance = countDownDate - now; if (distance<0) { cb_obj.label = "Time's up !"; clearInterval(countDownLoop); } else { var days = Math.floor(distance / (1000 * 60 * 60 * 24)); var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)); var seconds = Math.floor((distance % (1000 * 60)) / 1000); //var stuff = days + "d " + hours + "h " + minutes + "m " + seconds + "s "; var stuff = minutes + "m " + seconds + "s "; cb_obj.label = "Countdown: " + stuff; } }, 1000); } """) self.vi_countdown_toggle = Toggle(label='Start countdown ('+str(vi_countdown)+' min)', active=False, button_type="success") self.vi_countdown_toggle.js_on_change('active', self.vi_countdown_callback)
def plotspectra(spectra, zcatalog=None, model=None, notebook=False, title=None): ''' TODO: document ''' if notebook: bk.output_notebook() #- If inputs are frames, convert to a spectra object if isinstance(spectra, list) and isinstance(spectra[0], desispec.frame.Frame): spectra = frames2spectra(spectra) frame_input = True else: frame_input = False if frame_input and title is None: meta = spectra.meta title = 'Night {} ExpID {} Spectrograph {}'.format( meta['NIGHT'], meta['EXPID'], meta['CAMERA'][1], ) #- Gather spectra into ColumnDataSource objects for Bokeh nspec = spectra.num_spectra() cds_spectra = list() for band in spectra.bands: #- Set masked bins to NaN so that Bokeh won't plot them bad = (spectra.ivar[band] == 0.0) | (spectra.mask[band] != 0) spectra.flux[band][bad] = np.nan cdsdata = dict( origwave=spectra.wave[band].copy(), plotwave=spectra.wave[band].copy(), ) for i in range(nspec): key = 'origflux' + str(i) cdsdata[key] = spectra.flux[band][i] cdsdata['plotflux'] = cdsdata['origflux0'] cds_spectra.append(bk.ColumnDataSource(cdsdata, name=band)) #- Reorder zcatalog to match input targets #- TODO: allow more than one zcatalog entry with different ZNUM per targetid targetids = spectra.target_ids() if zcatalog is not None: ii = np.argsort(np.argsort(targetids)) jj = np.argsort(zcatalog['TARGETID']) kk = jj[ii] zcatalog = zcatalog[kk] #- That sequence of argsorts may feel like magic, #- so make sure we got it right assert np.all(zcatalog['TARGETID'] == targetids) assert np.all(zcatalog['TARGETID'] == spectra.fibermap['TARGETID']) #- Also need to re-order input model fluxes if model is not None: mwave, mflux = model model = mwave, mflux[kk] #- Gather models into ColumnDataSource objects, row matched to spectra if model is not None: mwave, mflux = model model_obswave = mwave.copy() model_restwave = mwave.copy() cds_model_data = dict( origwave=mwave.copy(), plotwave=mwave.copy(), plotflux=np.zeros(len(mwave)), ) for i in range(nspec): key = 'origflux' + str(i) cds_model_data[key] = mflux[i] cds_model_data['plotflux'] = cds_model_data['origflux0'] cds_model = bk.ColumnDataSource(cds_model_data) else: cds_model = None #- Subset of zcatalog and fibermap columns into ColumnDataSource target_info = list() for i, row in enumerate(spectra.fibermap): target_bit_names = ' '.join(desi_mask.names(row['DESI_TARGET'])) txt = 'Target {}: {}'.format(row['TARGETID'], target_bit_names) if zcatalog is not None: txt += '<BR/>{} z={:.4f} ± {:.4f} ZWARN={}'.format( zcatalog['SPECTYPE'][i], zcatalog['Z'][i], zcatalog['ZERR'][i], zcatalog['ZWARN'][i], ) target_info.append(txt) cds_targetinfo = bk.ColumnDataSource(dict(target_info=target_info), name='targetinfo') if zcatalog is not None: cds_targetinfo.add(zcatalog['Z'], name='z') plot_width = 800 plot_height = 400 # tools = 'pan,box_zoom,wheel_zoom,undo,redo,reset,save' tools = 'pan,box_zoom,wheel_zoom,reset,save' fig = bk.figure(height=plot_height, width=plot_width, title=title, tools=tools, toolbar_location='above', y_range=(-10, 20)) fig.toolbar.active_drag = fig.tools[1] #- box zoom fig.toolbar.active_scroll = fig.tools[2] #- wheel zoom fig.xaxis.axis_label = 'Wavelength [Å]' fig.yaxis.axis_label = 'Flux' fig.xaxis.axis_label_text_font_style = 'normal' fig.yaxis.axis_label_text_font_style = 'normal' colors = dict(b='#1f77b4', r='#d62728', z='maroon') data_lines = list() for spec in cds_spectra: lx = fig.line('plotwave', 'plotflux', source=spec, line_color=colors[spec.name]) data_lines.append(lx) if cds_model is not None: model_lines = list() lx = fig.line('plotwave', 'plotflux', source=cds_model, line_color='black') model_lines.append(lx) legend = Legend(items=[ ("data", data_lines[-1::-1]), #- reversed to get blue as lengend entry ("model", model_lines), ]) else: legend = Legend(items=[ ("data", data_lines[-1::-1]), #- reversed to get blue as lengend entry ]) fig.add_layout(legend, 'center') fig.legend.click_policy = 'hide' #- or 'mute' #- Zoom figure around mouse hover of main plot zoomfig = bk.figure( height=plot_height // 2, width=plot_height // 2, y_range=fig.y_range, x_range=(5000, 5100), # output_backend="webgl", toolbar_location=None, tools=[]) for spec in cds_spectra: zoomfig.line('plotwave', 'plotflux', source=spec, line_color=colors[spec.name], line_width=1, line_alpha=1.0) if cds_model is not None: zoomfig.line('plotwave', 'plotflux', source=cds_model, line_color='black') #- Callback to update zoom window x-range zoom_callback = CustomJS(args=dict(zoomfig=zoomfig), code=""" zoomfig.x_range.start = cb_obj.x - 100; zoomfig.x_range.end = cb_obj.x + 100; """) fig.js_on_event(bokeh.events.MouseMove, zoom_callback) #----- #- Emission and absorption lines z = zcatalog['Z'][0] if (zcatalog is not None) else 0.0 line_data, lines, line_labels = add_lines(fig, z=z) #----- #- Add widgets for controling plots z1 = np.floor(z * 100) / 100 dz = z - z1 zslider = Slider(start=0.0, end=4.0, value=z1, step=0.01, title='Redshift') dzslider = Slider(start=0.0, end=0.01, value=dz, step=0.0001, title='+ Delta redshift') dzslider.format = "0[.]0000" #- Observer vs. Rest frame wavelengths waveframe_buttons = RadioButtonGroup(labels=["Obs", "Rest"], active=0) ifiberslider = Slider(start=0, end=nspec - 1, value=0, step=1) if frame_input: ifiberslider.title = 'Fiber' else: ifiberslider.title = 'Target' zslider_callback = CustomJS( args=dict( spectra=cds_spectra, model=cds_model, targetinfo=cds_targetinfo, ifiberslider=ifiberslider, zslider=zslider, dzslider=dzslider, waveframe_buttons=waveframe_buttons, line_data=line_data, lines=lines, line_labels=line_labels, fig=fig, ), #- TODO: reorder to reduce duplicated code code=""" var z = zslider.value + dzslider.value var line_restwave = line_data.data['restwave'] var ifiber = ifiberslider.value var zfit = 0.0 if(targetinfo.data['z'] != undefined) { zfit = targetinfo.data['z'][ifiber] } // Observer Frame if(waveframe_buttons.active == 0) { var x = 0.0 for(var i=0; i<line_restwave.length; i++) { x = line_restwave[i] * (1+z) lines[i].location = x line_labels[i].x = x } for(var i=0; i<spectra.length; i++) { var data = spectra[i].data var origwave = data['origwave'] var plotwave = data['plotwave'] for (var j=0; j<plotwave.length; j++) { plotwave[j] = origwave[j] } spectra[i].change.emit() } // Update model wavelength array if(model) { var origwave = model.data['origwave'] var plotwave = model.data['plotwave'] for(var i=0; i<plotwave.length; i++) { plotwave[i] = origwave[i] * (1+z) / (1+zfit) } model.change.emit() } // Rest Frame } else { for(i=0; i<line_restwave.length; i++) { lines[i].location = line_restwave[i] line_labels[i].x = line_restwave[i] } for (var i=0; i<spectra.length; i++) { var data = spectra[i].data var origwave = data['origwave'] var plotwave = data['plotwave'] for (var j=0; j<plotwave.length; j++) { plotwave[j] = origwave[j] / (1+z) } spectra[i].change.emit() } // Update model wavelength array if(model) { var origwave = model.data['origwave'] var plotwave = model.data['plotwave'] for(var i=0; i<plotwave.length; i++) { plotwave[i] = origwave[i] / (1+zfit) } model.change.emit() } } """) zslider.js_on_change('value', zslider_callback) dzslider.js_on_change('value', zslider_callback) waveframe_buttons.js_on_click(zslider_callback) plotrange_callback = CustomJS(args=dict( zslider=zslider, dzslider=dzslider, waveframe_buttons=waveframe_buttons, fig=fig, ), code=""" var z = zslider.value + dzslider.value // Observer Frame if(waveframe_buttons.active == 0) { fig.x_range.start = fig.x_range.start * (1+z) fig.x_range.end = fig.x_range.end * (1+z) } else { fig.x_range.start = fig.x_range.start / (1+z) fig.x_range.end = fig.x_range.end / (1+z) } """) waveframe_buttons.js_on_click(plotrange_callback) smootherslider = Slider(start=0, end=31, value=0, step=1.0, title='Gaussian Sigma Smooth') target_info_div = Div(text=target_info[0]) #----- #- Toggle lines lines_button_group = CheckboxButtonGroup(labels=["Emission", "Absorption"], active=[]) lines_callback = CustomJS(args=dict(line_data=line_data, lines=lines, line_labels=line_labels), code=""" var show_emission = false var show_absorption = false if (cb_obj.active.indexOf(0) >= 0) { // index 0=Emission in active list show_emission = true } if (cb_obj.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['emission'][i]) { lines[i].visible = show_emission line_labels[i].visible = show_emission } else { lines[i].visible = show_absorption line_labels[i].visible = show_absorption } } """) lines_button_group.js_on_click(lines_callback) # lines_button_group.js_on_change('value', lines_callback) #----- update_plot = CustomJS(args=dict( spectra=cds_spectra, model=cds_model, targetinfo=cds_targetinfo, target_info_div=target_info_div, ifiberslider=ifiberslider, smootherslider=smootherslider, zslider=zslider, dzslider=dzslider, lines_button_group=lines_button_group, fig=fig, ), code=""" var ifiber = ifiberslider.value var nsmooth = smootherslider.value target_info_div.text = targetinfo.data['target_info'][ifiber] if(targetinfo.data['z'] != undefined) { var z = targetinfo.data['z'][ifiber] var z1 = Math.floor(z*100) / 100 zslider.value = z1 dzslider.value = (z - z1) } function get_y_minmax(pmin, pmax, data) { // copy before sorting to not impact original, and filter out NaN var dx = data.slice().filter(Boolean) dx.sort() var imin = Math.floor(pmin * dx.length) var imax = Math.floor(pmax * dx.length) return [dx[imin], dx[imax]] } // Smoothing kernel var kernel = []; for(var i=-2*nsmooth; i<=2*nsmooth; i++) { kernel.push(Math.exp(-(i**2)/(2*nsmooth))) } var kernel_offset = Math.floor(kernel.length/2) // Smooth plot and recalculate ymin/ymax // TODO: add smoother function to reduce duplicated code var ymin = 0.0 var ymax = 0.0 for (var i=0; i<spectra.length; i++) { var data = spectra[i].data var plotflux = data['plotflux'] var origflux = data['origflux'+ifiber] for (var j=0; j<plotflux.length; j++) { if(nsmooth == 0) { plotflux[j] = origflux[j] } else { plotflux[j] = 0.0 var weight = 0.0 // TODO: speed could be improved by moving `if` out of loop for (var k=0; k<kernel.length; k++) { var m = j+k-kernel_offset if((m >= 0) && (m < plotflux.length)) { var fx = origflux[m] if(fx == fx) { plotflux[j] = plotflux[j] + fx * kernel[k] weight += kernel[k] } } } plotflux[j] = plotflux[j] / weight } } spectra[i].change.emit() tmp = get_y_minmax(0.01, 0.99, plotflux) ymin = Math.min(ymin, tmp[0]) ymax = Math.max(ymax, tmp[1]) } // update model if(model) { var plotflux = model.data['plotflux'] var origflux = model.data['origflux'+ifiber] for (var j=0; j<plotflux.length; j++) { if(nsmooth == 0) { plotflux[j] = origflux[j] } else { plotflux[j] = 0.0 var weight = 0.0 // TODO: speed could be improved by moving `if` out of loop for (var k=0; k<kernel.length; k++) { var m = j+k-kernel_offset if((m >= 0) && (m < plotflux.length)) { var fx = origflux[m] if(fx == fx) { plotflux[j] = plotflux[j] + fx * kernel[k] weight += kernel[k] } } } plotflux[j] = plotflux[j] / weight } } model.change.emit() } // update y_range if(ymin<0) { fig.y_range.start = ymin * 1.4 } else { fig.y_range.start = ymin * 0.6 } fig.y_range.end = ymax * 1.4 """) smootherslider.js_on_change('value', update_plot) ifiberslider.js_on_change('value', update_plot) #----- #- Add navigation buttons navigation_button_width = 30 prev_button = Button(label="<", width=navigation_button_width) next_button = Button(label=">", width=navigation_button_width) prev_callback = CustomJS(args=dict(ifiberslider=ifiberslider), code=""" if(ifiberslider.value>0) { ifiberslider.value-- } """) next_callback = CustomJS(args=dict(ifiberslider=ifiberslider, nspec=nspec), code=""" if(ifiberslider.value<nspec+1) { ifiberslider.value++ } """) prev_button.js_on_event('button_click', prev_callback) next_button.js_on_event('button_click', next_callback) #----- slider_width = plot_width - 2 * navigation_button_width navigator = bk.Row( widgetbox(prev_button, width=navigation_button_width), widgetbox(next_button, width=navigation_button_width + 20), widgetbox(ifiberslider, width=slider_width - 20)) bk.show( bk.Column( bk.Row(fig, zoomfig), widgetbox(target_info_div, width=plot_width), navigator, widgetbox(smootherslider, width=plot_width // 2), bk.Row( widgetbox(waveframe_buttons, width=120), widgetbox(zslider, width=plot_width // 2 - 60), widgetbox(dzslider, width=plot_width // 2 - 60), ), widgetbox(lines_button_group), ))
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 #----- #- Ifiberslider and smoothing widgets # Ifiberslider'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 self.ifiberslider = Slider(start=0, end=slider_end, value=0, step=1, title='Spectrum (of '+str(nspec)+')') self.smootherslider = Slider(start=0, end=26, value=0, step=1.0, title='Gaussian Sigma Smooth') self.coaddcam_buttons = None self.model_select = None 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(ifiberslider=self.ifiberslider), code=""" if(ifiberslider.value>0 && ifiberslider.end>=1) { ifiberslider.value-- } """) self.next_callback = CustomJS( args=dict(ifiberslider=self.ifiberslider, nspec=nspec), code=""" if(ifiberslider.value<nspec-1 && ifiberslider.end>=1) { ifiberslider.value++ } """) self.prev_button.js_on_event('button_click', self.prev_callback) self.next_button.js_on_event('button_click', self.next_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), 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.dzslider.format = "0[.]0000" self.z_input = TextInput(value="{:.4f}".format(z), title="Redshift 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) 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, ifiberslider=self.ifiberslider), code=""" var ifiber = ifiberslider.value var z = metadata.data['Z'][ifiber] z_input.value = z.toFixed(4) """) self.zreset_button.js_on_event('button_click', self.zreset_callback) self.z_input_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, metadata = viewer_cds.cds_metadata, ifiberslider = self.ifiberslider, zslider = self.zslider, dzslider = self.dzslider, z_input = self.z_input, waveframe_buttons = self.waveframe_buttons, 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 ), code=""" var z = parseFloat(z_input.value) if ( z >=-0.1 && z <= 5.0 ) { // update zsliders only if needed (avoid recursive call) z_input.value = parseFloat(z_input.value).toFixed(4) var z1 = Math.floor(z*100) / 100 var z2 = z-z1 if ( Math.abs(z1-zslider.value) >= 0.01) zslider.value = parseFloat(parseFloat(z1).toFixed(2)) if ( Math.abs(z2-dzslider.value) >= 0.0001) dzslider.value = parseFloat(parseFloat(z2).toFixed(4)) } else { if (z_input.value < -0.1) z_input.value = (-0.1).toFixed(4) if (z_input.value > 5) z_input.value = (5.0).toFixed(4) } var line_restwave = line_data.data['restwave'] var ifiber = ifiberslider.value var waveshift_lines = (waveframe_buttons.active == 0) ? 1+z : 1 ; var waveshift_spec = (waveframe_buttons.active == 0) ? 1 : 1/(1+z) ; for(var i=0; i<line_restwave.length; i++) { lines[i].location = line_restwave[i] * waveshift_lines line_labels[i].x = line_restwave[i] * waveshift_lines zlines[i].location = line_restwave[i] * waveshift_lines zline_labels[i].x = line_restwave[i] * waveshift_lines } if (overlap_bands.length>0) { for (var i=0; i<overlap_bands.length; i++) { overlap_bands[i].left = overlap_waves[i][0] * waveshift_spec overlap_bands[i].right = overlap_waves[i][1] * waveshift_spec } } function shift_plotwave(cds_spec, waveshift) { var data = cds_spec.data var origwave = data['origwave'] var plotwave = data['plotwave'] if ( plotwave[0] != origwave[0] * waveshift ) { // Avoid redo calculation if not needed for (var j=0; j<plotwave.length; j++) { plotwave[j] = origwave[j] * waveshift ; } cds_spec.change.emit() } } for(var i=0; i<spectra.length; i++) { shift_plotwave(spectra[i], waveshift_spec) } if (coaddcam_spec) shift_plotwave(coaddcam_spec, waveshift_spec) // Update model wavelength array // NEW : don't shift model if othermodel is there if (othermodel) { var zref = othermodel.data['zref'][0] var waveshift_model = (waveframe_buttons.active == 0) ? (1+z)/(1+zref) : 1/(1+zref) ; shift_plotwave(othermodel, waveshift_model) } else if (model) { var zfit = 0.0 if(metadata.data['Z'] !== undefined) { zfit = metadata.data['Z'][ifiber] } var waveshift_model = (waveframe_buttons.active == 0) ? (1+z)/(1+zfit) : 1/(1+zfit) ; shift_plotwave(model, waveshift_model) } """) self.z_input.js_on_change('value', self.z_input_callback) self.waveframe_buttons.js_on_click(self.z_input_callback) self.plotrange_callback = CustomJS( args = dict( z_input=self.z_input, waveframe_buttons=self.waveframe_buttons, fig=plots.fig, ), code=""" var z = parseFloat(z_input.value) // Observer Frame if(waveframe_buttons.active == 0) { fig.x_range.start = fig.x_range.start * (1+z) fig.x_range.end = fig.x_range.end * (1+z) } else { fig.x_range.start = fig.x_range.start / (1+z) fig.x_range.end = fig.x_range.end / (1+z) } """ ) self.waveframe_buttons.js_on_click(self.plotrange_callback) # TODO: for record: is this related to waveframe bug? : 2 callbakcs for same click... def add_oii_widgets(self, plots): #------ #- Zoom on the OII doublet TODO mv js code to other file # TODO: is there another trick than using a cds to pass the "oii_saveinfo" ? # TODO: optimize smoothing for autozoom (current value: 0) cds_oii_saveinfo = ColumnDataSource( {'xmin':[plots.fig.x_range.start], 'xmax':[plots.fig.x_range.end], 'nsmooth':[self.smootherslider.value]}) self.oii_zoom_button = Button(label="OII-zoom", button_type="default") self.oii_zoom_callback = CustomJS( args = dict(z_input=self.z_input, fig=plots.fig, smootherslider=self.smootherslider, cds_oii_saveinfo=cds_oii_saveinfo), code = """ // Save previous setting (for the "Undo" button) cds_oii_saveinfo.data['xmin'] = [fig.x_range.start] cds_oii_saveinfo.data['xmax'] = [fig.x_range.end] cds_oii_saveinfo.data['nsmooth'] = [smootherslider.value] // Center on the middle of the redshifted OII doublet (vaccum) var z = parseFloat(z_input.value) fig.x_range.start = 3728.48 * (1+z) - 100 fig.x_range.end = 3728.48 * (1+z) + 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, cds_oii_saveinfo=cds_oii_saveinfo), code = """ fig.x_range.start = cds_oii_saveinfo.data['xmin'][0] fig.x_range.end = cds_oii_saveinfo.data['xmax'][0] smootherslider.value = cds_oii_saveinfo.data['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) def add_metadata_tables(self, viewer_cds, show_zcat=True, template_dicts=None, top_metadata=['TARGETID', 'EXPID']): """ 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 = ['TARGETID', 'HPXPIXEL', 'TILEID', 'COADD_NUMEXP', 'COADD_EXPTIME', 'NIGHT', 'EXPID', 'FIBER', 'CAMERA', 'MORPHTYPE'] metadata_to_check += [ ('mag_'+x) for x in viewer_cds.phot_bands ] 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 is not None : if template_dicts is not None : # Add other best fits fit_results = template_dicts[1] # Case of DeltaChi2 : compute it from Chi2s # The "DeltaChi2" in rr fits is between best fits for a given (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=[]) self.majorline_checkbox = CheckboxGroup( labels=['Show only major lines'], active=[]) 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, template_dicts, num_approx_fits, with_full_2ndfit=True): #------ #- Select secondary model to display model_options = ['Best fit', '2nd best fit'] 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 with_full_2ndfit is False : model_options.remove('2nd best fit') for std_template in ['QSO', 'GALAXY', 'STAR'] : 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(ifiberslider = self.ifiberslider, model_select = self.model_select, fit_templates=template_dicts[0], cds_othermodel = viewer_cds.cds_othermodel, cds_model_2ndfit = viewer_cds.cds_model_2ndfit, cds_model = viewer_cds.cds_model, fit_results=template_dicts[1], std_templates=template_dicts[2], 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, template_dicts): #----- #- 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"]) # TMP handling of template_dicts the_fit_results = None if template_dicts is None else template_dicts[1] # dirty 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, fit_results = the_fit_results, 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, ifiberslider = self.ifiberslider, smootherslider = self.smootherslider, z_input = self.z_input, fig = plots.fig, imfig_source = plots.imfig_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.ifiberslider.js_on_change('value', self.update_plot_callback)