#!/usr/local/bin/perl ############################################################################### # $Id$ # # SBEAMS is Copyright (C) 2000-2021 Institute for Systems Biology # This program is governed by the terms of the GNU General Public License (GPL) # version 2 as published by the Free Software Foundation. It is provided # WITHOUT ANY WARRANTY. See the full description of GPL terms in the # LICENSE file distributed with this software. ############################################################################### ############################################################################### # Get the script set up with everything it will need ############################################################################### use strict; use lib qw (../../lib/perl); use Data::Dumper; use File::Basename; use File::Copy qw( move copy ); use LWP::UserAgent; use HTTP::Request; use JSON; use SBEAMS::Connection qw($q $log); use SBEAMS::Connection::Settings; use SBEAMS::Connection::Tables; use SBEAMS::Connection::DataTable; use SBEAMS::Connection::TabMenu; use SBEAMS::PeptideAtlas; use SBEAMS::PeptideAtlas::Settings; use SBEAMS::PeptideAtlas::Tables; ############################################################################### # Global Variables ############################################################################### my $sbeams = new SBEAMS::Connection; $sbeams->setSBEAMS_SUBDIR($SBEAMS_SUBDIR); my $atlas = new SBEAMS::PeptideAtlas; $atlas->setSBEAMS($sbeams); my $json = new JSON; # Read input parameters my $params = process_params(); my $username = $sbeams->Authenticate( allow_anonymous_access => 1) || exit; $sbeams->setSessionAttribute( key => 'PA_resource', value => 'DIAAtlas' ); # Get a list of accessible project_ids my @project_ids = $sbeams->getAccessibleProjects(); my $project_ids = join( ",", @project_ids ) || '0'; my $show_image = 0; my $tabMenu; my %basespace = ( basespace_params => '' ); { # Main # Authenticate or exit ## get current settings my $project_id = $sbeams->getCurrent_project_id(); if ( $params->{mode} eq 'get_public_libsets' ) { $params->{output_format} ||= 'html'; unless ( $params->{output_format} eq 'json' ) { print $sbeams->get_http_header(); } get_public_libsets(); exit; } $params->{action} ||= 'trigger'; if ( $params->{appsessionuri} && $params->{authorization_code} ) { $basespace{basespace_params} = qq~ {appsessionuri}> ~; $basespace{appsessionuri} = $params->{appsessionuri}; $basespace{authorization_code} = $params->{authorization_code}; $basespace{url_params} = ";authorization_code=$params->{authorization_code};appsessionuri=$params->{appsessionuri}"; authenticate_to_basespace(); get_appsession_properties(); # $log->info( "mode is $params->{mode}" ); $params->{mode} ||= $basespace{mode}; $params->{format} ||= 'peakview'; # $log->info( "mode is $params->{mode}" ); } $params->{mode} ||= 'download_libs'; my $program_name = ( $params->{mode} =~ /download/ ) ? 'DIA_library_download' : 'DIA_library_subset'; my $page = $sbeams->getGifSpacer( 700 ) . "
\n"; # Get the HTML to display the tabs $tabMenu = $atlas->getTabMenu( parameters_ref => $params, program_name => $program_name, ); my $css = $sbeams->printStyleSheet( module_only => 1 ); $page .=<<" END";
END # my $load_script = "set_toggle_box( 'protein_list_table' );sortables_init()"; if ( $params->{lib_id} ) { $params->{mode} = 'subset_libs'; } $params->{mode} ||= 'download_libs'; # die "header coming"; $atlas->display_page_header( tracker_type => 'swath' ); if ( $params->{mode} eq 'subset_libs' ) { my $title = qq~
| Custom Library Download

~; # $page .= $title; $page .= $tabMenu; $page .= get_status_box("Generating library, may take several minutes") unless $params->{action} eq 'trigger'; print $page; $page = get_subset_form(); } elsif ( $params->{mode} eq 'download_libs' ) { $page .= $tabMenu; if ( $params->{action} eq 'download' ) { $page .= get_status_box( "" ); print $page; upload_to_basespace(); $page = ''; } $page .= get_library_table(); } $page .= "
"; # Print what we already have, speed up apparent page loading time. print $page; $atlas->display_page_footer(); $sbeams->setSessionAttribute( key => 'PA_resource', value => '' ); } # end main sub get_status_box { my $msg = shift || ''; my $wait = ""; return qq~
$wait $msg


~; } sub get_help_links { my %title2text = ( 'Library' => "Source library file to customize.

Note that libraries that have already had SWATHS applied can only be filtered by protein list", 'min_num_frags' => "Minimum number of fragment ions to include an assay.

Must be less than or equal to Max num fragments ", 'max_num_frags' => "Maximum number of fragment ions to use in an assay for a specific precursor.

Must be greater than or equal to Min num fragments ", 'basename' => "Base name for exported library files (optional). Default is library name. ", 'prec_min_mz' => "Minimum precursor ion m/z value for library entries ", 'prec_max_mz' => "Maximum precursor ion m/z value for library entries ", 'frag_min_mz' => "Minimum fragment ion m/z value for library entries ", 'frag_max_mz' => "Maximum fragment ion m/z value for library entries ", 'cmod_mass' => 'Generate transision with heavy-labeled versions of selected amino acids, Use ctrl-shift for multiple options.', 'cmod_opts' => $q->escape( 'Generate heavy transitions, light and heavy (L & H), or light only (default). Relevant only if one or more heavy label options are selected.' ), 'swaths_file' => "File of SWATH bins, format is lower mz bound 'tab' upper bound mz 'newline' ", 'no_swaths_file' => "SWATH file not needed, ion library filtered at analysis time (Peakview) ", 'swath_size' => "Size of each SWATH bin, default is 25 Th ", 'swath_overlap' => "Overlap between adjacent SWATH bins, on each side - so 1 Th will result in 2 Th overlap ", 'Proteins' => "File of protein accessions, one per line, with which to filter library. One accession space per organism allowed: Human - Uniprot, Mtb - Tuberculist (Rv)", 'domain_protein_list_id' => "Set of protein accessions from one of the Human Proteome Project Biology/Disease lists
Note that this will only work with Human libraries" ); my %title2link; for my $title ( keys( %title2text ) ) { my $text = $title2text{$title}; $title2link{$title} = qq~~; } return \%title2link; } sub get_protein_list_selector { my $sql =<<" END"; SELECT title, protein_list_id FROM $TBAT_DOMAIN_PROTEIN_LIST DPL JOIN $TB_CONTACT C ON DPL.owner_contact_id = C.contact_id WHERE project_id IN ( $project_ids ) ORDER BY title END my $sth = $sbeams->get_statement_handle( $sql ); my $select; while( my @row = $sth->fetchrow_array() ) { my $selected = ( $row[1] == $params->{domain_protein_list_id} ) ? 'selected' : ''; my $title = $row[0]; $title =~ s/known to be associated with human/associated with/g; my $option = "\n"; $select .= $option; } $select .= "\n"; my $blank = ( $select =~ /selected/ ) ? '' : ''; $select = qq~ \n"; $lib_select = ""; for my $inst ( sort( keys( %inst ) ) ) { $inst_select .= "\n"; } $inst_select .= ""; my $sql =<<" END"; SELECT title, first_name, last_name, protein_list_id FROM $TBAT_DOMAIN_PROTEIN_LIST DPL JOIN $TB_CONTACT C ON DPL.owner_contact_id = C.contact_id ORDER BY title END my $title2help = get_help_links(); my $or = '~or~'; my $prec_min_mz = $params->{prec_min_mz} || 400; my $prec_max_mz = $params->{prec_max_mz} || 1200; my $frag_min_mz = $params->{frag_min_mz} || 350; my $frag_max_mz = $params->{frag_max_mz} || 1600; my $swath_size = $params->{swath_size} || 25; my $range = $prec_max_mz - $prec_min_mz; my $n_bins = int( $range/$swath_size ); $n_bins++ if $range/$swath_size > $n_bins; my $min_num_frags = $params->{min_num_frags} || 6; my $max_num_frags = $params->{max_num_frags} || 6; my $domain_protein_list_select = get_protein_list_selector(); my $spacer = ' ' x 30; my $no_swaths = ''; if ( $params->{format} && $params->{format} =~ /peakview/i ) { $no_swaths = 'checked'; } my $cmod_mass_select = get_cmod_select( $params->{cmod_mass} ); my $cmod_opts_radio = get_cmod_radio( $params->{cmod_opts} ); my $form = qq~ $help

$basespace{basespace_params} $title2help->{Library} $title2help->{basename} $title2help->{Proteins} $title2help->{domain_protein_list_id} $title2help->{prec_min_mz} $title2help->{prec_max_mz} $title2help->{frag_min_mz} $title2help->{frag_max_mz} $title2help->{min_num_frags} $title2help->{max_num_frags} $title2help->{cmod_mass} $title2help->{cmod_opts} $title2help->{swaths_file} $title2help->{no_swaths_file} $title2help->{swath_size} $title2help->{swath_overlap}
Library selection
Input library
:
$lib_select
Output file name:
 
Protein filtering
Proteins:
    $or
HPP/BD list:
$domain_protein_list_select
 
M/Z filtering
Min precursor mz:
Max precursor mz:
Min fragment mz:
Max fragment mz:
 
Number of fragment ions
Min num fragments:
Max num fragments:
 
Isotopic labelling
Heavy isotopes:
$cmod_mass_select
Transition types:
$cmod_opts_radio
 
SWATH Settings
Upload SWATHs file:
    $or
No SWATHs file:
    $or
SWATH window size:
($n_bins total bins)
SWATH window overlap:
 
 


~; return $form; } sub process_params { my $params = {}; $sbeams->parse_input_parameters( q => $q, parameters_ref => $params ); $sbeams->processStandardParameters( parameters_ref => $params ); return( $params ); } sub get_cmod_select { my $curr_val = shift || ''; my %sel = ( K8 => '', R10 => '', K6 => '', R6 => '' ); my %seen; for my $curr ( split( ',', $curr_val ) ) { $sel{$curr} = 'SELECTED'; } my $cmod_sel = qq~ ~; return $cmod_sel; } sub get_cmod_radio { my $curr_val = shift || 'light_only'; my %sel = ( light_only => '', 'heavy_only' => '', both => '' ); $sel{$curr_val} = 'checked'; my $cmod_radio = qq~ Light only Heavy only L & H ~; return $cmod_radio; } sub authenticate_to_basespace { my $uri = $basespace{appsessionuri}; my @appsession = split( /\//, $uri ); my $appsession_id = $appsession[$#appsession]; $basespace{appsession_id} = $appsession_id; # Have we been through authentication in this page? if ( $basespace{access_token} ) { # $log->info( "Already authenticated, returning" ); return; } # Will need a new user agent for subsequent transactions my $ua = LWP::UserAgent->new(); $ua->cookie_jar( {} ); # Is there a cached value? my $token = $sbeams->getSessionAttribute( key => $basespace{appsessionuri} ); if ( $token ) { # Yes, will use to set header # $log->info( "Fetched token from Session Cookie" ); } else { # No, fetch from bs using client ids and auth_code my $client = $CONFIG_SETTING{BASESPACE_IONLIBRARY_CLIENT_ID} || ''; my $client_secret = $CONFIG_SETTING{BASESPACE_IONLIBRARY_CLIENT_SECRET_ID} || ''; my $redirect = "https://db.systemsbiology.net" . $HTML_BASE_DIR . '/cgi/PeptideAtlas/GetDIALibs'; my $auth_code = $basespace{authorization_code}; my $auth_url = 'https://api.basespace.illumina.com/v1pre3/oauthv2/token'; my $post_param_ref = { client_id => $client, client_secret => $client_secret, code => $auth_code, redirect_uri => $redirect, grant_type => 'authorization_code' }; my $response = $ua->post( $auth_url, $post_param_ref ); for my $line ( split( "\n", $response->content() ) ) { if ( $line =~ /"access_token":"([^"]+)"/ ) { $token = $1; last; } } } if ( !$token ) { $log->warn( "Unable to get authorization from Base Space" ); } else { # Place access token in default header for user agent # for all subsequent transactions. $ua->default_header( 'x-access-token' => $token ); $basespace{ua} = $ua; $basespace{token} = $token; # Cache token for subsequent cgi calls with this appsession $sbeams->setSessionAttribute( key => $basespace{appsessionuri}, value => $token ); } } # End authenticate_to_basespace # Fetch appsession info, includes project name and ID to write to sub get_appsession_properties { if ( !$basespace{access_token} ) { authenticate_to_basespace(); } # User Agent created during authentication my $ua = $basespace{ua} || return; my $appsession_url = $basespace{appsession_url} || 'https://api.basespace.illumina.com/' . $basespace{appsessionuri}; $basespace{appsession_url} ||= $appsession_url; my $response = $ua->get( $appsession_url ); my @content = $response->content(); my $appsession = $json->decode( $content[0] ); $basespace{appsession} = $appsession; # Fetch project_id from Response->Properties, as per Illumina. # Response->References also contains info, but is deprecated. if ( $appsession->{Response} ) { if ( $appsession->{Response}->{Properties} ) { for my $ref ( @{$appsession->{Response}->{Properties}->{Items}} ) { my $name = $ref->{Name}; $name =~ s/Input\.//; if ( $ref->{Name} eq 'Input.project-id' ) { $basespace{project} = $ref->{Content}->{Name}; $basespace{project_id} = $ref->{Content}->{Id}; } else { $basespace{$name} = $ref->{Content}; } } } } # $basespace{ua} = ''; # $basespace{appsession} = ''; # die Dumper( %basespace ); } # End get_appsession_properties sub upload_to_basespace { my %args = @_; my $tmp_file = $args{tmpfile_name} || "$PHYSICAL_BASE_DIR/tmp/$params->{tmp_file}"; # If we came from basespace, are downloading an entire peakview file: if ( $params->{format} && $params->{format} =~ /peakview/i && $params->{name} && $params->{tmp_file} ) { $params->{name} =~ s/.csv$/.txt/; $params->{name} =~ s/.tsv$/.txt/; } my $basename = $args{basename} || $params->{name}; if ( $basename !~ /\.txt$/ ) { $basename =~ s/\....$/.txt/; } if ( $basename !~ /\.txt$/ ) { $basename .= '.txt'; } my $status = $args{status} || ''; if ( ! -e $tmp_file ) { die "No tmp file $tmp_file"; $log->warn( "Missing file $tmp_file" ); return; } if ( !$basespace{appsession} ) { get_appsession_properties(); } # set_appsession_status( 'Running' ); my $project = $basespace{project}; my $project_id = $basespace{project_id}; my $appsessionuri = $basespace{appsessionuri}; my $appsession_id = $basespace{appsession_id}; my $ua = $basespace{ua}; my $token = $basespace{token}; if ( !$token ) { $basespace{ua} = ''; $basespace{appsession} = ''; die Dumper( %basespace ); } if ( $project_id && $project ) { # We will assume that no App Results called 'SWATHIonLibraries' exists # for this project. Try to create. # Required info: Project.Id, AppResult.Name, .Desc, AppSession.ID $basespace{ua} = ''; $basespace{appsession} = ''; my $appresult_url = "https://api.basespace.illumina.com/v1pre3/projects/$project_id/appresults"; # my $appresult_param_ref = { Name => 'SWATHAtlasIonLibraries.txt', my $appresult_param_ref = { Name => $basename, Description => "Ion Library $basename from SWATH Atlas for SWATH Analysis", HrefAppSession => $appsessionuri }; my $ar_result = $ua->post( $appresult_url, $appresult_param_ref ); my @content = $ar_result->content(); my $ar_object = $json->decode( $content[0] ); my $HrefFiles = $ar_object->{Response}->{HrefFiles} || die Dumper( $ar_object ); # TODO validate ar_result? # We have a project, and have created an AppResult. Now upload file # $log->info( join( "\t", qw( Auth_code URI Token AppResult Project Project_name basename tmp_file client client_secret ) ) ); # $log->info( join( "\t", $auth_code, $appsession_id, $token, 'AppResultId', $project_id, $project, $basename, $tmp_file, $client, $client_secret ) ); print_js_status( "Uploading file to Base Space, may take a minute or two.
" ); my $t0 = time(); upload_file_multipart( token => $token, tmp_file => $tmp_file, href => $HrefFiles, name => $basename ); # upload_file( token => $token, tmp_file => $tmp_file, href => $HrefFiles, name => $basename ); my $t1 = time(); my $tdelta = $t1 - $t0; # Set analysis Status to Complete set_appsession_status( 'Complete', $status ); # if ( $params->{mode} eq 'download_libs' ) { # print_js_status( "Finished upload

", 1 ); # } print_js_status( "File $basename uploaded to project $project in Base Space in $tdelta seconds
" ); print_js_status( "Returning to Base Space...
" ); sleep 4; js_redirect( "https://basespace.illumina.com/analyses/$appsession_id/files" ); sleep 4; } else { print_js_status( "
Failure: no Base Space projects to write to" ); return; } } # End upload_to_basespace sub upload_file { my %args = @_; my $token = $args{token}; my $tmp_file = $args{tmp_file}; my $basename = $args{name}; my $HrefFiles = $args{href}; # my $exe = "curl -v -H 'x-access-token: $token' -d '\@$tmp_file' -H 'Content-Type: text/tab-separated-values' 'https://api.basespace.illumina.com/" . $HrefFiles . "?name=$basename'"; my $exe = "curl -v -H 'x-access-token: $token' -d '\@$tmp_file' -H 'Content-Type: application/octet-stream' 'https://api.basespace.illumina.com/" . $HrefFiles . "?name=$basename'"; my @result = `$exe`; # $log->info( $exe ); # $log->info( join( "\n", @result ) ); } #curl -H 'x-access-token: 20dd0ed0340942a49a5b938a592b6900' -H 'Content-Type: text/tab-separated-values' -X POST https://api.basespace.illumina.com/v1pre3/appresults/16026018/files?name=Frankenfile\&multipart=true #curl -H 'x-access-token: 20dd0ed0340942a49a5b938a592b6900' -H 'Content-Type: text/tab-separated-values' -X POST -T /tmp/20_parts00 -X PUT https://api.basespace.illumina.com/v1pre3/files/16026108/parts/1 #curl -H 'x-access-token: 20dd0ed0340942a49a5b938a592b6900' -X PUT https://api.basespace.illumina.com/v1pre3/files/998044314?uploadstatus=Complete sub upload_file_multipart { my %args = @_; my $token = $args{token}; my $tmp_file = $args{tmp_file}; my $basename = $args{name}; my $HrefFiles = $args{href}; # /v1pre3/appresults/XXXXX/files/; my $parts_href = $HrefFiles; $parts_href =~ s/\/files/\/parts/; $parts_href =~ s/\/appresults/\/files/; my $finish_href = $HrefFiles; $finish_href =~ s/\/files//; $finish_href =~ s/\/appresults/\/files/; my $rdir = $sbeams->getRandomString( num_chars => 20 ); `mkdir /tmp/$rdir/`; `split -b10000000 -d $tmp_file /tmp/$rdir/upload_ `; # Initiate upload my $exe = "curl -H 'x-access-token: $token' -H 'Content-Type: text/tab-separated-values' -X POST https://api.basespace.illumina.com/$HrefFiles?name=$basename" . '\&multipart=true'; my @results = `$exe`; # $log->info( "started multipart upload" ); # $log->info( $exe ); # $log->info( $results[0] ); my $multipart_upload = $json->decode( $results[0] ); my $upload_href = $multipart_upload->{Response}->{Href}; # Upload the parts opendir DIR, "/tmp/$rdir/"; my @files = sort( grep( !/\./, readdir DIR ) ); my $part = 1; for my $file ( @files ) { if ( -e $file ) { # $log->info( $file . " exists" ); } $exe = "curl -H 'x-access-token: $token' -H 'Content-Type: text/tab-separated-values' -X POST -T /tmp/$rdir/$file -X PUT https://api.basespace.illumina.com/$upload_href/parts/$part"; # $log->info( $exe ); @results = `$exe`; # $log->info( "uploaded part $part" ); # $log->info( join( "\n", @results ) ); $part++; } # Finish upload # curl -H 'x-access-token: 20dd0ed0340942a49a5b938a592b6900' -X POST https://api.basespace.illumina.com/v1pre3/files/998044314?uploadstatus=Complete $exe = "curl -H 'x-access-token: $token' -X POST https://api.basespace.illumina.com/$upload_href?uploadstatus=complete"; @results = `$exe`; # $log->info( $exe ); # $log->info( "finished multipart upload" ); # $log->info( join( "\n", @results ) ); `rm -Rf /tmp/$rdir/`; } sub set_appsession_status { my $new_status = shift || return; my $status_description = shift || ''; $status_description =~ s/
//gi; if ( !$basespace{ua} ) { authenticate_to_basespace(); } my $ua = $basespace{ua}; my $appsession_url = $basespace{appsession_url}; $ua->post( $appsession_url, [ Status => $new_status, Statussummary => $status_description ] ); $ua->post( "$appsession_url/properties", [ 'Logs.tail' => $status_description ] ); $ua->post( "$appsession_url/properties", [ 'Logs' => $status_description ] ); # die "$appsession_url/properties"; } sub js_redirect { my $url = shift || return; print qq~~; } sub print_js_status { my $msg = shift or return; my $replace = shift || 0; if ( $replace ) { print "" ; } else { print "" ; } } sub get_build_path { my %args = @_; return unless $args{build_id}; my $path = $atlas->getAtlasBuildDirectory( atlas_build_id => $args{build_id} ); $path =~ s/DATA_FILES//; return $path; } sub get_draw_chart_function { my $sample_arrayref = shift || return ''; my @samples; for my $s ( @{$sample_arrayref} ) { push @samples, [ $s->[1], $s->[4], $s->[7] ]; } my $GV = SBEAMS::Connection::GoogleVisualization->new(); my ( $chart ) = $GV->setDrawBarChart( samples => \@samples, data_types => [ 'string', 'number', 'number' ], headings => [ 'Sample', 'Distinct peptides (n_obs > 1)', 'Cumulative peptides (n_obs > 1)' ], show_table => 0, truncate_labels => 24 ); my $header = $GV->getHeaderInfo(); return ( $chart, $header ); } sub get_public_libsets { my $sql = qq~ SELECT set_tag, instrument_type_name, organism_name, px_identifier, contributors, coverage_statement, COUNT(*) AS cnt FROM $TBAT_DIA_LIBRARY DIL JOIN $TBAT_DIA_LIBRARY_SET DILS ON DILS.dia_library_set_id = DIL.dia_library_set_id JOIN $TBAT_INSTRUMENT_TYPE IT ON IT.instrument_type_id = DIL.instrument_type_id JOIN $TB_ORGANISM O ON O.organism_id = DIL.organism_id WHERE project_id IN ( $project_ids ) AND DIL.record_status = 'N' GROUP BY set_tag, instrument_type_name, organism_name, px_identifier, contributors, coverage_statement ORDER BY set_tag, instrument_type_name ~; # 0 set_tag # 1 instrument_type_name # 2 organism_name # 3 px_identifier # 4 contributors # 5 coverage_statement # 6 cnt my $sth = $sbeams->get_statement_handle( $sql ); my %sets; while ( my @row = $sth->fetchrow_array() ) { if ( $sets{$row[0]} ) { $sets{$row[0]}->{inst} = $row[1] if $row[1] =~ /TripleTOF/; $sets{$row[0]}->{lib_cnt} += $row[6]; } else { my $px = ( $row[3] ) ? ( $row[3] =~ /^PXD/ ) ? "$row[3]" : "$row[3]" : $sbeams->makeInactiveText( 'na' ); $row[5] =~ s/Proteome Coverage//gi; $sets{$row[0]} = { inst => $row[1], lib_cnt => $row[6], px => $px, org => $row[2], cov => $row[5], cont => $row[4] }; } } if ($params->{output_format} eq 'json') { use JSON; my $json = new JSON; my $jstr = $json->utf8->encode(\%sets); print("Access-Control-Allow-Origin: *\n"); print("Content-Type: application/json\n\n"); print $jstr; return; } # my @lib_data = ( [ qw( Library Organism Instrument Contributors Coverage ProteomeExchange NumFiles ) ] ); # my @lib_data = ( [ qw( Library Instrument Contributors Coverage ) ] ); my @lib_data = ( [ 'Library', 'Proteome Coverage', 'Contributors' ] ); for my $lib ( sort( keys( %sets ) ) ) { # push @lib_data, [ $lib, $sets{$lib}->{org}, $sets{$lib}->{inst}, $sets{$lib}->{cont}, $sets{$lib}->{cov}, $sets{$lib}->{px}, $sets{$lib}->{lib_cnt} ]; # push @lib_data, [ $lib, $sets{$lib}->{inst}, $sets{$lib}->{cont}, $sets{$lib}->{cov} ]; push @lib_data, [ $lib, $sets{$lib}->{cov}, $sets{$lib}->{cont} ]; } my $lib_table = $atlas->encodeSectionTable( header => 1, width => '800', align => [ qw(left left left left ) ], rows => \@lib_data, chg_bkg_idx => 0, bg_color => '#EAEAEA', sortable => 0, nowrap => [1,2,3], table_id => 'libraries', table_only => 1, close_table => 1, ); print qq~ $lib_table ~; } __DATA__