Source code for desispec.workflow.proc_dashboard_funcs

"""
desispec.workflow.proc_dashboard_funcs
======================================

"""
import os,glob
import json
import sys
import re
import time,datetime
import numpy as np
from os import listdir
from astropy.table import Table
from astropy.io import fits

########################
### Helper Functions ###
########################
from desispec.io import rawdata_root, specprod_root
from desispec.io.util import camword_to_spectros, decode_camword, \
    difference_camwords, create_camword, parse_badamps
from desispec.workflow.exptable import get_exposure_table_column_types, \
    default_obstypes_for_exptable, get_exposure_table_column_defaults, \
    get_exposure_table_pathname
from desispec.workflow.proctable import get_processing_table_pathname
from desispec.workflow.tableio import load_table


def get_output_dir(desi_spectro_redux, specprod, output_dir, makedir=True):
    if 'DESI_SPECTRO_DATA' not in os.environ.keys():
        os.environ['DESI_SPECTRO_DATA'] = '/global/cfs/cdirs/desi/spectro/data/'

    if specprod is None:
        if 'SPECPROD' not in os.environ.keys():
            os.environ['SPECPROD'] = 'daily'
        specprod = os.environ['SPECPROD']
    else:
        os.environ['SPECPROD'] = specprod

    if desi_spectro_redux is None:
        if 'DESI_SPECTRO_REDUX' not in os.environ.keys():  # these are not set by default in cronjob mode.
            os.environ['DESI_SPECTRO_REDUX'] = \
                '/global/cfs/cdirs/desi/spectro/redux/'
        desi_spectro_redux = os.environ['DESI_SPECTRO_REDUX']
    else:
        os.environ['DESI_SPECTRO_REDUX'] = desi_spectro_redux

    ## Verify the production directory exists
    prod_dir = os.path.join(desi_spectro_redux, specprod)
    if not os.path.exists(prod_dir):
        raise ValueError(
            f"Path {prod_dir} doesn't exist for production directory.")

    ## Define output_dir if not defined
    if output_dir is None:
        if 'DESI_DASHBOARD' not in os.environ.keys():
            os.environ['DESI_DASHBOARD'] = os.path.join(prod_dir,
                                                        'run', 'dashboard')
        output_dir = os.environ["DESI_DASHBOARD"]
    else:
        os.environ['DESI_DASHBOARD'] = output_dir

    ## Ensure we have directories to output to
    if makedir:
        os.makedirs(output_dir, exist_ok=True)

    return output_dir, prod_dir

def get_nights_dict(nights_arg, start_night, end_night, prod_dir):
    if nights_arg is None or nights_arg == 'all' \
            or (',' not in nights_arg and int(nights_arg) < 20000000):
        nights = list()
        for n in listdir(
                os.path.join(prod_dir, 'run', 'scripts', 'night')):
            # - nights are 20YYMMDD
            if re.match('^20\d{6}$', n):
                nights.append(n)
    else:
        nights = [nigh.strip(' \t') for nigh in nights_arg.split(',')]

    # tonight=what_night_is_it()   # Disabled per Anthony's request
    # if str(tonight) not in nights:
    #    nights.append(str(tonight))
    nights.sort(reverse=True)

    nights = np.array(nights)

    if start_night is not None:
        nights = nights[
            np.where(int(start_night) <= nights.astype(int))[0]]
    if end_night is not None:
        nights = nights[np.where(int(end_night) >= nights.astype(int))[0]]

    if nights_arg is not None and nights_arg.isnumeric() and len(
            nights) >= int(nights_arg):
        if end_night is None or start_night is not None:
            print(f"Only showing the most recent {int(nights_arg)} days")
            nights = nights[:int(nights_arg)]
        else:
            nights = nights[-1 * int(nights_arg):]

    nights_dict = dict()
    for night in nights:
        month = night[:6]
        if month not in nights_dict.keys():
            nights_dict[month] = [night]
        else:
            nights_dict[month].append(night)

    return nights_dict, nights

def get_tables(night, check_on_disk=False, exptab_colnames=None):
    if exptab_colnames is None:
        exptab_colnames = ['EXPID', 'FA_SURV', 'FAPRGRM', 'CAMWORD', 'BADCAMWORD',
                           'BADAMPS', 'EXPTIME', 'OBSTYPE', 'TILEID', 'COMMENTS',
                           'LASTSTEP']

    file_exptable = get_exposure_table_pathname(night)
    file_processing = get_processing_table_pathname(specprod=None,
                                                    prodmod=night)
    # procpath,procname = os.path.split(file_processing)
    # file_unprocessed = os.path.join(procpath,procname.replace('processing','unprocessed'))
    edefs = get_exposure_table_column_defaults(asdict=True)
    for col in exptab_colnames:
        if col not in edefs.keys():
            ValueError(f"requested dashboard exposure table column {col} not" +
                       f" in the exposure table columns: {edefs.keys()}.")

    try:  # Try reading tables first. Switch to counting files if failed.
        d_exp = load_table(file_exptable, tabletype='exptable')
        for col in exptab_colnames:
            if col not in d_exp.colnames:
                d_exp[col] = edefs[col]
    except:
        print(
            f'WARNING: Error reading exptable for {night}. Changing check_on_disk to True and scanning files on disk.')
        etypes = get_exposure_table_column_types(asdict=True)
        exptab_dtypes = [etypes[col] for col in exptab_colnames]
        d_exp = Table(names=exptab_colnames, dtype=exptab_dtypes)
        check_on_disk = True

    unaccounted_for_expids, unaccounted_for_tileids = [], []
    if check_on_disk:
        rawdatatemplate = os.path.join(rawdata_root(), night, '{zexpid}',
                                       'desi-{zexpid}.fits.fz')
        rawdata_fileglob = rawdatatemplate.format(zexpid='*')
        known_exposures = set(list(d_exp['EXPID']))
        newexpids = list(find_new_exps(rawdata_fileglob, known_exposures))
        newexpids.sort(reverse=True)
        default_obstypes = default_obstypes_for_exptable()
        for expid in newexpids:
            zfild_expid = str(expid).zfill(8)
            filename = rawdatatemplate.format(zexpid=zfild_expid)
            h1 = fits.getheader(filename, 1)
            header_info = {keyword: 'unknown' for keyword in
                           ['SPCGRPHS', 'EXPTIME',
                            'FA_SURV', 'FAPRGRM'
                                       'OBSTYPE', 'TILEID']}
            for keyword in header_info.keys():
                if keyword in h1.keys():
                    header_info[keyword] = h1[keyword]

            if header_info['OBSTYPE'] in default_obstypes:
                header_info['EXPID'] = expid
                header_info['LASTSTEP'] = 'all'
                header_info['COMMENTS'] = []
                if header_info['SPCGRPHS'] != 'unknown':
                    specs = str(header_info['SPCGRPHS']).replace(' ', '').replace(',', '')
                    header_info['CAMWORD'] = f'a{specs}'
                else:
                    header_info['CAMWORD'] = header_info['SPCGRPHS']
                header_info.pop('SPCGRPHS')
                d_exp.add_row(header_info)
                unaccounted_for_expids.append(expid)
                unaccounted_for_tileids.append(header_info['TILEID'])

    try:
        d_processing = load_table(file_processing, tabletype='proctable')
    except:
        d_processing = None
        print('WARNING: Error reading proctable. Only exposures in preproc'
              + ' directory will be marked as processing.')

    return d_exp, d_processing, np.array(unaccounted_for_expids), \
           np.unique(unaccounted_for_tileids)

def get_terminal_steps(expected_by_type):
    ## Determine the last filetype that is expected for each obstype
    terminal_steps = dict()
    for obstype, expected in expected_by_type.items():
        terminal_steps[obstype] = None
        keys = list(expected.keys())
        for key in reversed(keys):
            if expected[key] > 0:
                terminal_steps[obstype] = key
                break
    return terminal_steps

def get_file_list(filename, doaction=True):
    if doaction and filename is not None and os.path.exists(filename):
        output = np.atleast_1d(np.loadtxt(filename, dtype=int)).tolist()
    else:
        output = []
    return output

def get_skipped_ids(expid_filename, skip_ids=True):
    return get_file_list(filename=expid_filename, doaction=skip_ids)

[docs]def what_night_is_it(): """ Return the current night """ d = datetime.datetime.utcnow() - datetime.timedelta(7 / 24 + 0.5) tonight = int(d.strftime('%Y%m%d')) return tonight
[docs]def find_new_exps(fileglob, known_exposures): """ Check the path given for new exposures """ datafiles = sorted(glob.glob(fileglob)) newexp = list() for filepath in datafiles: expid = int(os.path.basename(os.path.dirname(filepath))) if expid not in known_exposures: newexp.append(expid) return set(newexp)
[docs]def check_running(proc_name= 'desi_dailyproc',suppress_outputs=False): """ Check if the desi_dailyproc process is running """ import psutil running = False mypid = os.getpid() for p in psutil.process_iter(): if p.pid != mypid and proc_name in ' '.join(p.cmdline()): if not suppress_outputs: print('ERROR: {} already running as PID {}:'.format(proc_name,p.pid)) print(' ' + ' '.join(p.cmdline())) running = True break return running
################################# ### HTML Generating Functions ### ################################# def return_color_profile(): color_profile = dict() color_profile['DEFAULT'] = {'font':'#000000' ,'background':'#ccd1d1'} # gray color_profile['NULL'] = {'font': '#34495e', 'background': '#ccd1d1'} # gray on gray color_profile['BAD'] = {'font':'#000000' ,'background':'#d98880'} # red color_profile['INCOMPLETE'] = {'font': '#000000','background':'#f39c12'} # orange color_profile['GOOD'] = {'font':'#000000' ,'background':'#7fb3d5'} # blue color_profile['OVERFULL'] = {'font': '#000000','background':'#c39bd3'} # purple return color_profile def make_html_page(monthly_tables, outfile, titlefill='Processing', show_null=False, color_profile=None): if color_profile is None: color_profile = return_color_profile() html_page = _initialize_page(color_profile, titlefill=titlefill) for month, nightly_tables in monthly_tables.items(): if len(nightly_tables) == 0: continue print( "Month: {}, nights: {}".format(month, list(nightly_tables.keys()))) nightly_table_htmls, statuses = list(), list() for night, night_info in nightly_tables.items(): if len(night_info) == 0: continue #################################### ### Table for individual night #### #################################### nightly_table_html, status = \ generate_nightly_table_html(night_info, night, show_null) nightly_table_htmls.append(nightly_table_html) statuses.append(status) html_page += generate_monthly_table_html(nightly_table_htmls, statuses, month) # html_page += js_import_str(os.environ['DESI_DASHBOARD']) html_page += js_str() html_page += _closing_str() with open(outfile, 'w') as hs: hs.write(html_page) print(f"Write to {outfile} complete.") if 'NERSC_HOST' in os.environ and outfile.startswith( '/global/cfs/cdirs/desi'): url = outfile.replace('/global/cfs/cdirs/desi', 'https://data.desi.lbl.gov/desi') print(f"This can be found via webserver at: {url}")
[docs]def generate_monthly_table_html(tables, statuses, month): """ Add a collapsible and extendable table to the html file for a specific month Input tables: list of tables generated by 'nightly_table' month: string of YYYYMM, e.g. 202001 output: The string to be added to the html file """ month_dict = {'01':'January','02':'February','03':'March','04':'April','05':'May','06':'June', '07':'July','08':'August','09':'September','10':'October','11':'November','12':'December'} heading = f"{month_dict[month[4:]]} {month[:4]} ({month})" month_table_str = '\n<!--Begin {}-->\n'.format(month) statuses = np.array(statuses) monthlystatus = 'DEFAULT' if np.all(statuses == 'GOOD'): monthlystatus = 'GOOD' elif np.any(statuses == 'BAD'): monthlystatus = 'BAD' elif np.any(statuses == 'INCOMPLETE'): monthlystatus = 'INCOMPLETE' elif np.any(statuses == 'OVERFULL'): monthlystatus = 'OVERFULL' month_table_str += f'<button class="collapsible" id="{monthlystatus}">' \ + heading + '</button>' month_table_str += '<div class="content" style="display:inline-block;min-height:0%;">\n' #month_table_str += "<table id='c'>" for table_str in tables: month_table_str += table_str #month_table_str += "</table></div>\n" month_table_str += "</div>\n" month_table_str += '<!--End {}-->\n\n'.format(month) return month_table_str
def generate_nightly_table_html(night_info, night, show_null): if len(night_info) == 0: return '', 'NULL' ngood, ninter, nbad, nnull, nover, n_notnull, noprocess, norecord = \ 0, 0, 0, 0, 0, 0, 0, 0 main_body = "" for key, row_info in reversed(night_info.items()): table_row = _table_row(row_info) if not show_null and 'NULL' in table_row: continue main_body += ("\t" + table_row + "\n") status = str(row_info["STATUS"]).lower() if status == 'processing': if 'COLOR' in row_info: color = row_info['COLOR'] else: color = table_row.split('">')[0].split('id="')[1] if color == 'GOOD': ngood += 1 n_notnull += 1 elif color == 'BAD': nbad += 1 n_notnull += 1 elif color == 'INCOMPLETE': ninter += 1 n_notnull += 1 elif color == 'OVERFULL': nover += 1 n_notnull += 1 else: nnull += 1 elif status == 'unprocessed': noprocess += 1 elif status == 'unrecorded': norecord += 1 else: nnull += 1 # Night dropdown table htmltab = r'&nbsp;&nbsp;&nbsp;&nbsp;' heading = (f"Night {night}{htmltab}" + f"Complete: {ngood}/{n_notnull}{htmltab}" + f"Incomplete: {ninter}/{n_notnull}{htmltab}" + f"Failed: {nbad}/{n_notnull}{htmltab}" + f"Overfull: {nover}/{n_notnull}{htmltab}" + f"Unprocessed: {noprocess}{htmltab}" + f"NoTabEntry: {norecord}{htmltab}" + f"Other: {nnull}" ) night_status = 'DEFAULT' if ngood == n_notnull: night_status = "GOOD" elif ninter > 0: night_status = "INCOMPLETE" elif nbad > 0: night_status = "BAD" elif nover > 0: night_status = "OVERFULL" nightly_table_str = '<!--Begin {}-->\n'.format(night) nightly_table_str += f'<button class="collapsible" id="{night_status}">{heading}</button>' nightly_table_str += '<div class="content" style="display:inline-block;min-height:0%;">\n' # table header nightly_table_str += "<table id='c' class='nightTable'><tbody>\n\t<tr>" for col in list(row_info.keys()): colname = str(col).upper() if colname != 'COLOR': nightly_table_str += f"<th>{colname}</th>" nightly_table_str += "</tr>\n" # Add body nightly_table_str += main_body # End table nightly_table_str += "</tbody></table></div>\n" nightly_table_str += '<!--End {}-->\n\n'.format(night) return nightly_table_str, night_status def read_json(filename_json): night_json_info = None if os.path.exists(filename_json): with open(filename_json) as json_file: try: night_json_info = json.load(json_file) except: print(f"Error trying to load {filename_json}, " + "continuing without that information.") return night_json_info def write_json(output_data, filename_json): ## write out the night_info to json file with open(filename_json, 'w') as json_file: try: json.dump(output_data, json_file) except: print(f"Error trying to dump {filename_json}, " + "not saving that information.")
[docs]def _initialize_page(color_profile, titlefill='Processing'): """ Initialize the html file for showing the statistics, giving all the headers and CSS setups. """ # strTable="<html><style> table {font-family: arial, sans-serif;border-collapse: collapse;width: 100%;}" # strTable=strTable+"td, th {border: 1px solid #dddddd;text-align: left;padding: 8px;}" # strTable=strTable+"tr:nth-child(even) {background-color: #dddddd;}</style>" html_page = """<html><head> <meta http-equiv="content-type" content="text/html; charset=UTF-8"><style> h1 {font-family: 'sans-serif';font-size:50px;color:#4CAF50} #c {font-family: 'Trebuchet MS', Arial, Helvetica, sans-serif;border-collapse: collapse;width: 100%;} #c td, #c th {border: 1px solid #ddd;padding: 8px;} /* #c tr:nth-child(even){background-color: #f2f2f2;} */ #c tr:hover {background-color: #ddd;} #c th {padding-top: 12px; padding-bottom: 12px; text-align: left; background-color: #34495e; color: white;} .collapsible {background-color: #eee;color: #444;cursor: pointer;padding: 18px;width: 100%;border: none;text-align: left;outline: none;font-size: 25px;} .regular {background-color: #eee;color: #444; cursor: pointer; padding: 18px; width: 25%; border: 18px; text-align: left; outline: none; font-size: 25px;} .active, .collapsible:hover { background-color: #ccc;} .content {padding: 0 18px;display: table;overflow: hidden;background-color: #f1f1f1;maxHeight:0px;} /* The Modal (background) */ .modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ z-index: 1; /* Sit on top */ padding-top: 100px; /* Location of the box */ left: 0; top: 0; width: 100%; /* Full width */ height: 90%; /* Full height */ overflow: auto; /* Enable scroll if needed */ background-color: rgb(0,0,0); /* Fallback color */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ } /* Modal Content */ .modal-content { background-color: #fefefe; margin: auto; padding: 20px; border: 1px solid #888; width: 80%; } /* The Close Button */ .close { color: #aaaaaa; float: right; font-size: 28px; font-weight: bold; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; } #obstypelist { background-position: 10px 10px; background-repeat: no-repeat; width: 10%; font-size: 16px; padding: 12px 20px 12px 40px; border: 1px solid #ddd; margin-bottom: 12px; } #exptimelist { background-position: 10px 10px; background-repeat: no-repeat; width: 10%; font-size: 16px; padding: 12px 20px 12px 40px; border: 1px solid #ddd; margin-bottom: 12px; } """ for ctype,cdict in color_profile.items(): background = cdict['background'] html_page += f'\t#{ctype} ' + '{background-color:' + f'{background}' + ';}\n' html_page + "\n" ## Table rows shouldn't do the default background because of cell coloring for ctype,cdict in color_profile.items(): font = cdict['font'] background = '#eee'#cdict['background'] # no background for a whole table after implementing color codes for processing columns html_page += f'\ttable tr#{ctype} '+'{background-color:'+f'{background}; color:{font}'+';}\n' ## Finally there is a class of table element that is null in a good way ## Label as such html_page += 'table td#GOODNULL {background-color:#7fb3d5;color:gray}\n' html_page += '</style>\n\n' html_page += f"</head><body><h1>DESI '{os.environ['SPECPROD']}' " html_page += f'{titlefill} Status Monitor</h1>\n' timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) # running='No' # if check_running(proc_name='desi_dailyproc',suppress_outputs=True): # running='Yes' # strTable=strTable+"<div style='color:#00FF00'>{} {} running: {}</div>\n".format(timestamp,'desi_dailyproc',running) script = os.path.basename(sys.argv[0]) html_page += f'<div style="color:#00FF00;margin-bottom:20px"> {script} running at: {timestamp}</div>\n' html_page += 'Color Legend:\n' html_page += '<table style="margin-bottom:20px;margin-left:20px"><tr>\n' for ctype in color_profile.keys(): html_page += f' <td id="{ctype}">{ctype}</td>\n' html_page += f'</tr></table>\n' html_page += '\n\n' html_page += """Filter By Status: <select style="margin-bottom:10px" id="statuslist" onchange="filterByStatus()" class='form-control'> <option>processing</option> <option>unprocessed</option> <option>unaccounted</option> <option>ALL</option> </select> """ # The following codes are for filtering rows by obstype and exptime. Not in use for now, but basically can be enabled anytime. # html_page +="""Filter By OBSTYPE: # <select id="obstypelist" onchange="filterByObstype()" class='form-control'> # <option>ALL</option> # <option>SCIENCE</option> # <option>FLAT</option> # <option>ARC</option> # <option>DARK</option> # </select> # Exptime Limit: # <select id="exptimelist" onchange="filterByExptime()" class='form-control'> # <option>ALL</option> # <option>5</option> # <option>30</option> # <option>120</option> # <option>900</option> # <option>1200</option> # </select> # """ return html_page
def _closing_str(): closing = """<div class="crt-wrapper"></div> <div class="aadvantage-wrapper"></div> </body></html>""" return closing def _table_row(dictionary): idlabel = dictionary.pop('COLOR') color_profile = return_color_profile() if dictionary["STATUS"] != 'processing': style_str = 'display:none;' else: style_str = '' if idlabel is None: row_str = '<tr style="{}">'.format(style_str) else: row_str = '<tr style="'+style_str+'" id="'+str(idlabel)+'">' for elem in dictionary.values(): chars = str(elem).split('/') if len(chars)==2: # m/n if chars[0]=='0' and chars[1]=='0': row_str += _table_element_id(elem, 'GOODNULL') elif chars[0]=='0' and chars[1]!='0': row_str += _table_element_id(elem, 'BAD') elif chars[0]!='0' and int(chars[0])<int(chars[1]): row_str += _table_element_id(elem, 'INCOMPLETE') elif chars[0]!='0' and int(chars[0])==int(chars[1]): row_str += _table_element_id(elem, 'GOOD') else: row_str += _table_element_id(elem, 'OVERFULL') else: row_str += _table_element(elem) row_str += '</tr>'#\n' return row_str def _table_element(elem): return '<td>{}</td>'.format(elem) def _table_element_style(elem,style): return f'<td style="{style}">{elem}</td>' def _table_element_id(elem,id): return f'<td id="{id}">{elem}</td>' def _hyperlink(rel_path,displayname): hlink = f'<a href="{rel_path}" target="_blank"' \ + f' rel="noopener noreferrer">{displayname}</a>' return hlink def _str_frac(numerator,denominator): frac = f'{numerator}/{denominator}' return frac def _js_path(output_dir): return os.path.join(output_dir,'js','open_nightly_table.js') def js_import_str(output_dir): # Not used output_path = _js_path(output_dir) if not os.path.exists(os.path.join(output_dir,'js')): os.makedirs(os.path.join(output_dir,'js')) if not os.path.exists(output_path): _write_js_script(output_path) return f'<script type="text/javascript" src="{output_path}"></script>'
[docs]def _write_js_script(output_path): """ Return the javascript script to be added to the html file """ s=""" var coll = document.getElementsByClassName('collapsible'); var i; for (i = 0; i < coll.length; i++) { coll[i].nextElementSibling.style.maxHeight='0px'; coll[i].addEventListener('click', function() { this.classList.toggle('active'); var content = this.nextElementSibling; if (content.style.maxHeight){ content.style.maxHeight = null; } else { content.style.maxHeight = '0px'; } }); }; var b1 = document.getElementById('b1'); b1.addEventListener('click',function() { for (i = 0; i < coll.length; i++) { coll[i].nextElementSibling.style.maxHeight=null; }}); var b2 = document.getElementById('b2'); b2.addEventListener('click',function() { for (i = 0; i < coll.length; i++) { coll[i].nextElementSibling.style.maxHeight='0px' }}); """ with open(output_path,'w') as outjs: outjs.write(s)
[docs]def js_str(): # Used """ Return the javascript script to be added to the html file """ s=""" <script > var coll = document.getElementsByClassName('collapsible'); var i; for (i = 0; i < coll.length; i++) { coll[i].nextElementSibling.style.maxHeight='0px'; coll[i].addEventListener('click', function() { this.classList.toggle('active'); var content = this.nextElementSibling; if (content.style.maxHeight){ content.style.maxHeight = null; } else { content.style.maxHeight = '0px'; } }); }; var b1 = document.getElementById('b1'); b1.addEventListener('click',function() { for (i = 0; i < coll.length; i++) { coll[i].nextElementSibling.style.maxHeight=null; }}); var b2 = document.getElementById('b2'); b2.addEventListener('click',function() { for (i = 0; i < coll.length; i++) { coll[i].nextElementSibling.style.maxHeight='0px' }}); function filterByStatus() { var input, filter, table, tr, td, i; input = document.getElementById("statuslist"); filter = input.value.toUpperCase(); tables = document.getElementsByClassName("nightTable") for (j = 0; j < tables.length; j++){ table = tables[j] tr = table.getElementsByTagName("tr"); for (i = 0; i < tr.length; i++) { td = tr[i].getElementsByTagName("td")[15]; console.log(td) if (td) { if (td.innerHTML.toUpperCase().indexOf(filter) > -1 || filter==='ALL') { tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } }} function filterByObstype() { var input, filter, table, tr, td, i; input = document.getElementById("obstypelist"); filter = input.value.toUpperCase(); tables = document.getElementsByClassName("nightTable") for (j = 0; j < tables.length; j++){ table = tables[j] tr = table.getElementsByTagName("tr"); for (i = 0; i < tr.length; i++) { td = tr[i].getElementsByTagName("td")[2]; if (td) { if (td.innerHTML.toUpperCase().indexOf(filter) > -1 || filter==='ALL') { tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } }} function filterByExptime() { var input, filter, table, tr, td, i; input = document.getElementById("exptimelist"); filter = input.value.toUpperCase(); tables = document.getElementsByClassName("nightTable") for (j = 0; j < tables.length; j++){ table = tables[j] tr = table.getElementsByTagName("tr"); for (i = 0; i < tr.length; i++) { td = tr[i].getElementsByTagName("td")[3]; if (td) { if (filter==='ALL') { tr[i].style.display = ""; } else if (parseInt(td.innerHTML) <= parseInt(filter)){ tr[i].style.display = ""; } else { tr[i].style.display = "none"; } } } }} </script> """ return s