diff --git a/.DS_Store b/.DS_Store new file mode 100755 index 0000000..2e2435f Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..c0c6440 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Backups files +*~ +*.rbc +*.orig +*.bak +*.bkp + +# Temporary files +*.tmp +/tmp/* +/tmp/storage/* +!/tmp/storage/.gitkeep + +# log files +*.log +/log/ + +# Storage directory +/users/* +!/users/.gitkeep + +# Config +config/config.yml +config/environments/* +!config/environments/*.keep +config/initializers/devise.rb +.env + +# Database +/db/seeds.rb +config/database.yml + +# Secrets +config/initializers/secret_token.rb +config/secrets.yml + +# Precompiled assets +/public/assets/ diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..04a825c --- /dev/null +++ b/Gemfile @@ -0,0 +1,95 @@ +source 'https://rubygems.org' + + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 5.0.0', '>= 5.0.0.1' +# Use sqlite3 as the database for Active Record +#gem 'sqlite3' +# Use Puma as the app server +gem 'puma', '~> 3.0' +# Use SCSS for stylesheets +gem 'sass-rails', '~> 5.0' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '>= 1.3.0' +# Use CoffeeScript for .coffee assets and views +gem 'coffee-rails', '~> 4.2' +# See https://github.com/rails/execjs#readme for more supported runtimes +# gem 'therubyracer', platforms: :ruby +gem 'pg' +#gem "d3-rails" +gem 'tether-rails' + +#gem 'carrierwave' +gem 'activesupport' + +gem 'therubyracer', platforms: :ruby +gem 'devise' +gem 'rucaptcha', '>=2.1.3' +gem 'dalli' +gem 'kgio' +gem 'jquery-ui-rails' +gem 'daemons' +gem 'delayed_job_active_record' +gem 'delayed-web' +gem 'jquery-datatables-rails' +#gem 'rails-assets-DataTables', source: 'https://rails-assets.org' +gem 'descriptive_statistics' +gem 'sys-proctable' +gem 'rails4-autocomplete' +gem 'jquery-fileupload-rails' +gem 'paperclip' +gem 'multipart-parser' +gem 'haml' +gem 'activerecord-session_store' + +# Get some icons +gem "font-awesome-rails" + +# Have fun with parallax +gem 'skrollr-rails', '~> 0.6.26' + +# Sort tables +#gem 'jquery-tablesorter' + +# Manipulate json files +gem 'json' + +# d3.js graphs library +gem "d3-rails" + +# Use jquery as the JavaScript library +gem 'jquery-rails' +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +gem 'turbolinks', '~> 2.5.3' ### '~> 5' +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder', '~> 2.5' +# Use Redis adapter to run Action Cable in production +# gem 'redis', '~> 3.0' +# Use ActiveModel has_secure_password +# gem 'bcrypt', '~> 3.1.7' + +gem 'bootstrap-sass', '3.2.0.2' # important to specify version here! +gem 'sprockets-rails', '3.2.0' + +# List gem licenses +gem 'gem-licenses' + +# Use Capistrano for deployment +# gem 'capistrano-rails', group: :development + +group :development, :test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platform: :mri +end + +group :development do + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'web-console' + gem 'listen', '~> 3.0.5' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..63a7fb8 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,276 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.0.1) + actionpack (= 5.0.1) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.0.1) + actionview (= 5.0.1) + activesupport (= 5.0.1) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.0.1) + activesupport (= 5.0.1) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (5.0.1) + activesupport (= 5.0.1) + globalid (>= 0.3.6) + activemodel (5.0.1) + activesupport (= 5.0.1) + activerecord (5.0.1) + activemodel (= 5.0.1) + activesupport (= 5.0.1) + arel (~> 7.0) + activerecord-session_store (1.0.0) + actionpack (>= 4.0, < 5.1) + activerecord (>= 4.0, < 5.1) + multi_json (~> 1.11, >= 1.11.2) + rack (>= 1.5.2, < 3) + railties (>= 4.0, < 5.1) + activesupport (5.0.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + arel (7.1.4) + bcrypt (3.1.11) + bootstrap-sass (3.2.0.2) + sass (~> 3.2) + builder (3.2.3) + byebug (9.0.6) + climate_control (0.0.3) + activesupport (>= 3.0) + cocaine (0.5.8) + climate_control (>= 0.0.3, < 1.0) + coffee-rails (4.2.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + concurrent-ruby (1.0.4) + d3-rails (4.9.1) + railties (>= 3.1) + daemons (1.2.4) + dalli (2.7.6) + debug_inspector (0.0.2) + delayed-web (0.4.4) + rails (>= 3.2.13) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_active_record (4.1.1) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) + descriptive_statistics (2.5.1) + devise (4.2.0) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0, < 5.1) + responders + warden (~> 1.2.3) + erubis (2.7.0) + execjs (2.7.0) + ffi (1.9.17) + font-awesome-rails (4.7.0.2) + railties (>= 3.2, < 5.2) + gem-licenses (0.2.1) + globalid (0.3.7) + activesupport (>= 4.1.0) + haml (4.0.7) + tilt + i18n (0.8.0) + jbuilder (2.6.1) + activesupport (>= 3.0.0, < 5.1) + multi_json (~> 1.2) + jquery-datatables-rails (3.3.0) + actionpack (>= 3.1) + jquery-rails + railties (>= 3.1) + sass-rails + jquery-fileupload-rails (0.4.7) + actionpack (>= 3.1) + railties (>= 3.1) + sass (>= 3.2) + jquery-rails (4.2.2) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + jquery-ui-rails (5.0.5) + railties (>= 3.2.16) + json (2.1.0) + kgio (2.11.1) + libv8 (3.16.14.15) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.4) + mime-types (>= 1.16, < 4) + method_source (0.8.2) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mimemagic (0.3.0) + mini_portile2 (2.1.0) + minitest (5.10.1) + multi_json (1.12.1) + multipart-parser (0.1.1) + nio4r (1.2.1) + nokogiri (1.7.0.1) + mini_portile2 (~> 2.1.0) + orm_adapter (0.5.0) + paperclip (4.3.2) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (= 0.3.0) + pg (0.19.0) + puma (3.7.0) + rack (2.0.1) + rack-test (0.6.3) + rack (>= 1.0) + rails (5.0.1) + actioncable (= 5.0.1) + actionmailer (= 5.0.1) + actionpack (= 5.0.1) + actionview (= 5.0.1) + activejob (= 5.0.1) + activemodel (= 5.0.1) + activerecord (= 5.0.1) + activesupport (= 5.0.1) + bundler (>= 1.3.0, < 2.0) + railties (= 5.0.1) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.2) + activesupport (>= 4.2.0, < 6.0) + nokogiri (~> 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + rails4-autocomplete (1.1.1) + rails (>= 3.0) + railties (5.0.1) + actionpack (= 5.0.1) + activesupport (= 5.0.1) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.0.0) + rb-fsevent (0.9.8) + rb-inotify (0.9.8) + ffi (>= 0.5.0) + ref (2.0.0) + responders (2.3.0) + railties (>= 4.2.0, < 5.1) + rucaptcha (2.2.0) + railties (>= 3.2) + sass (3.4.23) + sass-rails (5.0.6) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + skrollr-rails (0.6.29) + rails (>= 3.1.0) + spring (2.0.1) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.0) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + sys-proctable (1.1.3) + tether-rails (1.4.0) + rails (>= 3.1) + therubyracer (0.12.2) + libv8 (~> 3.16.14.0) + ref + thor (0.19.4) + thread_safe (0.3.5) + tilt (2.0.6) + turbolinks (2.5.3) + coffee-rails + tzinfo (1.2.2) + thread_safe (~> 0.1) + uglifier (3.0.4) + execjs (>= 0.3.0, < 3) + warden (1.2.6) + rack (>= 1.0) + web-console (3.4.0) + actionview (>= 5.0) + activemodel (>= 5.0) + debug_inspector + railties (>= 5.0) + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord-session_store + activesupport + bootstrap-sass (= 3.2.0.2) + byebug + coffee-rails (~> 4.2) + d3-rails + daemons + dalli + delayed-web + delayed_job_active_record + descriptive_statistics + devise + font-awesome-rails + gem-licenses + haml + jbuilder (~> 2.5) + jquery-datatables-rails + jquery-fileupload-rails + jquery-rails + jquery-ui-rails + json + kgio + listen (~> 3.0.5) + multipart-parser + paperclip + pg + puma (~> 3.0) + rails (~> 5.0.0, >= 5.0.0.1) + rails4-autocomplete + rucaptcha (>= 2.1.3) + sass-rails (~> 5.0) + skrollr-rails (~> 0.6.26) + spring + spring-watcher-listen (~> 2.0.0) + sprockets-rails (= 3.2.0) + sys-proctable + tether-rails + therubyracer + turbolinks (~> 2.5.3) + tzinfo-data + uglifier (>= 1.3.0) + web-console + +BUNDLED WITH + 1.14.6 diff --git a/LICENCE.txt b/LICENCE.txt new file mode 100755 index 0000000..a871fcf --- /dev/null +++ b/LICENCE.txt @@ -0,0 +1,662 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..1746859 --- /dev/null +++ b/README.md @@ -0,0 +1,417 @@ +Genocrunch +========== + +A web-based platform for mining metagenomic data + +**Official web server:** + +## Rights + +- **Copyright:** All rights reserved. ECOLE POLYTECHNIQUE FEDERALE DE LAUSANNE, Switzerland, Laboratory of Intestinal Immunology, 2016-2018 +- **Licence:** GNU AGPL 3 (See LICENCE.txt for details) +- **Authors:** AR Rapin, FPA David, C Pattaroni, J Rougemont, BJ Marsland and NL Harris + +## Resources + +- **Git clone URL:** +- **Documentation:** +- **Licence:** + +## Framework + +Genocrunch uses the ruby on [Rails](http://rubyonrails.org/) framework with a PostgreSQL database. + +## Supported platforms + +- **Linux** (tested on Ubuntu 16.04 LTS and CentOS 7) +- **macOS** (tested on 10.12 Sierra) + +## Supported web browsers + +- **Mozilla Firefox** *(Mobile versions are not supported)* + +## Requirements + +- **Ruby version 2.3.1** +- **Rails version 5.0.0** +- **Python version >=2.7.0 <3.0.0** +- **R version >3.2.2** + +## Installation *(Debian Linux and macOS)* + +### Ruby, Rails and PostgreSQL + +**Debian Linux** + + Uninstall possible pre-installed versions of ruby: +
+$ sudo apt-get purge ruby
+
+ + (Re-)install ruby and Rails using rbenv (see [here](https://www.digitalocean.com/community/tutorials/how-to-install-ruby-on-rails-with-rbenv-on-ubuntu-16-04#prerequisites)). + + Install PostgreSQL and start PostgreSQL server: +
+$ sudo apt-get install postgresql postgresql-contrib libpq-dev
+$ sudo service postgresql start
+
+ +**macOS** + + Install ruby and Rails using homebrew and rbenv (see [here](https://www.gorails.com/setup/osx/10.12-sierra)). + + Install PostgreSQL and start PostgreSQL server with homebrew: +
+$ brew install postgresql
+$ brew services start postgresql
+
+ +### Python + +**Debian Linux** + +
+$ sudo apt-get install build-essential python-dev python-pip
+$ pip install numpy
+
+ + Check that python version is between 2.7.0 and 3.0.0: +
+$ python -V
+
+ +**macOS** + + If not done yet, install Homebrew: +
+$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
+
+ + Install python 2.7 with Homebrew: +
+$ brew install python
+
+ + Check that python version is between 2.7.0 and 3.0.0: +
+$ python -V
+
+ +### R + +**Debian Linux** + +
+$ sudo apt install r-base-core
+
+ + Open the R environment and check that R version is above 3.2.2: +
+$ R
+> R.version.string
+
+ +**macOS** + + Update XQuartz and Xcode if needed. + + Download the R binary from cran at . + Click on the downloaded .pkg file and follow the instructions. + +### R dependencies + +Install required R packages from CRAN and bioconductor: + +*Note: Each package can be installed separately. If RcppEigen fails to compile, allocate more memory.* +
+$ sudo R
+> install.packages(c("ineq", "rjson", "fpc", "multcomp", "FactoMineR", "colorspace", "vegan", "optparse", "gplots", "igraph", "fossil", "coin", "SNFtool"))
+> source("https://bioconductor.org/biocLite.R")
+> biocLite("sva")
+> q()
+
+ +In case of issue with igraph installation: +
+> install.packages("devtools")
+> library(devtools)
+> install_github("igraph/rigraph")
+
+ +### Genocrunch web application + + Create an empty rails project: +
+$ rails new genocrunch -d postgresql -B
+
+ + Set the git repository: +
+$ cd genocrunch
+$ git init
+$ git remote add origin https://c4science.ch/source/genocrunch-2.1.git
+
+ + Get a copy of genocrunch files: +
+$ git fetch --all
+$ git reset --hard origin/master
+
+ + Set the .gitignore file: + +
+$ cp gitignore.keep .gitignore
+
+ + Run the install.sh script: +
+$ cd /path/to/genocrunch
+$ chmod 755 install.sh
+$ ./install.sh
+
+ This simply uses the .bashrc or .bashprofile to include executable analysis scripts in the PATH. + + Source .bashrc (or .bash_profile on macOS): +
+$ source .bashrc  # for macOS replace .bashrc by .bash_profile
+
+ +**Additional steps for macOS** + + Install the coreutils package with homebrew: +
+$ brew install coreutils
+
+ +### Ruby libraries (gems) + + Use the Gemefile to install required gems: +
+$ cd /path/to/genocrunch
+$ bundle install
+
+ + A function in the activesupport library may cause the following error at creation of a job: undefined method `to_datetime' for false:FalseClass. This can be fixed by editing its calculations.rb file as described [here](http://stackoverflow.com/questions/36805639/rails-3-2-to-4-0-upgrade-undefined-method-to-datetime-for-falsefalseclass)): + Replace the calculations.rb file by a fixed copy: + +**Debian Linux** + +
+$ wget https://raw.githubusercontent.com/alexisrapin/fix/master/calculations.rb -O ~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-4.0.0/lib/active_support/core_ext/date_time/calculations.rb
+
+ +**macOS** + +
+$ curl "https://raw.githubusercontent.com/alexisrapin/fix/master/calculations.rb" -o ~/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-4.0.0/lib/active_support/core_ext/date_time/calculations.rb
+
+ +### Set application configuration variables + + Set the application configuration variables in the genocrunch/config/config.yml file to fit the current installation: + +
+$ cd /path/to/genocrunch
+$ cp config/config.yml.keep config/config.yml
+
+ +
+#config/config.yml
+
+development:
+  # Genocrunch main directory
+  data_dir: /path/to/genocrunch
+
+  # Additional link(s) that should be included in the Infos menu of the topbar
+  info_links: [{name: 'link_name', href: 'link_url', target: '_blank'}]
+
+  # Webmaster email
+  webmaster_email: 'webmaster_email'
+
+  # Send a validation link to user email to confirm registration?
+  user_confirmable: false
+
+production:
+  data_dir: /path/to/genocrunch
+  info_links: [{name: 'link_name', href: 'link_url', target: '_blank'}]
+  webmaster_email: 'webmaster_email'
+  user_confirmable: false
+
+ +### Set genocrunch emails + + Set the email details that will be used by Genocrunch to send information such as registration confirmation link or password recovery link to users. +The following example would set Genocrunch to use an hypothetical gmail address (app_email@gmail.com) in development. + +
+$ cd /path/to/genocrunch
+$ cp config/initializers/devise.rb.keep config/initializers/devise.rb
+$ cp config/environments/development.rb.keep config/environments/development.rb
+
+ +
+#config/initializers/devise.rb
+
+Devise.setup do |config|
+  ...
+  config.mailer_sender = "app_email@gmail.com"
+  ...
+
+ +
+#config/environments/development.rb
+
+Rails.application.configure do
+  ...
+  config.action_mailer.default_url_options = { :host => 'localhost:3000' }
+  config.action_mailer.smtp_settings = {
+    :address => "smtp.gmail.com",
+    :port => 587,
+    :domain => "mail.google.com",
+    :user_name => "app_email@gmail.com",
+    :password => "app_email_password",
+    :authentication => :plain,
+    :enable_starttls_auto => true
+  }
+  ...
+
+ +### Setup the PostgreSQL server + + Create a new role and a new database (you can create different users and databases for development, test and/or production): + +
+$ sudo su postgres  # for macOS, replace postgres by _postgres
+$ psql
+postgres=# CREATE ROLE myusername WITH LOGIN PASSWORD 'mypassword';
+postgres=# CREATE DATABASE my_application_db_name OWNER myusername;
+postgres=# \q
+$ exit
+
+ + Set the genocrunch/config/database.yml file: + In development, test and/or production sections, set the database, username and password to fit the corresponding PostgreSQL database: + +
+$ cd /path/to/genocrunch
+$ cp config/database.yml.keep config/database.yml
+
+ +
+#config/database.yml
+
+...
+database: my_application_db_name
+...
+username: myusername
+...
+password: mypassword
+...
+
+ +### Initialize the database + +Two default users will be created: guest and admin. The guest user is required to try the application without registering/signing-in. The admin user is optional. +Change the default passwords and emails. +This can be done after seeding the database, with psql or prior to seeding the database, in the genocrunch/db/seeds.rb file: + +
+$ cp /path/to/genocrunch/db/seeds.rb.keep /path/to/genocrunch/db/seeds.rb
+
+ +
+#db/seeds.rb
+
+User.create!([{username: 'guest',
+               role: 'guest',
+               email: 'guest@guestmailbox.com', # <- HERE
+               confirmed_at: '2017-01-01 00:00:00.000000',
+               password: 'guest_account_password'}, # <- HERE
+              {username: 'admin',
+               role: 'admin',
+               email: 'admin@adminmailbox.com', # <- HERE
+               confirmed_at: '2017-01-01 00:00:00.000000',
+               password: 'admin_account_password'}]) # <- AND THERE
+...
+
+ +Run the folowing commands to create and seed the database. +This would erase previous database tables. Use it for installation, not update. +For updates, use migrations. + +
+$ cd /path/to/genocrunch
+$ rake db:schema:load
+$ rake db:seed
+
+ +To set users emails and password after seeding: +
+$ psql db_name
+db_name=# update users set email = 'guest_email' where username = 'guest';
+db_name=# update users set password = 'guest_password' where username = 'guest';
+db_name=# update users set email = 'admin_email' where username = 'admin';
+db_name=# update users set password = 'admin_password' where username = 'admin';
+
+ +### Run the Rails server + + * Development mode +
+$ cd /path/to/genocrunch
+$ rails server
+
+ +You can now access the application in your browser at http://localhost:3000 on your machine and your_ip_address:3000 on your network. + +*By default, the server runs in development mode.* + + * Production mode + +This section is under construction. + +### Start workers + + * Prefered way: +
+$ cd /path/to/genocrunch
+$ RAILS_ENV=development bin/delayed_job -n 2  start
+
+OR +
+$ cd /path/to/genocrunch
+$ RAILS_ENV=development bin/delayed_job -n 2 restart
+
+ + * Alternative way: +
+$ cd /path/to/genocrunch
+$ rake jobs:work
+
+ +You can now create new jobs (run analysis). +Read the documentation (http://localhost:3000/home/doc) for details. + +### Create a new version + +Versions of installed R packages can be referenced in the version page (http://localhost:3000/versions). +For this, run the get_version.py script: +
+$ cd /path/to/genocrunch
+$ get_version.py
+
+This will create a .json file in the genocrunch main directory with a name looking like version_2017-12-18_18:03:08.898906.json. + +Sign in as admin and navigate to Infos>Versions +Click on the New Version button and fill the form. +In the JSON field, copy the json string contained in the .json file previously created using the get_version.py script. +Finally, click on the Create Version button. + +### Terms of service + +Terms of service can be edited in /path/to/genocrunch/public/app/TERMS_OF_SERVICE.txt + +## Usage + +See Infos>Doc in the application web page (http://localhost:3000/home/doc). diff --git a/Rakefile b/Rakefile new file mode 100755 index 0000000..4a99ec9 --- /dev/null +++ b/Rakefile @@ -0,0 +1,10 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +require 'gem-licenses' +Gem::GemLicenses.install_tasks + +Rails.application.load_tasks + diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..b16e53d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/EPFL_logo.png b/app/assets/images/EPFL_logo.png new file mode 100644 index 0000000..5299cb5 Binary files /dev/null and b/app/assets/images/EPFL_logo.png differ diff --git a/app/assets/images/bg-index-big.jpg b/app/assets/images/bg-index-big.jpg new file mode 100644 index 0000000..e1cd7cc Binary files /dev/null and b/app/assets/images/bg-index-big.jpg differ diff --git a/app/assets/images/bg-index.jpg b/app/assets/images/bg-index.jpg new file mode 100644 index 0000000..40144e6 Binary files /dev/null and b/app/assets/images/bg-index.jpg differ diff --git a/app/assets/images/bg-welcome-big.jpg b/app/assets/images/bg-welcome-big.jpg new file mode 100644 index 0000000..fa72f32 Binary files /dev/null and b/app/assets/images/bg-welcome-big.jpg differ diff --git a/app/assets/images/bg-welcome.jpg b/app/assets/images/bg-welcome.jpg new file mode 100644 index 0000000..22867f0 Binary files /dev/null and b/app/assets/images/bg-welcome.jpg differ diff --git a/app/assets/images/genocrunch_pipeline.png b/app/assets/images/genocrunch_pipeline.png new file mode 100644 index 0000000..5b1c201 Binary files /dev/null and b/app/assets/images/genocrunch_pipeline.png differ diff --git a/app/assets/javascripts/abundance.js b/app/assets/javascripts/abundance.js new file mode 100644 index 0000000..dc0bccc --- /dev/null +++ b/app/assets/javascripts/abundance.js @@ -0,0 +1,321 @@ +function barchart(id, legend_id, json, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory20c) { + + // Size + var margin = {top: 20, right: 20, bottom:150, left: 50}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + left_label_space = 30, + bottom_text_angle = 55; + + // Colors and scale + var colors = d3.scaleOrdinal(color_palette), + xScale = d3.scaleBand() + .rangeRound([0, width]) + .align(0), + yScale = d3.scaleLinear() + .rangeRound([height, 0]); + + // General functions + function filter_abundance(json, thres, other_key) { + var filtered = JSON.parse(JSON.stringify(json)), + keys = Object.keys(json[0].data); + if (keys.indexOf(other_key) != -1) { + keys.splice(keys.indexOf(other_key), 1) + } + var filter = [...Array(keys.length)]; + filter.fill(false) + json.map(function(d) { + for (var i = 0; i < keys.length; i++) { + if ((filter[i] == false) && (Number(d.data[keys[i]]) >= Number(thres))) { + filter[i] = true; + }; + }; + }); + + for (var i = 0; i < filtered.length; i++) { + filtered[i].data[other_key] = 0; + for (var j = 0; j < keys.length; j++) { + if (filter[j] == false) { + filtered[i].data[other_key] = Number(filtered[i].data[other_key])+Number(filtered[i].data[keys[j]]); + delete filtered[i].data[keys[j]]; + } + }; + }; + + return filtered; + }; + + // Set variables depending only on the primary data + var keys0 = Object.keys(json[0].data), + other_key = 'Filtered'; + while (keys0.indexOf(other_key) != -1) { + other_key = 'Filtered_'+randomKey(3) + } + json.map(function(d) { d.data[other_key] = 0; }) + keys0.unshift(other_key) + + xScale.domain(json.map(function(d) { return d['name']; })); + yScale.domain([0, 100]).nice(); + colors.domain(keys0); + + var stack = d3.stack() + .order(d3.stackOrderNone) + .offset(d3.stackOffsetNone); + + //////////////// Draw figure //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-1') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Draw plot + var plot = svg.selectAll(); + + // Add plot labels + var plotLabel = svg.selectAll(); + + + // Add axis + svg.append("g") + .attr("class", "axis") + .attr("transform", "translate(0," + height + ")") + .call(d3.axisBottom(xScale)) + .selectAll("text") + .attr("font-family", font_family) + .style("text-anchor", "end") + .attr("dx", "-.8em") + .attr("dy", ".15em") + .attr("transform", "rotate(-" + bottom_text_angle + ")"); + + svg.append("g") + .call(d3.axisLeft(yScale).ticks(10)) + + svg.append("g") + .attr("transform", "translate("+ (-left_label_space) +", "+ (height/2) +")") + .attr("class", "axis-label") + .append("text") + .text("%") + .attr("font-family", font_family) + .style("text-anchor", "middle") + .attr("transform", "rotate(-90)"); + + // Add legend + var legend = legendContainer.append("ul") + .attr("id", "svg-legend") + .style("font-family", font_family) + .style("list-style-type", "none") + .selectAll("ul"); + + var filtered_json = [], + names = [], + k = [], + keys = [], + data = []; + + //////////////// Restart function //////////////// + var restart = function() { + + // Filter data on abundance threshold + filtered_json = filter_abundance(json, d3.select("#thresRange").property("value"), other_key); + names = filtered_json.map(function(d){return d.name}); + data = filtered_json.map(function(d){return d.data}); + keys = Object.keys(data[0]); + keys.splice(k.indexOf(other_key), 1); + keys.unshift(other_key); + + stack = stack + .keys(keys) + + // Update barplot + plot = plot + .data(stack(data)) + + d3.selectAll(".barplot-rect") + .transition() + .duration(700) + .style("opacity", 0) + .remove(); + + plot + .enter() + .append("g") + .attr("class", "barplot") + .selectAll("rect") + .data(function(d) {return d;}) + .enter().append("rect") + .attr("class", "barplot-rect") + .attr("x", function(d, i) {return xScale(names[i]);}) + .attr("y", function(d) {return yScale(d[1]);}) + .attr("height", function(d) { + return yScale(d[0]) - yScale(d[1]); + }) + .attr("width", xScale.bandwidth()); + + d3.selectAll(".barplot") + .transition() + .duration(700) + .attr("fill", function(d) { return colors(d.key); }) + .attr("fill-opacity", 1) + + // Update labels + plotLabel = plotLabel + .data(stack(data)); + + var plotLabel_main = plotLabel + .enter() + .append("g") + + plotLabel_main.append("title") + .html(function(d, i) { return keys[i];}) + + plotLabel_main + .selectAll("rect") + .data(function(d) { return d; }) + .enter() + .append("rect") + .attr("class", "barplot-rect square") + .attr("selected", 0) + .attr("stroke", "#ff0066") + .attr("stroke-width", 2) + .attr("stroke-opacity", 0) + .attr("fill-opacity", 0) + .attr("x", function(d, i) { return xScale(names[i]);}) + .attr("y", function(d) { return yScale(d[1]);}) + .attr("height", function(d) { + return yScale(d[0]) - yScale(d[1]); + }) + .attr("width", xScale.bandwidth()); + + highlightLegend("square"); + + // Update legend + + legend = legend.data([]); + legend.exit().remove(); + + legend = legend + .data(keys) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + legendSpan = legend.append("span") + + legendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .append("rect") + .attr("width", "10px") + .attr("height", "10px") + .attr("fill", function(d) { return colors(d); }) + + legendSpan.append("span") + .html(function(d) { + if (d == other_key) + return "Others (below "+ d3.select("#thresRange").property("value") +"%)"; + var name = d.split(';'); + if (name.length > 1) { + return name[name.length - 2]+"; "+name[name.length - 1]; + }; + return name[0]; + }); + + highlightBars("legend"); + //showLegendLabels(); + + }; + + //////////////// Control buttons //////////////// + + // Buttons + var buttons = d3.select("#d3-buttons"); + buttons.html(""); + + // Search in legend + var searchLegend = function() { + + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + d3.selectAll(".square").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.parentNode.__data__.key.toUpperCase().indexOf(key.toUpperCase()) != -1);}).attr("stroke-opacity", 1); + d3.selectAll(".square").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.parentNode.__data__.key.toUpperCase().indexOf(key.toUpperCase()) == -1);}).attr("stroke-opacity", 0); + d3.selectAll(".legend").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.getAttribute("id").toUpperCase().indexOf(key.toUpperCase()) != -1);}).style("background-color", "#ffcce0"); + d3.selectAll(".legend").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.getAttribute("id").toUpperCase().indexOf(key.toUpperCase()) == -1);}).style("background-color", "rgba(0, 0, 0, 0)"); + } else { + selected = d3.selectAll(".square").filter(function(d){ return this.getAttribute("selected") == 0;}).attr("stroke-opacity", 0); + selected_legend = d3.selectAll(".legend").filter(function(d){ return this.getAttribute("selected") == 0;}).style("background-color", "rgba(0, 0, 0, 0)"); + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLegend); + + // Abundance Cutoff + appendRange(buttons, "Abundance filter", "% cutoff", "thresRange", 0, 20, 0, restart) + + + // Highlight functions + function highlightBars (id) { + $("."+id).on("mouseenter", function(d) { + var key = this.getAttribute("id"); + d3.selectAll(".square").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.parentNode.__data__.key == key);}).attr("stroke-opacity", 1); + d3.select(this).style("background-color", "#ffcce0"); + }); + $("."+id).on("mouseleave", function(d) { + var key = this.getAttribute("id"); + d3.selectAll(".square").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.parentNode.__data__.key == key);}).attr("stroke-opacity", 0); + if (this.getAttribute("selected") == 0) { + d3.select(this).style("background-color", "rgba(0, 0, 0, 0)"); + } + }); + $("."+id).on("click", function(d) { + var key = this.getAttribute("id"), + selection = d3.selectAll(".square").filter(function(d){ return this.parentNode.__data__.key == key;}); + if (selection.attr("selected") == 1) { + selection.attr("selected", 0); + d3.select(this).attr("selected", 0); + } else { + selection.attr("selected", 1); + d3.select(this).attr("selected", 1); + }; + }); + }; + + function highlightLegend (id) { + $("."+id).on("mouseenter", function(d) { + var key = this.parentNode.__data__.key; + d3.selectAll(".legend").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.getAttribute("id") == key);}).style("background-color", "#ffcce0"); + d3.selectAll(".square").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.parentNode.__data__.key == key);}).attr("stroke-opacity", 1); + }); + $("."+id).on("mouseleave", function(d) { + var key = this.parentNode.__data__.key; + d3.selectAll(".legend").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.getAttribute("id") == key);}).style("background-color", "rgba(0, 0, 0, 0)"); + d3.selectAll(".square").filter(function(d){ return (this.getAttribute("selected") == 0) && (this.parentNode.__data__.key == key);}).attr("stroke-opacity", 0); + }); + $("."+id).on("click", function(d) { + var key = this.parentNode.__data__.key, + selected_legend = d3.selectAll(".legend").filter(function(d){ return this.getAttribute("id") == key;}), + selection = d3.selectAll(".square").filter(function(d){ return this.parentNode.__data__.key == key;}); + if (this.getAttribute("selected") == 0) { + selection.attr("selected", 1); + selected_legend.attr("selected", 1); + } else { + selection.attr("selected", 0); + selected_legend.attr("selected", 0); + }; + }); + }; + + restart(); + +}; diff --git a/app/assets/javascripts/adonis.js b/app/assets/javascripts/adonis.js new file mode 100644 index 0000000..b467b35 --- /dev/null +++ b/app/assets/javascripts/adonis.js @@ -0,0 +1,342 @@ +function adonisPieChart(id, legend_id, json, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif") { + // Size + var margin = {top: 10, right: 100, bottom: 10, left: 100}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + radius = Math.min(width, height)/2, + thickness = 0.4*radius, + label_padding = 10; + + // Colors + var colors = {pos:"#FF0039", + neutral:"#666666"}, + signThres = [{value:0.001, opacity:0.9, text:'***'}, + {value:0.01, opacity:0.7, text:'**'}, + {value:0.05, opacity:0.5, text:'*'}, + {value:1, opacity:0.1, text:'ns'}, + {value:'NA', opacity:0.3, text:'na'}]; + + // General functions + function getMidAngle(d){ + return d.startAngle + (d.endAngle - d.startAngle)/2; + }; + + function defineLabelTextAnchor(d, t){ + if (getMidAngle(d) < Math.PI) { + if (t == "out") { + return "start"; + } else { + return "end"; + }; + } else { + if (t == "out") { + return "end"; + } else { + return "start"; + }; + }; + }; + + // Buttons + var buttons = d3.select("#d3-buttons") + buttons.html(""); + + //$.getJSON(data, function(json) { + + + //////////////// Draw the figure //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-1') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + + // Figure title + svg.append("text") + .attr("x", width/2) + .attr("y", 20) + .style("text-anchor", "middle") + .attr("font-family", font_family) + .text("Explained variance by factor"); + + // Draw pie chart + var pie = svg.selectAll(), + pieSlice = pie.selectAll(".arc"); + + // Add info at center + var centralInfo = svg.append("text") + .attr("id", "central-info") + .attr("x", width/2) + .attr("y", height/2) + .style("text-anchor", "middle") + .attr("font-family", font_family) + .text(""); + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + + var legend_svg_symsize = 18, + legend_svg_height = legend_svg_symsize*signThres.length; + + legend.append('p') + .html('Color key') + + var colorLegend = legend.append("svg") + .attr("width", '100%') + .attr("height", legend_svg_height+'px') + .style("overflow", "visible") + .selectAll() + .data(signThres) + .enter().append('g') + .attr("transform", function(d, i){return "translate(0,"+(i*legend_svg_height/signThres.length)+")";}) + + colorLegend.append("text") + .attr("x", legend_svg_symsize+2) + .attr("y", legend_svg_symsize) + .text(function(d, i){ + if(isNaN(d.value)) { + return signThres[i]["text"]; + } + if (d.value == 1) { + return 'p>0.05'+'('+signThres[i]["text"]+')'; + } + return 'p<'+d.value+'('+signThres[i]["text"]+')'; + }) + + colorLegend.append("rect") + .attr("width", legend_svg_symsize) + .attr("height", legend_svg_symsize) + .attr("stroke", "none") + .attr("fill", function (d, i){ + if (isNaN(signThres[i]["value"])) { + return colors.neutral; + }; + return colors.pos; + }) + .attr("fill-opacity", function(d, i){return signThres[i]["opacity"];}) + + legend.append("span") + .attr('class', 'btn-sep') + + legend.append('p') + .html('Factors') + + var dataLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + + + + //////////////// Restart function //////////////// + var restart = function() { + + var selected_model = d3.select("#modelSelect").property("value"); + + pie = pie.data([]); + pie.exit().remove(); + + pie = pie + .data([json[selected_model]]) + .enter().append("g") + .attr("transform", "translate("+[width/2, height/2]+")") + + pieSlice = pieSlice.data([]); + pieSlice.exit().remove(); + + pieSlice = pie.selectAll(".arc") + .data(d3.pie(json[selected_model]) + .sort(null) + .value(function(d) {return d.explained;})) + .enter().append("g") + .classed("arc", true) + .attr("selected", 0) + + // Draw slices + pieSlice.append("path") + .attr("d", d3.arc() + .outerRadius(radius) + .innerRadius(radius-thickness) + .cornerRadius(3) + .padAngle(0.02)) + .attr("id", function(d, i){ return "piece-"+i;}) + .attr("fill", function(d){ + if (isNaN(d.data['p-value'])) { + return colors['neutral']; + } + return colors['pos']; + }) + .attr("fill-opacity", function(d){ + return signThres.filter(function(e){ + if (isNaN(d.data['p-value'])) { + return e.value == 'NA'; + } + return d.data['p-value'] <= e.value; + })[0].opacity; + }) + .attr("stroke-width", "2px") + .attr("stroke", "gray"); + + // Add labels + pieSlice.append("text") + .attr("x", function(d) { return Math.sin(getMidAngle(d))*(radius+label_padding);}) + .attr("y", function(d) { return -Math.cos(getMidAngle(d))*(radius+label_padding);}) + .style("text-anchor", function (d) {return defineLabelTextAnchor(d, "out");}) + .style("font-size", "10px") + .attr("font-family", font_family) + .text(function(d) { return d.data.name;}) + .attr("display", "none") + + showLabels(); + displayLabels("arc"); + + // Update legend + + dataLegend = dataLegend.data([]); + dataLegend.exit().remove(); + + dataLegend = dataLegend + .data(json[selected_model]) + .enter().append("li") + .attr("id", function(d) { return d.name;}) + .attr("class", "legend") + .attr("selected", 0) + .attr("title", function(d) { return d.name;}) + .html(function(d) { + var sig = signThres.filter(function(e){ + if (isNaN(d['p-value'])) { + return e.value == 'NA'; + } + return d['p-value'] <= e.value; + })[0].text; + + return d.name+" ("+Number(d.explained*100).toFixed(1)+"%"+")"+sig; + }) + + + legendAction("legend"); + + }; + + + //////////////// Control buttons //////////////// + + // Display labels button + var showLabels = function() { + var labels = d3.selectAll(".arc").select(function(){return this.childNodes[1];}); + if ($("#labelButton").is(':checked')) { + labels.attr("display", "inline"); + labels.attr("selected", 1); + } else { + labels.attr("display", "none"); + labels.attr("selected", 0); + }; + }; + + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + $("#labelButton").attr("checked", true) + + // Select model + var modelSelect = buttons.append("div") + .attr("title", "Select a model.") + .attr("class", "form-group") + + modelSelect.append("label") + .html("Model") + + modelSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "modelSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(Object.keys(json)) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("modelSelect").value = Object.keys(json)[0]; + + + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + $("#modelSelect").on("change", restart) + + restart(); + + // Labels functions + function displayLabels (id) { + $("."+id).on("mouseenter", function(d) { + var data = this.__data__.data; + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[0]).attr("stroke-width", 4); + $("#central-info").text(Number(data.explained*100).toFixed(1)+"%"); + d3.selectAll(".legend").filter(function(){ return this.getAttribute("id") == data.name;}).style("background-color", "#ffcce0"); + }); + $("."+id).on("mouseleave", function(d) { + var data = this.__data__.data; + d3.select(this.childNodes[1]).filter(function(){return this.getAttribute("selected") == 0}).attr("display", "none"); + d3.select(this.childNodes[0]).attr("stroke-width", 2); + d3.selectAll(".legend").filter(function(){ return (this.getAttribute("selected") == 0) && (this.getAttribute("id") == data.name);}).style("background-color", "rgba(0, 0, 0, 0)"); + $("#central-info").text(""); + }); + $("."+id).on("click", function(d) { + var data = this.__data__.data; + if (this.childNodes[1].getAttribute("selected") == 0) { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", 1); + d3.selectAll(".legend").filter(function(){ return this.getAttribute("id") == data.name;}).attr("selected", 1) + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", 0); + d3.selectAll(".legend").filter(function(){return this.getAttribute("id") == data.name;}).attr("selected", 0) + } + }); + }; + + function legendAction (id) { + $("."+id).on("mouseenter", function(d) { + var name = this.getAttribute("id"), + label = d3.selectAll(".arc").filter(function(){return this.__data__.data.name == name;}); + d3.select(this).style("background-color", "#ffcce0"); + label.select(function(){ return this.childNodes[1];}).attr("display", "inline"); + label.select(function(){ + $("#central-info").text(Number(this.__data__.data.explained*100).toFixed(1)+"%") + }); + }); + $("."+id).on("mouseleave", function(d) { + var name = this.getAttribute("id"), + label = d3.selectAll(".arc").filter(function(){return this.__data__.data.name == name;}).select(function(){ return this.childNodes[1];}).filter(function(){return this.getAttribute("selected") == 0}); + d3.select(this).filter(function(){return this.getAttribute("selected") == 0}).style("background-color", "rgba(0, 0, 0, 0)"); + label.attr("display", "none"); + $("#central-info").text(""); + }); + $("."+id).on("click", function(d) { + var name = this.getAttribute("id"), + label = d3.selectAll(".arc").filter(function(){return this.__data__.data.name == name;}).select(function(){ return this.childNodes[1];}); + if (this.getAttribute("selected") == 0) { + d3.select(this).attr("selected", 1); + label.attr("selected", 1); + } else { + d3.select(this).attr("selected", 0); + label.attr("selected", 0); + } + }); + }; + + //}); +}; + diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 0000000..2cd1149 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,22 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. +// +// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require dataTables/jquery.dataTables +//= require tether +//= require turbolinks +//= require_tree . +//= require bootstrap-multiselect +//= require skrollr +//= require d3 + diff --git a/app/assets/javascripts/bootstrap.min.js b/app/assets/javascripts/bootstrap.min.js new file mode 100644 index 0000000..d9c72df --- /dev/null +++ b/app/assets/javascripts/bootstrap.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.0.0-alpha.6 (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery. jQuery must be included before Bootstrap's JavaScript.");+function(t){var e=t.fn.jquery.split(" ")[0].split(".");if(e[0]<2&&e[1]<9||1==e[0]&&9==e[1]&&e[2]<1||e[0]>=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}(jQuery),+function(){function t(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function e(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},o=function(){function t(t,e){for(var n=0;nthis._items.length-1||e<0)){if(this._isSliding)return void t(this._element).one(m.SLID,function(){return n.to(e)});if(i===e)return this.pause(),void this.cycle();var o=e>i?p.NEXT:p.PREVIOUS;this._slide(o,this._items[e])}},h.prototype.dispose=function(){t(this._element).off(l),t.removeData(this._element,a),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},h.prototype._getConfig=function(n){return n=t.extend({},_,n),r.typeCheckConfig(e,n,g),n},h.prototype._addEventListeners=function(){var e=this;this._config.keyboard&&t(this._element).on(m.KEYDOWN,function(t){return e._keydown(t)}),"hover"!==this._config.pause||"ontouchstart"in document.documentElement||t(this._element).on(m.MOUSEENTER,function(t){return e.pause(t)}).on(m.MOUSELEAVE,function(t){return e.cycle(t)})},h.prototype._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case d:t.preventDefault(),this.prev();break;case f:t.preventDefault(),this.next();break;default:return}},h.prototype._getItemIndex=function(e){return this._items=t.makeArray(t(e).parent().find(v.ITEM)),this._items.indexOf(e)},h.prototype._getItemByDirection=function(t,e){var n=t===p.NEXT,i=t===p.PREVIOUS,o=this._getItemIndex(e),r=this._items.length-1,s=i&&0===o||n&&o===r;if(s&&!this._config.wrap)return e;var a=t===p.PREVIOUS?-1:1,l=(o+a)%this._items.length;return l===-1?this._items[this._items.length-1]:this._items[l]},h.prototype._triggerSlideEvent=function(e,n){var i=t.Event(m.SLIDE,{relatedTarget:e,direction:n});return t(this._element).trigger(i),i},h.prototype._setActiveIndicatorElement=function(e){if(this._indicatorsElement){t(this._indicatorsElement).find(v.ACTIVE).removeClass(E.ACTIVE);var n=this._indicatorsElement.children[this._getItemIndex(e)];n&&t(n).addClass(E.ACTIVE)}},h.prototype._slide=function(e,n){var i=this,o=t(this._element).find(v.ACTIVE_ITEM)[0],s=n||o&&this._getItemByDirection(e,o),a=Boolean(this._interval),l=void 0,h=void 0,c=void 0;if(e===p.NEXT?(l=E.LEFT,h=E.NEXT,c=p.LEFT):(l=E.RIGHT,h=E.PREV,c=p.RIGHT),s&&t(s).hasClass(E.ACTIVE))return void(this._isSliding=!1);var d=this._triggerSlideEvent(s,c);if(!d.isDefaultPrevented()&&o&&s){this._isSliding=!0,a&&this.pause(),this._setActiveIndicatorElement(s);var f=t.Event(m.SLID,{relatedTarget:s,direction:c});r.supportsTransitionEnd()&&t(this._element).hasClass(E.SLIDE)?(t(s).addClass(h),r.reflow(s),t(o).addClass(l),t(s).addClass(l),t(o).one(r.TRANSITION_END,function(){t(s).removeClass(l+" "+h).addClass(E.ACTIVE),t(o).removeClass(E.ACTIVE+" "+h+" "+l),i._isSliding=!1,setTimeout(function(){return t(i._element).trigger(f)},0)}).emulateTransitionEnd(u)):(t(o).removeClass(E.ACTIVE),t(s).addClass(E.ACTIVE),this._isSliding=!1,t(this._element).trigger(f)),a&&this.cycle()}},h._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(a),o=t.extend({},_,t(this).data());"object"===("undefined"==typeof e?"undefined":i(e))&&t.extend(o,e);var r="string"==typeof e?e:o.slide;if(n||(n=new h(this,o),t(this).data(a,n)),"number"==typeof e)n.to(e);else if("string"==typeof r){if(void 0===n[r])throw new Error('No method named "'+r+'"');n[r]()}else o.interval&&(n.pause(),n.cycle())})},h._dataApiClickHandler=function(e){var n=r.getSelectorFromElement(this);if(n){var i=t(n)[0];if(i&&t(i).hasClass(E.CAROUSEL)){var o=t.extend({},t(i).data(),t(this).data()),s=this.getAttribute("data-slide-to");s&&(o.interval=!1),h._jQueryInterface.call(t(i),o),s&&t(i).data(a).to(s),e.preventDefault()}}},o(h,null,[{key:"VERSION",get:function(){return s}},{key:"Default",get:function(){return _}}]),h}();return t(document).on(m.CLICK_DATA_API,v.DATA_SLIDE,T._dataApiClickHandler),t(window).on(m.LOAD_DATA_API,function(){t(v.DATA_RIDE).each(function(){var e=t(this);T._jQueryInterface.call(e,e.data())})}),t.fn[e]=T._jQueryInterface,t.fn[e].Constructor=T,t.fn[e].noConflict=function(){return t.fn[e]=c,T._jQueryInterface},T}(jQuery),function(t){var e="collapse",s="4.0.0-alpha.6",a="bs.collapse",l="."+a,h=".data-api",c=t.fn[e],u=600,d={toggle:!0,parent:""},f={toggle:"boolean",parent:"string"},_={SHOW:"show"+l,SHOWN:"shown"+l,HIDE:"hide"+l,HIDDEN:"hidden"+l,CLICK_DATA_API:"click"+l+h},g={SHOW:"show",COLLAPSE:"collapse",COLLAPSING:"collapsing",COLLAPSED:"collapsed"},p={WIDTH:"width",HEIGHT:"height"},m={ACTIVES:".card > .show, .card > .collapsing",DATA_TOGGLE:'[data-toggle="collapse"]'},E=function(){function l(e,i){n(this,l),this._isTransitioning=!1,this._element=e,this._config=this._getConfig(i),this._triggerArray=t.makeArray(t('[data-toggle="collapse"][href="#'+e.id+'"],'+('[data-toggle="collapse"][data-target="#'+e.id+'"]'))),this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}return l.prototype.toggle=function(){t(this._element).hasClass(g.SHOW)?this.hide():this.show()},l.prototype.show=function(){var e=this;if(this._isTransitioning)throw new Error("Collapse is transitioning");if(!t(this._element).hasClass(g.SHOW)){var n=void 0,i=void 0;if(this._parent&&(n=t.makeArray(t(this._parent).find(m.ACTIVES)),n.length||(n=null)),!(n&&(i=t(n).data(a),i&&i._isTransitioning))){var o=t.Event(_.SHOW);if(t(this._element).trigger(o),!o.isDefaultPrevented()){n&&(l._jQueryInterface.call(t(n),"hide"),i||t(n).data(a,null));var s=this._getDimension();t(this._element).removeClass(g.COLLAPSE).addClass(g.COLLAPSING),this._element.style[s]=0,this._element.setAttribute("aria-expanded",!0),this._triggerArray.length&&t(this._triggerArray).removeClass(g.COLLAPSED).attr("aria-expanded",!0),this.setTransitioning(!0);var h=function(){t(e._element).removeClass(g.COLLAPSING).addClass(g.COLLAPSE).addClass(g.SHOW),e._element.style[s]="",e.setTransitioning(!1),t(e._element).trigger(_.SHOWN)};if(!r.supportsTransitionEnd())return void h();var c=s[0].toUpperCase()+s.slice(1),d="scroll"+c;t(this._element).one(r.TRANSITION_END,h).emulateTransitionEnd(u),this._element.style[s]=this._element[d]+"px"}}}},l.prototype.hide=function(){var e=this;if(this._isTransitioning)throw new Error("Collapse is transitioning");if(t(this._element).hasClass(g.SHOW)){var n=t.Event(_.HIDE);if(t(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension(),o=i===p.WIDTH?"offsetWidth":"offsetHeight";this._element.style[i]=this._element[o]+"px",r.reflow(this._element),t(this._element).addClass(g.COLLAPSING).removeClass(g.COLLAPSE).removeClass(g.SHOW),this._element.setAttribute("aria-expanded",!1),this._triggerArray.length&&t(this._triggerArray).addClass(g.COLLAPSED).attr("aria-expanded",!1),this.setTransitioning(!0);var s=function(){e.setTransitioning(!1),t(e._element).removeClass(g.COLLAPSING).addClass(g.COLLAPSE).trigger(_.HIDDEN)};return this._element.style[i]="",r.supportsTransitionEnd()?void t(this._element).one(r.TRANSITION_END,s).emulateTransitionEnd(u):void s()}}},l.prototype.setTransitioning=function(t){this._isTransitioning=t},l.prototype.dispose=function(){t.removeData(this._element,a),this._config=null,this._parent=null,this._element=null,this._triggerArray=null,this._isTransitioning=null},l.prototype._getConfig=function(n){return n=t.extend({},d,n),n.toggle=Boolean(n.toggle),r.typeCheckConfig(e,n,f),n},l.prototype._getDimension=function(){var e=t(this._element).hasClass(p.WIDTH);return e?p.WIDTH:p.HEIGHT},l.prototype._getParent=function(){var e=this,n=t(this._config.parent)[0],i='[data-toggle="collapse"][data-parent="'+this._config.parent+'"]';return t(n).find(i).each(function(t,n){e._addAriaAndCollapsedClass(l._getTargetFromElement(n),[n])}),n},l.prototype._addAriaAndCollapsedClass=function(e,n){if(e){var i=t(e).hasClass(g.SHOW);e.setAttribute("aria-expanded",i),n.length&&t(n).toggleClass(g.COLLAPSED,!i).attr("aria-expanded",i)}},l._getTargetFromElement=function(e){var n=r.getSelectorFromElement(e);return n?t(n)[0]:null},l._jQueryInterface=function(e){return this.each(function(){var n=t(this),o=n.data(a),r=t.extend({},d,n.data(),"object"===("undefined"==typeof e?"undefined":i(e))&&e);if(!o&&r.toggle&&/show|hide/.test(e)&&(r.toggle=!1),o||(o=new l(this,r),n.data(a,o)),"string"==typeof e){if(void 0===o[e])throw new Error('No method named "'+e+'"');o[e]()}})},o(l,null,[{key:"VERSION",get:function(){return s}},{key:"Default",get:function(){return d}}]),l}();return t(document).on(_.CLICK_DATA_API,m.DATA_TOGGLE,function(e){e.preventDefault();var n=E._getTargetFromElement(this),i=t(n).data(a),o=i?"toggle":t(this).data();E._jQueryInterface.call(t(n),o)}),t.fn[e]=E._jQueryInterface,t.fn[e].Constructor=E,t.fn[e].noConflict=function(){return t.fn[e]=c,E._jQueryInterface},E}(jQuery),function(t){var e="dropdown",i="4.0.0-alpha.6",s="bs.dropdown",a="."+s,l=".data-api",h=t.fn[e],c=27,u=38,d=40,f=3,_={HIDE:"hide"+a,HIDDEN:"hidden"+a,SHOW:"show"+a,SHOWN:"shown"+a,CLICK:"click"+a,CLICK_DATA_API:"click"+a+l,FOCUSIN_DATA_API:"focusin"+a+l,KEYDOWN_DATA_API:"keydown"+a+l},g={BACKDROP:"dropdown-backdrop",DISABLED:"disabled",SHOW:"show"},p={BACKDROP:".dropdown-backdrop",DATA_TOGGLE:'[data-toggle="dropdown"]',FORM_CHILD:".dropdown form",ROLE_MENU:'[role="menu"]',ROLE_LISTBOX:'[role="listbox"]',NAVBAR_NAV:".navbar-nav",VISIBLE_ITEMS:'[role="menu"] li:not(.disabled) a, [role="listbox"] li:not(.disabled) a'},m=function(){function e(t){n(this,e),this._element=t,this._addEventListeners()}return e.prototype.toggle=function(){if(this.disabled||t(this).hasClass(g.DISABLED))return!1;var n=e._getParentFromElement(this),i=t(n).hasClass(g.SHOW);if(e._clearMenus(),i)return!1;if("ontouchstart"in document.documentElement&&!t(n).closest(p.NAVBAR_NAV).length){var o=document.createElement("div");o.className=g.BACKDROP,t(o).insertBefore(this),t(o).on("click",e._clearMenus)}var r={relatedTarget:this},s=t.Event(_.SHOW,r);return t(n).trigger(s),!s.isDefaultPrevented()&&(this.focus(),this.setAttribute("aria-expanded",!0),t(n).toggleClass(g.SHOW),t(n).trigger(t.Event(_.SHOWN,r)),!1)},e.prototype.dispose=function(){t.removeData(this._element,s),t(this._element).off(a),this._element=null},e.prototype._addEventListeners=function(){t(this._element).on(_.CLICK,this.toggle)},e._jQueryInterface=function(n){return this.each(function(){var i=t(this).data(s);if(i||(i=new e(this),t(this).data(s,i)),"string"==typeof n){if(void 0===i[n])throw new Error('No method named "'+n+'"');i[n].call(this)}})},e._clearMenus=function(n){if(!n||n.which!==f){var i=t(p.BACKDROP)[0];i&&i.parentNode.removeChild(i);for(var o=t.makeArray(t(p.DATA_TOGGLE)),r=0;r0&&a--,n.which===d&&adocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},h.prototype._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},h.prototype._checkScrollbar=function(){this._isBodyOverflowing=document.body.clientWidth=n){var i=this._targets[this._targets.length-1];return void(this._activeTarget!==i&&this._activate(i))}if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){var r=this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&(void 0===this._offsets[o+1]||t "+g.NAV_LINKS).addClass(_.ACTIVE),t(this._scrollElement).trigger(f.ACTIVATE,{relatedTarget:e})},h.prototype._clear=function(){t(this._selector).filter(g.ACTIVE).removeClass(_.ACTIVE)},h._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(a),o="object"===("undefined"==typeof e?"undefined":i(e))&&e; +if(n||(n=new h(this,o),t(this).data(a,n)),"string"==typeof e){if(void 0===n[e])throw new Error('No method named "'+e+'"');n[e]()}})},o(h,null,[{key:"VERSION",get:function(){return s}},{key:"Default",get:function(){return u}}]),h}();return t(window).on(f.LOAD_DATA_API,function(){for(var e=t.makeArray(t(g.DATA_SPY)),n=e.length;n--;){var i=t(e[n]);m._jQueryInterface.call(i,i.data())}}),t.fn[e]=m._jQueryInterface,t.fn[e].Constructor=m,t.fn[e].noConflict=function(){return t.fn[e]=c,m._jQueryInterface},m}(jQuery),function(t){var e="tab",i="4.0.0-alpha.6",s="bs.tab",a="."+s,l=".data-api",h=t.fn[e],c=150,u={HIDE:"hide"+a,HIDDEN:"hidden"+a,SHOW:"show"+a,SHOWN:"shown"+a,CLICK_DATA_API:"click"+a+l},d={DROPDOWN_MENU:"dropdown-menu",ACTIVE:"active",DISABLED:"disabled",FADE:"fade",SHOW:"show"},f={A:"a",LI:"li",DROPDOWN:".dropdown",LIST:"ul:not(.dropdown-menu), ol:not(.dropdown-menu), nav:not(.dropdown-menu)",FADE_CHILD:"> .nav-item .fade, > .fade",ACTIVE:".active",ACTIVE_CHILD:"> .nav-item > .active, > .active",DATA_TOGGLE:'[data-toggle="tab"], [data-toggle="pill"]',DROPDOWN_TOGGLE:".dropdown-toggle",DROPDOWN_ACTIVE_CHILD:"> .dropdown-menu .active"},_=function(){function e(t){n(this,e),this._element=t}return e.prototype.show=function(){var e=this;if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&t(this._element).hasClass(d.ACTIVE)||t(this._element).hasClass(d.DISABLED))){var n=void 0,i=void 0,o=t(this._element).closest(f.LIST)[0],s=r.getSelectorFromElement(this._element);o&&(i=t.makeArray(t(o).find(f.ACTIVE)),i=i[i.length-1]);var a=t.Event(u.HIDE,{relatedTarget:this._element}),l=t.Event(u.SHOW,{relatedTarget:i});if(i&&t(i).trigger(a),t(this._element).trigger(l),!l.isDefaultPrevented()&&!a.isDefaultPrevented()){s&&(n=t(s)[0]),this._activate(this._element,o);var h=function(){var n=t.Event(u.HIDDEN,{relatedTarget:e._element}),o=t.Event(u.SHOWN,{relatedTarget:i});t(i).trigger(n),t(e._element).trigger(o)};n?this._activate(n,n.parentNode,h):h()}}},e.prototype.dispose=function(){t.removeClass(this._element,s),this._element=null},e.prototype._activate=function(e,n,i){var o=this,s=t(n).find(f.ACTIVE_CHILD)[0],a=i&&r.supportsTransitionEnd()&&(s&&t(s).hasClass(d.FADE)||Boolean(t(n).find(f.FADE_CHILD)[0])),l=function(){return o._transitionComplete(e,s,a,i)};s&&a?t(s).one(r.TRANSITION_END,l).emulateTransitionEnd(c):l(),s&&t(s).removeClass(d.SHOW)},e.prototype._transitionComplete=function(e,n,i,o){if(n){t(n).removeClass(d.ACTIVE);var s=t(n.parentNode).find(f.DROPDOWN_ACTIVE_CHILD)[0];s&&t(s).removeClass(d.ACTIVE),n.setAttribute("aria-expanded",!1)}if(t(e).addClass(d.ACTIVE),e.setAttribute("aria-expanded",!0),i?(r.reflow(e),t(e).addClass(d.SHOW)):t(e).removeClass(d.FADE),e.parentNode&&t(e.parentNode).hasClass(d.DROPDOWN_MENU)){var a=t(e).closest(f.DROPDOWN)[0];a&&t(a).find(f.DROPDOWN_TOGGLE).addClass(d.ACTIVE),e.setAttribute("aria-expanded",!0)}o&&o()},e._jQueryInterface=function(n){return this.each(function(){var i=t(this),o=i.data(s);if(o||(o=new e(this),i.data(s,o)),"string"==typeof n){if(void 0===o[n])throw new Error('No method named "'+n+'"');o[n]()}})},o(e,null,[{key:"VERSION",get:function(){return i}}]),e}();return t(document).on(u.CLICK_DATA_API,f.DATA_TOGGLE,function(e){e.preventDefault(),_._jQueryInterface.call(t(this),"show")}),t.fn[e]=_._jQueryInterface,t.fn[e].Constructor=_,t.fn[e].noConflict=function(){return t.fn[e]=h,_._jQueryInterface},_}(jQuery),function(t){if("undefined"==typeof Tether)throw new Error("Bootstrap tooltips require Tether (http://tether.io/)");var e="tooltip",s="4.0.0-alpha.6",a="bs.tooltip",l="."+a,h=t.fn[e],c=150,u="bs-tether",d={animation:!0,template:'',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:"0 0",constraints:[],container:!1},f={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"string",constraints:"array",container:"(string|element|boolean)"},_={TOP:"bottom center",RIGHT:"middle left",BOTTOM:"top center",LEFT:"middle right"},g={SHOW:"show",OUT:"out"},p={HIDE:"hide"+l,HIDDEN:"hidden"+l,SHOW:"show"+l,SHOWN:"shown"+l,INSERTED:"inserted"+l,CLICK:"click"+l,FOCUSIN:"focusin"+l,FOCUSOUT:"focusout"+l,MOUSEENTER:"mouseenter"+l,MOUSELEAVE:"mouseleave"+l},m={FADE:"fade",SHOW:"show"},E={TOOLTIP:".tooltip",TOOLTIP_INNER:".tooltip-inner"},v={element:!1,enabled:!1},T={HOVER:"hover",FOCUS:"focus",CLICK:"click",MANUAL:"manual"},I=function(){function h(t,e){n(this,h),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._isTransitioning=!1,this._tether=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}return h.prototype.enable=function(){this._isEnabled=!0},h.prototype.disable=function(){this._isEnabled=!1},h.prototype.toggleEnabled=function(){this._isEnabled=!this._isEnabled},h.prototype.toggle=function(e){if(e){var n=this.constructor.DATA_KEY,i=t(e.currentTarget).data(n);i||(i=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(t(this.getTipElement()).hasClass(m.SHOW))return void this._leave(null,this);this._enter(null,this)}},h.prototype.dispose=function(){clearTimeout(this._timeout),this.cleanupTether(),t.removeData(this.element,this.constructor.DATA_KEY),t(this.element).off(this.constructor.EVENT_KEY),t(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&t(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._tether=null,this.element=null,this.config=null,this.tip=null},h.prototype.show=function(){var e=this;if("none"===t(this.element).css("display"))throw new Error("Please use show on visible elements");var n=t.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){if(this._isTransitioning)throw new Error("Tooltip is transitioning");t(this.element).trigger(n);var i=t.contains(this.element.ownerDocument.documentElement,this.element);if(n.isDefaultPrevented()||!i)return;var o=this.getTipElement(),s=r.getUID(this.constructor.NAME);o.setAttribute("id",s),this.element.setAttribute("aria-describedby",s),this.setContent(),this.config.animation&&t(o).addClass(m.FADE);var a="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,l=this._getAttachment(a),c=this.config.container===!1?document.body:t(this.config.container);t(o).data(this.constructor.DATA_KEY,this).appendTo(c),t(this.element).trigger(this.constructor.Event.INSERTED),this._tether=new Tether({attachment:l,element:o,target:this.element,classes:v,classPrefix:u,offset:this.config.offset,constraints:this.config.constraints,addTargetClasses:!1}),r.reflow(o),this._tether.position(),t(o).addClass(m.SHOW);var d=function(){var n=e._hoverState;e._hoverState=null,e._isTransitioning=!1,t(e.element).trigger(e.constructor.Event.SHOWN),n===g.OUT&&e._leave(null,e)};if(r.supportsTransitionEnd()&&t(this.tip).hasClass(m.FADE))return this._isTransitioning=!0,void t(this.tip).one(r.TRANSITION_END,d).emulateTransitionEnd(h._TRANSITION_DURATION);d()}},h.prototype.hide=function(e){var n=this,i=this.getTipElement(),o=t.Event(this.constructor.Event.HIDE);if(this._isTransitioning)throw new Error("Tooltip is transitioning");var s=function(){n._hoverState!==g.SHOW&&i.parentNode&&i.parentNode.removeChild(i),n.element.removeAttribute("aria-describedby"),t(n.element).trigger(n.constructor.Event.HIDDEN),n._isTransitioning=!1,n.cleanupTether(),e&&e()};t(this.element).trigger(o),o.isDefaultPrevented()||(t(i).removeClass(m.SHOW),this._activeTrigger[T.CLICK]=!1,this._activeTrigger[T.FOCUS]=!1,this._activeTrigger[T.HOVER]=!1,r.supportsTransitionEnd()&&t(this.tip).hasClass(m.FADE)?(this._isTransitioning=!0,t(i).one(r.TRANSITION_END,s).emulateTransitionEnd(c)):s(),this._hoverState="")},h.prototype.isWithContent=function(){return Boolean(this.getTitle())},h.prototype.getTipElement=function(){return this.tip=this.tip||t(this.config.template)[0]},h.prototype.setContent=function(){var e=t(this.getTipElement());this.setElementContent(e.find(E.TOOLTIP_INNER),this.getTitle()),e.removeClass(m.FADE+" "+m.SHOW),this.cleanupTether()},h.prototype.setElementContent=function(e,n){var o=this.config.html;"object"===("undefined"==typeof n?"undefined":i(n))&&(n.nodeType||n.jquery)?o?t(n).parent().is(e)||e.empty().append(n):e.text(t(n).text()):e[o?"html":"text"](n)},h.prototype.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},h.prototype.cleanupTether=function(){this._tether&&this._tether.destroy()},h.prototype._getAttachment=function(t){return _[t.toUpperCase()]},h.prototype._setListeners=function(){var e=this,n=this.config.trigger.split(" ");n.forEach(function(n){if("click"===n)t(e.element).on(e.constructor.Event.CLICK,e.config.selector,function(t){return e.toggle(t)});else if(n!==T.MANUAL){var i=n===T.HOVER?e.constructor.Event.MOUSEENTER:e.constructor.Event.FOCUSIN,o=n===T.HOVER?e.constructor.Event.MOUSELEAVE:e.constructor.Event.FOCUSOUT;t(e.element).on(i,e.config.selector,function(t){return e._enter(t)}).on(o,e.config.selector,function(t){return e._leave(t)})}t(e.element).closest(".modal").on("hide.bs.modal",function(){return e.hide()})}),this.config.selector?this.config=t.extend({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},h.prototype._fixTitle=function(){var t=i(this.element.getAttribute("data-original-title"));(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},h.prototype._enter=function(e,n){var i=this.constructor.DATA_KEY;return n=n||t(e.currentTarget).data(i),n||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusin"===e.type?T.FOCUS:T.HOVER]=!0),t(n.getTipElement()).hasClass(m.SHOW)||n._hoverState===g.SHOW?void(n._hoverState=g.SHOW):(clearTimeout(n._timeout),n._hoverState=g.SHOW,n.config.delay&&n.config.delay.show?void(n._timeout=setTimeout(function(){n._hoverState===g.SHOW&&n.show()},n.config.delay.show)):void n.show())},h.prototype._leave=function(e,n){var i=this.constructor.DATA_KEY;if(n=n||t(e.currentTarget).data(i),n||(n=new this.constructor(e.currentTarget,this._getDelegateConfig()),t(e.currentTarget).data(i,n)),e&&(n._activeTrigger["focusout"===e.type?T.FOCUS:T.HOVER]=!1),!n._isWithActiveTrigger())return clearTimeout(n._timeout),n._hoverState=g.OUT,n.config.delay&&n.config.delay.hide?void(n._timeout=setTimeout(function(){n._hoverState===g.OUT&&n.hide()},n.config.delay.hide)):void n.hide()},h.prototype._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},h.prototype._getConfig=function(n){return n=t.extend({},this.constructor.Default,t(this.element).data(),n),n.delay&&"number"==typeof n.delay&&(n.delay={show:n.delay,hide:n.delay}),r.typeCheckConfig(e,n,this.constructor.DefaultType),n},h.prototype._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},h._jQueryInterface=function(e){return this.each(function(){var n=t(this).data(a),o="object"===("undefined"==typeof e?"undefined":i(e))&&e;if((n||!/dispose|hide/.test(e))&&(n||(n=new h(this,o),t(this).data(a,n)),"string"==typeof e)){if(void 0===n[e])throw new Error('No method named "'+e+'"');n[e]()}})},o(h,null,[{key:"VERSION",get:function(){return s}},{key:"Default",get:function(){return d}},{key:"NAME",get:function(){return e}},{key:"DATA_KEY",get:function(){return a}},{key:"Event",get:function(){return p}},{key:"EVENT_KEY",get:function(){return l}},{key:"DefaultType",get:function(){return f}}]),h}();return t.fn[e]=I._jQueryInterface,t.fn[e].Constructor=I,t.fn[e].noConflict=function(){return t.fn[e]=h,I._jQueryInterface},I}(jQuery));(function(r){var a="popover",l="4.0.0-alpha.6",h="bs.popover",c="."+h,u=r.fn[a],d=r.extend({},s.Default,{placement:"right",trigger:"click",content:"",template:''}),f=r.extend({},s.DefaultType,{content:"(string|element|function)"}),_={FADE:"fade",SHOW:"show"},g={TITLE:".popover-title",CONTENT:".popover-content"},p={HIDE:"hide"+c,HIDDEN:"hidden"+c,SHOW:"show"+c,SHOWN:"shown"+c,INSERTED:"inserted"+c,CLICK:"click"+c,FOCUSIN:"focusin"+c,FOCUSOUT:"focusout"+c,MOUSEENTER:"mouseenter"+c,MOUSELEAVE:"mouseleave"+c},m=function(s){function u(){return n(this,u),t(this,s.apply(this,arguments))}return e(u,s),u.prototype.isWithContent=function(){return this.getTitle()||this._getContent()},u.prototype.getTipElement=function(){return this.tip=this.tip||r(this.config.template)[0]},u.prototype.setContent=function(){var t=r(this.getTipElement());this.setElementContent(t.find(g.TITLE),this.getTitle()),this.setElementContent(t.find(g.CONTENT),this._getContent()),t.removeClass(_.FADE+" "+_.SHOW),this.cleanupTether()},u.prototype._getContent=function(){return this.element.getAttribute("data-content")||("function"==typeof this.config.content?this.config.content.call(this.element):this.config.content)},u._jQueryInterface=function(t){return this.each(function(){var e=r(this).data(h),n="object"===("undefined"==typeof t?"undefined":i(t))?t:null;if((e||!/destroy|hide/.test(t))&&(e||(e=new u(this,n),r(this).data(h,e)),"string"==typeof t)){if(void 0===e[t])throw new Error('No method named "'+t+'"');e[t]()}})},o(u,null,[{key:"VERSION",get:function(){return l}},{key:"Default",get:function(){return d}},{key:"NAME",get:function(){return a}},{key:"DATA_KEY",get:function(){return h}},{key:"Event",get:function(){return p}},{key:"EVENT_KEY",get:function(){return c}},{key:"DefaultType",get:function(){return f}}]),u}(s);return r.fn[a]=m._jQueryInterface,r.fn[a].Constructor=m,r.fn[a].noConflict=function(){return r.fn[a]=u,m._jQueryInterface},m})(jQuery)}(); \ No newline at end of file diff --git a/app/assets/javascripts/changes.js b/app/assets/javascripts/changes.js new file mode 100644 index 0000000..23cac0d --- /dev/null +++ b/app/assets/javascripts/changes.js @@ -0,0 +1,843 @@ +function foldChange(id, legend_id, json, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif") { + + // Size + var margin = {top: 10, right: 10, bottom: 75, left: 75}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + xPadding = [0.07, 0.07], + yPadding = [0.07, 0.07], + axis_margin = 50; + + // Colors and stuff + var colors = {pos:"#FF0039", + neg:"#0031FF", + neutral:"#666666"}, + baseline_opacity = 0.5, + signThres = [{value:-Math.log2(0.001), opacity:0.9, text:'***'}, + {value:-Math.log2(0.01), opacity:0.7, text:'**'}, + {value:-Math.log2(0.05), opacity:0.5, text:'*'}, + {value:0, opacity:0.1, text:'ns'}, + {value:'NA', opacity:0.3, text:'na'}]; + + // General functions + function getValuesExtrema(values, selected, which='max') { + var val = []; + for (var i = 0; i < values.length; i++) { + if (['-Inf', 'Inf', 'NA', 'NaN', ''].indexOf(values[i][selected]) == -1) { + val.push(values[i][selected]); + } + } + + if (which == 'min') { + return Math.min.apply(null, val); + } else if (which == 'max') { + return Math.max.apply(null, val); + } + }; + + // Buttons + var buttons = d3.select("#d3-buttons"); + buttons.html(""); + + + //$.getJSON(data, function(json) { + + var model_choice = Object.keys(json.data), + effect_choice0 = Object.keys(json.data[model_choice[0]]), + comp_choice0 = Object.keys(json.data[model_choice[0]][effect_choice0[0]]), + axis_choice_x = Object.keys(json.data[model_choice[0]][effect_choice0[0]][comp_choice0[0]][0]), + axis_choice_y = axis_choice_x.slice(); + axis_choice_y.push(''); + + //////////////// Draw plot //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-1') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Draw points + var plot = svg.selectAll(), + plotLabel = svg.selectAll(); + + // Draw axis + var xAxis = svg.append("g") + .attr("transform", "translate(0," + height + ")"); + + var xAxisLegend = svg.append("g") + .attr("transform", "translate("+ width/2 +", "+ (height+axis_margin) +")") + + var yAxis = svg.append("g") + + var yAxisLegend = svg.append("g") + .attr("transform", "translate("+ -axis_margin +", "+ height/2 +")") + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + legend.append("p") + .html("Color key") + + var colorLegend = legend.append("ul") + .style("list-style-type", "none") + .style("padding-top", "25px") + .selectAll("ul") + .data(["pos", "neg"]) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + colorLegendSpan = colorLegend.append("span") + + var legend_svg_symsize = 15, + legend_svg_width = legend_svg_symsize*signThres.length; + + colorLegendSpanSvg = colorLegendSpan.append("svg") + .attr("width", legend_svg_width+"px") + .attr("height", legend_svg_symsize+"px") + .style("margin-right", "5px") + .style("overflow", "visible") + + for (var i = 0; i < signThres.length; i++) { + // (V)(°,,,°)(V) + + colorLegendSpanSvg.append("g") + .attr("transform", "translate("+(i*legend_svg_width/signThres.length)+", -2)") + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", legend_svg_symsize) + .text(function(d, j){ + if (j == 0) + return signThres[i]["text"]; + }) + + colorLegendSpanSvg.append("rect") + .attr("transform", "translate("+(i*legend_svg_width/signThres.length)+", 0)") + .attr("width", legend_svg_symsize) + .attr("height", legend_svg_symsize) + .attr("stroke", "none") + .attr("fill", function (d){ + if (isNaN(signThres[i]["value"])) { + return colors.neutral; + }; + return colors[d]; + }) + .attr("fill-opacity", signThres[i]["opacity"]) + + }; + + colorLegendSpan.append("span") + .attr("id", function(d) { return "color-legend-text-"+d;}) + .html(function(d) { return d;}) + + //////////////// Restart function //////////////// + var data; + var restart = function() { + + // Define data + var selected_model = $("#modelSelect").val(), + selected_effect = $('#effectSelect').val(), + selected_comp = $("#compSelect").val(); + data = JSON.parse(JSON.stringify(json.data[selected_model][selected_effect][selected_comp])); + + data.forEach(function(d, i){ + d['name'] = json.names[i]; + }) + + function filterPval(d) { + return d['-log2(p-value)'] >= -Math.log2($("#pThreshold").val()) + } + + function filterFc(d) { + return Math.abs(d['log2(fold-change)']) >= Math.log2($("#fcThreshold").val()) + } + + data = data.filter(filterPval); + data = data.filter(filterFc); + + // Define axis + var selected_x = $("#xSelect").val(), + selected_y = $("#ySelect").val(); + + // Define axis settings + var xMin = getValuesExtrema(data, selected_x, 'min'), + xMax = getValuesExtrema(data, selected_x, 'max'), + xValue = function(d) { + if (d[selected_x] == 'Inf') { + return xMax; + } else if (d[selected_x] == '-Inf') { + return xMin; + } else if (isNaN(d[selected_x])) { + return 0; + } else { + return d[selected_x]; + }; + }, + xRange = xMax-xMin, + xScale = d3.scaleLinear() + .range([0, width]) + .domain([xMin-xPadding[0]*xRange, xMax+xPadding[1]*xRange]).nice(), + xMap = function(d) { return xScale(xValue(d));}; + + if (selected_y == '') { + + var sort_factor = JSON.parse(JSON.stringify(selected_x)); + if (sort_factor == '-log2(p-value)') { + sort_factor = 'log2(fold-change)'; + }; + + var sMin = getValuesExtrema(data, sort_factor, 'min'), + sMax = getValuesExtrema(data, sort_factor, 'max'); + + function sortByValue(e1, e2) { + var v1 = e1[sort_factor], + v2 = e2[sort_factor]; + if (v1 == 'Inf') { + v1 = sMax+1; + }; + if (v2 == 'Inf') { + v2 = sMax+1; + }; + if (v1 == '-Inf') { + v1 = sMin-1; + }; + if (v2 == '-Inf') { + v2 = sMin-1; + }; + if (isNaN(v1)) { + v1 = 0; + }; + if (isNaN(v2)) { + v2 = 0; + }; + return v1-v2; + }; + + function sortByIndex(e1, e2) { + e1['index']-e2['index']; + }; + + data.forEach(function(d, i){ + d.index = i; + }); + + data.sort(sortByValue); + data.forEach(function(d, i){ + d.sorting_index = i; + }); + + data.sort(sortByIndex); + + selected_y = 'sorting_index'; + } + + var yMin = getValuesExtrema(data, selected_y, 'min'), + yMax = getValuesExtrema(data, selected_y, 'max'), + yValue = function(d) { + if (d[selected_y] == 'Inf') { + return yMax; + } else if (d[selected_y] == '-Inf') { + return yMin; + } else if (isNaN(d[selected_y])) { + return 0; + } else { + return d[selected_y]; + }; + }, + yRange = yMax-yMin, + yScale = d3.scaleLinear() + .range([height, 0]) + .domain([yMin-yPadding[0]*yRange, yMax+yPadding[1]*yRange]).nice(), + yMap = function(d) { return yScale(yValue(d));}; + + // Update plot + plot = plot.data([]); + plot.exit().remove(); + + plot = plot + .data(data) + .enter().append("g") + .attr("class", "plot"); + + // Update labels + plotLabel = plotLabel.data([]); + plotLabel.exit().remove(); + + plotLabel = plotLabel + .data(data) + .enter().append("g") + .attr("class", "plot-label") + + if (selected_y == 'sorting_index') { + + yScale.domain([yMin, yMax]); + + var flagHeight = (yScale(yMin)-yScale(yMax))/data.length; + + yScale.range([height-flagHeight/2, flagHeight/2]); + + plot.attr("transform", function (d) { + if (xMin < 0) { + if (xValue(d) >= 0) { + return "translate(" + xScale(0) + ", " + yMap(d) + ")"; + } else { + return "translate(" + xMap(d) + ", " + yMap(d) + ")"; + } + } else { + return "translate("+ (xScale(xMin)-5) +", " + yMap(d) + ")"; + } + }) + .append("rect") + .attr("width", function (d){ + if (xMin < 0) { + if (xValue(d) >= 0) { + return xMap(d)-xScale(0); + } else { + return xScale(0)-xMap(d); + } + } else { + return xMap(d)-xScale(xMin)+5; + } + }) + .attr("height", flagHeight) + .attr("y", -flagHeight/2) + .style("fill", function(d) { + if (isNaN(d['-log2(p-value)']) || d['log2(fold-change)'] == 0 || isNaN(d['log2(fold-change)'])) { + return colors.neutral; + } else if (d['log2(fold-change)'] == 'Inf') { + return colors.pos; + } else if (d['log2(fold-change)'] == '-Inf') { + return colors.neg; + } else if (d['log2(fold-change)'] < 0) { + return colors.neg; + } + return colors.pos; + }) + .style("stroke-opacity", 0) + .style("fill-opacity", function(d) { + for (var i = 0; i < signThres.length; i++) { + if (isNaN(d['-log2(p-value)']) && d['-log2(p-value)'] == signThres[i].value) { + return signThres[i].opacity; + } + if (!isNaN(d['-log2(p-value)']) && d['-log2(p-value)'] >= signThres[i].value) { + return signThres[i].opacity; + }; + }; + }) + + plotLabel.attr("transform", function (d) { + if (xMin < 0) { + if (xValue(d) >= 0) { + return "translate(" + xScale(0) + ", " + yMap(d) + ")"; + } else { + return "translate(" + xMap(d) + ", " + yMap(d) + ")"; + } + } else { + return "translate("+ (xScale(xMin)-5) +", " + yMap(d) + ")"; + } + }) + .append("rect") + .attr("width", function (d){ + if (xMin < 0) { + if (xValue(d) >= 0) { + return xMap(d)-xScale(0); + } else { + return xScale(0)-xMap(d); + } + } else { + return xMap(d)-xScale(xMin)+5; + } + }) + .attr("height", flagHeight) + .attr("y", -flagHeight/2) + .attr("opacity", 0); + + plotLabel.append("text") + .attr("dx", function (d){ + if (xValue(d) < 0) { + return xScale(0)-xMap(d); + } + return 0; + }) + .style("text-anchor", function (d) { + if (xValue(d) < 0 || xMin >= 0) { + return "start"; + } else { + return "end"; + } + }) + .attr("display", "none") + .attr("selected", false) + .attr("font-family", font_family) + .text(function (d, i) { + var label = d.name.split(";"); + if (label.length > 1) { + return label[label.length-2] +";"+ label[label.length-1]; + } else { + return label[0]; + }; + }); + + } else { + + plot.attr("transform", function (d) { return "translate(" + xMap(d) + ", " + yMap(d) + ")"; }) + .append("path") + .attr("class", "symbol") + .attr("d", d3.symbol() + .type(function(d) { + if ([d[selected_x], d[selected_y]].indexOf('Inf') != -1 || [d[selected_x], d[selected_y]].indexOf('-Inf') != -1) { + return d3.symbolTriangle; + }; + return d3.symbolCircle; + }) + .size(function(d) { + if ([d[selected_x], d[selected_y]].indexOf('Inf') != -1 || [d[selected_x], d[selected_y]].indexOf('-Inf') != -1) { + return 100; + }; + return 200; + })) + .style("fill", function(d) { + if (isNaN(d['-log2(p-value)']) || d['log2(fold-change)'] == 0 || isNaN(d['log2(fold-change)'])) { + return colors.neutral; + } else if (d['log2(fold-change)'] == 'Inf') { + return colors.pos; + } else if (d['log2(fold-change)'] == '-Inf') { + return colors.neg; + } else if (d['log2(fold-change)'] < 0) { + return colors.neg; + } + return colors.pos; + }) + .style("stroke-opacity", 0) + .style("fill-opacity", function(d) { + for (var i = 0; i < signThres.length; i++) { + if (isNaN(d['-log2(p-value)']) && d['-log2(p-value)'] == signThres[i].value) { + return signThres[i].opacity; + } + if (!isNaN(d['-log2(p-value)']) && d['-log2(p-value)'] >= signThres[i].value) { + return signThres[i].opacity; + }; + }; + }) + .attr("transform", function (d){ + if (d[selected_y] == "-Inf") { + return "rotate(180)"; + }; + if (d[selected_x] == "-Inf") { + return "rotate(-90)"; + }; + if (d[selected_x] == "Inf") { + return "rotate(90)"; + }; + }); + + // Update labels + plotLabel.attr("transform", function (d) { return "translate(" + xMap(d) + ", " + yMap(d) + ")"; }) + .append("path") + .attr("class", "symbol") + .attr("d", d3.symbol() + .type(function(d) { + if ([d[selected_x], d[selected_y]].indexOf('Inf') != -1 || [d[selected_x], d[selected_y]].indexOf('-Inf') != -1) { + return d3.symbolTriangle; + }; + return d3.symbolCircle; + }) + .size(function(d) { + if ([d[selected_x], d[selected_y]].indexOf('Inf') != -1 || [d[selected_x], d[selected_y]].indexOf('-Inf') != -1) { + return 100; + }; + return 200; + })) + .attr("opacity", 0) + .attr("transform", function (d){ + if (d[selected_y] == "-Inf") { + return "rotate(180)"; + }; + if (d[selected_x] == "-Inf") { + return "rotate(-90)"; + }; + if (d[selected_x] == "Inf") { + return "rotate(90)"; + }; + }); + + plotLabel.append("text") + .attr("dy", function (d){ + return -10; + }) + .style("text-anchor", function (d) { + if (xMap(d) <= width/2) { + return "start"; + } else { + return "end"; + } + }) + .attr("display", "none") + .attr("selected", false) + .attr("font-family", font_family) + .text(function (d, i) { + var label = d.name.split(";"); + if (label.length > 1) { + return label[label.length-2] +";"+ label[label.length-1]; + } else { + return label[0]; + }; + }); + + } + + showLabels(); + displayLabels("plot-label"); + + // Update axis + xAxis.selectAll("*").remove(); + xAxisLegend.selectAll("*").remove(); + yAxis.selectAll("*").remove(); + yAxisLegend.selectAll("*").remove(); + svg.selectAll(".frame").remove(); + svg.selectAll(".info-line").remove(); + + // Add axis + xAxis.call(d3.axisBottom(xScale).ticks(10)); + + xAxisLegend.append("text") + .text(selected_x) + .attr("font-family", font_family) + .style("text-anchor", "middle"); + + if (selected_y != 'sorting_index') { + + yAxis.call(d3.axisLeft(yScale).ticks(10)) + + yAxisLegend.append("text") + .text(selected_y) + .style("text-anchor", "middle") + .attr("font-family", font_family) + .attr("transform", "rotate(-90)"); + + // Close the plot + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate(0, 0)") + .call(d3.axisTop(xScale).ticks(0) + .tickSize(0, 0)); + + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate("+width+", 0)") + .call(d3.axisRight(yScale).ticks(0) + .tickSize(0, 0)); + + } + + // Additional lines + if (selected_x == 'log2(fold-change)') { + svg.append("g") + .attr("class", "info-line") + .attr("transform", "translate(" + xScale(0) + ", 0)") + .call(d3.axisLeft(yScale).ticks(0) + .tickSize(0, 0)); + }; + if (selected_y == 'log2(fold-change)') { + svg.append("g") + .attr("class", "info-line") + .attr("transform", "translate(0," + yScale(0) + ")") + .call(d3.axisBottom(xScale).ticks(0) + .tickSize(0, 0)); + }; + + if (selected_x == '-log2(p-value)') { + for (var i = 0; i < signThres.length; i++) { + if (!isNaN(signThres[i].value) && signThres[i].value != 0 && xScale(signThres[i].value) > 0) { + svg.append("g") + .attr("class", "info-line") + .style("stroke-dasharray", ("5, 5")) + .attr("transform", "translate(" + xScale(signThres[i].value) + ", 0)") + .call(d3.axisLeft(yScale).ticks(0) + .tickSize(0, 0)); + }; + }; + }; + if (selected_y == '-log2(p-value)') { + for (var i = 0; i < signThres.length; i++) { + if (!isNaN(signThres[i].value) && signThres[i].value != 0 && yScale(signThres[i].value) < height) { + svg.append("g") + .attr("class", "info-line") + .attr("transform", "translate(0," + yScale(signThres[i].value) + ")") + .call(d3.axisBottom(xScale).ticks(0) + .tickSize(0, 0)); + }; + }; + }; + + // Update legend text + var color_legend_text = selected_comp.split("-"); + + $("#color-legend-text-pos").html("more abundant in "+color_legend_text[1]); + $("#color-legend-text-neg").html("more abundant in "+color_legend_text[0]); + + } + + var exportData = function() { + + var dataBlob= new Blob([['name\tmean abundance\tlog2(fold-change)\t-log2(p-value)', data.map(function(d){return d.name+'\t'+d['mean abundance']+'\t'+d['log2(fold-change)']+'\t'+d['-log2(p-value)'];}).join('\n')].join('\n')], {type: 'txt'}), + link = document.createElement('a'), + event = new MouseEvent('click'); + + link.href = window.URL.createObjectURL(dataBlob); + link.download = 'fold_change.txt'; + link.dataset.downloadurl = ['txt', link.download, link.href].join(':'); + link.dispatchEvent(event); + } + + // Add download button for data + $("#export-data-btn").remove(); + + d3.select("#export-btn") + .append("li") + .attr("id", "export-data-btn") + .append("a") + .html("Data TXT") + .on("click", exportData) + + // Labels functions + function displayLabels (id) { + $("."+id).on("mouseenter", function(d) { + d3.select(this.childNodes[1]).attr("display", "inline"); + }); + $("."+id).on("mouseleave", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + }; + }); + $("."+id).on("click", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", true); + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", false); + } + }); + }; + + // Model update function + var updateEffect = function() { + var keys = Object.keys(json.data[$('#modelSelect').val()]) + new_data = [...Array(keys.length)]; + for (var i = 0; i < keys.length; i++) { + new_data[i] = {label:keys[i], value:keys[i]} + } + $('#effectSelect').multiselect('dataprovider', new_data) + .multiselect("refresh"); + + updateComp(); + + restart(); + } + + // Effect update function + var updateComp = function() { + var keys = Object.keys(json.data[$('#modelSelect').val()][$('#effectSelect').val()]) + new_data = [...Array(keys.length)]; + for (var i = 0; i < keys.length; i++) { + new_data[i] = {label:keys[i], value:keys[i]} + } + $('#compSelect').multiselect('dataprovider', new_data) + .multiselect("refresh"); + + restart(); + } + + + // Display labels button + var showLabels = function() { + var label = d3.selectAll(".plot-label").select(function(){ return this.childNodes[1];}); + if ($("#labelButton").is(':checked')) { + label.attr("display", "inline"); + label.attr("selected", true); + } else { + label.attr("display", "none"); + label.attr("selected", false); + }; + }; + + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + + // Search in labels + var searchLabels = function() { + $("#labelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".plot-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) != -1 }).select(function(){ return this.childNodes[1];}); + non_selected = d3.selectAll(".plot-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) == -1 }).select(function(){ return this.childNodes[1];}); + selected.attr("display", "inline"); + selected.attr("selected", true); + non_selected.attr("display", "none"); + non_selected.attr("selected", false); + } else { + to_free = d3.selectAll(".plot-label").select(function(){return this.childNodes[1];}); + to_free.attr("display", "none"); + to_free.attr("selected", false); + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + // Select axis + var xSelect = buttons.append("div") + .attr("title", "Chose X axis.") + .attr("class", "form-group") + + xSelect.append("label") + .html("X axis") + + xSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "xSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(axis_choice_x) + .enter().append("option") + .text(function (d){ return d;}); + + $('#xSelect').val(axis_choice_x[0]) + .on('change', restart); + + var ySelect = buttons.append("div") + .attr("title", "Chose Y axis.") + .attr("class", "form-group") + + ySelect.append("label") + .html("Y axis") + + ySelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "ySelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(axis_choice_y) + .enter().append("option") + .text(function (d){ return d;}); + + $('#ySelect').val(axis_choice_y[1]) + .on('change', restart); + + // Select model + var modelSelect = buttons.append("div") + .attr("title", "Chose model") + .attr("class", "form-group") + + modelSelect.append("label") + .html("Model") + + modelSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "modelSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.data)) + .enter().append("option") + .text(function (d){ return d;}); + + $('#modelSelect').on('change', updateEffect); + + // Select effect + var effectSelect = buttons.append("div") + .attr("title", "Chose effect") + .attr("class", "form-group") + + effectSelect.append("label") + .html("Effect") + + effectSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "effectSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.data[$('#modelSelect').val()])) + .enter().append("option") + .text(function (d){ return d;}); + + $('#effectSelect').on('change', updateComp); + + // Select comparison + var compSelect = buttons.append("div") + .attr("title", "Chose comparison") + .attr("class", "form-group") + + compSelect.append("label") + .html("Comparison") + + compSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "compSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.data[$('#modelSelect').val()][$('#effectSelect').val()])) + .enter().append("option") + .text(function (d){ return d;}); + + $('#compSelect').on('change', restart); + + // Select p-value cutoff + var pThreshold = buttons.append("div") + .attr("title", "Set a p-value cutoff.") + .attr("class", "form-group") + + pThreshold.append("label") + .html("P-value cutoff") + + pThreshold.append("input") + .attr("id", "pThreshold") + .attr("type", "number") + .attr("class", "form-control form-number-field") + .attr("min", 0) + .attr("max", 1) + .attr("step", 0.001) + .attr("value", 1) + .on("change", restart); + + // Select fold-change cutoff + var fcThreshold = buttons.append("div") + .attr("title", "Set a fold-change cutoff.") + .attr("class", "form-group") + + fcThreshold.append("label") + .html("Fold-change cutoff") + + fcThreshold.append("input") + .attr("id", "fcThreshold") + .attr("type", "number") + .attr("class", "form-control form-number-field") + .attr("value", 0) + .on("change", restart); + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + + restart(); + + //}); + + + +}; + diff --git a/app/assets/javascripts/correlationNetwork.js b/app/assets/javascripts/correlationNetwork.js new file mode 100644 index 0000000..ecf6810 --- /dev/null +++ b/app/assets/javascripts/correlationNetwork.js @@ -0,0 +1,743 @@ +function correlationNetwork(id, legend_id, json, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10) { + + // Size + var margin = {top: 10, right: 10, bottom: 40, left: 10}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom; + + // Restrictions + drag_size_limit = 80; + + // Colors, symbols and scaling + var colors = d3.scaleOrdinal(color_palette), + link_color = [["#999999", "Pos. correlation"], + ["#ff3385", "Neg. correlation"]], + symbols = d3.scaleOrdinal([d3.symbolCircle, + d3.symbolSquare, + d3.symbolTriangle, + d3.symbolStar, + d3.symbolDiamond, + d3.symbolCross]), + legend_svg_symsize = 15, + rRange = [6, 24], + size_legend_data = [15, 10, 5], + wRange = [0.1*rRange[0], 0.9*rRange[0]], + sizeScale = d3.scaleLinear() + .range(rRange), + widthScale = d3.scaleLinear() + .range(wRange), + attractionScale = d3.scaleLinear() + .range([100, 50]), + repulsionScale = d3.scaleLinear() + .range([50, 100]), + chargeScale = d3.scaleLinear() + .range([-100, -200]), + sizeOptions = [{"text":"", "value":"", "title":""}, + {"text":"Degree", "value":"degree", "title":"Node degree"}, + {"text":"Mean abundance", "value":"mean", "title":"Mean abundance"}, + {"text":"Max abundance", "value":"max", "title":"Max abundance"}, + {"text":"Min abundance", "value":"min", "title":"Min abundance"} + ]; + oRange = [0.1, 0.3], + p_value_legend_data = [[0.001, "***"], + [0.01, "**"], + [0.05, "*"], + [1, "ns"], + ["NA", "na"]], + stat_settings = {"range":[0.2, 0.6, 0.8, 1], + "domain":[1, 0.05, 0.01, 0.001], + "text":["ns", "*", "**", "***"]}, + opacity = d3.scaleLinear() + .range(stat_settings.range) + .domain(stat_settings.domain), + colorBarSize = 10; + + + // General functions + function getSizeExtrema(json, fun, extrema="max", absolute=false) { + + var values = json.map(function(d){ + if (absolute) { + return Math.abs(d[fun]); + } + return d[fun]; + }) + + if (extrema == "max") { + return Math.max.apply(null, values); + } else if (extrema == "min") { + return Math.min.apply(null, values); + }; + }; + + function getNodeDegree(links, nodes) { + var degree = {}; + links.forEach(function (d, i) { + if (Object.keys(degree).indexOf(d.target.toString()) == -1) { + degree[d.target.toString()] = 1; + } else { + degree[d.target.toString()] = degree[d.target.toString()]+1; + }; + if (Object.keys(degree).indexOf(d.source.toString()) == -1) { + degree[d.source.toString()] = 1; + } else { + degree[d.source.toString()] = degree[d.source.toString()]+1; + }; + }); + nodes.forEach(function (d) { + if (Object.keys(degree).indexOf(d.id) != -1) { + d["degree"] = degree[d.id]; + } else { + d["degree"] = 0; // Just to avoid scale domain to start at 0... + } + }); + }; + + function unique(value, index, self) { + return self.indexOf(value) === index; + } + + // Buttons + var buttons = d3.select("#d3-buttons") + buttons.html(""); + + //$.getJSON(data, function(json) { + + //////////////// Simulation //////////////// + var simulation = d3.forceSimulation() + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("y", d3.forceY()) + .force("x", d3.forceX()); + + var ticked = function() { + + link + .attr("x1", function(d) { return Math.max(0, Math.min(d.source.x, width)); }) + .attr("y1", function(d) { return Math.max(0, Math.min(d.source.y, height)); }) + .attr("x2", function(d) { return Math.max(0, Math.min(d.target.x, width)); }) + .attr("y2", function(d) { return Math.max(0, Math.min(d.target.y, height)); }); + + + + node + .attr("transform", function(d) { + return "translate(" + Math.max(0, Math.min(d.x, width)) + "," + Math.max(0, Math.min(d.y, height)) + ")"; }); + + nodeLabel + .attr("transform", function(d, i) { + return "translate(" + Math.max(0, Math.min(d.x, width)) + "," + Math.max(0, Math.min(d.y, height)) + ")"; }); + }; + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.25).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + + //////////////// Draw links and nodes //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-2') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure network-well") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .style("pointer-events", "all") + .call(d3.zoom() + .scaleExtent([1, 4]) + .duration(1000) + .translateExtent([[margin.left, margin.top], [width + margin.right, height + margin.top + margin.bottom]]) + .on("zoom", zoomed)); + + + var g = svg.append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + function zoomed() { + g.attr("transform", d3.event.transform); + }; + + // Draw links + var link = g.append("g") + .selectAll(); + + // Draw Nodes + var node = g.append("g") + .selectAll("g"); + + var nodeLabel = g.append("g") + .selectAll("g"); + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + var colorLegend = legend.append("ul") + .style("list-style-type", "none") + .style("padding-top", "25px") + .selectAll("ul"); + + legend.append("div") + .style("border-top", "solid #ccc 1px") + + var symbolLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + legend.append("div") + .style("border-top", "solid #ccc 1px") + + var linkLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + + //////////////// NODES COLORS //////////////// + var setSymbolColor = function() { + var selected_model = $('#modelSelect').val(), + selected_effect = $('#effectSelect').val(), + color_domain = json.nodes.map(function (d){return d.stat[selected_model][selected_effect]['highest-mean'];}).filter(unique); + if (selected_model != '') + colors.domain(color_domain); + + // Update symbols color + d3.selectAll(".coloured-symbol") + .style("fill", function (d){ + if (selected_model != '') + return colors(d.stat[selected_model][selected_effect]['highest-mean']); + return colors(''); + }) + .attr("fill-opacity", function (d){ + if (selected_model != '') + return opacity(d.stat[selected_model][selected_effect]['p-value']); + return 1; + }); + + // Update color legend + colorLegend = colorLegend.data([]); + colorLegend.exit().remove(); + + colorLegend = colorLegend + .data(color_domain) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + colorLegendSpan = colorLegend.append("span") + + var legend_svg_width = legend_svg_symsize*p_value_legend_data.length; + + colorLegendSpanSvg = colorLegendSpan.append("svg") + .attr("width", legend_svg_width+"px") + .attr("height", legend_svg_symsize+"px") + .style("margin-right", "5px") + .style("overflow", "visible") + + for (var i = 0; i < p_value_legend_data.length; i++) { + // (V)(°,,,°)(V) + + colorLegendSpanSvg.append("g") + .attr("transform", "translate("+(i*legend_svg_width/p_value_legend_data.length)+", -2)") + .append("text") + .attr("transform", "rotate(-90)") + .attr("y", legend_svg_symsize) + .text(function(d, j){ + if (j == 0 && color_domain.length > 0) + return p_value_legend_data[i][1]; + }) + + colorLegendSpanSvg.append("rect") + .attr("transform", "translate("+(i*legend_svg_width/p_value_legend_data.length)+", 0)") + .attr("width", legend_svg_symsize) + .attr("height", legend_svg_symsize) + .attr("stroke", "none") + .attr("fill", function (d, j){ + if (p_value_legend_data[i][0] == "NA") { + return "lightgrey"; + }; + return colors(d); + }) + .attr("fill-opacity", function (d, j){ + if (p_value_legend_data[i][0] == "NA") { + return 1; + }; + return opacity(p_value_legend_data[i][0]); + }) + + }; + + + colorLegendSpan.append("span") + .html(function(d) { return d;}) + }; + + //////////////// NODES SHAPE AND SIZE //////////////// + var setSymbolSize = function() { + var selected_size_factor = $("#sizeSelect").val(), + symbols_domain = json.nodes.map(function (d){return d['data-type'];}).filter(unique), + size = {}; + symbols.domain(symbols_domain); + if (selected_size_factor != '') { + + for (var i = 0; i < symbols_domain.length; i++) { + size[symbols_domain[i]] = {value:json.nodes.filter(function (d){return d['data-type'] == symbols_domain[i];}).map(function(d){return Number(d[selected_size_factor]);})}; + size[symbols_domain[i]]['min'] = Math.min.apply(null, size[symbols_domain[i]]['value']); + size[symbols_domain[i]]['max'] = Math.max.apply(null, size[symbols_domain[i]]['value']); + } + } + + // Set symbols size + d3.selectAll(".symbol") + .transition().duration(400) + .attr("d", d3.symbol() + .type(function (d){ return symbols(d['data-type']);}) + .size(function (d){ + if (selected_size_factor != '') { + sizeScale.domain([size[d['data-type']]['min'], size[d['data-type']]['max']]); + d.r = sizeScale(d[selected_size_factor]); + } else { + d.r = 20; + } + return d.r*d.r; + })) + .attr('radius', function(d){return d.r;}); + + // Set size-dependent simulation variables + var symbol_data = []; + d3.selectAll(".symbol").each(function(){return symbol_data.push(d3.select(this).attr("radius"));}); + var force_size_factor = (Math.log2(symbol_data.length+1)+1)/2; + chargeScale.domain([Math.min.apply(null, symbol_data), Math.max.apply(null, symbol_data)]); + + if (force_size_factor == 0) { + force_size_factor = 1; + }; + + simulation + .force("collide", d3.forceCollide() + .radius(function (d){ + return d.r; + }) + .iterations(2) + .strength(1/force_size_factor)) + .force("charge", d3.forceManyBody() + .strength(function (d){ + return chargeScale(d.r)/force_size_factor; + })); + + // Update symbol legend + symbolLegend = symbolLegend.data([]); + symbolLegend.exit().remove(); + + symbolLegend = symbolLegend + .data(symbols_domain) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + symbolLegendSpan = symbolLegend.append("span") + + var legend_svg_width = legend_svg_symsize*size_legend_data.length; + + symbolLegendSpanSvg = symbolLegendSpan.append("svg") + .attr("width", legend_svg_width+"px") + .attr("height", legend_svg_symsize+"px") + .style("margin-right", "5px") + .style("overflow", "visible") + + for (var i = 0; i < size_legend_data.length; i++) { + // (V)(°,,,°)(V) + + symbolLegendSpanSvg.append("path") + .attr("transform", "translate("+(i*legend_svg_width/size_legend_data.length)+", "+legend_svg_symsize/2+")") + .attr("d", d3.symbol() + .type(function (d){ return symbols(d);}) + .size(size_legend_data[i]*size_legend_data[i])) + .attr("stroke", "#333") + .attr("fill-opacity", 0) + + }; + + + symbolLegendSpan.append("span") + .html(function(d) { return d;}) + + } + + //////////////// RESTART //////////////// + var restart = function() { + + var weight_thres = [$("#pThresRange").val(), $("#nThresRange").val()], + weight_p_value_thres = $("#weightPvalThres").val(), + link_data = JSON.parse(JSON.stringify(json.links.filter(function(d){ + if (d.weight >= 0) { + return (d.weight >= weight_thres[0] && d['p-value'] <= weight_p_value_thres); + } + return (d.weight < -weight_thres[1] && d['p-value'] <= weight_p_value_thres); + }))); + getNodeDegree(link_data, json.nodes); + + var kept_node = link_data.map(function(d){return d['source'];}).concat(link_data.map(function(d){return d['target'];})).filter(unique), + node_data = JSON.parse(JSON.stringify(json.nodes.filter(function(d){return kept_node.indexOf(d.id) != -1;}))), + link_width = {weight:link_data.map(function(d){return d.weight;})}; + link_width['min'] = Math.min.apply(null, link_width.weight.map(Math.abs)); + link_width['max'] = Math.max.apply(null, link_width.weight.map(Math.abs)); + widthScale.domain([link_width['min'], link_width['max']]); + attractionScale.domain([link_width['min'], link_width['max']]); + + // Update links + link = link.data([]); + link.exit().remove(); + + link = link + .data(link_data); + + link = link.enter() + .append("line") + .attr("class", "link-line") + .attr("stroke-linecap", "round") + .attr("stroke", function (d) { + if (Number(d.weight) >= 0) { + return link_color[0][0]; + } else { + return link_color[1][0]; + }; + }) + .attr("stroke-width", function (d) { + return widthScale(Math.abs(d.weight)); + }); + + // Update nodes + node = node.data([]); + node.exit().remove(); + + node = node + .data(node_data); + + node = node.enter() + .append("g") + .append("path") + .attr("class", "coloured-symbol symbol") + .attr("stroke", "white"); + + // Update nodes labels + nodeLabel = nodeLabel.data([]); + nodeLabel.exit().remove(); + + nodeLabel = nodeLabel + .data(node_data); + + nodeLabel = nodeLabel.enter() + .append("g") + .attr('class', 'node-label'); + + nodeLabel.append("path") + .attr("class", "symbol") + .attr("stroke", "#333333") + .attr("fill-opacity", 0) + .attr("stroke-opacity", 0); + + nodeLabel.append("text") + .text(function (d) { + var label = d.name.split(";"); + if (label.length > 1) + return label[label.length-2] +";"+ label[label.length-1]; + return label[0]; + }) + .attr("text-anchor", "start") + .attr("font-family", font_family) + .attr("display", "none") + .attr("selected", false); + + // Add interactive option if not too many nodes + //if (node_data.length < drag_size_limit) { + node.call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + + nodeLabel.call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)); + //}; + + setSymbolSize(); + setSymbolColor(); + displayLabels(); + showLabels(); + + // Restart simulation + simulation = simulation + .nodes(node_data) + .on("tick", ticked); + + var data_size_factor = 1+node_data.length*node_data.length; + + simulation + .force("link", d3.forceLink() + .id(function(d) { return d.id;}) + .distance(function(d) { + if (d.weight >= 0) { + return attractionScale(Math.abs(d.weight))/data_size_factor; + } else { + return repulsionScale(Math.abs(d.weight))/data_size_factor; + }; + }) + .strength(function(d) { + if (d.weight >= 0) { + return repulsionScale(Math.abs(d.weight))/100; + } else { + return attractionScale(Math.abs(d.weight))/100; + }; + })); + + simulation.force("link") + .links(link_data); + + simulation.alpha(0.5).restart(); + + // Update links legend + linkLegend = linkLegend.data([]); + linkLegend.exit().remove(); + + linkLegend = linkLegend + .data(link_color) + .enter().append("li") + .attr("id", function(d) { return d[1];}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d[1];}) + + linkLegendSpan = linkLegend.append("span") + + linkLegendSpanSvg = linkLegendSpan.append("svg") + .attr("width", "25px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + + linkLegendSpanSvg.append("rect") + .attr("width", 25) + .attr("height", 5) + .attr("y", 2.5) + .attr("stroke", "none") + .attr("fill", function(d){return d[0]}) + + linkLegendSpan.append("span") + .html(function(d, i) { return ['Coeff. > ', 'Coeff. < -'][i]+weight_thres[i];}) + } + + // Nodes labels functions + function displayLabels () { + var selected_label = $(".node-label"); + selected_label.on("mouseenter", function() { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[0]).attr("stroke-opacity", 1); + }); + selected_label.on("mouseleave", function() { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + }; + d3.select(this.childNodes[0]).attr("stroke-opacity", 0); + }); + selected_label.on("click", function() { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", true); + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", false); + } + }); + }; + + // Model update function + var updateEffect = function() { + var keys = Object.keys(json.legend[$('#modelSelect').val()]) + new_data = [...Array(keys.length)]; + for (var i = 0; i < keys.length; i++) { + new_data[i] = {label:keys[i], value:keys[i]} + } + $('#effectSelect').multiselect('dataprovider', new_data) + .multiselect("refresh"); + + setSymbolColor(); + } + + // Display labels button + var showLabels = function() { + var label_text = d3.selectAll(".node-label").select(function(){ return this.childNodes[1];}); + if ($("#labelButton").is(':checked')) { + label_text.attr("display", "inline"); + label_text.attr("selected", true); + } else { + label_text.attr("display", "none"); + label_text.attr("selected", false); + }; + }; + + // Search in labels + var searchLabels = function() { + $("#labelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".node-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) != -1 }); + non_selected = d3.selectAll(".node-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) == -1 }); + selected.select(function(){ return this.childNodes[1];}).attr("display", "inline"); + selected.select(function(){ return this.childNodes[1];}).attr("selected", true); + non_selected.select(function(){ return this.childNodes[1];}).attr("display", "none"); + non_selected.select(function(){ return this.childNodes[1];}).attr("selected", false); + } else { + to_free = d3.selectAll(".node-label"); + to_free.select(function(){return this.childNodes[1];}).attr("display", "none"); + to_free.select(function(){return this.childNodes[1];}).attr("selected", false); + }; + }; + + + // Label button and search + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + + // Select model + var modelSelect = buttons.append("div") + .attr("title", "Chose model") + .attr("class", "form-group") + + modelSelect.append("label") + .html("Color (model)") + + modelSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "modelSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.legend)) + .enter().append("option") + .text(function (d){ return d;}); + + $('#modelSelect').on('change', updateEffect); + + // Select effect + var effectSelect = buttons.append("div") + .attr("title", "Chose effect") + .attr("class", "form-group") + + effectSelect.append("label") + .html("Color (effect)") + + effectSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "effectSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.legend[$('#modelSelect').val()])) + .enter().append("option") + .text(function (d){ return d;}); + + $('#effectSelect').on('change', setSymbolColor); + + // Select size + var sizeSelect = buttons.append("div") + .attr("title", "Chose size variable") + .attr("class", "form-group") + + sizeSelect.append("label") + .html("Radius") + + sizeSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "sizeSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + + $('#sizeSelect').on('change', setSymbolSize); + + // Button for link weight threshold + var weightPvalThres = buttons.append("div") + .attr("title", "Set a p-value cutoff for links.") + .attr("class", "form-group") + + weightPvalThres.append("label") + .html("Links p-value cutoff") + + weightPvalThres.append("input") + .attr("id", "weightPvalThres") + .attr("type", "number") + .attr("class", "form-control form-number-field") + .attr("min", 0) + .attr("max", 1) + .attr("step", 0.001) + .attr("value", 1) + .on("change", restart); + + var pThresRange = buttons.append("span") + .attr("title", "Cut-off for positive correlations.") + + pThresRange.append("label") + .append("p") + .html("Pos. corr. cut-off ().") + + pThresRange.append("input") + .attr("id", "pThresRange") + .attr("type", "range") + .attr("class", "full-width") + .attr("min", 0) + .attr("max", 1) + .attr("step", 0.05) + .on("change", restart); + $("#pThresRange").val('0.75') + + var nThresRange = buttons.append("span") + .attr("title", "Cut-off for negative correlations.") + + nThresRange.append("label") + .append("p") + .html("Neg. corr. cut-off ().") + + nThresRange.append("input") + .attr("id", "nThresRange") + .attr("type", "range") + .attr("class", "full-width") + .attr("min", 0) + .attr("max", 1) + .attr("step", 0.05) + .on("change", restart); + $("#nThresRange").val('0.75') + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + + $('#sizeSelect').multiselect('dataprovider', sizeOptions) + .multiselect("refresh"); + + restart(); + + //}); + +}; diff --git a/app/assets/javascripts/diversity.js b/app/assets/javascripts/diversity.js new file mode 100644 index 0000000..c6366f2 --- /dev/null +++ b/app/assets/javascripts/diversity.js @@ -0,0 +1,521 @@ +function diversity(id, legend_id, json, RfunctionsDiversity, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10) { + + + + // Size + var margin = {top: 20, right: 75, bottom: 65, left: 65}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + left_label_space = 40, + bottom_label_space = 40, + stat_font_size = 10; + + // Colors and line width + var colors = d3.scaleOrdinal(color_palette), + stroke_width_thick = "5px", + stroke_width_thin = "3px"; + + // Buttons + var buttons = d3.select("#d3-buttons"); + buttons.html(""); + + // General functions + function json2coord(json) { + var arr = [], + keys = Object.keys(json); + for (var i = 0; i < keys.length; i++) { + arr.push({x:Number(keys[i]), y:Number(json[keys[i]])}); + }; + return arr; + }; + + function json2jsonMeans(json, factor) { + + var json_means = [], + unique_factors = []; + json.forEach(function (d){ + if (unique_factors.indexOf(d[factor]) == -1) { + unique_factors.push(d[factor]); + }; + }); + for (var i = 0; i < unique_factors.length; i++) { + var arr = [], + data = []; + for (var j = 0; j < json.length; j++) { + if (json[j][factor] == unique_factors[i]) { + arr.push(json2coord(json[j].data)); + }; + }; + for (var j = 0; j < arr[0].length; j++) { + data.push({x:arr[0][j].x, + y:d3.mean(arr, function(d){ return d[j].y;}), + stdev:d3.deviation(arr, function(d){ return d[j].y;})}); + }; + json_means.push({name:unique_factors[i], data:data}); + }; + + return json_means; + }; + + function getPvalFromStat(stat, n) { + var sign_table = [[0.05, "*"], + [0.01, "**"], + [0.001, "***"]]; + + for (var i = 0; i < stat.length; i++) { + if (stat[i].name == n) { + var p = Number(stat[i]["p-value"]).toFixed(3), + sign = "ns"; + + for (var j = 0; j < sign_table.length; j++) { + if (p <= sign_table[j][0]) { + sign = sign_table[j][1]; + }; + }; + + return "p="+ p +" ("+ sign +")"; + }; + }; + return "na"; + }; + + //$.getJSON(data, function(json) { + + // Set variables depending only on the primary data + var color_select_options = [], + metric_select_options = {}, + metrics = Object.keys(json), + json_keys = Object.keys(json[metrics[0]].data[0]); + + for ( var i = 0; i < json_keys.length; i++) { + if (["name", "data", "id"].indexOf(json_keys[i]) == -1) { + color_select_options.push(json_keys[i]); + }; + }; + + RfunctionsDiversity.forEach(function (d) { + if (metrics.indexOf(d["value"]) != -1) + metric_select_options[d["value"]] = d["label"]; + }); + + //////////////// Draw graph //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Draw lines + var graph = svg.selectAll(); + + // Put stats + var pValue = svg.append("g"); + + // Draw axis + xAxis = svg.append("g") + .attr("class", "axis") + .attr("transform", "translate(0," + height + ")"); + + xAxisLegend = svg.append("text") + .attr("class", "axis-label") + .attr("x", width/2) + .attr("y", height+bottom_label_space) + .style("text-anchor", "middle") + .attr("font-family", font_family) + .text("Rarefaction depth"); + + yAxis = svg.append("g") + .attr("class", "axis"); + + yAxisLegend = svg.append("g") + .attr("transform", "translate("+ -left_label_space +", "+ height/2 +")") + .attr("class", "axis-label"); + + // Add legend + var legend = legendContainer.append("ul") + .attr("id", "svg-legend") + .style("font-family", font_family) + .style("list-style-type", "none") + .selectAll("ul"); + + //////////////// Restart function //////////////// + var restart = function() { + + // Set coordinates settings + var selected_metric = d3.select("#metricSelect").property("value"), + xMins = [], + yMins = [], + xMaxs = [], + yMaxs = []; + + json[selected_metric].data.forEach(function (d) { + var x = Object.keys(d.data), + y = Object.keys(d.data).map(function (n) { return d.data[n];}); + xMins.push(Math.min.apply(null, x)); + yMins.push(Math.min.apply(null, y)); + xMaxs.push(Math.max.apply(null, x)); + yMaxs.push(Math.max.apply(null, y)); + }); + + var xMin = Math.min.apply(null, xMins), + xMax = Math.max.apply(null, xMaxs), + xScale = d3.scaleLinear() + .range([0, width]) + .domain([xMin, xMax]).nice(), + yMin = Math.min.apply(null, yMins), + yMax = Math.max.apply(null, yMaxs), + yScale = d3.scaleLinear() + .range([height, 0]) + .domain([yMin, yMax]).nice(); + + var line = d3.line() + .x(function(d) { return xScale(d.x); }) + .y(function(d) { return yScale(d.y); }); + + // Set color settings + var selected_color_factor = d3.select("#colorSelect").property("value"), + color_factors = []; + + json[selected_metric].data.forEach(function (d) { + if (color_factors.indexOf(d[selected_color_factor]) == -1) { + color_factors.push(d[selected_color_factor]); + }; + }); + + colors.domain(color_factors); + + // Draw graph + graph = graph.data([]); + graph.exit().remove(); + + var draw_means = $("#meansButton").is(':checked'); + if (draw_means) { + var data = json2jsonMeans(json[selected_metric].data, selected_color_factor), + yMeans = []; + data.forEach(function (d) { + yMeans.push(d.data[d.data.length-1].y); + }); + var yMeansMin = Math.min.apply(null, yMeans), + yMeansMax = Math.max.apply(null, yMeans); + } else { + var data = json[selected_metric].data; + } + + graph = graph + .data(data, function (d) { return d;}); + + graph = graph.enter() + .append("g") + .attr("class", "graph") + .attr("display", "inline"); + + graph.append("path") + .attr("d", function(d) { + if (draw_means) { + return line(d.data); + } else { + return line(json2coord(d.data)); + }; + }) + .attr("fill", "none") + .attr("stroke-width", stroke_width_thin) + .attr("stroke", function (d){ + if (draw_means) { + return colors(d.name); + } else { + return colors(d[selected_color_factor]); + }; + }); + + graph.append("text") + .attr("x", function () { + if (draw_means) { + return xScale(xMax)+5+stat_font_size; + } else { + return xScale(xMax)+2; + }; + }) + .attr("y", function (d) { + if (draw_means) { + return yScale(d.data[d.data.length-1].y); + } else { + return yScale(d.data[xMax]); + }; + }) + .text(function (d) { return d.name;}) + .attr("class", "plot-label") + .attr("font-family", font_family) + .attr("selected", false) + .attr("mass-selected", false) + .attr("display", "none"); + + // Add stats + pValue.selectAll("*").remove(); + if (draw_means) { + + if (Object.keys(json[selected_metric]).indexOf('stat') != -1) { + + pValue + .attr("transform", "translate("+xScale(xMax) +", 0)") + + pValue.append("line") + .attr("x1", 2) + .attr("y1", yScale(yMeansMin)) + .attr("x2", 2) + .attr("y2", yScale(yMeansMax)) + .attr("stroke", "black") + + pValue.append("text") + .attr("transform", "rotate(-90)") + .attr("text-anchor", "middle") + .attr("font-family", font_family) + .attr("font-size", stat_font_size+2) + .attr("x", -yScale((yMeansMin+yMeansMax)/2)) + .attr("y", stat_font_size+2) + .attr("font-size", stat_font_size) + .text(getPvalFromStat(json[selected_metric].stat, selected_color_factor)); + } + + graph.append("g") + .selectAll("line") + .data(function(d) {return d.data;}) + .enter().append("line") + .attr("class", "error-bars") + .attr("display", "none") + .attr("x1", function(d) { return xScale(d.x);}) + .attr("x2", function(d) { return xScale(d.x);}) + .attr("y1", function(d) { return yScale(d.y+d.stdev);}) + .attr("y2", function(d) { return yScale(d.y-d.stdev);}) + .attr("stroke-width", "1px") + .attr("stroke", function(d) { return colors(this.parentNode.__data__.name);}); + + }; + + showErrorBars(); + displayLabels("graph"); + showLabels(); + + xAxis.call(d3.axisBottom(xScale).ticks(5)) + .selectAll("text") + .attr("class", "axis-label") + .style("text-anchor", "end") + .attr("font-family", font_family) + .attr("dx", "-.8em") + .attr("dy", ".15em") + .attr("transform", "rotate(-25)"); + + yAxis.call(d3.axisLeft(yScale).ticks(10)) + + yAxisLegend.selectAll("*").remove(); + yAxisLegend.append("text") + .text(function () { + return metric_select_options[selected_metric]; + }) + .style("text-anchor", "middle") + .attr("font-family", font_family) + .attr("transform", "rotate(-90)"); + + // Update legend + + legend = legend.data([]); + legend.exit().remove(); + + legend = legend + .data(color_factors) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + legendSpan = legend.append("span") + + legendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .append("rect") + .attr("width", "10px") + .attr("height", "10px") + .attr("fill", function(d) { return colors(d); }) + + legendSpan.append("span") + .html(function(d) { return d;}) + + }; + + + //////////////// Control buttons //////////////// + + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + + // Search in labels + var searchLabels = function() { + $("#labelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".graph").filter(function(){return this.childNodes[0].__data__.name.toUpperCase().indexOf(key.toUpperCase()) != -1 }), + non_selected = d3.selectAll(".graph").filter(function(){return this.childNodes[0].__data__.name.toUpperCase().indexOf(key.toUpperCase()) == -1 }), + selected_line = selected.select(function(){return this.childNodes[0];}), + selected_text = selected.select(function(){return this.childNodes[1];}), + non_selected_line = non_selected.select(function(){return this.childNodes[0];}), + non_selected_text = non_selected.select(function(){return this.childNodes[1];}); + selected_line.attr("stroke-width", stroke_width_thick); + selected_text.attr("display", "inline"); + selected_text.attr("selected", true); + non_selected_line.attr("stroke-width", stroke_width_thin); + non_selected_text.attr("display", "none"); + non_selected_text.attr("selected", false); + } else { + to_free = d3.selectAll(".graph"), + to_free_line = to_free.select(function(){return this.childNodes[0];}), + to_free_text = to_free.select(function(){return this.childNodes[1];}); + to_free_line.attr("stroke-width", stroke_width_thin); + to_free_text.attr("display", "none"); + to_free_text.attr("selected", false); + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + // Select metric + var metricSelect = buttons.append("div") + .attr("title", "Diversity metric.") + .attr("class", "form-group") + + metricSelect.append("label") + .html("Metric") + + metricSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "metricSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(metrics) + .enter().append("option") + .attr("value", function (d) { return d;}) + .text(function (d) { + return metric_select_options[d]; + }); + + + // Select color variables + var colorSelect = buttons.append("div") + .attr("title", "Chose variable to use for colors.") + .attr("class", "form-group") + + colorSelect.append("label") + .html("Color") + + colorSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "colorSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(color_select_options) + .enter().append("option") + .text(function (d){ return d;}); + + // Display labels button + var showLabels = function() { + var label = d3.selectAll(".plot-label"); + if ($("#labelButton").is(':checked')) { + label.attr("display", "inline") + label.attr("mass-selected", "true") + } else { + label.attr("display", "none") + label.attr("mass-selected", "false") + }; + }; + + // Draw means button + var meansButton = buttons.append("div") + .attr("title", "Draw mean curves.") + .attr("class", "form-bool") + .append("label") + + meansButton.append("input") + .attr("id", "meansButton") + .attr('class', 'form-check-input') + .attr("type", "checkbox") + .on("click", restart); + + meansButton.append("p") + .html("Means curves"); + + + // Draw error bars button + var showErrorBars = function() { + + if ($("#errorBarsButton").is(':checked')) { + d3.selectAll(".error-bars").attr("display", "inline") + } else { + d3.selectAll(".error-bars").attr("display", "none") + }; + }; + + var errorBarsButton = buttons.append("div") + .attr("title", "Show error bars (standard deviation).") + .attr("class", "form-bool") + .append("label") + + errorBarsButton.append("input") + .attr("id", "errorBarsButton") + .attr('class', 'form-check-input') + .attr("type", "checkbox") + .on("click", showErrorBars); + + errorBarsButton.append("p") + .html("Error bars"); + + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('.col-md-12', 1, '#d3-buttons', false); + $("#metricSelect").on("change", restart) + $("#colorSelect").on("change", restart) + + restart(); + + // Labels functions + function displayLabels (id) { + $("."+id).on("mouseenter", function(d) { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[0]).attr("stroke-width", stroke_width_thick); + }); + $("."+id).on("mouseleave", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + if (this.childNodes[1].getAttribute("mass-selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + }; + d3.select(this.childNodes[0]).attr("stroke-width", stroke_width_thin); + }; + }); + $("."+id).on("click", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[0]).attr("stroke-width", stroke_width_thick); + d3.select(this.childNodes[1]).attr("selected", true); + } else { + if (this.childNodes[1].getAttribute("mass-selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + }; + d3.select(this.childNodes[0]).attr("stroke-width", stroke_width_thin); + d3.select(this.childNodes[1]).attr("selected", false); + } + }); + }; + +//}); + +}; diff --git a/app/assets/javascripts/examples.coffee b/app/assets/javascripts/examples.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/examples.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/figures.js b/app/assets/javascripts/figures.js new file mode 100644 index 0000000..fcfd209 --- /dev/null +++ b/app/assets/javascripts/figures.js @@ -0,0 +1,141 @@ +function exportFigure(id, format, filename) { + + var svgData = d3.select(id).node().outerHTML, + type = 'txt'; + if (format == "svg") { + type = 'image/svg+xml'; + } else if (format == "html") { + type = 'html'; + }; + + var svgBlob= new Blob([svgData], {type: type}), + link = document.createElement('a'), + event = new MouseEvent('click'); + + link.href = window.URL.createObjectURL(svgBlob); + link.download = filename+'.'+format; + link.dataset.downloadurl = [type, link.download, link.href].join(':'); + link.dispatchEvent(event); + +}; + +function displayFigure(divid, type, id, figures, url, Rfunctions, demo) { + + document.getElementById(divid).innerHTML=''; + var index = 0; + if (document.getElementById("select_level") != null) { + index = levels.indexOf($("#select_level").val()); + }; + + if (demo) { + fig = "/demos/"+id+"/serve?file="+figures[indexes[index]]['url']+"&type=pdf", + fig_data = "/demos/"+id+"/serve?file="+data[indexes[index]]['url']+"&type=file"; + } else { + //fig = "/jobs/"+id+"/serve?file="+figures[indexes[index]]['url']+"&type=pdf", + fig_data = "/jobs/"+id+"/serve?file="+data[indexes[index]]['url']+"&type=file"; + }; + + //document.getElementById('pdf-link').setAttribute('href', fig); + document.getElementById('data-link').setAttribute('href', fig_data); + + var W = 600, + H = 600, + font_family = "verdana, arial, helvetica, sans-serif"; + + if (type == 'pdf') { + document.getElementById(divid).setAttribute('data', fig); + } else if (type == 'clustering') { + overview(divid, fig_data); + } else if (type == 'abundance') { + barchart(divid, fig_data, W, H, font_family, d3.schemeCategory20c); + } else if (type == 'diversity') { + diversity(divid, fig_data, Rfunctions.diversity, W, H, font_family, d3.schemeCategory10); + } else if (type == 'adonis') { + pieChart(divid, fig_data, W, H, font_family, d3.schemeCategory20c); + } else if (type == 'pca') { + pca(divid, fig_data, W, H, font_family, d3.schemeCategory10); + } else if (type == 'pcoa') { + pcoa(divid, fig_data, W, H, font_family, d3.schemeCategory10); + } else if (type == 'change') { + foldChange(divid, fig_data, W, H, font_family); + } else if (type == 'heatmap') { + heatMap(divid, fig_data, 750, 750, font_family); + } else if (type == 'correlationNetwork') { + correlationNetwork(divid, fig_data, W, H, font_family, d3.schemeCategory10); + } else if (type == 'similarityNetwork') { + similarityNetwork(divid, fig_data, W, H, font_family, d3.schemeCategory10); + }; +}; + +// FIGURE DESCRIPTION +function showDescription() { + document.getElementById("sidebar").style.cssText="width:40vw;overflow:auto;padding-left:10px;padding-right:10px;"; + document.getElementById("description").style.cssText="visibility:visible;opacity:1;"; + $("#sidebar")[0].style["height"]=$("#figure-container").height()+'px'; + document.getElementById("sidebar-icon").style.cssText="transform:rotate(180deg)"; +}; + +function hideDescription() { + document.getElementById("sidebar").style.cssText="width:20px;overflow:hidden;padding-left:2px;padding-right:2px;"; + document.getElementById("description").style.cssText="opacity:0;visibility:hidden;"; + $("#sidebar")[0].style["height"]=$("#figure-container").height()+'px'; + document.getElementById("sidebar-icon").style.cssText="transform:rotate(0deg)"; +}; + +// FIGURE BUTTONS/CONTROLS +function appendRange(appendTo, title, label, id, min, max, value, onchange) { + + var range = appendTo.append("div") + .attr("title", title) + .attr("class", "form-group") + + range.append("label") + .html(label) + + range.append("input") + .attr('class', 'full-width') + .attr("id", id) + .attr("type", "range") + .attr("min", min) + .attr("max", max) + .attr("value", value) + .on("change", onchange); +} + +function appendLabelCheckBox(appendTo, title, label, id, onclick) { + + var labelCheckBox = appendTo.append("div") + .attr("title", title) + .attr("class", "form-bool") + .append("label") + + labelCheckBox.append("input") + .attr("id", id) + .attr('class', 'form-check-input') + .attr("type", "checkbox") + .on("click", onclick); + + labelCheckBox.append("p") + .html(label); +} + +function appendSearchInput(appendTo, title, id, onclick) { + + var searchInput = appendTo.append("div") + .attr("title", title) + .attr("class", "form-group has-feedback") + + searchInput.append("label") + .attr("class", "control-label sr-only") + + searchInput.append("input") + .attr("id", id) + .attr("type", "text") + .attr("class", "form-control") + .attr("placeholder", "Search") + .on("keyup", onclick); + + //searchInput.append("i") + // .attr("class", "form-control-feedback fa fa-search") +} + diff --git a/app/assets/javascripts/form.js b/app/assets/javascripts/form.js new file mode 100644 index 0000000..da533e2 --- /dev/null +++ b/app/assets/javascripts/form.js @@ -0,0 +1,164 @@ +function update_bin_levels(col_name, val, url){ + val = (val) ? val : [1] +// alert("toto: " + $("#p_primary_dataset").val() + "url:" + url); + var ori_filename = $("#p2_primary_dataset").val(); + getColumn("bin_levels", $("#p_primary_dataset")[0].files[0], col_name, val, url); +} + +function populate_multiselect(id, new_data, value, url){ + // alert("set " + id + " : " + value + " all : " + new_data.length) + $('#p_'+id).multiselect('enable') + .multiselect('dataprovider', new_data) + .multiselect('select', value) + .multiselect("refresh"); + + $('#field-'+id).removeClass("hidden"); + + if (id == 'category_column'){ + var val = $("#default_bin_levels").val() + +// alert("value url: " + url); + var new_url = (url) ? ($("#url_read_file_column").val() + "?file_key=primary_dataset") : null; + update_bin_levels($("#p_category_column").val(), JSON.parse(val), new_url); + //"http://genocrunch.epfl.ch/jobs/read_file_column") + } + +} + +function getColumn(id, file, col_name, value, url) { + + var new_data = []; + + if (!url){ + + var reader = new FileReader; + reader.onload = function (event) { + var lines = event.target.result.split(/\r\n|\r|\n/g); + + var i=0; + for (var i = 0; i < (lines.length-1); i++) { + if (lines[i].charAt(0) != '#') + break; + } + i = (i > 1) ? i-1 : 0; + + var header_els = lines[i].trim().split("\t") + + var pos_col = 0 + for (var k = 0; k max_val){ + max_val = n + } + } + + if (max_val > 0){ + + var new_data=[]; + for (var j=1 ; j < max_val+1; j++) { + new_data.push({label: j, value: j}); + } + + populate_multiselect(id, new_data, value, url) + + } + }; + // console.log(file); + reader.readAsText(file); + + }else{ + $.ajax({ + url: url + "&col_name=" + col_name, + type: "get", + dataType: "html", + beforeSend: function(){ + }, + success: function(new_data){ + populate_multiselect(id, JSON.parse(new_data), value, url) + }, + error: function(e){ + } + }); + } +}; + +function setSelectFromFileRow(id, file, value = 0, add_blank = null, url = null) { + + var new_data = []; + + if (!url){ + + var reader = new FileReader; + reader.onload = function (event) { + var lines = event.target.result.split(/\r\n|\r|\n/g); + + var i=0 + for (i = 0; i < (lines.length-1); i++) { + if (lines[i].charAt(0) != '#') + break; + } + i = (i > 1) ? i-1 : 0; + + var content = lines[i].split("\t") + + if (add_blank) + new_data.push({label:"", value:""}); + + if (content.length > 0){ + for (var i in content) { + new_data.push({label:content[i], value:content[i]}); + }; + } + populate_multiselect(id, new_data, value, url) + }; + reader.readAsText(file); + }else{ + $.ajax({ + url: url + "&add_blank=" + ((add_blank == null) ? '' : '1'), + type: "get", + dataType: "html", + beforeSend: function(){ + }, + success: function(new_data){ + populate_multiselect(id, JSON.parse(new_data), value, url) + }, + error: function(e){ + } + }); + + } +}; + + +// Transform a select input into a nice multiselect field +function setMultiselect(id, nonSelectedText = 'Select', where){ + $(id).multiselect({ + includeSelectAllOption: false, // Would append a value currently not supported by genocrunch_console + nonSelectedText: nonSelectedText, + numberDisplayed: 1, + maxHeight: 150, + buttonClass: 'btn btn-secondary', + templates: { + li: '
  • ', + } + }); + $(id + ".form-control").addClass('hidden'); +} + + +function fold_section(fold_bar_class) { + $(fold_bar_class).click(function(){ + var e = $(this).parent().parent().parent().parent().parent().children().filter(".card-block").first(); //(".field_group_content").first(); + var angle = (e.hasClass('hidden')) ? 180 : 0; + $(this).filter("i.fa-chevron-down").css({transform:'rotate(' + angle + 'deg)'}); + e.toggleClass('hidden'); + }); +}; diff --git a/app/assets/javascripts/general.js b/app/assets/javascripts/general.js new file mode 100644 index 0000000..97391fb --- /dev/null +++ b/app/assets/javascripts/general.js @@ -0,0 +1,21 @@ +function backToTop() { + document.body.scrollIntoView({ behavior: 'smooth' }); + document.documentElement.scrollIntoView({ behavior: 'smooth' }); +} + +function randomKey(n) { + var choice = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', + k = [...Array(n)]; + for (var i = 0; i < n; i++) { + k[i] = choice.charAt(Math.floor(Math.random() * choice.length)); + } + return k; +} + +function scrollTo(source, target) { + $('#'+source).on('click', function(){ + $('body, html').animate({scrollTop: $('#'+target).position().top}, + {duration: 400, + easing: 'swing'}); + }); +} diff --git a/app/assets/javascripts/heatmap.js b/app/assets/javascripts/heatmap.js new file mode 100644 index 0000000..16e3d51 --- /dev/null +++ b/app/assets/javascripts/heatmap.js @@ -0,0 +1,874 @@ +function heatMap(id, legend_id, json, W = 750, H = 750, font_family = "verdana, arial, helvetica, sans-serif") { + + // Size + var margin = {top: 10, right: 150, bottom: 150, left: 10}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + dendrogram_space = 30, + dendrogram_margin = 3, + color_bar_size = 12, + color_bar_margin = 3; + + // Colors and symbols + var heatmapColorRange = ["#3366ff", "#f2ffe6", "#ff0066"], + topbarColors = d3.scaleOrdinal(d3.schemeCategory20), + sidebarColors = {"p-values":d3.scaleLinear() + .range(["#e6ffff", "#ffe6e6", "#ff8080", "#ff0000"]) + .domain([1, 0.05, 0.01, 0.001]), + "correlations":d3.scaleLinear() + .range(["#990000", "#ffffe6", "#009900"]) + .domain([-1, 0, 1]), + "levels":d3.scaleOrdinal(d3.schemeCategory20) + }, + selector_opacity = 0.5; + + // Legend + var color_scale_labels = ["min", "mean", "max"], + color_scale_width = 100, + color_scale_font_size = 11; + + // General functions + function getHeatmapRowsExtrema(heatmap, which) { + var nrow = heatmap[0].value.length, + extrema = [...Array(nrow)]; + for (var i = 0; i < nrow; i++) { + var ncol = heatmap.length, + row = [...Array(ncol)]; + for (var j = 0; j < ncol; j++) { + row[j] = heatmap[j].value[i]; + }; + row = row.filter(function(d){return !isNaN(d)}) + + if (which == "min") { + extrema[i] = Math.min.apply(null, row); + } else if (which == "max") { + extrema[i] = Math.max.apply(null, row); + } else if (which == "median") { + row.sort(); + if (row.length % 2 == 0) { + extrema[i] = (Number(row[row.length/2])+Number(row[1+row.length/2]))/2; + } else { + extrema[i] = row[1+(row.length-1)/2]; + } + } else if (which == "mean") { + var sum = 0; + row.forEach(function(d){sum = sum+Number(d);}); + extrema[i] = Number(sum)/row.length; + } + }; + return extrema; + }; + + function normalizeHeatmapColors(colorRange, Min, Max, Middle) { + var nrow = Min.length, + normalized_color = [...Array(nrow)]; + for (var i = 0; i < nrow; i++) { + var color = d3.scaleLinear() + .range(colorRange) + .domain([Min[i], Middle[i], Max[i]]); + normalized_color[i] = color; + }; + return normalized_color; + }; + + function generatePvalFilter(data, thres) { + + var bool = [...Array(data[0]['value'].length)].fill(0), + index = [...Array(data[0]['value'].length)].fill(0), + count = 0; + data.forEach(function(d){ + d.value.forEach(function(d, i) { + if (d <= thres) { + bool[i] = 1; + } + }) + }) + bool.forEach(function(d, i){ + if (d == 1) { + index[i] = count; + count += 1; + } + }) + + return {bool:bool, index:index}; + } + + function countReplicates(arr, x) { + var count = 0; + arr.forEach(function (d){ + if (d == x) { + count += 1; + }; + }); + return count; + }; + + function unique(arr) { + var u = []; + arr.forEach(function(d){ + if (u.indexOf(d) == -1) { + u.push(d); + } + }) + return u; + } + + function getLevels(names) { + var arr = []; + arr.push({"name":"level1", "value":[]}); + for (var i = 0; i < names.length; i++) { + var nlevels = names[i].split(";").length; + for (var j = 0; j < nlevels; j++) { + if (j > arr.length) { + arr.push({"name":"level"+j, "value":[]}); + }; + }; + }; + for (var i = 0; i < names.length; i++) { + var names_a = names[i].split(";"); + for (var j = 0; j < arr.length; j++) { + if (names_a.length > j) { + arr[j].value.push(names_a[j]); + } else { + arr[j].value.push("Unknown"); + }; + }; + }; + return arr; + }; + + // Buttons + var buttons = d3.select("#d3-buttons"); + buttons.html(""); + + //$.getJSON(data, function(json) { + + + // Set variables depending only on the json + var rowMin = getHeatmapRowsExtrema(json.heatmap, "min"), + rowMax = getHeatmapRowsExtrema(json.heatmap, "max"), + rowMean = getHeatmapRowsExtrema(json.heatmap, "mean"), + heatmapColors = normalizeHeatmapColors(heatmapColorRange, rowMin, rowMax, rowMean), + nrow = json.heatmap[0].value.length, + topbar_data = json.topbar.category, + ntopbar = topbar_data.length, + top_space = dendrogram_space+dendrogram_margin+color_bar_size*ntopbar+color_bar_margin; + // Add levels as an optional sidebar + json.sidebar["levels"] = getLevels(json.rownames); + + + //////////////// Draw the figure //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-1') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Draw heatmap + var heatmap = svg.selectAll() + .data(json.heatmap) + .enter().append("g") + + heatmap.selectAll("rect") + .data(function(d, i) { + d.index = i; + return d.value; + }).enter() + .append("rect") + .attr("x", width/2) + .attr("y", height/2) + .style("fill", function(d, i) { return heatmapColors[i](d);}); + + // Draw dendrograms + var colDendrogram = svg.selectAll(), + rowDendrogram = svg.selectAll(), + dendrogram = d3.cluster() + .separation(function(a, b) { + return 1; + }); + + // Draw topbar + var topbar = svg.selectAll() + .data(topbar_data) + .enter().append("g") + + var topbarSquares = topbar.selectAll("g") + .data(function(d, i) { + d.index = i; + return d.value; + }) + .enter() + .append("g") + .attr("transform", function (d, i) { return "translate(" + (width/2) + "," + (height/2) + ")";}); + + topbarSquares.append("rect") + .attr("height", color_bar_size) + .style("fill", function (d) { + if (d == "NA") { + return "lightgrey"; + }; + return topbarColors(d); + }); + + topbar.append("text") + .attr("x", width) + .attr("y", function(d, i){ + return (dendrogram_space+dendrogram_margin+color_bar_size*i+color_bar_size); + }) + .attr("font-size", color_bar_size) + .text(function(d){return d.name;}) + + // Draw sidebar + var sidebar = svg.selectAll(), + sidebarSquares = null; + + // Add heatmap labels + var colLabel = svg.selectAll() + .data(json.colnames) + .enter().append("g") + .attr("transform", "translate("+ (width/2) +", "+ height +")"); + + colLabel.append("text") + .attr("class", "col-label") + .attr("text-anchor", "end") + .attr("font-family", font_family) + .attr("transform", "rotate(-90)") + .attr("display", "none") + .text(function (d) { return d;}); + + var rowLabel = svg.selectAll() + .data(json.rownames) + .enter().append("g") + .attr("class", "rowLabel") + .attr("transform", "translate("+ width +", "+ (height/2) +")") + + var rowLabelBox = rowLabel.append("rect") + .attr("class", "row-label-box") + .attr("fill", "yellow") + .attr("fill-opacity", 0) + .attr("y", 0) + + var rowLabelText = rowLabel.append("text") + .attr("class", "row-label heatmap-label") + .attr("text-anchor", "end") + .attr("filtered", 'false') + .attr("selected", 'false') + .attr("mass-selected", 'false') + .attr("display", "none") + .attr("x", margin.right) + .attr("font-family", font_family) + .text(function (d) { + var label = d.split(';'); + if (label.length > 1) { + return label[label.length - 2]+"; "+label[label.length - 1]; + }; + return d; + }) + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + // Add color scale to legend + var colorScale = legend.append("div") + .attr("title", "Color scale") + .attr("class", "legend legend-no-interaction") + .style("margin-bottom", (color_scale_font_size+15)+"px") + + colorScale.append("p") + .html("Color scale") + + var colorScaleSvg = colorScale.append("svg") + .attr("width", color_scale_width) + .attr("height", "20px") + .style("margin-right", "15px") + .style("overflow", "visible") + .style("margin-left", "15px") + + + var colorScaleSvgDefs = colorScaleSvg.append("defs") + + var linearGradient = colorScaleSvgDefs.append("linearGradient") + .attr("id", "linear-gradient"); + + linearGradient.append("stop") + .attr("offset", "0%") + .attr("stop-color", heatmapColorRange[0]); + + linearGradient.append("stop") + .attr("offset", "50%") + .attr("stop-color", heatmapColorRange[1]); + + linearGradient.append("stop") + .attr("offset", "100%") + .attr("stop-color", heatmapColorRange[2]); + + colorScaleSvg.append("rect") + .attr("width", color_scale_width) + .attr("height", 20) + .style("fill", "url(#linear-gradient)") + .style("stroke", "#ccc") + + colorScaleSvgLabel = colorScaleSvg.append("g") + .selectAll() + .data(color_scale_labels) + .enter() + + colorScaleSvgLabel.append("rect") + .attr("y", 20) + .attr("x", function(d, i) { + if (i == 0) { + return i*color_scale_width/(color_scale_labels.length-1); + } + return i*color_scale_width/(color_scale_labels.length-1)-1; + }) + .attr("width", 1) + .attr("height", 4) + .style("fill", "#333"); + + colorScaleSvgLabel.append("text") + .attr("y", 26+color_scale_font_size) + .attr("x", function(d, i){return i*color_scale_width/(color_scale_labels.length-1);}) + .attr("font-size", color_scale_font_size) + .attr("text-anchor", "middle") + .text(function(d){return d;}) + + colorScale.append("text") + .attr("font-family", font_family) + .text("z-score"); + + legend.append("div") + .style("border-top", "solid #ccc 1px") + .style("height", "6px") + .style("border-bottom", "solid #ccc 1px") + + // Add topbar legend + var topbarLegend = legend.append("div") + .attr("title", "Topbar color key") + + topbarLegend.append("p") + .html("Topbar color key") + + for (var i = 0; i < json.topbar.category.length; i++) { + + var topbarData = unique(json.topbar.category[i].value); + + topbarLegend.append("span") + .attr("class", "sidebar-sub-title") + .html(json.topbar.category[i].name) + + var legendSpan = topbarLegend.append("ul") + .style("list-style-type", "none") + .selectAll("ul") + .data(topbarData) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("title", function(d) { return d;}) + .append("span") + + legendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .append("rect") + .attr("width", "10px") + .attr("height", "10px") + .attr("fill", function(d) { return topbarColors(d); }) + + legendSpan.append("span") + .html(function(d) { + return d; + }); + + if (i < json.topbar.category.length-1) { + topbarLegend.append("div") + .style("border-top", "solid #e6e6e6 1px") + } + } + + legend.append("div") + .style("border-top", "solid #ccc 1px") + .style("height", "6px") + .style("border-bottom", "solid #ccc 1px") + + // Add sidebar legend + var sidebarLegend = legend.append("div") + .attr("title", "Sidebar color key") + .attr("class", "legend legend-no-interaction") + .style("margin-bottom", (color_scale_font_size+5)+"px") + + + //////////////// Restart function //////////////// + var restart = function() { + + var selected_sidebar = $("#sidebarSelect").val(), + selected_model = $("#modelSelect").val(); + + // Filter data + var p_thres = 1 //$("#pThreshold").val(), + sign_filter = generatePvalFilter(json.sidebar["p-values"][selected_model], p_thres), + nsign = countReplicates(sign_filter.bool, 1); + var nsidebar = 0; + + if (selected_sidebar != '') { + if (Object.keys(json.sidebar[selected_sidebar]).indexOf(selected_model) != -1) { + nsidebar = json.sidebar[selected_sidebar][selected_model].length; + } else { + nsidebar = json.sidebar[selected_sidebar].length; + } + }; + + var left_space = dendrogram_space+dendrogram_margin+color_bar_size*nsidebar+color_bar_margin; + + // Update the heatmap + var squareWidth = (width-left_space)/json.heatmap.length, + squareHeight = 0; + if (nsign != 0) { + squareHeight = (height-top_space)/nsign; + }; + + heatmap.selectAll("rect") + .transition().duration(150) + .attr("x", function(d) { + return left_space+squareWidth*(this.parentNode.__data__.index); + }) + .attr("y", function(d, i) { + if (sign_filter.bool[i] == 1) { + return top_space+squareHeight*sign_filter.index[i]; + } + return height/2; + }) + .attr("width", squareWidth) + .attr("height", function(d, i) { + return sign_filter.bool[i]*squareHeight; + }) + + // Update dendrogram for columns + dendrogram.size([width-left_space, dendrogram_space]); + + colDendrogram = colDendrogram.data([]); + colDendrogram.exit().remove(); + + colDendrogram = colDendrogram + .data(dendrogram(d3.hierarchy(json.colDendrogram)).descendants().slice(1)) + .enter().append("path") + .attr("d", function(d) { + return "M" + (d.x+left_space) + "," + d.y + + "V" + d.parent.y + "H" + (d.parent.x+left_space); + }) + .attr("fill", "none") + .attr("stroke", "#999999"); + + // Update dendrogram for rows + rowDendrogram = rowDendrogram.data([]); + rowDendrogram.exit().remove(); + + if (nsign == nrow) { + + dendrogram.size([height-top_space, dendrogram_space]); + + rowDendrogram = rowDendrogram + .data(dendrogram(d3.hierarchy(json.rowDendrogram)).descendants().slice(1)) + .enter().append("path") + .attr("d", function(d) { + return "M" + d.y + "," + (d.x+top_space) + + "H" + d.parent.y + "V" + (d.parent.x+top_space); + }) + .attr("fill", "none") + .attr("stroke", "#999999"); + + } + + // Update topbar + topbarSquares + .transition().duration(150) + .attr("transform", function (d, i) { return "translate(" + (left_space+i*squareWidth) + "," + (dendrogram_space+dendrogram_margin+color_bar_size*(this.parentNode.__data__.index)) + ")";}); + + topbarSquares.selectAll("rect") + .attr("width", squareWidth) + + // Update sidebar + sidebar = sidebar.data([]); + sidebar.exit().remove(); + + if (selected_sidebar != '') { + sidebar = sidebar + .data(function(){ + if (Object.keys(json.sidebar[selected_sidebar]).indexOf(selected_model) != -1) { + return json.sidebar[selected_sidebar][selected_model]; + } else { + return json.sidebar[selected_sidebar]; + } + }) + .enter().append("g") + + sidebarSquares = sidebar.selectAll("g") + .data(function(d, i) { + d.index = i; + if (selected_sidebar == 'levels') { + sidebarColors[selected_sidebar].domain(unique(d.value)); + } + return d.value; + }) + .enter() + .append("g") + .attr("transform", function (d, i) { + if (sign_filter.bool[i] == 1) { + return "translate(" + (dendrogram_space+dendrogram_margin+color_bar_size*(this.parentNode.__data__.index)) + "," + (top_space+squareHeight*sign_filter.index[i]) + ")"; + } + return "translate(" + (dendrogram_space+dendrogram_margin+color_bar_size*(this.parentNode.__data__.index)) + "," + (height/2) + ")"; + }) + + sidebarSquares.append("rect") + .attr("width", color_bar_size) + .attr("height", function(d, i) { + return sign_filter.bool[i]*squareHeight; + }) + .attr("fill", function(d) { + return sidebarColors[selected_sidebar](d); + }) + + // Add sidebar labels + sidebar.append("g") + .attr("transform", function(d, i){ + return "translate("+(dendrogram_space+dendrogram_margin+color_bar_size+color_bar_size*i)+","+(height+5)+")"; + }) + .append("text") + .attr("transform", "rotate(-90)") + .attr("text-anchor", "end") + .attr("font-size", color_bar_size) + .text(function(d){return d.name;}) + + } + + // Update column labels + colLabel + .transition().duration(150) + .attr("transform", function (d, i) { return "translate(" + (left_space+i*squareWidth+squareWidth) + "," + height + ")";}) + + // Update row labels + rowLabel + .transition().duration(150) + .attr("transform", function (d, i) { + if (sign_filter.bool[i] == 1) { + return "translate("+ width +", "+ (top_space+squareHeight*sign_filter.index[i]) +")"; + }; + return "translate("+ width +", "+ (height/2) +")"; + }) + + rowLabelBox + .attr("fill-opacity", function(d, i){ + if (sign_filter.bool[i] == 0) { + return 0; + } + return this.getAttribute("fill-opacity"); + }) + .attr("x", left_space-width) + .attr("width", width-left_space) + .attr("height", function (d, i) { + if (sign_filter.bool[i] == 0) { + return 0; + }; + return squareHeight; + }) + + rowLabelText + .attr("display", function(d, i){ + if (sign_filter.bool[i] == 0) { + return "none"; + } + return this.getAttribute("display"); + }) + .attr("selected", function(d, i){ + if (sign_filter.bool[i] == 0) { + return 'false'; + } + return this.getAttribute("selected"); + }) + .attr("mass-selected", function(d, i){ + if (sign_filter.bool[i] == 0) { + return 'false'; + } + return this.getAttribute("mass-selected"); + }) + .attr("filtered", function (d, i) { + if (sign_filter.bool[i] == 0) { + return 'true'; + }; + return 'false'; + }) + .attr("y", squareHeight/2) + + } + + // Sidebar update function + var updateSidebar = function(){ + + sidebarLegend.html('') + + var selected_sidebar = $("#sidebarSelect").val(); + + // For linear scales + if (sidebarColors[selected_sidebar].domain().length > 0) { + + var selected_sidebar_domain = sidebarColors[selected_sidebar].domain(), + selected_sidebar_range = sidebarColors[selected_sidebar].range(); + + sidebarLegend + .append("p") + .html("Sidebar color scale") + + var sideBarColorScaleSvg = sidebarLegend.append("svg") + .attr("width", color_scale_width) + .attr("height", "20px") + .style("margin-left", "15px") + .style("margin-right", "15px") + .style("overflow", "visible") + + var sideBarColorScaleSvgDefs = sideBarColorScaleSvg.append("defs") + + var linearGradient = sideBarColorScaleSvgDefs.append("linearGradient") + .attr("id", "sidebar-linear-gradient"); + + for (var i = 0; i < selected_sidebar_domain.length; i++) { + linearGradient.append("stop") + .attr("offset", (i*100/(selected_sidebar_domain.length-1))+"%") + .attr("stop-color", selected_sidebar_range[i]); + } + + sideBarColorScaleSvg.append("rect") + .attr("width", color_scale_width) + .attr("height", 20) + .style("fill", "url(#sidebar-linear-gradient)") + .style("stroke", "#ccc") + + sideBarColorScaleSvgLabel = sideBarColorScaleSvg.append("g") + .selectAll() + .data(selected_sidebar_domain) + .enter() + + sideBarColorScaleSvgLabel.append("rect") + .attr("y", 20) + .attr("x", function(d, i) { + if (i == 0) { + return i*color_scale_width/(selected_sidebar_domain.length-1); + } + return i*color_scale_width/(selected_sidebar_domain.length-1)-1; + }) + .attr("width", 1) + .attr("height", 4) + .style("fill", "#333"); + + sideBarColorScaleSvgLabel.append("text") + .attr("y", 26+color_scale_font_size) + .attr("x", function(d, i){return i*color_scale_width/(selected_sidebar_domain.length-1);}) + .attr("font-size", color_scale_font_size) + .attr("text-anchor", "middle") + .text(function(d){return d;}) + + sidebarLegend.append("text") + .attr("font-family", font_family) + .text(selected_sidebar); + + } else { + // For ordinal scales + + sidebarLegend.style("margin-left", "0px") + .style("margin-top", "0px") + .append("p") + .html("Sidebar color key") + + for (var i = 0; i < json.sidebar[selected_sidebar].length; i++) { + + var sideBarData = unique(json.sidebar[selected_sidebar][i].value); + sidebarColors[selected_sidebar].domain(sideBarData); + + sidebarLegend.append("span") + .attr("class", "sidebar-sub-title") + .html(json.sidebar[selected_sidebar][i].name) + + var legendSpan = sidebarLegend.append("ul") + .style("list-style-type", "none") + .selectAll("ul") + .data(sideBarData) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("title", function(d) { return d;}) + .append("span") + + legendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .append("rect") + .attr("width", "10px") + .attr("height", "10px") + .attr("fill", function(d) { return sidebarColors[selected_sidebar](d); }) + + legendSpan.append("span") + .html(function(d) { + return d; + }); + + if (i < json.sidebar[selected_sidebar].length-1) { + sidebarLegend.append("div") + .style("border-top", "solid #e6e6e6 1px") + } + } + + + } + + restart(); + } + + + // Label selection function + function selectLabel (id) { + $("."+id).on("mouseenter", function(d) { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[0]).attr("fill-opacity", selector_opacity); + }); + $("."+id).on("mouseleave", function(d) { + d3.select(this.childNodes[0]).attr("fill-opacity", 0); + if (this.childNodes[1].getAttribute("selected") == 'false') { + d3.select(this.childNodes[1]).attr("display", "none"); + } + }); + $("."+id).on("click", function(d) { + if (this.childNodes[1].getAttribute("selected") == 'false') { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", 'true'); + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", 'false'); + } + }); + }; + + // Display labels button + var showColLabel = function() { + var label = d3.selectAll(".col-label"); + if ($("#colLabelButton").is(':checked')) { + label.attr("display", "inline"); + } else { + label.attr("display", "none"); + }; + }; + + appendLabelCheckBox(buttons, "Show columns labels", "Col. labels", "colLabelButton", showColLabel) + + var showRowLabel = function() { + var label = d3.selectAll(".row-label").filter(function(){return this.getAttribute("filtered") == 'false'}); + if ($("#rowLabelButton").is(':checked')) { + label.attr("display", "inline"); + label.attr("selected", 'true'); + label.attr("mass-selected", 'true'); + } else { + label.attr("display", "none"); + label.attr("selected", 'false'); + label.attr("mass-selected", 'false'); + }; + }; + + appendLabelCheckBox(buttons, "Show rows labels", "Rows labels", "rowLabelButton", showRowLabel) + + // Search in labels + var searchLabels = function() { + $("#rowLabelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".row-label").filter(function(d){return d.toUpperCase().indexOf(key.toUpperCase()) != -1 }).filter(function(d){return this.getAttribute('filtered') == 'false' }); + non_selected = d3.selectAll(".row-label").filter(function(d){return d.toUpperCase().indexOf(key.toUpperCase()) == -1 }); + selected.attr("display", "inline"); + selected.attr("selected", 'true'); + non_selected.attr("display", "none"); + non_selected.attr("selected", 'false'); + } else { + to_free = d3.selectAll(".row-label"); + to_free.attr("display", "none"); + to_free.attr("selected", 'false'); + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + + // Select model + var modelSelect = buttons.append("div") + .attr("title", "Chose a model.") + .attr("class", "form-group") + + modelSelect.append("label") + .html("Model") + + modelSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "modelSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.sidebar['p-values'])) + .enter().append("option") + .text(function (d){ return d;}); + $("#modelSelect").on("change", restart) + + // Select sidebar + var sidebarSelect = buttons.append("div") + .attr("title", "Chose a sidebar.") + .attr("class", "form-group") + + sidebarSelect.append("label") + .html("Sidebar") + + sidebarSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "sidebarSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .selectAll("option") + .data(Object.keys(json.sidebar).concat('')) + .enter().append("option") + .text(function (d){ return d;}); + $("#sidebarSelect").on("change", updateSidebar) + + // Select p-value cutoff +/* + var pThreshold = buttons.append("div") + .attr("title", "Set a p-value cutoff.") + .attr("class", "form-group") + + pThreshold.append("label") + .html("P-value cutoff") + + pThreshold.append("input") + .attr("id", "pThreshold") + .attr("type", "number") + .attr("class", "form-control form-number-field") + .attr("min", 0) + .attr("max", 1) + .attr("step", 0.001) + .attr("value", 1) + .on("change", restart); +*/ + + + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + + restart(); + selectLabel("rowLabel"); + updateSidebar(); + //}); + +}; diff --git a/app/assets/javascripts/home.coffee b/app/assets/javascripts/home.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/home.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/jobs.js.coffee b/app/assets/javascripts/jobs.js.coffee new file mode 100644 index 0000000..43d766d --- /dev/null +++ b/app/assets/javascripts/jobs.js.coffee @@ -0,0 +1,4 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ + diff --git a/app/assets/javascripts/pca.js b/app/assets/javascripts/pca.js new file mode 100644 index 0000000..904b856 --- /dev/null +++ b/app/assets/javascripts/pca.js @@ -0,0 +1,609 @@ +function pca(id, legend_id, json, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10) { + + // Size + var margin = {top: 10, right: 10, bottom: 75, left: 75}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + left_label_space = 35, + bottom_label_space = 30; + + // Colors and symbols + var colors = d3.scaleOrdinal(color_palette), + symbols = d3.scaleOrdinal([d3.symbolCircle, + d3.symbolSquare, + d3.symbolTriangle, + d3.symbolStar, + d3.symbolDiamond, + d3.symbolCross]); + + // Buttons + var buttons = d3.select("#d3-buttons"); + buttons.html(""); + + // $.getJSON(data, function(json) { + + // Set variables depending only on the primary data + var color_select_options = [], + json_keys = Object.keys(json[Object.keys(json)[0]][0]), + axisValues = Object.keys(json[Object.keys(json)[0]][0].data), + nArrowMax = json[Object.keys(json)[0]].length; + //space_options = Object.keys(json); + //space_options.splice(space_options.indexOf("eig"), 1); + + for ( var i = 0; i < json_keys.length; i++) { + if (["data", "id"].indexOf(json_keys[i]) == -1) { + color_select_options.push(json_keys[i]); + }; + }; + + //////////////// Draw plot //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-2') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Draw plot + var plot = svg.selectAll(); + + // Add plot labels + var plotLabel = svg.selectAll(); + + // Draw bi-plot (arrows) + var biPlot = svg.selectAll(); + + // Add bi-plot labels + var biPlotLabel = svg.selectAll(); + + // Draw axis + var xAxis = svg.append("g") + .attr("class", "axis") + .attr("transform", "translate(0," + height + ")"); + + var xAxisLegend = svg.append("g") + .attr("transform", "translate("+ width/2 +", "+ (height+bottom_label_space) +")") + .attr("class", "axis-label"); + + var yAxis = svg.append("g") + .attr("class", "axis"); + + var yAxisLegend = svg.append("g") + .attr("transform", "translate("+ -left_label_space +", "+ height/2 +")") + .attr("class", "axis-label"); + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + legend.append('p') + .html('Color key') + var colorLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + legend.append('p') + .html('Symbol key') + var symLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + //////////////// Restart function //////////////// + var restart = function() { + + // Set coordinates settings + var space = "ind", + X = d3.select("#xSelect").property("value"), + Y = d3.select("#ySelect").property("value"), + x = [], + y = []; + + json[space].forEach(function (d) { + x.push(d.data[X]); + y.push(d.data[Y]); + }); + + var xMin = Math.min.apply(null, x), + xMax = Math.max.apply(null, x), + xPadding = [0.05, 0.03], + xRange = xMax-xMin, + xScale = d3.scaleLinear() + .range([0, width]) + .domain([xMin-xPadding[0]*xRange, xMax+xPadding[1]*xRange]).nice(), + xValue = function(d) { return d.data[X];}, + xMap = function(d) { return xScale(xValue(d));}, + yMin = Math.min.apply(null, y), + yMax = Math.max.apply(null, y), + yPadding = [0.05, 0.03], + yRange = yMax-yMin, + yScale = d3.scaleLinear() + .range([height, 0]) + .domain([yMin-yPadding[0]*yRange, yMax+yPadding[1]*yRange]).nice() + yValue = function(d) { return d.data[Y];}, + yMap = function(d) { return yScale(yValue(d));}; + + // Set color settings + var selected_color_factor = d3.select("#colorSelect").property("value"), + selected_symbol_factor = d3.select("#symbolSelect").property("value"), + color_factors = [], + symbol_factors = []; + + json[space].forEach(function (d) { + if (color_factors.indexOf(d[selected_color_factor]) == -1) { + color_factors.push(d[selected_color_factor]); + }; + if (symbol_factors.indexOf(d[selected_symbol_factor]) == -1) { + symbol_factors.push(d[selected_symbol_factor]); + }; + }); + + colors.domain(color_factors); + symbols.domain(symbol_factors); + + // Draw plot + plot = plot.data([]); + plot.exit().remove(); + + plot = plot + .data(json[space]); + + // Draw dots + plot = plot.enter() + .append("g") + .attr("class", "plot") + .attr("transform", function (d) { return "translate(" + xMap(d) + ", " + yMap(d) + ")"; }); + + plot.append("path") + .attr("class", "symbol") + .attr("d", d3.symbol() + .type(function(d) { return symbols(d[selected_symbol_factor]);}) + .size(200)) + .style("fill", function(d) { return colors(d[selected_color_factor]);}) + .style("stroke-opacity", 0) + .style("fill-opacity", 0.8); + + // Add dots labels + plotLabel = plotLabel.data([]); + plotLabel.exit().remove(); + + plotLabel = plotLabel + .data(json[space]) + .enter() + .append("g") + .attr("class", "plot-label") + .attr("transform", function (d) { return "translate(" + xMap(d) + ", " + yMap(d) + ")"; }); + + plotLabel.append("path") + .attr("class", "symbol") + .attr("d", d3.symbol() + .type(function(d) { return symbols(d[selected_symbol_factor]);}) + .size(200)) + .style("opacity", 0) + + plotLabel.append("text") + .attr("class", "plot-label") + .attr("dy", -10) + .style("text-anchor", function (d) { + if (xMap(d) <= width/2) { + return "start"; + } else { + return "end"; + }; + }) + .attr("font-family", font_family) + .attr("display", "none") + .attr("selected", false) + .text(function (d) { + var label = d.name.split(";"); + if (label.length > 1) { + return label[label.length-2] +";"+ label[label.length-1]; + } else { + return label[0]; + }; + }); + + // Draw arrows (bi-plot) + var arrow_space = "var", //space_options, + nArrows = d3.select("#nArrow").property("value"), + minDx = Math.min(xScale(0), width-xScale(0)), + minDy = Math.min(yScale(0), height-yScale(0)), + minD = Math.min(minDx, minDy), + xRadius = Math.abs(xScale.invert(xScale(0)+minD)/1.5), + yRadius = Math.abs(yScale.invert(yScale(0)+minD)/1.5), + norm = function (d) {return Math.sqrt(Math.pow(xValue(d), 2)+Math.pow(yValue(d), 2));}, + angle = function (d) { + var a = Math.atan(xValue(d)/yValue(d))*180/Math.PI; + if (yValue(d) < 0) { + a = a-180; // apply some majik... + } + return a; + }, + xMapArrow = function (d) {return xScale(xRadius*xValue(d)/norm(d));}, + yMapArrow = function (d) {return yScale(yRadius*yValue(d)/norm(d));}; + + //arrow_space.splice(arrow_space.indexOf(space), 1); + + var arrow_data = JSON.parse(JSON.stringify(json[arrow_space])); + nArrowMax = arrow_data.length; + + function sortByNorm(e1, e2) { + var v1 = norm(e1), + v2 = norm(e2); + return v2-v1; + }; + + arrow_data.sort(sortByNorm); + arrow_data = arrow_data.splice(0, nArrows); + + biPlot = biPlot.data([]); + biPlot.exit().remove(); + + biPlot = biPlot + .data(arrow_data) + .enter() + .append("g"); + + biPlot.append("line") + .attr("class", "arrow-line") + .attr("x1", xScale(0)) + .attr("y1", yScale(0)) + .attr("x2", function (d) {return xMapArrow(d);}) + .attr("y2", function (d) {return yMapArrow(d);}) + .attr("stroke", "#333"); + + var arrowHead = biPlot.append("g") + .attr("transform", function (d) { return "translate(" + xMapArrow(d) + ", " + yMapArrow(d) + ") rotate("+angle(d)+")"; }); + + arrowHead.append("path") + .attr("class", "arrow-head") + .attr("d", d3.symbol() + .type(d3.symbolTriangle) + .size(50)); + + // Add bi-plot (arrows) labels + biPlotLabel = biPlotLabel.data([]); + biPlotLabel.exit().remove(); + + biPlotLabel = biPlotLabel + .data(arrow_data) + .enter() + .append("g") + .attr("class", "plot-label") + .attr("transform", function (d) { return "translate(" + xMapArrow(d) + ", " + yMapArrow(d) + ")"; }); + + biPlotLabel.append("path") + .attr("class", "arrow-head") + .attr("transform", function (d) { return "rotate("+angle(d)+")"; }) + .attr("d", d3.symbol() + .type(d3.symbolTriangle) + .size(50)) + .style("opacity", 1); + + biPlotLabel.append("text") + .attr("dy", -10) + .style("text-anchor", function (d) { + if (xMap(d) <= width/2) { + return "start"; + } else { + return "end"; + }; + }) + .attr("font-family", font_family) + .attr("display", "none") + .attr("selected", false) + .text(function (d) { + var label = d.name.split(";"); + if (label.length > 1) { + return label[label.length-2] +";"+ label[label.length-1]; + } else { + return label[0]; + }; + }); + + displayLabels("plot-label"); + showLabels(); + + // Add axis + xAxis.selectAll("*").remove(); + xAxis.call(d3.axisBottom(xScale).ticks(10)); + + xAxisLegend.selectAll("*").remove(); + xAxisLegend.append("text") + .text(X+" ("+Math.round(json.eig[X])+"%)") + .attr("font-family", font_family) + .style("text-anchor", "middle"); + + yAxis.selectAll("*").remove(); + yAxis.call(d3.axisLeft(yScale).ticks(10)) + + yAxisLegend.selectAll("*").remove(); + yAxisLegend.append("text") + .text(Y+" ("+Math.round(json.eig[Y])+"%)") + .attr("font-family", font_family) + .style("text-anchor", "middle") + .attr("transform", "rotate(-90)"); + + // Add 0-lines + svg.selectAll(".zero-line").remove(); + svg.append("g") + .attr("class", "zero-line") + .attr("transform", "translate(0," + yScale(0) + ")") + .call(d3.axisBottom(xScale).ticks(0) + .tickSize(0, 0)); + + svg.append("g") + .attr("class", "zero-line") + .attr("transform", "translate(" + xScale(0) + ", 0)") + .call(d3.axisLeft(yScale).ticks(0) + .tickSize(0, 0)); + + // Close the plot + svg.selectAll(".frame").remove(); + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate(0, 0)") + .call(d3.axisTop(xScale).ticks(0) + .tickSize(0, 0)); + + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate("+width+", 0)") + .call(d3.axisRight(yScale).ticks(0) + .tickSize(0, 0)); + + + // Update legend + + colorLegend = colorLegend.data([]); + colorLegend.exit().remove(); + + colorLegend = colorLegend + .data(color_factors.reverse()) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + colorLegendSpan = colorLegend.append("span") + + colorLegendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + .append("path") + .attr("transform", "translate(5, 5)") + .attr("d", d3.symbol() + .type(function (d, i){ + return d3.symbolSquare; + }) + .size(75)) + .attr("stroke", "none") + .attr("fill", function (d, i){ + return colors(d); + }) + .attr("fill-opacity", 1) + + colorLegendSpan.append("span") + .html(function(d) { return d;}) + + symLegend = symLegend.data([]); + symLegend.exit().remove(); + + symLegend = symLegend + .data(symbol_factors.reverse()) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + + symLegendSpan = symLegend.append("span") + + symLegendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + .append("path") + .attr("transform", "translate(5, 5)") + .attr("d", d3.symbol() + .type(function (d, i){ + return symbols(d); + }) + .size(75)) + .attr("stroke", function (d, i){ + return "#333"; + }) + .attr("fill", function (d, i){ + return "#333"; + }) + .attr("fill-opacity", function (d, i){ + return 0; + }) + + symLegendSpan.append("span") + .html(function(d) { return d;}) + + }; + + //////////////// Control buttons //////////////// + + // Display labels button + var showLabels = function() { + var labels = d3.selectAll(".plot-label").select(function(){ return this.childNodes[1];}); + if ($("#labelButton").is(':checked')) { + labels.attr("display", "inline"); + labels.attr("selected", true); + } else { + labels.attr("display", "none"); + labels.attr("selected", false); + }; + }; + + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + + // Search in labels + var searchLabels = function() { + $("#labelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".plot-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) != -1 }).select(function(){return this.childNodes[1];}), + non_selected = d3.selectAll(".plot-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) == -1 }).select(function(){return this.childNodes[1];}); + selected.attr("display", "inline"); + selected.attr("selected", true); + non_selected.attr("display", "none"); + non_selected.attr("selected", false); + } else { + to_free = d3.selectAll(".plot-label").select(function(){return this.childNodes[1];}); + to_free.attr("display", "none"); + to_free.attr("selected", false); + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + // Select axis + var xSelect = buttons.append("div") + .attr("title", "Chose X axis.") + .attr("class", "form-group") + + xSelect.append("label") + .html("X axis") + + xSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "xSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(axisValues) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("xSelect").value = axisValues[0]; + + var ySelect = buttons.append("div") + .attr("title", "Chose Y axis.") + .attr("class", "form-group") + + ySelect.append("label") + .html("Y axis") + + ySelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "ySelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(axisValues) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("ySelect").value = axisValues[1]; + + // Select color variables + var colorSelect = buttons.append("div") + .attr("title", "Chose variable to use for colors.") + .attr("class", "form-group") + + colorSelect.append("label") + .html("Color") + + colorSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "colorSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(color_select_options) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("colorSelect").value = color_select_options[color_select_options.length-1]; + + // Select symbol variables + var symbolSelect = buttons.append("div") + .attr("title", "Chose variable to use for symbols.") + .attr("class", "form-group") + + symbolSelect.append("label") + .html("Symbols") + + symbolSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "symbolSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(color_select_options) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("symbolSelect").value = color_select_options[color_select_options.length-1]; + + // Select number of arrows + var nArrow = buttons.append("div") + .attr("title", "Set the number of variables (arrows) to display on the bi-plot.") + .attr("class", "form-group") + + nArrow.append("label") + .html("Arrows nb") + + nArrow.append("input") + .attr("id", "nArrow") + .attr("type", "number") + .attr("class", "form-control form-number-field") + .attr("min", 0) + .attr("max", nArrowMax) + .attr("value", Math.min(5, nArrowMax)) + .on("change", restart); + + + setMultiselect('.figtool-multiselect'); + // resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + $("#xSelect").on("change", restart) + $("#ySelect").on("change", restart) + $("#colorSelect").on("change", restart) + $("#symbolSelect").on("change", restart) + + restart(); + + // Labels functions + function displayLabels (id) { + $("."+id).on("mouseenter", function(d) { + d3.select(this.childNodes[1]).attr("display", "inline"); + }); + $("."+id).on("mouseleave", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + }; + }); + $("."+id).on("click", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", true); + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", false); + } + }); + }; + +//}); + +}; diff --git a/app/assets/javascripts/pcoa.js b/app/assets/javascripts/pcoa.js new file mode 100644 index 0000000..5fbca91 --- /dev/null +++ b/app/assets/javascripts/pcoa.js @@ -0,0 +1,484 @@ +function pcoa(id, legend_id, json, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10) { + + // Size + var margin = {top: 10, right: 10, bottom: 75, left: 75}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom, + left_label_space = 35, + bottom_label_space = 30; + + // Colors and symbols + var colors = d3.scaleOrdinal(color_palette), + symbols = d3.scaleOrdinal([d3.symbolCircle, + d3.symbolSquare, + d3.symbolTriangle, + d3.symbolStar, + d3.symbolDiamond, + d3.symbolCross]); + + // Buttons + var buttons = d3.select("#d3-buttons"); + buttons.html(""); + +// $.getJSON(data, function(json) { + + // Set variables depending only on the primary data + var color_select_options = [], + data_keys = Object.keys(json.data[0]), + axisValues = Object.keys(json.data[0].data); + + for ( var i = 0; i < data_keys.length; i++) { + if (["data", "id"].indexOf(data_keys[i]) == -1) { + color_select_options.push(data_keys[i]); + }; + }; + + //////////////// Draw plot //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-2') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + // Draw plot + var plot = svg.selectAll(); + + // Add plot labels + var plotLabel = svg.selectAll(); + + // Draw axis + var xAxis = svg.append("g") + .attr("class", "axis") + .attr("transform", "translate(0," + height + ")"); + + var xAxisLegend = svg.append("g") + .attr("transform", "translate("+ width/2 +", "+ (height+bottom_label_space) +")") + .attr("class", "axis-label"); + + var yAxis = svg.append("g") + .attr("class", "axis"); + + var yAxisLegend = svg.append("g") + .attr("transform", "translate("+ -left_label_space +", "+ height/2 +")") + .attr("class", "axis-label"); + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + legend.append('p') + .html('Color key') + var colorLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + legend.append('p') + .html('Symbol key') + var symLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + + //////////////// Restart function //////////////// + var restart = function() { + + // Set coordinates settings + var X = d3.select("#xSelect").property("value"), + Y = d3.select("#ySelect").property("value"), + x = [], + y = []; + + json.data.forEach(function (d) { + x.push(d.data[X]); + y.push(d.data[Y]); + }); + + var xMin = Math.min.apply(null, x), + xMax = Math.max.apply(null, x), + xPadding = [0.05, 0.03], + xRange = xMax-xMin, + xScale = d3.scaleLinear() + .range([0, width]) + .domain([xMin-xPadding[0]*xRange, xMax+xPadding[1]*xRange]).nice(), + xValue = function(d) { return d.data[X];}, + xMap = function(d) { return xScale(xValue(d));}, + yMin = Math.min.apply(null, y), + yMax = Math.max.apply(null, y), + yPadding = [0.05, 0.03], + yRange = yMax-yMin, + yScale = d3.scaleLinear() + .range([height, 0]) + .domain([yMin-yPadding[0]*yRange, yMax+yPadding[1]*yRange]).nice() + yValue = function(d) { return d.data[Y];}, + yMap = function(d) { return yScale(yValue(d));}; + + // Set color settings + var selected_color_factor = d3.select("#colorSelect").property("value"), + selected_symbol_factor = d3.select("#symbolSelect").property("value"), + color_factors = [], + symbol_factors = []; + + json.data.forEach(function (d) { + if (color_factors.indexOf(d[selected_color_factor]) == -1) { + color_factors.push(d[selected_color_factor]); + }; + if (symbol_factors.indexOf(d[selected_symbol_factor]) == -1) { + symbol_factors.push(d[selected_symbol_factor]); + }; + }); + + colors.domain(color_factors); + symbols.domain(symbol_factors); + + // Draw plot + plot = plot.data([]); + plot.exit().remove(); + + plot = plot + .data(json.data); + + // Draw dots + plot = plot.enter() + .append("g") + .attr("class", "plot") + .attr("transform", function (d) { return "translate(" + xMap(d) + ", " + yMap(d) + ")"; }); + + plot.append("path") + .attr("class", "symbol") + .attr("d", d3.symbol() + .type(function(d) { return symbols(d[selected_symbol_factor]);}) + .size(200)) + .style("fill", function(d) { return colors(d[selected_color_factor]);}) + .style("stroke-opacity", 0) + .style("fill-opacity", 0.8); + + // Add dots labels + plotLabel = plotLabel.data([]); + plotLabel.exit().remove(); + + plotLabel = plotLabel + .data(json.data) + .enter() + .append("g") + .attr("class", "plot-label") + .attr("transform", function (d) { return "translate(" + xMap(d) + ", " + yMap(d) + ")"; }); + + plotLabel.append("path") + .attr("class", "symbol") + .attr("d", d3.symbol() + .type(function(d) { return symbols(d[selected_symbol_factor]);}) + .size(200)) + .style("opacity", 0) + + plotLabel.append("text") + .attr("class", "plot-label") + .attr("dy", -10) + .style("text-anchor", function (d) { + if (xMap(d) <= width/2) { + return "start"; + } else { + return "end"; + }; + }) + .attr("font-family", font_family) + .attr("display", "none") + .attr("selected", false) + .text(function (d) { + var label = d.name.split(";"); + if (label.length > 1) { + return label[label.length-2] +";"+ label[label.length-1]; + } else { + return label[0]; + }; + }); + + displayLabels("plot-label"); + showLabels(); + + // Add axis + xAxis.selectAll("*").remove(); + xAxis.call(d3.axisBottom(xScale).ticks(10)); + + xAxisLegend.selectAll("*").remove(); + xAxisLegend.append("text") + .text(X+" ("+Math.round(json.eig[X])+"%)") + .attr("font-family", font_family) + .style("text-anchor", "middle"); + + yAxis.selectAll("*").remove(); + yAxis.call(d3.axisLeft(yScale).ticks(10)) + + yAxisLegend.selectAll("*").remove(); + yAxisLegend.append("text") + .text(Y+" ("+Math.round(json.eig[Y])+"%)") + .attr("font-family", font_family) + .style("text-anchor", "middle") + .attr("transform", "rotate(-90)"); + + // Close the plot + svg.selectAll(".frame").remove(); + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate(0, 0)") + .call(d3.axisTop(xScale).ticks(0) + .tickSize(0, 0)); + + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate("+width+", 0)") + .call(d3.axisRight(yScale).ticks(0) + .tickSize(0, 0)); + + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate(0," + yScale(0) + ")") + .call(d3.axisBottom(xScale).ticks(0) + .tickSize(0, 0)); + + svg.append("g") + .attr("class", "frame") + .attr("transform", "translate(" + xScale(0) + ", 0)") + .call(d3.axisLeft(yScale).ticks(0) + .tickSize(0, 0)); + + + // Update legend + colorLegend = colorLegend.data([]); + colorLegend.exit().remove(); + + colorLegend = colorLegend + .data(color_factors.reverse()) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + colorLegendSpan = colorLegend.append("span") + + colorLegendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + .append("path") + .attr("transform", "translate(5, 5)") + .attr("d", d3.symbol() + .type(function (d, i){ + return d3.symbolSquare; + }) + .size(75)) + .attr("stroke", "none") + .attr("fill", function (d, i){ + return colors(d); + }) + .attr("fill-opacity", 1) + + colorLegendSpan.append("span") + .html(function(d) { return d;}) + + symLegend = symLegend.data([]); + symLegend.exit().remove(); + + symLegend = symLegend + .data(symbol_factors.reverse()) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + + symLegendSpan = symLegend.append("span") + + symLegendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + .append("path") + .attr("transform", "translate(5, 5)") + .attr("d", d3.symbol() + .type(function (d, i){ + return symbols(d); + }) + .size(75)) + .attr("stroke", function (d, i){ + return "#333"; + }) + .attr("fill", function (d, i){ + return "#333"; + }) + .attr("fill-opacity", function (d, i){ + return 0; + }) + + symLegendSpan.append("span") + .html(function(d) { return d;}) + + + }; + + //////////////// Control buttons //////////////// + + // Display labels button + var showLabels = function() { + var labels = d3.selectAll(".plot-label").select(function(){ return this.childNodes[1];}); + if ($("#labelButton").is(':checked')) { + labels.attr("display", "inline"); + labels.attr("selected", true); + } else { + labels.attr("display", "none"); + labels.attr("selected", false); + }; + }; + + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + + // Search in labels + var searchLabels = function() { + $("#labelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".plot-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) != -1 }).select(function(){return this.childNodes[1];}), + non_selected = d3.selectAll(".plot-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) == -1 }).select(function(){return this.childNodes[1];}); + selected.attr("display", "inline"); + selected.attr("selected", true); + non_selected.attr("display", "none"); + non_selected.attr("selected", false); + } else { + to_free = d3.selectAll(".plot-label").select(function(){return this.childNodes[1];}); + to_free.attr("display", "none"); + to_free.attr("selected", false); + + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + + // Select axis + var xSelect = buttons.append("div") + .attr("title", "Chose X axis.") + .attr("class", "form-group") + + xSelect.append("label") + .html("X axis") + + xSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "xSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(axisValues) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("xSelect").value = axisValues[0]; + + var ySelect = buttons.append("div") + .attr("title", "Chose Y axis.") + .attr("class", "form-group") + + ySelect.append("label") + .html("Y axis") + + ySelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "ySelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(axisValues) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("ySelect").value = axisValues[1]; + + // Select color variables + var colorSelect = buttons.append("div") + .attr("title", "Chose variable to use for colors.") + .attr("class", "form-group") + + colorSelect.append("label") + .html("Color") + + colorSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "colorSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(color_select_options) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("colorSelect").value = color_select_options[color_select_options.length-1]; + + // Select symbol variables + var symbolSelect = buttons.append("div") + .attr("title", "Chose variable to use for symbols.") + .attr("class", "form-group") + + symbolSelect.append("label") + .html("Symbols") + + symbolSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "symbolSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(color_select_options) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("symbolSelect").value = color_select_options[color_select_options.length-1]; + + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + $("#xSelect").on("change", restart) + $("#ySelect").on("change", restart) + $("#colorSelect").on("change", restart) + $("#symbolSelect").on("change", restart) + + restart(); + + // Labels functions + function displayLabels (id) { + $("."+id).on("mouseenter", function(d) { + d3.select(this.childNodes[1]).attr("display", "inline"); + }); + $("."+id).on("mouseleave", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + }; + }); + $("."+id).on("click", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", true); + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", false); + } + }); + }; +// }); + +}; diff --git a/app/assets/javascripts/similarityNetwork.js b/app/assets/javascripts/similarityNetwork.js new file mode 100644 index 0000000..6e1730f --- /dev/null +++ b/app/assets/javascripts/similarityNetwork.js @@ -0,0 +1,646 @@ +function similarityNetwork(id, legend_id, json0, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10) { + + // Size + var margin = {top: 10, right: 10, bottom: 40, left: 10}, + width = W - margin.left - margin.right, + height = H - margin.top - margin.bottom; + + // Network selector + var networkSelector = ["Data", "Metadata", "Fusion"]; + + // Colors, symbols and scaling + var colors = d3.scaleOrdinal(color_palette), + sim_color = ["#999999", "Similarity"], + symbols = d3.scaleOrdinal([d3.symbolCircle, + d3.symbolSquare, + d3.symbolTriangle, + d3.symbolStar, + d3.symbolDiamond, + d3.symbolCross]), + radius = 20, + wRange = [0.1*radius, 0.9*radius]; + + // General functions + function getWeightExtrema(json, index=0, t="max", Abs=false) { + var arr = []; + for (var i = 0; i < json.length; i++) { + if (Abs) { + arr.push(Math.abs(json[i].weight[index])); + } else { + arr.push(json[i].weight[index]); + }; + }; + + if (t == "max") { + return Math.max.apply(null, arr); + } else if (t == "min") { + return Math.min.apply(null, arr); + }; + }; + + function scale(fRange, value, iRange) { + if (iRange[0] != iRange[1]) { + return fRange[0] + (fRange[1] - fRange[0]) * (value - iRange[0]) / (iRange[1] - iRange[0]); + } else { + return (fRange[0] + fRange[1]) / 2; + }; + }; + + function organizeLegend(factor, SymSize, legendXSpace, legendYSpace) { + var legend_ncol = Math.ceil(SymSize*factor.length/legendYSpace), + legend_nrow = Math.ceil(factor.length/legend_ncol), + legend_pos = []; + for (var i = 0; i < legend_ncol; i++) { + for (var j = 0; j < legend_nrow; j++) { + if (legend_pos.length < factor.length) { + legend_pos.push({x:i*legendXSpace/legend_ncol, y:j*SymSize}); + }; + }; + }; + return legend_pos.reverse(); + }; + + // Buttons + var buttons = d3.select("#d3-buttons") + buttons.html(""); + + //$.getJSON(data, function(json0) { + + // Set variables depending only on the primary data + + // Make a working copy of the data + var json = JSON.parse(JSON.stringify(json0)); + + //////////////// Simulation //////////////// + var simulation = d3.forceSimulation() + .force("center", d3.forceCenter(width / 2, height / 2)) + .force("y", d3.forceY()) + .force("x", d3.forceX()) + .force("collide", d3.forceCollide() + .radius(radius) + .iterations(2) + .strength(0.5)) + .force("charge", d3.forceManyBody() + .strength(-100)); + + var ticked = function() { + + link + .attr("x1", function(d) { return Math.max(0, Math.min(d.source.x, width)); }) + .attr("y1", function(d) { return Math.max(0, Math.min(d.source.y, height)); }) + .attr("x2", function(d) { return Math.max(0, Math.min(d.target.x, width)); }) + .attr("y2", function(d) { return Math.max(0, Math.min(d.target.y, height)); }); + + + + node + .attr("transform", function(d) { + return "translate(" + Math.max(0, Math.min(d.x, width)) + "," + Math.max(0, Math.min(d.y, height)) + ")"; }); + + nodeLabel + .attr("transform", function(d, i) { + return "translate(" + Math.max(0, Math.min(d.x, width)) + "," + Math.max(0, Math.min(d.y, height)) + ")"; }); + }; + + function dragstarted(d) { + if (!d3.event.active) simulation.alphaTarget(0.25).restart(); + d.fx = d.x; + d.fy = d.y; + } + + function dragged(d) { + d.fx = d3.event.x; + d.fy = d3.event.y; + } + + function dragended(d) { + if (!d3.event.active) simulation.alphaTarget(0); + d.fx = null; + d.fy = null; + } + + //////////////// Draw links and nodes //////////////// + var legendContainer = d3.select("#"+legend_id).append("div") + .attr('class', 'columns-2') + + var svgContainer = d3.select("#"+id) + .style("height", (height + margin.top + margin.bottom)+"px") + + var svg = svgContainer.append("svg") + .attr("id", "svg-figure") + .attr("class", "svg-figure network-well") + .attr("width", (width + margin.left + margin.right)+"px") + .attr("height",(height + margin.top + margin.bottom)+"px") + .style("pointer-events", "all") + .call(d3.zoom() + .scaleExtent([1, 4]) + .duration(1000) + .translateExtent([[margin.left, margin.top], [width + margin.right, height + margin.top + margin.bottom]]) + .on("zoom", zoomed)); + + var g = svg.append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + function zoomed() { + g.attr("transform", d3.event.transform); + }; + + // Draw links + var link = g.append("g") + .selectAll(); + + // Draw Nodes + var node = g.append("g") + .selectAll("g"); + + var nodeLabel = g.append("g") + .selectAll("g"); + + // Add legend + var legend = legendContainer.append("div") + .attr("id", "svg-legend") + .style("font-family", font_family) + + var colorLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + legend.append("div") + .style("border-top", "solid #ccc 1px") + + var symbolLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + legend.append("div") + .style("border-top", "solid #ccc 1px") + + var linkLegend = legend.append("ul") + .style("list-style-type", "none") + .selectAll("ul"); + + //////////////// Nodes color settings //////////////// + var colorFactors = Object.keys(json.nodes[0]); + colorFactors.splice(colorFactors.indexOf("id"), 1); + colorFactors.splice(colorFactors.indexOf("name"), 1); + colorFactors.push(null); + + var setSymbolColor = function() { + var color_factor = d3.select("#colorSelect").property("value"), + color_labels = []; + + if (color_factor != "") { + for (var i = 0; i < json0.nodes.length; i++) { + if(color_labels.indexOf(json0.nodes[i][color_factor]) == -1) { + color_labels.push(json0.nodes[i][color_factor]); + }; + }; + }; + + colors.domain(color_labels); + + // Set symbols color + d3.selectAll(".coloured-symbol") + .style("fill", function (d){ + if (color_factor != "") + return colors(d[color_factor]); + return colors(""); + }); + + + // Update legend + + colorLegend = colorLegend.data([]); + colorLegend.exit().remove(); + + colorLegend = colorLegend + .data(color_labels) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + colorLegendSpan = colorLegend.append("span") + + colorLegendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + .append("path") + .attr("transform", "translate(5, 5)") + .attr("d", d3.symbol() + .type(d3.symbolSquare) + .size(75)) + .attr("stroke", "none") + .attr("fill", function (d, i){ + return colors(d); + }) + + colorLegendSpan.append("span") + .html(function(d) { return d;}) + + }; + + //////////////// Nodes shape settings //////////////// + var setSymbolShape = function() { + var shape_factor = d3.select("#symbolSelect").property("value"), + shape_labels = []; + + if (shape_factor != "") { + for (var i = 0; i < json0.nodes.length; i++) { + if(shape_labels.indexOf(json0.nodes[i][shape_factor]) == -1) { + shape_labels.push(json0.nodes[i][shape_factor]); + }; + }; + }; + + symbols.domain(shape_labels); + + // Set symbols shape + d3.selectAll(".symbol") + .attr("d", d3.symbol() + .type(function (d){ + if (shape_factor != "") + return symbols(d[shape_factor]); + return symbols(""); + }) + .size(radius*radius)); + + + symbolLegend = symbolLegend.data([]); + symbolLegend.exit().remove(); + + if (shape_labels.length > 0) { + + symbolLegend = symbolLegend + .data(shape_labels) + .enter().append("li") + .attr("id", function(d) { return d;}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d;}) + + symbolLegendSpan = symbolLegend.append("span") + + symbolLegendSpanSvg = symbolLegendSpan.append("svg") + .attr("width", "10px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + .append("path") + .attr("transform", "translate(5, 5)") + .attr("d", d3.symbol() + .type(function(d) { + return symbols(d); + }) + .size(75)) + .attr("stroke", "#333") + .attr("fill-opacity", 0) + + symbolLegendSpan.append("span") + .html(function(d) { return d;}) + + } + + }; + + //////////////// Control buttons //////////////// + + // Select colors + var colorSelect = buttons.append("div") + .attr("title", "Chose variable to use for colors.") + .attr("class", "form-group") + + colorSelect.append("label") + .html("Color") + + colorSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "colorSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", setSymbolColor) + .selectAll("option") + .data(colorFactors) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("colorSelect").value = colorFactors[0]; + + // Button for node size + var symbolSelect = buttons.append("div") + .attr("title", "Chose variable to use for symbols.") + .attr("class", "form-group") + + symbolSelect.append("label") + .html("Symbol") + + symbolSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "symbolSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", setSymbolShape) + .selectAll("option") + .data(colorFactors) + .enter().append("option") + .text(function (d){ return d;}); + + document.getElementById("symbolSelect").value = colorFactors[colorFactors.length-1]; + + //////////////// Link width settings //////////////// + var setLinkWidth = function() { + var wIndex = d3.select("#networkSelect").property("value"); + + var newMin = getWeightExtrema(json.links, wIndex, "min", Abs=true), + newMax = getWeightExtrema(json.links, wIndex, "max", Abs=true); + + link + .attr("stroke-width", function (d) { + return scale(wRange, Math.abs(d.weight[wIndex]), [newMin, newMax]); + }); + + var force_factor = 1+json.nodes.length*json.nodes.length; + + simulation + .force("link", d3.forceLink() + .id(function(d) {return d.id;}) + .distance(function(d) { + return scale([100, 50], d.weight[wIndex], [newMin, newMax])/force_factor; + }) + .strength(function(d) { + return scale([0.3, 1], d.weight[wIndex], [newMin, newMax]); + })); + }; + + //////////////// Threshold settings //////////////// + var filterThres = function() { + var wIndex = d3.select("#networkSelect").property("value"); + + var wMin = getWeightExtrema(json0.links, wIndex, "min"), + wMax = getWeightExtrema(json0.links, wIndex, "max"); + + + // Filter links based on weight + if (wMin > 0 || wMax > 0) { + + var similarity_thres = d3.select("#sThresRange").property("value"), + sThres = scale([0, wMax], similarity_thres, [0, 100]), + sTxt = ">"+ Math.round(sThres*100)/100; + } else { + var sThres = 0; + }; + + json.links = []; + + json0.links.forEach(function (d) { + if (Number(d.weight[wIndex]) > Number(sThres)) { + json.links.push(JSON.parse(JSON.stringify(d))); + }; + }); + + + // Update links legend + + linkLegend = linkLegend.data([]); + linkLegend.exit().remove(); + + linkLegend = linkLegend + .data([sim_color]) + .enter().append("li") + .attr("id", function(d) { return d[1];}) + .attr("class", "legend legend-no-interaction") + .attr("selected", 0) + .attr("title", function(d) { return d[1];}) + + linkLegendSpan = linkLegend.append("span") + + linkLegendSpanSvg = linkLegendSpan.append("svg") + .attr("width", "25px") + .attr("height", "10px") + .style("margin-right", "5px") + .style("overflow", "visible") + + linkLegendSpanSvg.append("rect") + .attr("width", 25) + .attr("height", 5) + .attr("y", 2.5) + .attr("stroke", "none") + .attr("fill", function(d){return d[0]}) + + + linkLegendSpan.append("span") + .html(function(d, i) { return d[1] +" "+sTxt;}) + + }; + + + //////////////// Restart function //////////////// + var restart = function() { + + filterThres(); + + // Update links + link = link.data([]); + link.exit().remove(); + + link = link + .data(json.links); + + link = link.enter() + .append("line") + .attr("class", "link-line") + .attr("stroke-linecap", "round") + .attr("stroke", sim_color[0]); + + // Update nodes + node = node.data([]); + node.exit().remove(); + + node = node + .data(json.nodes); + + node = node.enter() + .append("g") + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)) + .append("path") + .attr("class", "coloured-symbol symbol") + .attr("stroke", "white"); + + + // Update labels + nodeLabel = nodeLabel.data([]); + nodeLabel.exit().remove(); + + nodeLabel = nodeLabel + .data(json.nodes); + + nodeLabel = nodeLabel.enter() + .append("g") + .call(d3.drag() + .on("start", dragstarted) + .on("drag", dragged) + .on("end", dragended)) + .append("g") + .attr("class", "node-label"); + + nodeLabel.append("path") + .attr("class", "symbol") + .attr("stroke", "#333333") + .attr("fill-opacity", 0) + .style("stroke-opacity", 0); + + nodeLabel.append("text") + .text(function (d) { + var label = d.name.split(";"); + if (label.length > 1) { + return label[label.length-2] +";"+ label[label.length-1]; + } else { + return label[0]; + }; + }) + .attr("text-anchor", "start") + .attr("font-family", font_family) + .attr("display", "none") + .attr("selected", false); + + displayLabels("node-label"); + showLabels(); + + // Apply display settings + setSymbolColor(); + setSymbolShape(); + setLinkWidth(); + + simulation = simulation + .nodes(json.nodes) + .on("tick", ticked); + + simulation.force("link") + .links(json.links); + + simulation.alpha(0.5).restart(); + + }; + + + //////////////// Control buttons //////////////// + + // Display labels button + var showLabels = function() { + var label_sym = d3.selectAll(".node-label").select(function(){ return this.childNodes[0];}), + label_text = d3.selectAll(".node-label").select(function(){ return this.childNodes[1];}); + if ($("#labelButton").is(':checked')) { + label_text.attr("display", "inline"); + label_text.attr("selected", true); + label_sym.style("stroke-opacity", 1); + } else { + label_text.attr("display", "none"); + label_text.attr("selected", false); + label_sym.style("stroke-opacity", 0); + }; + }; + appendLabelCheckBox(buttons, "Show labels", "Labels", "labelButton", showLabels) + + // Search in labels + var searchLabels = function() { + $("#labelButton").attr("checked", false); + var key = $("#searchInput").val().toUpperCase(); + if (key != '') { + var selected = d3.selectAll(".node-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) != -1 }); + non_selected = d3.selectAll(".node-label").filter(function(){return this.__data__.name.toUpperCase().indexOf(key.toUpperCase()) == -1 }); + selected.select(function(){ return this.childNodes[1];}).attr("display", "inline"); + selected.select(function(){ return this.childNodes[1];}).attr("selected", true); + selected.select(function(){ return this.childNodes[0];}).style("stroke-opacity", 1); + non_selected.select(function(){ return this.childNodes[1];}).attr("display", "none"); + non_selected.select(function(){ return this.childNodes[1];}).attr("selected", false); + non_selected.select(function(){ return this.childNodes[0];}).style("stroke-opacity", 0); + } else { + to_free = d3.selectAll(".node-label"); + to_free.select(function(){return this.childNodes[1];}).attr("display", "none"); + to_free.select(function(){return this.childNodes[1];}).attr("selected", false); + to_free.select(function(){ return this.childNodes[0];}).style("stroke-opacity", 0); + }; + }; + + appendSearchInput(buttons, "Search", "searchInput", searchLabels); + + // Select network + var networkSelect = buttons.append("div") + .attr("title", "Chose network to show.") + .attr("class", "form-group") + + networkSelect.append("label") + .html("Network") + + networkSelect.append("div") + .attr("class", "multiselect-btn-container figtool-multiselect-btn-container") + .append("select") + .attr("id", "networkSelect") + .attr("class", "form-control multiselect figtool-multiselect") + .on("change", restart) + .selectAll("option") + .data(networkSelector) + .enter().append("option") + .attr("value", function (d, i){ return i;}) + .text(function (d){ return d;}); + + document.getElementById("networkSelect").value = 0; + + // Button for link weight threshold + var sThresRange = buttons.append("span") + .attr("title", "Cut-off for similarity links.") + + sThresRange.append("label") + .append("p") + .html("Similarity link cut-off ().") + + sThresRange.append("input") + .attr("id", "sThresRange") + .attr("type", "range") + .attr("class", "full-width") + .attr("min", 0) + .attr("max", 100) + .attr("value", 75) + .on("change", restart); + + + setMultiselect('.figtool-multiselect'); + //resizeMultiselect('#d3-buttons', 1, '#d3-buttons', false); + $("#networkSelect").on("change", restart) + $("#colorSelect").on("change", setSymbolColor) + $("#symbolSelect").on("change", setSymbolShape) + + restart(); + + // Nodes labels functions + function displayLabels (id) { + $("."+id).on("mouseenter", function(d) { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[0]).style("stroke-opacity", 1); + }); + $("."+id).on("mouseleave", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[0]).style("stroke-opacity", 0); + }; + }); + $("."+id).on("click", function(d) { + if (this.childNodes[1].getAttribute("selected") == "false") { + d3.select(this.childNodes[1]).attr("display", "inline"); + d3.select(this.childNodes[1]).attr("selected", true); + d3.select(this.childNodes[0]).style("stroke-opacity", 1); + } else { + d3.select(this.childNodes[1]).attr("display", "none"); + d3.select(this.childNodes[1]).attr("selected", false); + } + }); + }; + //}); + +}; diff --git a/app/assets/javascripts/tables.js b/app/assets/javascripts/tables.js new file mode 100644 index 0000000..742448e --- /dev/null +++ b/app/assets/javascripts/tables.js @@ -0,0 +1,68 @@ +function avoidSorting(evt) { + if (evt.avoidSorting !== undefined) { + evt.avoidSorting(); + } else { + evt.cancelBubble = true; + } +} + +function searchTable(tableId, inputId, colnb) { + + var filter = $("#"+inputId).val().toUpperCase(), + table = document.getElementById(tableId), + tr = table.getElementsByTagName("tr"), + td; + + for (var i = 0; i < tr.length; i++) { + if (isNaN(colnb) == true) { + td = tr[i].getElementsByTagName("td"); + for (var j = 0; j < td.length; j++) { + if (td[j]) { + if (td[j].innerHTML.toUpperCase().indexOf(filter) != -1) { + tr[i].style.display = ""; + break; + } else { + tr[i].style.display = "none"; + } + } + } + } else { + td = tr[i].getElementsByTagName("td")[colnb]; + if (td) { + if (td.innerHTML.toUpperCase().indexOf(filter) != -1) { + tr[i].style.display = ""; + } else { + tr[i].style.display = "none"; + } + } + } + } +} + +function colorTable(selector, data) { + + // Color the table + var colors = d3.scaleOrdinal(d3.schemeCategory10); + + for (var i = 0; i < data.length; i++) { + data[i] = data[i].split("\t"); + } + + for (var c = 0; c < data[0].length; c++) { + var factors = []; + for (var r = 0; r < data.length; r++) { + if (factors.indexOf(data[r][c]) == -1) { + factors.push(data[r][c]); + } + } + + if (factors.length < (data.length-1)) { + colors.domain(factors); + var elements = document.querySelectorAll(selector+c); + for (var i = 0; i < elements.length; i++) { + elements[i].style.backgroundColor=colors(elements[i].innerHTML); + } + } + } + +}; diff --git a/app/assets/javascripts/versions.coffee b/app/assets/javascripts/versions.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/versions.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/#bootstrap-changes.scss# b/app/assets/stylesheets/#bootstrap-changes.scss# new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..3192ec8 --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,13 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require_self + *= require_tree . + */ diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss new file mode 100644 index 0000000..510bd23 --- /dev/null +++ b/app/assets/stylesheets/application.scss @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the top of the + * compiled file, but it's generally better to create a new file per style scope. + * + *= require tether + *= require_self + *= require_tree . + */ + diff --git a/app/assets/stylesheets/backgrounds.css.scss b/app/assets/stylesheets/backgrounds.css.scss new file mode 100644 index 0000000..044f1cc --- /dev/null +++ b/app/assets/stylesheets/backgrounds.css.scss @@ -0,0 +1,15 @@ +.bg-welcome, +.bg-error { + min-height:100vh; + height:auto; + background-size: cover; + background-repeat: no-repeat; +} + +.bg-welcome { + background-image: asset-url("bg-welcome.jpg") +} + +.bg-error { + background-image: asset-url("bg-error.jpg") +} diff --git a/app/assets/stylesheets/bootstrap-changes.scss b/app/assets/stylesheets/bootstrap-changes.scss new file mode 100644 index 0000000..70824a7 --- /dev/null +++ b/app/assets/stylesheets/bootstrap-changes.scss @@ -0,0 +1,19 @@ +$grid-breakpoints: ( + xxs: 0, + xs: 375px, + sm: 544px, + md: 768px, + lg: 992px, + xl: 1200px, + xxl: 1400px +) !default; + +$container-max-widths: ( + xxs: 375px, + xs: 375px, + sm: 544px, + md: 768px, + lg: 992px, + xl: 1200px, + xxl: 1400px +) !default; \ No newline at end of file diff --git a/app/assets/stylesheets/bootstrap.min.css b/app/assets/stylesheets/bootstrap.min.css new file mode 100644 index 0000000..cf842d3 --- /dev/null +++ b/app/assets/stylesheets/bootstrap.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.0.0-alpha.6 (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors + * Copyright 2011-2017 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}@media print{*,::after,::before,blockquote::first-letter,blockquote::first-line,div::first-letter,div::first-line,li::first-letter,li::first-line,p::first-letter,p::first-line{text-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,::after,::before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}html{-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}body{font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#292b2c;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{cursor:help}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle}[role=button]{cursor:pointer}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse;background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#636c72;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,select,textarea{line-height:inherit}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit}input[type=search]{-webkit-appearance:none}output{display:inline-block}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:5px}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;margin-bottom:1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote-footer{display:block;font-size:80%;color:#636c72}.blockquote-footer::before{content:"\2014 \00A0"}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse .blockquote-footer::before{content:""}.blockquote-reverse .blockquote-footer::after{content:"\00A0 \2014"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#636c72}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{padding:.2rem .4rem;font-size:90%;color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}a>code{padding:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1500px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1500px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1500px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1500px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1500px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1500px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1500px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1500px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1500px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1500px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1500px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1500px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1500px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file diff --git a/app/assets/stylesheets/bootstrap.min.css_codepen b/app/assets/stylesheets/bootstrap.min.css_codepen new file mode 100644 index 0000000..a8da074 --- /dev/null +++ b/app/assets/stylesheets/bootstrap.min.css_codepen @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.0.0-alpha.6 (https://getbootstrap.com) + * Copyright 2011-2017 The Bootstrap Authors + * Copyright 2011-2017 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + *//*! normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}template{display:none}[hidden]{display:none}@media print{*,::after,::before,blockquote::first-letter,blockquote::first-line,div::first-letter,div::first-line,li::first-letter,li::first-line,p::first-letter,p::first-line{text-shadow:none!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap!important}blockquote,pre{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}h2,h3,p{orphans:3;widows:3}h2,h3{page-break-after:avoid}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse!important}.table td,.table th{background-color:#fff!important}.table-bordered td,.table-bordered th{border:1px solid #ddd!important}}html{-webkit-box-sizing:border-box;box-sizing:border-box}*,::after,::before{-webkit-box-sizing:inherit;box-sizing:inherit}@-ms-viewport{width:device-width}html{-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:transparent}body{font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-size:1rem;font-weight:400;line-height:1.5;color:#292b2c;background-color:#fff}[tabindex="-1"]:focus{outline:0!important}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[data-original-title],abbr[title]{cursor:help}address{margin-bottom:1rem;font-style:normal;line-height:inherit}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}a{color:#0275d8;text-decoration:none}a:focus,a:hover{color:#014c8c;text-decoration:underline}a:not([href]):not([tabindex]){color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus,a:not([href]):not([tabindex]):hover{color:inherit;text-decoration:none}a:not([href]):not([tabindex]):focus{outline:0}pre{margin-top:0;margin-bottom:1rem;overflow:auto}figure{margin:0 0 1rem}img{vertical-align:middle}[role=button]{cursor:pointer}[role=button],a,area,button,input,label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse;background-color:transparent}caption{padding-top:.75rem;padding-bottom:.75rem;color:#636c72;text-align:left;caption-side:bottom}th{text-align:left}label{display:inline-block;margin-bottom:.5rem}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,select,textarea{line-height:inherit}input[type=checkbox]:disabled,input[type=radio]:disabled{cursor:not-allowed}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit}input[type=search]{-webkit-appearance:none}output{display:inline-block}[hidden]{display:none!important}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-bottom:.5rem;font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.h1,h1{font-size:2.5rem}.h2,h2{font-size:2rem}.h3,h3{font-size:1.75rem}.h4,h4{font-size:1.5rem}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.1}.display-2{font-size:5.5rem;font-weight:300;line-height:1.1}.display-3{font-size:4.5rem;font-weight:300;line-height:1.1}.display-4{font-size:3.5rem;font-weight:300;line-height:1.1}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,.1)}.small,small{font-size:80%;font-weight:400}.mark,mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:5px}.initialism{font-size:90%;text-transform:uppercase}.blockquote{padding:.5rem 1rem;margin-bottom:1rem;font-size:1.25rem;border-left:.25rem solid #eceeef}.blockquote-footer{display:block;font-size:80%;color:#636c72}.blockquote-footer::before{content:"\2014 \00A0"}.blockquote-reverse{padding-right:1rem;padding-left:0;text-align:right;border-right:.25rem solid #eceeef;border-left:0}.blockquote-reverse .blockquote-footer::before{content:""}.blockquote-reverse .blockquote-footer::after{content:"\00A0 \2014"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #ddd;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#636c72}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}code{padding:.2rem .4rem;font-size:90%;color:#bd4147;background-color:#f7f7f9;border-radius:.25rem}a>code{padding:0;color:inherit;background-color:inherit}kbd{padding:.2rem .4rem;font-size:90%;color:#fff;background-color:#292b2c;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;margin-top:0;margin-bottom:1rem;font-size:90%;color:#292b2c}pre code{padding:0;font-size:inherit;color:inherit;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container{padding-right:15px;padding-left:15px}}@media (min-width:576px){.container{width:540px;max-width:100%}}@media (min-width:768px){.container{width:720px;max-width:100%}}@media (min-width:992px){.container{width:960px;max-width:100%}}@media (min-width:1200px){.container{width:1140px;max-width:100%}}.container-fluid{position:relative;margin-left:auto;margin-right:auto;padding-right:15px;padding-left:15px}@media (min-width:576px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:768px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:992px){.container-fluid{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.container-fluid{padding-right:15px;padding-left:15px}}.row{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}@media (min-width:576px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:768px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:992px){.row{margin-right:-15px;margin-left:-15px}}@media (min-width:1200px){.row{margin-right:-15px;margin-left:-15px}}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*=col-]{padding-right:0;padding-left:0}.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{position:relative;width:100%;min-height:1px;padding-right:15px;padding-left:15px}@media (min-width:576px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:768px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:992px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}@media (min-width:1200px){.col,.col-1,.col-10,.col-11,.col-12,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-xl,.col-xl-1,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9{padding-right:15px;padding-left:15px}}.col{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-0{right:auto}.pull-1{right:8.333333%}.pull-2{right:16.666667%}.pull-3{right:25%}.pull-4{right:33.333333%}.pull-5{right:41.666667%}.pull-6{right:50%}.pull-7{right:58.333333%}.pull-8{right:66.666667%}.pull-9{right:75%}.pull-10{right:83.333333%}.pull-11{right:91.666667%}.pull-12{right:100%}.push-0{left:auto}.push-1{left:8.333333%}.push-2{left:16.666667%}.push-3{left:25%}.push-4{left:33.333333%}.push-5{left:41.666667%}.push-6{left:50%}.push-7{left:58.333333%}.push-8{left:66.666667%}.push-9{left:75%}.push-10{left:83.333333%}.push-11{left:91.666667%}.push-12{left:100%}.offset-1{margin-left:8.333333%}.offset-2{margin-left:16.666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.333333%}.offset-5{margin-left:41.666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.333333%}.offset-8{margin-left:66.666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.333333%}.offset-11{margin-left:91.666667%}@media (min-width:576px){.col-sm{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-sm-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-sm-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-sm-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-sm-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-sm-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-sm-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-sm-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-sm-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-sm-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-sm-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-sm-0{right:auto}.pull-sm-1{right:8.333333%}.pull-sm-2{right:16.666667%}.pull-sm-3{right:25%}.pull-sm-4{right:33.333333%}.pull-sm-5{right:41.666667%}.pull-sm-6{right:50%}.pull-sm-7{right:58.333333%}.pull-sm-8{right:66.666667%}.pull-sm-9{right:75%}.pull-sm-10{right:83.333333%}.pull-sm-11{right:91.666667%}.pull-sm-12{right:100%}.push-sm-0{left:auto}.push-sm-1{left:8.333333%}.push-sm-2{left:16.666667%}.push-sm-3{left:25%}.push-sm-4{left:33.333333%}.push-sm-5{left:41.666667%}.push-sm-6{left:50%}.push-sm-7{left:58.333333%}.push-sm-8{left:66.666667%}.push-sm-9{left:75%}.push-sm-10{left:83.333333%}.push-sm-11{left:91.666667%}.push-sm-12{left:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.333333%}.offset-sm-2{margin-left:16.666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.333333%}.offset-sm-5{margin-left:41.666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.333333%}.offset-sm-8{margin-left:66.666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.333333%}.offset-sm-11{margin-left:91.666667%}}@media (min-width:768px){.col-md{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-md-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-md-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-md-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-md-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-md-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-md-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-md-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-md-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-md-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-md-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-md-0{right:auto}.pull-md-1{right:8.333333%}.pull-md-2{right:16.666667%}.pull-md-3{right:25%}.pull-md-4{right:33.333333%}.pull-md-5{right:41.666667%}.pull-md-6{right:50%}.pull-md-7{right:58.333333%}.pull-md-8{right:66.666667%}.pull-md-9{right:75%}.pull-md-10{right:83.333333%}.pull-md-11{right:91.666667%}.pull-md-12{right:100%}.push-md-0{left:auto}.push-md-1{left:8.333333%}.push-md-2{left:16.666667%}.push-md-3{left:25%}.push-md-4{left:33.333333%}.push-md-5{left:41.666667%}.push-md-6{left:50%}.push-md-7{left:58.333333%}.push-md-8{left:66.666667%}.push-md-9{left:75%}.push-md-10{left:83.333333%}.push-md-11{left:91.666667%}.push-md-12{left:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.333333%}.offset-md-2{margin-left:16.666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.333333%}.offset-md-5{margin-left:41.666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.333333%}.offset-md-8{margin-left:66.666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.333333%}.offset-md-11{margin-left:91.666667%}}@media (min-width:992px){.col-lg{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-lg-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-lg-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-lg-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-lg-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-lg-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-lg-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-lg-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-lg-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-lg-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-lg-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-lg-0{right:auto}.pull-lg-1{right:8.333333%}.pull-lg-2{right:16.666667%}.pull-lg-3{right:25%}.pull-lg-4{right:33.333333%}.pull-lg-5{right:41.666667%}.pull-lg-6{right:50%}.pull-lg-7{right:58.333333%}.pull-lg-8{right:66.666667%}.pull-lg-9{right:75%}.pull-lg-10{right:83.333333%}.pull-lg-11{right:91.666667%}.pull-lg-12{right:100%}.push-lg-0{left:auto}.push-lg-1{left:8.333333%}.push-lg-2{left:16.666667%}.push-lg-3{left:25%}.push-lg-4{left:33.333333%}.push-lg-5{left:41.666667%}.push-lg-6{left:50%}.push-lg-7{left:58.333333%}.push-lg-8{left:66.666667%}.push-lg-9{left:75%}.push-lg-10{left:83.333333%}.push-lg-11{left:91.666667%}.push-lg-12{left:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.333333%}.offset-lg-2{margin-left:16.666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.333333%}.offset-lg-5{margin-left:41.666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.333333%}.offset-lg-8{margin-left:66.666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.333333%}.offset-lg-11{margin-left:91.666667%}}@media (min-width:1200px){.col-xl{-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;max-width:100%}.col-xl-auto{-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;width:auto}.col-xl-1{-webkit-box-flex:0;-webkit-flex:0 0 8.333333%;-ms-flex:0 0 8.333333%;flex:0 0 8.333333%;max-width:8.333333%}.col-xl-2{-webkit-box-flex:0;-webkit-flex:0 0 16.666667%;-ms-flex:0 0 16.666667%;flex:0 0 16.666667%;max-width:16.666667%}.col-xl-3{-webkit-box-flex:0;-webkit-flex:0 0 25%;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-webkit-flex:0 0 33.333333%;-ms-flex:0 0 33.333333%;flex:0 0 33.333333%;max-width:33.333333%}.col-xl-5{-webkit-box-flex:0;-webkit-flex:0 0 41.666667%;-ms-flex:0 0 41.666667%;flex:0 0 41.666667%;max-width:41.666667%}.col-xl-6{-webkit-box-flex:0;-webkit-flex:0 0 50%;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-webkit-flex:0 0 58.333333%;-ms-flex:0 0 58.333333%;flex:0 0 58.333333%;max-width:58.333333%}.col-xl-8{-webkit-box-flex:0;-webkit-flex:0 0 66.666667%;-ms-flex:0 0 66.666667%;flex:0 0 66.666667%;max-width:66.666667%}.col-xl-9{-webkit-box-flex:0;-webkit-flex:0 0 75%;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-webkit-flex:0 0 83.333333%;-ms-flex:0 0 83.333333%;flex:0 0 83.333333%;max-width:83.333333%}.col-xl-11{-webkit-box-flex:0;-webkit-flex:0 0 91.666667%;-ms-flex:0 0 91.666667%;flex:0 0 91.666667%;max-width:91.666667%}.col-xl-12{-webkit-box-flex:0;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.pull-xl-0{right:auto}.pull-xl-1{right:8.333333%}.pull-xl-2{right:16.666667%}.pull-xl-3{right:25%}.pull-xl-4{right:33.333333%}.pull-xl-5{right:41.666667%}.pull-xl-6{right:50%}.pull-xl-7{right:58.333333%}.pull-xl-8{right:66.666667%}.pull-xl-9{right:75%}.pull-xl-10{right:83.333333%}.pull-xl-11{right:91.666667%}.pull-xl-12{right:100%}.push-xl-0{left:auto}.push-xl-1{left:8.333333%}.push-xl-2{left:16.666667%}.push-xl-3{left:25%}.push-xl-4{left:33.333333%}.push-xl-5{left:41.666667%}.push-xl-6{left:50%}.push-xl-7{left:58.333333%}.push-xl-8{left:66.666667%}.push-xl-9{left:75%}.push-xl-10{left:83.333333%}.push-xl-11{left:91.666667%}.push-xl-12{left:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.333333%}.offset-xl-2{margin-left:16.666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.333333%}.offset-xl-5{margin-left:41.666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.333333%}.offset-xl-8{margin-left:66.666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.333333%}.offset-xl-11{margin-left:91.666667%}}.table{width:100%;max-width:100%;margin-bottom:1rem}.table td,.table th{padding:.75rem;vertical-align:top;border-top:1px solid #eceeef}.table thead th{vertical-align:bottom;border-bottom:2px solid #eceeef}.table tbody+tbody{border-top:2px solid #eceeef}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:.3rem}.table-bordered{border:1px solid #eceeef}.table-bordered td,.table-bordered th{border:1px solid #eceeef}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-active,.table-active>td,.table-active>th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,.075)}.table-success,.table-success>td,.table-success>th{background-color:#dff0d8}.table-hover .table-success:hover{background-color:#d0e9c6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#d0e9c6}.table-info,.table-info>td,.table-info>th{background-color:#d9edf7}.table-hover .table-info:hover{background-color:#c4e3f3}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#c4e3f3}.table-warning,.table-warning>td,.table-warning>th{background-color:#fcf8e3}.table-hover .table-warning:hover{background-color:#faf2cc}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#faf2cc}.table-danger,.table-danger>td,.table-danger>th{background-color:#f2dede}.table-hover .table-danger:hover{background-color:#ebcccc}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebcccc}.thead-inverse th{color:#fff;background-color:#292b2c}.thead-default th{color:#464a4c;background-color:#eceeef}.table-inverse{color:#fff;background-color:#292b2c}.table-inverse td,.table-inverse th,.table-inverse thead th{border-color:#fff}.table-inverse.table-bordered{border:0}.table-responsive{display:block;width:100%;overflow-x:auto;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive.table-bordered{border:0}.form-control{display:block;width:100%;padding:.5rem .75rem;font-size:1rem;line-height:1.25;color:#464a4c;background-color:#fff;background-image:none;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:focus{color:#464a4c;background-color:#fff;border-color:#5cb3fd;outline:0}.form-control::-webkit-input-placeholder{color:#636c72;opacity:1}.form-control::-moz-placeholder{color:#636c72;opacity:1}.form-control:-ms-input-placeholder{color:#636c72;opacity:1}.form-control::placeholder{color:#636c72;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#eceeef;opacity:1}.form-control:disabled{cursor:not-allowed}select.form-control:not([size]):not([multiple]){height:calc(2.25rem + 2px)}select.form-control:focus::-ms-value{color:#464a4c;background-color:#fff}.form-control-file,.form-control-range{display:block}.col-form-label{padding-top:calc(.5rem - 1px * 2);padding-bottom:calc(.5rem - 1px * 2);margin-bottom:0}.col-form-label-lg{padding-top:calc(.75rem - 1px * 2);padding-bottom:calc(.75rem - 1px * 2);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem - 1px * 2);padding-bottom:calc(.25rem - 1px * 2);font-size:.875rem}.col-form-legend{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;font-size:1rem}.form-control-static{padding-top:.5rem;padding-bottom:.5rem;margin-bottom:0;line-height:1.25;border:solid transparent;border-width:1px 0}.form-control-static.form-control-lg,.form-control-static.form-control-sm,.input-group-lg>.form-control-static.form-control,.input-group-lg>.form-control-static.input-group-addon,.input-group-lg>.input-group-btn>.form-control-static.btn,.input-group-sm>.form-control-static.form-control,.input-group-sm>.form-control-static.input-group-addon,.input-group-sm>.input-group-btn>.form-control-static.btn{padding-right:0;padding-left:0}.form-control-sm,.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-sm>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-sm>select.form-control:not([size]):not([multiple]),.input-group-sm>select.input-group-addon:not([size]):not([multiple]),select.form-control-sm:not([size]):not([multiple]){height:1.8125rem}.form-control-lg,.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-lg>.input-group-btn>select.btn:not([size]):not([multiple]),.input-group-lg>select.form-control:not([size]):not([multiple]),.input-group-lg>select.input-group-addon:not([size]):not([multiple]),select.form-control-lg:not([size]):not([multiple]){height:3.166667rem}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-check{position:relative;display:block;margin-bottom:.5rem}.form-check.disabled .form-check-label{color:#636c72;cursor:not-allowed}.form-check-label{padding-left:1.25rem;margin-bottom:0;cursor:pointer}.form-check-input{position:absolute;margin-top:.25rem;margin-left:-1.25rem}.form-check-input:only-child{position:static}.form-check-inline{display:inline-block}.form-check-inline .form-check-label{vertical-align:middle}.form-check-inline+.form-check-inline{margin-left:.75rem}.form-control-feedback{margin-top:.25rem}.form-control-danger,.form-control-success,.form-control-warning{padding-right:2.25rem;background-repeat:no-repeat;background-position:center right .5625rem;-webkit-background-size:1.125rem 1.125rem;background-size:1.125rem 1.125rem}.has-success .col-form-label,.has-success .custom-control,.has-success .form-check-label,.has-success .form-control-feedback,.has-success .form-control-label{color:#5cb85c}.has-success .form-control{border-color:#5cb85c}.has-success .input-group-addon{color:#5cb85c;border-color:#5cb85c;background-color:#eaf6ea}.has-success .form-control-success{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%235cb85c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3E%3C/svg%3E")}.has-warning .col-form-label,.has-warning .custom-control,.has-warning .form-check-label,.has-warning .form-control-feedback,.has-warning .form-control-label{color:#f0ad4e}.has-warning .form-control{border-color:#f0ad4e}.has-warning .input-group-addon{color:#f0ad4e;border-color:#f0ad4e;background-color:#fff}.has-warning .form-control-warning{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23f0ad4e' d='M4.4 5.324h-.8v-2.46h.8zm0 1.42h-.8V5.89h.8zM3.76.63L.04 7.075c-.115.2.016.425.26.426h7.397c.242 0 .372-.226.258-.426C6.726 4.924 5.47 2.79 4.253.63c-.113-.174-.39-.174-.494 0z'/%3E%3C/svg%3E")}.has-danger .col-form-label,.has-danger .custom-control,.has-danger .form-check-label,.has-danger .form-control-feedback,.has-danger .form-control-label{color:#d9534f}.has-danger .form-control{border-color:#d9534f}.has-danger .input-group-addon{color:#d9534f;border-color:#d9534f;background-color:#fdf7f7}.has-danger .form-control-danger{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23d9534f' viewBox='-2 -2 7 7'%3E%3Cpath stroke='%23d9534f' d='M0 0l3 3m0-3L0 3'/%3E%3Ccircle r='.5'/%3E%3Ccircle cx='3' r='.5'/%3E%3Ccircle cy='3' r='.5'/%3E%3Ccircle cx='3' cy='3' r='.5'/%3E%3C/svg%3E")}.form-inline{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width:576px){.form-inline label{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-static{display:inline-block}.form-inline .input-group{width:auto}.form-inline .form-control-label{margin-bottom:0;vertical-align:middle}.form-inline .form-check{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:auto;margin-top:0;margin-bottom:0}.form-inline .form-check-label{padding-left:0}.form-inline .form-check-input{position:relative;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0}.form-inline .custom-control-indicator{position:static;display:inline-block;margin-right:.25rem;vertical-align:text-bottom}.form-inline .has-feedback .form-control-feedback{top:0}}.btn{display:inline-block;font-weight:400;line-height:1.25;text-align:center;white-space:nowrap;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;border:1px solid transparent;padding:.5rem 1rem;font-size:1rem;border-radius:.25rem;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.btn:focus,.btn:hover{text-decoration:none}.btn.focus,.btn:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.25);box-shadow:0 0 0 2px rgba(2,117,216,.25)}.btn.disabled,.btn:disabled{cursor:not-allowed;opacity:.65}.btn.active,.btn:active{background-image:none}a.btn.disabled,fieldset[disabled] a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-primary:hover{color:#fff;background-color:#025aa5;border-color:#01549b}.btn-primary.focus,.btn-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-primary.disabled,.btn-primary:disabled{background-color:#0275d8;border-color:#0275d8}.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#025aa5;background-image:none;border-color:#01549b}.btn-secondary{color:#292b2c;background-color:#fff;border-color:#ccc}.btn-secondary:hover{color:#292b2c;background-color:#e6e6e6;border-color:#adadad}.btn-secondary.focus,.btn-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-secondary.disabled,.btn-secondary:disabled{background-color:#fff;border-color:#ccc}.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#292b2c;background-color:#e6e6e6;background-image:none;border-color:#adadad}.btn-info{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-info:hover{color:#fff;background-color:#31b0d5;border-color:#2aabd2}.btn-info.focus,.btn-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-info.disabled,.btn-info:disabled{background-color:#5bc0de;border-color:#5bc0de}.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#31b0d5;background-image:none;border-color:#2aabd2}.btn-success{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-success:hover{color:#fff;background-color:#449d44;border-color:#419641}.btn-success.focus,.btn-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-success.disabled,.btn-success:disabled{background-color:#5cb85c;border-color:#5cb85c}.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#449d44;background-image:none;border-color:#419641}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning:hover{color:#fff;background-color:#ec971f;border-color:#eb9316}.btn-warning.focus,.btn-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-warning.disabled,.btn-warning:disabled{background-color:#f0ad4e;border-color:#f0ad4e}.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#ec971f;background-image:none;border-color:#eb9316}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-danger:hover{color:#fff;background-color:#c9302c;border-color:#c12e2a}.btn-danger.focus,.btn-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-danger.disabled,.btn-danger:disabled{background-color:#d9534f;border-color:#d9534f}.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#c9302c;background-image:none;border-color:#c12e2a}.btn-outline-primary{color:#0275d8;background-image:none;background-color:transparent;border-color:#0275d8}.btn-outline-primary:hover{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-primary.focus,.btn-outline-primary:focus{-webkit-box-shadow:0 0 0 2px rgba(2,117,216,.5);box-shadow:0 0 0 2px rgba(2,117,216,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0275d8;background-color:transparent}.btn-outline-primary.active,.btn-outline-primary:active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#0275d8;border-color:#0275d8}.btn-outline-secondary{color:#ccc;background-image:none;background-color:transparent;border-color:#ccc}.btn-outline-secondary:hover{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-secondary.focus,.btn-outline-secondary:focus{-webkit-box-shadow:0 0 0 2px rgba(204,204,204,.5);box-shadow:0 0 0 2px rgba(204,204,204,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#ccc;background-color:transparent}.btn-outline-secondary.active,.btn-outline-secondary:active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#ccc;border-color:#ccc}.btn-outline-info{color:#5bc0de;background-image:none;background-color:transparent;border-color:#5bc0de}.btn-outline-info:hover{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-info.focus,.btn-outline-info:focus{-webkit-box-shadow:0 0 0 2px rgba(91,192,222,.5);box-shadow:0 0 0 2px rgba(91,192,222,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#5bc0de;background-color:transparent}.btn-outline-info.active,.btn-outline-info:active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#5bc0de;border-color:#5bc0de}.btn-outline-success{color:#5cb85c;background-image:none;background-color:transparent;border-color:#5cb85c}.btn-outline-success:hover{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-success.focus,.btn-outline-success:focus{-webkit-box-shadow:0 0 0 2px rgba(92,184,92,.5);box-shadow:0 0 0 2px rgba(92,184,92,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#5cb85c;background-color:transparent}.btn-outline-success.active,.btn-outline-success:active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#5cb85c;border-color:#5cb85c}.btn-outline-warning{color:#f0ad4e;background-image:none;background-color:transparent;border-color:#f0ad4e}.btn-outline-warning:hover{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-warning.focus,.btn-outline-warning:focus{-webkit-box-shadow:0 0 0 2px rgba(240,173,78,.5);box-shadow:0 0 0 2px rgba(240,173,78,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#f0ad4e;background-color:transparent}.btn-outline-warning.active,.btn-outline-warning:active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#f0ad4e;border-color:#f0ad4e}.btn-outline-danger{color:#d9534f;background-image:none;background-color:transparent;border-color:#d9534f}.btn-outline-danger:hover{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-outline-danger.focus,.btn-outline-danger:focus{-webkit-box-shadow:0 0 0 2px rgba(217,83,79,.5);box-shadow:0 0 0 2px rgba(217,83,79,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#d9534f;background-color:transparent}.btn-outline-danger.active,.btn-outline-danger:active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#d9534f;border-color:#d9534f}.btn-link{font-weight:400;color:#0275d8;border-radius:0}.btn-link,.btn-link.active,.btn-link:active,.btn-link:disabled{background-color:transparent}.btn-link,.btn-link:active,.btn-link:focus{border-color:transparent}.btn-link:hover{border-color:transparent}.btn-link:focus,.btn-link:hover{color:#014c8c;text-decoration:underline;background-color:transparent}.btn-link:disabled{color:#636c72}.btn-link:disabled:focus,.btn-link:disabled:hover{text-decoration:none}.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type=button].btn-block,input[type=reset].btn-block,input[type=submit].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.show{opacity:1}.collapse{display:none}.collapse.show{display:block}tr.collapse.show{display:table-row}tbody.collapse.show{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.dropdown,.dropup{position:relative}.dropdown-toggle::after{display:inline-block;width:0;height:0;margin-left:.3em;vertical-align:middle;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-left:.3em solid transparent}.dropdown-toggle:focus{outline:0}.dropup .dropdown-toggle::after{border-top:0;border-bottom:.3em solid}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#292b2c;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-divider{height:1px;margin:.5rem 0;overflow:hidden;background-color:#eceeef}.dropdown-item{display:block;width:100%;padding:3px 1.5rem;clear:both;font-weight:400;color:#292b2c;text-align:inherit;white-space:nowrap;background:0 0;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1d1e1f;text-decoration:none;background-color:#f7f7f9}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0275d8}.dropdown-item.disabled,.dropdown-item:disabled{color:#636c72;cursor:not-allowed;background-color:transparent}.show>.dropdown-menu{display:block}.show>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#636c72;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.dropup .dropdown-menu{top:auto;bottom:100%;margin-bottom:.125rem}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.btn-group-vertical>.btn:hover,.btn-group>.btn:hover{z-index:2}.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus{z-index:2}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group,.btn-group-vertical .btn+.btn,.btn-group-vertical .btn+.btn-group,.btn-group-vertical .btn-group+.btn,.btn-group-vertical .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child:not(:first-child)>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn+.dropdown-toggle-split::after{margin-left:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:1.125rem;padding-left:1.125rem}.btn-group-vertical{display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical .btn,.btn-group-vertical .btn-group{width:100%}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}[data-toggle=buttons]>.btn input[type=checkbox],[data-toggle=buttons]>.btn input[type=radio],[data-toggle=buttons]>.btn-group>.btn input[type=checkbox],[data-toggle=buttons]>.btn-group>.btn input[type=radio]{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;width:100%}.input-group .form-control{position:relative;z-index:2;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;margin-bottom:0}.input-group .form-control:active,.input-group .form-control:focus,.input-group .form-control:hover{z-index:3}.input-group .form-control,.input-group-addon,.input-group-btn{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center}.input-group .form-control:not(:first-child):not(:last-child),.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{white-space:nowrap;vertical-align:middle}.input-group-addon{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.25;color:#464a4c;text-align:center;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.input-group-addon.form-control-sm,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.input-group-addon.btn{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-addon.form-control-lg,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.input-group-addon.btn{padding:.75rem 1.5rem;font-size:1.25rem;border-radius:.3rem}.input-group-addon input[type=checkbox],.input-group-addon input[type=radio]{margin-top:0}.input-group .form-control:not(:last-child),.input-group-addon:not(:last-child),.input-group-btn:not(:first-child)>.btn-group:not(:last-child)>.btn,.input-group-btn:not(:first-child)>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group>.btn,.input-group-btn:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:not(:last-child){border-right:0}.input-group .form-control:not(:first-child),.input-group-addon:not(:first-child),.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group>.btn,.input-group-btn:not(:first-child)>.dropdown-toggle,.input-group-btn:not(:last-child)>.btn-group:not(:first-child)>.btn,.input-group-btn:not(:last-child)>.btn:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.form-control+.input-group-addon:not(:first-child){border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:active,.input-group-btn>.btn:focus,.input-group-btn>.btn:hover{z-index:3}.input-group-btn:not(:last-child)>.btn,.input-group-btn:not(:last-child)>.btn-group{margin-right:-1px}.input-group-btn:not(:first-child)>.btn,.input-group-btn:not(:first-child)>.btn-group{z-index:2;margin-left:-1px}.input-group-btn:not(:first-child)>.btn-group:active,.input-group-btn:not(:first-child)>.btn-group:focus,.input-group-btn:not(:first-child)>.btn-group:hover,.input-group-btn:not(:first-child)>.btn:active,.input-group-btn:not(:first-child)>.btn:focus,.input-group-btn:not(:first-child)>.btn:hover{z-index:3}.custom-control{position:relative;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;min-height:1.5rem;padding-left:1.5rem;margin-right:1rem;cursor:pointer}.custom-control-input{position:absolute;z-index:-1;opacity:0}.custom-control-input:checked~.custom-control-indicator{color:#fff;background-color:#0275d8}.custom-control-input:focus~.custom-control-indicator{-webkit-box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8;box-shadow:0 0 0 1px #fff,0 0 0 3px #0275d8}.custom-control-input:active~.custom-control-indicator{color:#fff;background-color:#8fcafe}.custom-control-input:disabled~.custom-control-indicator{cursor:not-allowed;background-color:#eceeef}.custom-control-input:disabled~.custom-control-description{color:#636c72;cursor:not-allowed}.custom-control-indicator{position:absolute;top:.25rem;left:0;display:block;width:1rem;height:1rem;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#ddd;background-repeat:no-repeat;background-position:center center;-webkit-background-size:50% 50%;background-size:50% 50%}.custom-checkbox .custom-control-indicator{border-radius:.25rem}.custom-checkbox .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3E%3Cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z'/%3E%3C/svg%3E")}.custom-checkbox .custom-control-input:indeterminate~.custom-control-indicator{background-color:#0275d8;background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 4'%3E%3Cpath stroke='%23fff' d='M0 2h4'/%3E%3C/svg%3E")}.custom-radio .custom-control-indicator{border-radius:50%}.custom-radio .custom-control-input:checked~.custom-control-indicator{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3E%3Ccircle r='3' fill='%23fff'/%3E%3C/svg%3E")}.custom-controls-stacked{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.custom-controls-stacked .custom-control{margin-bottom:.25rem}.custom-controls-stacked .custom-control+.custom-control{margin-left:0}.custom-select{display:inline-block;max-width:100%;height:calc(2.25rem + 2px);padding:.375rem 1.75rem .375rem .75rem;line-height:1.25;color:#464a4c;vertical-align:middle;background:#fff url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 4 5'%3E%3Cpath fill='%23333' d='M2 0L0 2h4zm0 5L0 3h4z'/%3E%3C/svg%3E") no-repeat right .75rem center;-webkit-background-size:8px 10px;background-size:8px 10px;border:1px solid rgba(0,0,0,.15);border-radius:.25rem;-moz-appearance:none;-webkit-appearance:none}.custom-select:focus{border-color:#5cb3fd;outline:0}.custom-select:focus::-ms-value{color:#464a4c;background-color:#fff}.custom-select:disabled{color:#636c72;cursor:not-allowed;background-color:#eceeef}.custom-select::-ms-expand{opacity:0}.custom-select-sm{padding-top:.375rem;padding-bottom:.375rem;font-size:75%}.custom-file{position:relative;display:inline-block;max-width:100%;height:2.5rem;margin-bottom:0;cursor:pointer}.custom-file-input{min-width:14rem;max-width:100%;height:2.5rem;margin:0;filter:alpha(opacity=0);opacity:0}.custom-file-control{position:absolute;top:0;right:0;left:0;z-index:5;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#fff;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.custom-file-control:lang(en)::after{content:"Choose file..."}.custom-file-control::before{position:absolute;top:-1px;right:-1px;bottom:-1px;z-index:6;display:block;height:2.5rem;padding:.5rem 1rem;line-height:1.5;color:#464a4c;background-color:#eceeef;border:1px solid rgba(0,0,0,.15);border-radius:0 .25rem .25rem 0}.custom-file-control:lang(en)::before{content:"Browse"}.nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5em 1em}.nav-link:focus,.nav-link:hover{text-decoration:none}.nav-link.disabled{color:#636c72;cursor:not-allowed}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-right-radius:.25rem;border-top-left-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#eceeef #eceeef #ddd}.nav-tabs .nav-link.disabled{color:#636c72;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#464a4c;background-color:#fff;border-color:#ddd #ddd #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-item.show .nav-link,.nav-pills .nav-link.active{color:#fff;cursor:default;background-color:#0275d8}.nav-fill .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-webkit-box-flex:1;-webkit-flex:1 1 100%;-ms-flex:1 1 100%;flex:1 1 100%;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding:.5rem 1rem}.navbar-brand{display:inline-block;padding-top:.25rem;padding-bottom:.25rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{text-decoration:none}.navbar-nav{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-text{display:inline-block;padding-top:.425rem;padding-bottom:.425rem}.navbar-toggler{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start;padding:.25rem .75rem;font-size:1.25rem;line-height:1;background:0 0;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:focus,.navbar-toggler:hover{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.navbar-toggler-left{position:absolute;left:1rem}.navbar-toggler-right{position:absolute;right:1rem}@media (max-width:575px){.navbar-toggleable .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable>.container{padding-right:0;padding-left:0}}@media (min-width:576px){.navbar-toggleable{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable .navbar-toggler{display:none}}@media (max-width:767px){.navbar-toggleable-sm .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-sm>.container{padding-right:0;padding-left:0}}@media (min-width:768px){.navbar-toggleable-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-sm>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-sm .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-sm .navbar-toggler{display:none}}@media (max-width:991px){.navbar-toggleable-md .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-md>.container{padding-right:0;padding-left:0}}@media (min-width:992px){.navbar-toggleable-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-md>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-md .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-md .navbar-toggler{display:none}}@media (max-width:1199px){.navbar-toggleable-lg .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-lg>.container{padding-right:0;padding-left:0}}@media (min-width:1200px){.navbar-toggleable-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-lg>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-lg .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-lg .navbar-toggler{display:none}}.navbar-toggleable-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-nav .dropdown-menu{position:static;float:none}.navbar-toggleable-xl>.container{padding-right:0;padding-left:0}.navbar-toggleable-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.navbar-toggleable-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-toggleable-xl>.container{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.navbar-toggleable-xl .navbar-collapse{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important;width:100%}.navbar-toggleable-xl .navbar-toggler{display:none}.navbar-light .navbar-brand,.navbar-light .navbar-toggler{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover,.navbar-light .navbar-toggler:focus,.navbar-light .navbar-toggler:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.5)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .nav-link.open,.navbar-light .navbar-nav .open>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(0, 0, 0, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-light .navbar-text{color:rgba(0,0,0,.5)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-toggler{color:#fff}.navbar-inverse .navbar-brand:focus,.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-toggler:focus,.navbar-inverse .navbar-toggler:hover{color:#fff}.navbar-inverse .navbar-nav .nav-link{color:rgba(255,255,255,.5)}.navbar-inverse .navbar-nav .nav-link:focus,.navbar-inverse .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-inverse .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-inverse .navbar-nav .active>.nav-link,.navbar-inverse .navbar-nav .nav-link.active,.navbar-inverse .navbar-nav .nav-link.open,.navbar-inverse .navbar-nav .open>.nav-link{color:#fff}.navbar-inverse .navbar-toggler{border-color:rgba(255,255,255,.1)}.navbar-inverse .navbar-toggler-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath stroke='rgba(255, 255, 255, 0.5)' stroke-width='2' stroke-linecap='round' stroke-miterlimit='10' d='M4 8h24M4 16h24M4 24h24'/%3E%3C/svg%3E")}.navbar-inverse .navbar-text{color:rgba(255,255,255,.5)}.card{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card-block{-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card>.list-group:first-child .list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.card>.list-group:last-child .list-group-item:last-child{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:#f7f7f9;border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.75rem 1.25rem;background-color:#f7f7f9;border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-primary{background-color:#0275d8;border-color:#0275d8}.card-primary .card-footer,.card-primary .card-header{background-color:transparent}.card-success{background-color:#5cb85c;border-color:#5cb85c}.card-success .card-footer,.card-success .card-header{background-color:transparent}.card-info{background-color:#5bc0de;border-color:#5bc0de}.card-info .card-footer,.card-info .card-header{background-color:transparent}.card-warning{background-color:#f0ad4e;border-color:#f0ad4e}.card-warning .card-footer,.card-warning .card-header{background-color:transparent}.card-danger{background-color:#d9534f;border-color:#d9534f}.card-danger .card-footer,.card-danger .card-header{background-color:transparent}.card-outline-primary{background-color:transparent;border-color:#0275d8}.card-outline-secondary{background-color:transparent;border-color:#ccc}.card-outline-info{background-color:transparent;border-color:#5bc0de}.card-outline-success{background-color:transparent;border-color:#5cb85c}.card-outline-warning{background-color:transparent;border-color:#f0ad4e}.card-outline-danger{background-color:transparent;border-color:#d9534f}.card-inverse{color:rgba(255,255,255,.65)}.card-inverse .card-footer,.card-inverse .card-header{background-color:transparent;border-color:rgba(255,255,255,.2)}.card-inverse .card-blockquote,.card-inverse .card-footer,.card-inverse .card-header,.card-inverse .card-title{color:#fff}.card-inverse .card-blockquote .blockquote-footer,.card-inverse .card-link,.card-inverse .card-subtitle,.card-inverse .card-text{color:rgba(255,255,255,.65)}.card-inverse .card-link:focus,.card-inverse .card-link:hover{color:#fff}.card-blockquote{padding:0;margin-bottom:0;border-left:0}.card-img{border-radius:calc(.25rem - 1px)}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img-top{border-top-right-radius:calc(.25rem - 1px);border-top-left-radius:calc(.25rem - 1px)}.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}@media (min-width:576px){.card-deck{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-deck .card{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.card-deck .card:not(:first-child){margin-left:15px}.card-deck .card:not(:last-child){margin-right:15px}}@media (min-width:576px){.card-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group .card{-webkit-box-flex:1;-webkit-flex:1 0 0%;-ms-flex:1 0 0%;flex:1 0 0%}.card-group .card+.card{margin-left:0;border-left:0}.card-group .card:first-child{border-bottom-right-radius:0;border-top-right-radius:0}.card-group .card:first-child .card-img-top{border-top-right-radius:0}.card-group .card:first-child .card-img-bottom{border-bottom-right-radius:0}.card-group .card:last-child{border-bottom-left-radius:0;border-top-left-radius:0}.card-group .card:last-child .card-img-top{border-top-left-radius:0}.card-group .card:last-child .card-img-bottom{border-bottom-left-radius:0}.card-group .card:not(:first-child):not(:last-child){border-radius:0}.card-group .card:not(:first-child):not(:last-child) .card-img-bottom,.card-group .card:not(:first-child):not(:last-child) .card-img-top{border-radius:0}}@media (min-width:576px){.card-columns{-webkit-column-count:3;-moz-column-count:3;column-count:3;-webkit-column-gap:1.25rem;-moz-column-gap:1.25rem;column-gap:1.25rem}.card-columns .card{display:inline-block;width:100%;margin-bottom:.75rem}}.breadcrumb{padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#eceeef;border-radius:.25rem}.breadcrumb::after{display:block;content:"";clear:both}.breadcrumb-item{float:left}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;padding-left:.5rem;color:#636c72;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#636c72}.pagination{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-item:first-child .page-link{margin-left:0;border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.page-item:last-child .page-link{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.page-item.active .page-link{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.page-item.disabled .page-link{color:#636c72;pointer-events:none;cursor:not-allowed;background-color:#fff;border-color:#ddd}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#0275d8;background-color:#fff;border:1px solid #ddd}.page-link:focus,.page-link:hover{color:#014c8c;text-decoration:none;background-color:#eceeef;border-color:#ddd}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-bottom-left-radius:.3rem;border-top-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-bottom-right-radius:.3rem;border-top-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-bottom-left-radius:.2rem;border-top-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-bottom-right-radius:.2rem;border-top-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}a.badge:focus,a.badge:hover{color:#fff;text-decoration:none;cursor:pointer}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-default{background-color:#636c72}.badge-default[href]:focus,.badge-default[href]:hover{background-color:#4b5257}.badge-primary{background-color:#0275d8}.badge-primary[href]:focus,.badge-primary[href]:hover{background-color:#025aa5}.badge-success{background-color:#5cb85c}.badge-success[href]:focus,.badge-success[href]:hover{background-color:#449d44}.badge-info{background-color:#5bc0de}.badge-info[href]:focus,.badge-info[href]:hover{background-color:#31b0d5}.badge-warning{background-color:#f0ad4e}.badge-warning[href]:focus,.badge-warning[href]:hover{background-color:#ec971f}.badge-danger{background-color:#d9534f}.badge-danger[href]:focus,.badge-danger[href]:hover{background-color:#c9302c}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#eceeef;border-radius:.3rem}@media (min-width:576px){.jumbotron{padding:4rem 2rem}}.jumbotron-hr{border-top-color:#d0d5d8}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible .close{position:relative;top:-.75rem;right:-1.25rem;padding:.75rem 1.25rem;color:inherit}.alert-success{background-color:#dff0d8;border-color:#d0e9c6;color:#3c763d}.alert-success hr{border-top-color:#c1e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{background-color:#d9edf7;border-color:#bcdff1;color:#31708f}.alert-info hr{border-top-color:#a6d5ec}.alert-info .alert-link{color:#245269}.alert-warning{background-color:#fcf8e3;border-color:#faf2cc;color:#8a6d3b}.alert-warning hr{border-top-color:#f7ecb5}.alert-warning .alert-link{color:#66512c}.alert-danger{background-color:#f2dede;border-color:#ebcccc;color:#a94442}.alert-danger hr{border-top-color:#e4b9b9}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;overflow:hidden;font-size:.75rem;line-height:1rem;text-align:center;background-color:#eceeef;border-radius:.25rem}.progress-bar{height:1rem;color:#fff;background-color:#0275d8}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:1rem 1rem;background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;-o-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}.media{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-webkit-flex:1 1 0%;-ms-flex:1 1 0%;flex:1 1 0%}.list-group{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0}.list-group-item-action{width:100%;color:#464a4c;text-align:inherit}.list-group-item-action .list-group-item-heading{color:#292b2c}.list-group-item-action:focus,.list-group-item-action:hover{color:#464a4c;text-decoration:none;background-color:#f7f7f9}.list-group-item-action:active{color:#292b2c;background-color:#eceeef}.list-group-item{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;padding:.75rem 1.25rem;margin-bottom:-1px;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.list-group-item:focus,.list-group-item:hover{text-decoration:none}.list-group-item.disabled,.list-group-item:disabled{color:#636c72;cursor:not-allowed;background-color:#fff}.list-group-item.disabled .list-group-item-heading,.list-group-item:disabled .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item:disabled .list-group-item-text{color:#636c72}.list-group-item.active{z-index:2;color:#fff;background-color:#0275d8;border-color:#0275d8}.list-group-item.active .list-group-item-heading,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active .list-group-item-heading>small{color:inherit}.list-group-item.active .list-group-item-text{color:#daeeff}.list-group-flush .list-group-item{border-right:0;border-left:0;border-radius:0}.list-group-flush:first-child .list-group-item:first-child{border-top:0}.list-group-flush:last-child .list-group-item:last-child{border-bottom:0}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success,button.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading,button.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:focus,a.list-group-item-success:hover,button.list-group-item-success:focus,button.list-group-item-success:hover{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,button.list-group-item-success.active{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info,button.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading,button.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:focus,a.list-group-item-info:hover,button.list-group-item-info:focus,button.list-group-item-info:hover{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,button.list-group-item-info.active{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning,button.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading,button.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:focus,a.list-group-item-warning:hover,button.list-group-item-warning:focus,button.list-group-item-warning:hover{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,button.list-group-item-warning.active{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger,button.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading,button.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:focus,a.list-group-item-danger:hover,button.list-group-item-danger:focus,button.list-group-item-danger:hover{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,button.list-group-item-danger.active{color:#fff;background-color:#a94442;border-color:#a94442}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive embed,.embed-responsive iframe,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.857143%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:focus,.close:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.75}button.close{padding:0;cursor:pointer;background:0 0;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;transition:transform .3s ease-out,-webkit-transform .3s ease-out,-o-transform .3s ease-out;-webkit-transform:translate(0,-25%);-o-transform:translate(0,-25%);transform:translate(0,-25%)}.modal.show .modal-dialog{-webkit-transform:translate(0,0);-o-transform:translate(0,0);transform:translate(0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:15px;border-bottom:1px solid #eceeef}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-webkit-flex:1 1 auto;-ms-flex:1 1 auto;flex:1 1 auto;padding:15px}.modal-footer{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;padding:15px;border-top:1px solid #eceeef}.modal-footer>:not(:first-child){margin-left:.25rem}.modal-footer>:not(:last-child){margin-right:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:30px auto}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg{max-width:800px}}.tooltip{position:absolute;z-index:1070;display:block;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip.bs-tether-element-attached-bottom,.tooltip.tooltip-top{padding:5px 0;margin-top:-3px}.tooltip.bs-tether-element-attached-bottom .tooltip-inner::before,.tooltip.tooltip-top .tooltip-inner::before{bottom:0;left:50%;margin-left:-5px;content:"";border-width:5px 5px 0;border-top-color:#000}.tooltip.bs-tether-element-attached-left,.tooltip.tooltip-right{padding:0 5px;margin-left:3px}.tooltip.bs-tether-element-attached-left .tooltip-inner::before,.tooltip.tooltip-right .tooltip-inner::before{top:50%;left:0;margin-top:-5px;content:"";border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.bs-tether-element-attached-top,.tooltip.tooltip-bottom{padding:5px 0;margin-top:3px}.tooltip.bs-tether-element-attached-top .tooltip-inner::before,.tooltip.tooltip-bottom .tooltip-inner::before{top:0;left:50%;margin-left:-5px;content:"";border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bs-tether-element-attached-right,.tooltip.tooltip-left{padding:0 5px;margin-left:-3px}.tooltip.bs-tether-element-attached-right .tooltip-inner::before,.tooltip.tooltip-left .tooltip-inner::before{top:50%;right:0;margin-top:-5px;content:"";border-width:5px 0 5px 5px;border-left-color:#000}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.tooltip-inner::before{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;padding:1px;font-family:-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;font-style:normal;font-weight:400;letter-spacing:normal;line-break:auto;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;white-space:normal;word-break:normal;word-spacing:normal;font-size:.875rem;word-wrap:break-word;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover.bs-tether-element-attached-bottom,.popover.popover-top{margin-top:-10px}.popover.bs-tether-element-attached-bottom::after,.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::after,.popover.popover-top::before{left:50%;border-bottom-width:0}.popover.bs-tether-element-attached-bottom::before,.popover.popover-top::before{bottom:-11px;margin-left:-11px;border-top-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-bottom::after,.popover.popover-top::after{bottom:-10px;margin-left:-10px;border-top-color:#fff}.popover.bs-tether-element-attached-left,.popover.popover-right{margin-left:10px}.popover.bs-tether-element-attached-left::after,.popover.bs-tether-element-attached-left::before,.popover.popover-right::after,.popover.popover-right::before{top:50%;border-left-width:0}.popover.bs-tether-element-attached-left::before,.popover.popover-right::before{left:-11px;margin-top:-11px;border-right-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-left::after,.popover.popover-right::after{left:-10px;margin-top:-10px;border-right-color:#fff}.popover.bs-tether-element-attached-top,.popover.popover-bottom{margin-top:10px}.popover.bs-tether-element-attached-top::after,.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::after,.popover.popover-bottom::before{left:50%;border-top-width:0}.popover.bs-tether-element-attached-top::before,.popover.popover-bottom::before{top:-11px;margin-left:-11px;border-bottom-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-top::after,.popover.popover-bottom::after{top:-10px;margin-left:-10px;border-bottom-color:#f7f7f7}.popover.bs-tether-element-attached-top .popover-title::before,.popover.popover-bottom .popover-title::before{position:absolute;top:0;left:50%;display:block;width:20px;margin-left:-10px;content:"";border-bottom:1px solid #f7f7f7}.popover.bs-tether-element-attached-right,.popover.popover-left{margin-left:-10px}.popover.bs-tether-element-attached-right::after,.popover.bs-tether-element-attached-right::before,.popover.popover-left::after,.popover.popover-left::before{top:50%;border-right-width:0}.popover.bs-tether-element-attached-right::before,.popover.popover-left::before{right:-11px;margin-top:-11px;border-left-color:rgba(0,0,0,.25)}.popover.bs-tether-element-attached-right::after,.popover.popover-left::after{right:-10px;margin-top:-10px;border-left-color:#fff}.popover-title{padding:8px 14px;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-right-radius:calc(.3rem - 1px);border-top-left-radius:calc(.3rem - 1px)}.popover-title:empty{display:none}.popover-content{padding:9px 14px}.popover::after,.popover::before{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover::before{content:"";border-width:11px}.popover::after{content:"";border-width:10px}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-item{position:relative;display:none;width:100%}@media (-webkit-transform-3d){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item{-webkit-transition:-webkit-transform .6s ease-in-out;transition:-webkit-transform .6s ease-in-out;-o-transition:-o-transform .6s ease-in-out;transition:transform .6s ease-in-out;transition:transform .6s ease-in-out,-webkit-transform .6s ease-in-out,-o-transform .6s ease-in-out;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-perspective:1000px;perspective:1000px}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.carousel-item-next,.carousel-item-prev{position:absolute;top:0}@media (-webkit-transform-3d){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@supports ((-webkit-transform:translate3d(0,0,0)) or (transform:translate3d(0,0,0))){.carousel-item-next.carousel-item-left,.carousel-item-prev.carousel-item-right{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.active.carousel-item-right,.carousel-item-next{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}.active.carousel-item-left,.carousel-item-prev{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:20px;height:20px;background:transparent no-repeat center center;-webkit-background-size:100% 100%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M4 0l-4 4 4 4 1.5-1.5-2.5-2.5 2.5-2.5-1.5-1.5z'/%3E%3C/svg%3E")}.carousel-control-next-icon{background-image:url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 8 8'%3E%3Cpath d='M1.5 0l-1.5 1.5 2.5 2.5-2.5 2.5 1.5 1.5 4-4-4-4z'/%3E%3C/svg%3E")}.carousel-indicators{position:absolute;right:0;bottom:10px;left:0;z-index:15;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{position:relative;-webkit-box-flex:1;-webkit-flex:1 0 auto;-ms-flex:1 0 auto;flex:1 0 auto;max-width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:rgba(255,255,255,.5)}.carousel-indicators li::before{position:absolute;top:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators li::after{position:absolute;bottom:-10px;left:0;display:inline-block;width:100%;height:10px;content:""}.carousel-indicators .active{background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.bg-faded{background-color:#f7f7f7}.bg-primary{background-color:#0275d8!important}a.bg-primary:focus,a.bg-primary:hover{background-color:#025aa5!important}.bg-success{background-color:#5cb85c!important}a.bg-success:focus,a.bg-success:hover{background-color:#449d44!important}.bg-info{background-color:#5bc0de!important}a.bg-info:focus,a.bg-info:hover{background-color:#31b0d5!important}.bg-warning{background-color:#f0ad4e!important}a.bg-warning:focus,a.bg-warning:hover{background-color:#ec971f!important}.bg-danger{background-color:#d9534f!important}a.bg-danger:focus,a.bg-danger:hover{background-color:#c9302c!important}.bg-inverse{background-color:#292b2c!important}a.bg-inverse:focus,a.bg-inverse:hover{background-color:#101112!important}.border-0{border:0!important}.border-top-0{border-top:0!important}.border-right-0{border-right:0!important}.border-bottom-0{border-bottom:0!important}.border-left-0{border-left:0!important}.rounded{border-radius:.25rem}.rounded-top{border-top-right-radius:.25rem;border-top-left-radius:.25rem}.rounded-right{border-bottom-right-radius:.25rem;border-top-right-radius:.25rem}.rounded-bottom{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-left{border-bottom-left-radius:.25rem;border-top-left-radius:.25rem}.rounded-circle{border-radius:50%}.rounded-0{border-radius:0}.clearfix::after{display:block;content:"";clear:both}.d-none{display:none!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-table{display:table!important}.d-table-cell{display:table-cell!important}.d-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}@media (min-width:576px){.d-sm-none{display:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-table{display:table!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-sm-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:768px){.d-md-none{display:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-table{display:table!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-md-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:992px){.d-lg-none{display:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-table{display:table!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-lg-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}@media (min-width:1200px){.d-xl-none{display:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-table{display:table!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:-webkit-box!important;display:-webkit-flex!important;display:-ms-flexbox!important;display:flex!important}.d-xl-inline-flex{display:-webkit-inline-box!important;display:-webkit-inline-flex!important;display:-ms-inline-flexbox!important;display:inline-flex!important}}.flex-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}@media (min-width:576px){.flex-sm-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-sm-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-sm-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-sm-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-sm-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-sm-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-sm-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-sm-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-sm-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-sm-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-sm-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-sm-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-sm-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-sm-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-sm-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-sm-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-sm-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-sm-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-sm-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-sm-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-sm-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-sm-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-sm-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-sm-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-sm-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-sm-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-sm-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-sm-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-sm-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-sm-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-sm-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-sm-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:768px){.flex-md-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-md-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-md-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-md-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-md-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-md-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-md-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-md-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-md-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-md-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-md-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-md-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-md-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-md-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-md-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-md-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-md-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-md-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-md-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-md-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-md-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-md-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-md-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-md-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-md-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-md-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-md-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-md-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-md-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-md-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-md-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-md-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:992px){.flex-lg-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-lg-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-lg-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-lg-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-lg-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-lg-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-lg-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-lg-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-lg-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-lg-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-lg-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-lg-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-lg-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-lg-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-lg-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-lg-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-lg-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-lg-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-lg-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-lg-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-lg-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-lg-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-lg-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-lg-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-lg-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-lg-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-lg-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-lg-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-lg-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-lg-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-lg-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-lg-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}@media (min-width:1200px){.flex-xl-first{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.flex-xl-last{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}.flex-xl-unordered{-webkit-box-ordinal-group:1;-webkit-order:0;-ms-flex-order:0;order:0}.flex-xl-row{-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-webkit-flex-direction:row!important;-ms-flex-direction:row!important;flex-direction:row!important}.flex-xl-column{-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-webkit-flex-direction:column!important;-ms-flex-direction:column!important;flex-direction:column!important}.flex-xl-row-reverse{-webkit-box-orient:horizontal!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:row-reverse!important;-ms-flex-direction:row-reverse!important;flex-direction:row-reverse!important}.flex-xl-column-reverse{-webkit-box-orient:vertical!important;-webkit-box-direction:reverse!important;-webkit-flex-direction:column-reverse!important;-ms-flex-direction:column-reverse!important;flex-direction:column-reverse!important}.flex-xl-wrap{-webkit-flex-wrap:wrap!important;-ms-flex-wrap:wrap!important;flex-wrap:wrap!important}.flex-xl-nowrap{-webkit-flex-wrap:nowrap!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important}.flex-xl-wrap-reverse{-webkit-flex-wrap:wrap-reverse!important;-ms-flex-wrap:wrap-reverse!important;flex-wrap:wrap-reverse!important}.justify-content-xl-start{-webkit-box-pack:start!important;-webkit-justify-content:flex-start!important;-ms-flex-pack:start!important;justify-content:flex-start!important}.justify-content-xl-end{-webkit-box-pack:end!important;-webkit-justify-content:flex-end!important;-ms-flex-pack:end!important;justify-content:flex-end!important}.justify-content-xl-center{-webkit-box-pack:center!important;-webkit-justify-content:center!important;-ms-flex-pack:center!important;justify-content:center!important}.justify-content-xl-between{-webkit-box-pack:justify!important;-webkit-justify-content:space-between!important;-ms-flex-pack:justify!important;justify-content:space-between!important}.justify-content-xl-around{-webkit-justify-content:space-around!important;-ms-flex-pack:distribute!important;justify-content:space-around!important}.align-items-xl-start{-webkit-box-align:start!important;-webkit-align-items:flex-start!important;-ms-flex-align:start!important;align-items:flex-start!important}.align-items-xl-end{-webkit-box-align:end!important;-webkit-align-items:flex-end!important;-ms-flex-align:end!important;align-items:flex-end!important}.align-items-xl-center{-webkit-box-align:center!important;-webkit-align-items:center!important;-ms-flex-align:center!important;align-items:center!important}.align-items-xl-baseline{-webkit-box-align:baseline!important;-webkit-align-items:baseline!important;-ms-flex-align:baseline!important;align-items:baseline!important}.align-items-xl-stretch{-webkit-box-align:stretch!important;-webkit-align-items:stretch!important;-ms-flex-align:stretch!important;align-items:stretch!important}.align-content-xl-start{-webkit-align-content:flex-start!important;-ms-flex-line-pack:start!important;align-content:flex-start!important}.align-content-xl-end{-webkit-align-content:flex-end!important;-ms-flex-line-pack:end!important;align-content:flex-end!important}.align-content-xl-center{-webkit-align-content:center!important;-ms-flex-line-pack:center!important;align-content:center!important}.align-content-xl-between{-webkit-align-content:space-between!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.align-content-xl-around{-webkit-align-content:space-around!important;-ms-flex-line-pack:distribute!important;align-content:space-around!important}.align-content-xl-stretch{-webkit-align-content:stretch!important;-ms-flex-line-pack:stretch!important;align-content:stretch!important}.align-self-xl-auto{-webkit-align-self:auto!important;-ms-flex-item-align:auto!important;-ms-grid-row-align:auto!important;align-self:auto!important}.align-self-xl-start{-webkit-align-self:flex-start!important;-ms-flex-item-align:start!important;align-self:flex-start!important}.align-self-xl-end{-webkit-align-self:flex-end!important;-ms-flex-item-align:end!important;align-self:flex-end!important}.align-self-xl-center{-webkit-align-self:center!important;-ms-flex-item-align:center!important;-ms-grid-row-align:center!important;align-self:center!important}.align-self-xl-baseline{-webkit-align-self:baseline!important;-ms-flex-item-align:baseline!important;align-self:baseline!important}.align-self-xl-stretch{-webkit-align-self:stretch!important;-ms-flex-item-align:stretch!important;-ms-grid-row-align:stretch!important;align-self:stretch!important}}.float-left{float:left!important}.float-right{float:right!important}.float-none{float:none!important}@media (min-width:576px){.float-sm-left{float:left!important}.float-sm-right{float:right!important}.float-sm-none{float:none!important}}@media (min-width:768px){.float-md-left{float:left!important}.float-md-right{float:right!important}.float-md-none{float:none!important}}@media (min-width:992px){.float-lg-left{float:left!important}.float-lg-right{float:right!important}.float-lg-none{float:none!important}}@media (min-width:1200px){.float-xl-left{float:left!important}.float-xl-right{float:right!important}.float-xl-none{float:none!important}}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1030}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.mw-100{max-width:100%!important}.mh-100{max-height:100%!important}.m-0{margin:0 0!important}.mt-0{margin-top:0!important}.mr-0{margin-right:0!important}.mb-0{margin-bottom:0!important}.ml-0{margin-left:0!important}.mx-0{margin-right:0!important;margin-left:0!important}.my-0{margin-top:0!important;margin-bottom:0!important}.m-1{margin:.25rem .25rem!important}.mt-1{margin-top:.25rem!important}.mr-1{margin-right:.25rem!important}.mb-1{margin-bottom:.25rem!important}.ml-1{margin-left:.25rem!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-2{margin:.5rem .5rem!important}.mt-2{margin-top:.5rem!important}.mr-2{margin-right:.5rem!important}.mb-2{margin-bottom:.5rem!important}.ml-2{margin-left:.5rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-3{margin:1rem 1rem!important}.mt-3{margin-top:1rem!important}.mr-3{margin-right:1rem!important}.mb-3{margin-bottom:1rem!important}.ml-3{margin-left:1rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-4{margin:1.5rem 1.5rem!important}.mt-4{margin-top:1.5rem!important}.mr-4{margin-right:1.5rem!important}.mb-4{margin-bottom:1.5rem!important}.ml-4{margin-left:1.5rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-5{margin:3rem 3rem!important}.mt-5{margin-top:3rem!important}.mr-5{margin-right:3rem!important}.mb-5{margin-bottom:3rem!important}.ml-5{margin-left:3rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-0{padding:0 0!important}.pt-0{padding-top:0!important}.pr-0{padding-right:0!important}.pb-0{padding-bottom:0!important}.pl-0{padding-left:0!important}.px-0{padding-right:0!important;padding-left:0!important}.py-0{padding-top:0!important;padding-bottom:0!important}.p-1{padding:.25rem .25rem!important}.pt-1{padding-top:.25rem!important}.pr-1{padding-right:.25rem!important}.pb-1{padding-bottom:.25rem!important}.pl-1{padding-left:.25rem!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-2{padding:.5rem .5rem!important}.pt-2{padding-top:.5rem!important}.pr-2{padding-right:.5rem!important}.pb-2{padding-bottom:.5rem!important}.pl-2{padding-left:.5rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-3{padding:1rem 1rem!important}.pt-3{padding-top:1rem!important}.pr-3{padding-right:1rem!important}.pb-3{padding-bottom:1rem!important}.pl-3{padding-left:1rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-4{padding:1.5rem 1.5rem!important}.pt-4{padding-top:1.5rem!important}.pr-4{padding-right:1.5rem!important}.pb-4{padding-bottom:1.5rem!important}.pl-4{padding-left:1.5rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-5{padding:3rem 3rem!important}.pt-5{padding-top:3rem!important}.pr-5{padding-right:3rem!important}.pb-5{padding-bottom:3rem!important}.pl-5{padding-left:3rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-auto{margin:auto!important}.mt-auto{margin-top:auto!important}.mr-auto{margin-right:auto!important}.mb-auto{margin-bottom:auto!important}.ml-auto{margin-left:auto!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}@media (min-width:576px){.m-sm-0{margin:0 0!important}.mt-sm-0{margin-top:0!important}.mr-sm-0{margin-right:0!important}.mb-sm-0{margin-bottom:0!important}.ml-sm-0{margin-left:0!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.m-sm-1{margin:.25rem .25rem!important}.mt-sm-1{margin-top:.25rem!important}.mr-sm-1{margin-right:.25rem!important}.mb-sm-1{margin-bottom:.25rem!important}.ml-sm-1{margin-left:.25rem!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-sm-2{margin:.5rem .5rem!important}.mt-sm-2{margin-top:.5rem!important}.mr-sm-2{margin-right:.5rem!important}.mb-sm-2{margin-bottom:.5rem!important}.ml-sm-2{margin-left:.5rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-sm-3{margin:1rem 1rem!important}.mt-sm-3{margin-top:1rem!important}.mr-sm-3{margin-right:1rem!important}.mb-sm-3{margin-bottom:1rem!important}.ml-sm-3{margin-left:1rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-sm-4{margin:1.5rem 1.5rem!important}.mt-sm-4{margin-top:1.5rem!important}.mr-sm-4{margin-right:1.5rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.ml-sm-4{margin-left:1.5rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-sm-5{margin:3rem 3rem!important}.mt-sm-5{margin-top:3rem!important}.mr-sm-5{margin-right:3rem!important}.mb-sm-5{margin-bottom:3rem!important}.ml-sm-5{margin-left:3rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-sm-0{padding:0 0!important}.pt-sm-0{padding-top:0!important}.pr-sm-0{padding-right:0!important}.pb-sm-0{padding-bottom:0!important}.pl-sm-0{padding-left:0!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.p-sm-1{padding:.25rem .25rem!important}.pt-sm-1{padding-top:.25rem!important}.pr-sm-1{padding-right:.25rem!important}.pb-sm-1{padding-bottom:.25rem!important}.pl-sm-1{padding-left:.25rem!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-sm-2{padding:.5rem .5rem!important}.pt-sm-2{padding-top:.5rem!important}.pr-sm-2{padding-right:.5rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pl-sm-2{padding-left:.5rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-sm-3{padding:1rem 1rem!important}.pt-sm-3{padding-top:1rem!important}.pr-sm-3{padding-right:1rem!important}.pb-sm-3{padding-bottom:1rem!important}.pl-sm-3{padding-left:1rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-sm-4{padding:1.5rem 1.5rem!important}.pt-sm-4{padding-top:1.5rem!important}.pr-sm-4{padding-right:1.5rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pl-sm-4{padding-left:1.5rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-sm-5{padding:3rem 3rem!important}.pt-sm-5{padding-top:3rem!important}.pr-sm-5{padding-right:3rem!important}.pb-sm-5{padding-bottom:3rem!important}.pl-sm-5{padding-left:3rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-sm-auto{margin:auto!important}.mt-sm-auto{margin-top:auto!important}.mr-sm-auto{margin-right:auto!important}.mb-sm-auto{margin-bottom:auto!important}.ml-sm-auto{margin-left:auto!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:768px){.m-md-0{margin:0 0!important}.mt-md-0{margin-top:0!important}.mr-md-0{margin-right:0!important}.mb-md-0{margin-bottom:0!important}.ml-md-0{margin-left:0!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.m-md-1{margin:.25rem .25rem!important}.mt-md-1{margin-top:.25rem!important}.mr-md-1{margin-right:.25rem!important}.mb-md-1{margin-bottom:.25rem!important}.ml-md-1{margin-left:.25rem!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-md-2{margin:.5rem .5rem!important}.mt-md-2{margin-top:.5rem!important}.mr-md-2{margin-right:.5rem!important}.mb-md-2{margin-bottom:.5rem!important}.ml-md-2{margin-left:.5rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-md-3{margin:1rem 1rem!important}.mt-md-3{margin-top:1rem!important}.mr-md-3{margin-right:1rem!important}.mb-md-3{margin-bottom:1rem!important}.ml-md-3{margin-left:1rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-md-4{margin:1.5rem 1.5rem!important}.mt-md-4{margin-top:1.5rem!important}.mr-md-4{margin-right:1.5rem!important}.mb-md-4{margin-bottom:1.5rem!important}.ml-md-4{margin-left:1.5rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-md-5{margin:3rem 3rem!important}.mt-md-5{margin-top:3rem!important}.mr-md-5{margin-right:3rem!important}.mb-md-5{margin-bottom:3rem!important}.ml-md-5{margin-left:3rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-md-0{padding:0 0!important}.pt-md-0{padding-top:0!important}.pr-md-0{padding-right:0!important}.pb-md-0{padding-bottom:0!important}.pl-md-0{padding-left:0!important}.px-md-0{padding-right:0!important;padding-left:0!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.p-md-1{padding:.25rem .25rem!important}.pt-md-1{padding-top:.25rem!important}.pr-md-1{padding-right:.25rem!important}.pb-md-1{padding-bottom:.25rem!important}.pl-md-1{padding-left:.25rem!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-md-2{padding:.5rem .5rem!important}.pt-md-2{padding-top:.5rem!important}.pr-md-2{padding-right:.5rem!important}.pb-md-2{padding-bottom:.5rem!important}.pl-md-2{padding-left:.5rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-md-3{padding:1rem 1rem!important}.pt-md-3{padding-top:1rem!important}.pr-md-3{padding-right:1rem!important}.pb-md-3{padding-bottom:1rem!important}.pl-md-3{padding-left:1rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-md-4{padding:1.5rem 1.5rem!important}.pt-md-4{padding-top:1.5rem!important}.pr-md-4{padding-right:1.5rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pl-md-4{padding-left:1.5rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-md-5{padding:3rem 3rem!important}.pt-md-5{padding-top:3rem!important}.pr-md-5{padding-right:3rem!important}.pb-md-5{padding-bottom:3rem!important}.pl-md-5{padding-left:3rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-md-auto{margin:auto!important}.mt-md-auto{margin-top:auto!important}.mr-md-auto{margin-right:auto!important}.mb-md-auto{margin-bottom:auto!important}.ml-md-auto{margin-left:auto!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:992px){.m-lg-0{margin:0 0!important}.mt-lg-0{margin-top:0!important}.mr-lg-0{margin-right:0!important}.mb-lg-0{margin-bottom:0!important}.ml-lg-0{margin-left:0!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.m-lg-1{margin:.25rem .25rem!important}.mt-lg-1{margin-top:.25rem!important}.mr-lg-1{margin-right:.25rem!important}.mb-lg-1{margin-bottom:.25rem!important}.ml-lg-1{margin-left:.25rem!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-lg-2{margin:.5rem .5rem!important}.mt-lg-2{margin-top:.5rem!important}.mr-lg-2{margin-right:.5rem!important}.mb-lg-2{margin-bottom:.5rem!important}.ml-lg-2{margin-left:.5rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-lg-3{margin:1rem 1rem!important}.mt-lg-3{margin-top:1rem!important}.mr-lg-3{margin-right:1rem!important}.mb-lg-3{margin-bottom:1rem!important}.ml-lg-3{margin-left:1rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-lg-4{margin:1.5rem 1.5rem!important}.mt-lg-4{margin-top:1.5rem!important}.mr-lg-4{margin-right:1.5rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.ml-lg-4{margin-left:1.5rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-lg-5{margin:3rem 3rem!important}.mt-lg-5{margin-top:3rem!important}.mr-lg-5{margin-right:3rem!important}.mb-lg-5{margin-bottom:3rem!important}.ml-lg-5{margin-left:3rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-lg-0{padding:0 0!important}.pt-lg-0{padding-top:0!important}.pr-lg-0{padding-right:0!important}.pb-lg-0{padding-bottom:0!important}.pl-lg-0{padding-left:0!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.p-lg-1{padding:.25rem .25rem!important}.pt-lg-1{padding-top:.25rem!important}.pr-lg-1{padding-right:.25rem!important}.pb-lg-1{padding-bottom:.25rem!important}.pl-lg-1{padding-left:.25rem!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-lg-2{padding:.5rem .5rem!important}.pt-lg-2{padding-top:.5rem!important}.pr-lg-2{padding-right:.5rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pl-lg-2{padding-left:.5rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-lg-3{padding:1rem 1rem!important}.pt-lg-3{padding-top:1rem!important}.pr-lg-3{padding-right:1rem!important}.pb-lg-3{padding-bottom:1rem!important}.pl-lg-3{padding-left:1rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-lg-4{padding:1.5rem 1.5rem!important}.pt-lg-4{padding-top:1.5rem!important}.pr-lg-4{padding-right:1.5rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pl-lg-4{padding-left:1.5rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-lg-5{padding:3rem 3rem!important}.pt-lg-5{padding-top:3rem!important}.pr-lg-5{padding-right:3rem!important}.pb-lg-5{padding-bottom:3rem!important}.pl-lg-5{padding-left:3rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-lg-auto{margin:auto!important}.mt-lg-auto{margin-top:auto!important}.mr-lg-auto{margin-right:auto!important}.mb-lg-auto{margin-bottom:auto!important}.ml-lg-auto{margin-left:auto!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}}@media (min-width:1200px){.m-xl-0{margin:0 0!important}.mt-xl-0{margin-top:0!important}.mr-xl-0{margin-right:0!important}.mb-xl-0{margin-bottom:0!important}.ml-xl-0{margin-left:0!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.m-xl-1{margin:.25rem .25rem!important}.mt-xl-1{margin-top:.25rem!important}.mr-xl-1{margin-right:.25rem!important}.mb-xl-1{margin-bottom:.25rem!important}.ml-xl-1{margin-left:.25rem!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.m-xl-2{margin:.5rem .5rem!important}.mt-xl-2{margin-top:.5rem!important}.mr-xl-2{margin-right:.5rem!important}.mb-xl-2{margin-bottom:.5rem!important}.ml-xl-2{margin-left:.5rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.m-xl-3{margin:1rem 1rem!important}.mt-xl-3{margin-top:1rem!important}.mr-xl-3{margin-right:1rem!important}.mb-xl-3{margin-bottom:1rem!important}.ml-xl-3{margin-left:1rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.m-xl-4{margin:1.5rem 1.5rem!important}.mt-xl-4{margin-top:1.5rem!important}.mr-xl-4{margin-right:1.5rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.ml-xl-4{margin-left:1.5rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.m-xl-5{margin:3rem 3rem!important}.mt-xl-5{margin-top:3rem!important}.mr-xl-5{margin-right:3rem!important}.mb-xl-5{margin-bottom:3rem!important}.ml-xl-5{margin-left:3rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.p-xl-0{padding:0 0!important}.pt-xl-0{padding-top:0!important}.pr-xl-0{padding-right:0!important}.pb-xl-0{padding-bottom:0!important}.pl-xl-0{padding-left:0!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.p-xl-1{padding:.25rem .25rem!important}.pt-xl-1{padding-top:.25rem!important}.pr-xl-1{padding-right:.25rem!important}.pb-xl-1{padding-bottom:.25rem!important}.pl-xl-1{padding-left:.25rem!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.p-xl-2{padding:.5rem .5rem!important}.pt-xl-2{padding-top:.5rem!important}.pr-xl-2{padding-right:.5rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pl-xl-2{padding-left:.5rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.p-xl-3{padding:1rem 1rem!important}.pt-xl-3{padding-top:1rem!important}.pr-xl-3{padding-right:1rem!important}.pb-xl-3{padding-bottom:1rem!important}.pl-xl-3{padding-left:1rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.p-xl-4{padding:1.5rem 1.5rem!important}.pt-xl-4{padding-top:1.5rem!important}.pr-xl-4{padding-right:1.5rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pl-xl-4{padding-left:1.5rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.p-xl-5{padding:3rem 3rem!important}.pt-xl-5{padding-top:3rem!important}.pr-xl-5{padding-right:3rem!important}.pb-xl-5{padding-bottom:3rem!important}.pl-xl-5{padding-left:3rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.m-xl-auto{margin:auto!important}.mt-xl-auto{margin-top:auto!important}.mr-xl-auto{margin-right:auto!important}.mb-xl-auto{margin-bottom:auto!important}.ml-xl-auto{margin-left:auto!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}}.text-justify{text-align:justify!important}.text-nowrap{white-space:nowrap!important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left!important}.text-right{text-align:right!important}.text-center{text-align:center!important}@media (min-width:576px){.text-sm-left{text-align:left!important}.text-sm-right{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.text-md-left{text-align:left!important}.text-md-right{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.text-lg-left{text-align:left!important}.text-lg-right{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.text-xl-left{text-align:left!important}.text-xl-right{text-align:right!important}.text-xl-center{text-align:center!important}}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.font-weight-normal{font-weight:400}.font-weight-bold{font-weight:700}.font-italic{font-style:italic}.text-white{color:#fff!important}.text-muted{color:#636c72!important}a.text-muted:focus,a.text-muted:hover{color:#4b5257!important}.text-primary{color:#0275d8!important}a.text-primary:focus,a.text-primary:hover{color:#025aa5!important}.text-success{color:#5cb85c!important}a.text-success:focus,a.text-success:hover{color:#449d44!important}.text-info{color:#5bc0de!important}a.text-info:focus,a.text-info:hover{color:#31b0d5!important}.text-warning{color:#f0ad4e!important}a.text-warning:focus,a.text-warning:hover{color:#ec971f!important}.text-danger{color:#d9534f!important}a.text-danger:focus,a.text-danger:hover{color:#c9302c!important}.text-gray-dark{color:#292b2c!important}a.text-gray-dark:focus,a.text-gray-dark:hover{color:#101112!important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.invisible{visibility:hidden!important}.hidden-xs-up{display:none!important}@media (max-width:575px){.hidden-xs-down{display:none!important}}@media (min-width:576px){.hidden-sm-up{display:none!important}}@media (max-width:767px){.hidden-sm-down{display:none!important}}@media (min-width:768px){.hidden-md-up{display:none!important}}@media (max-width:991px){.hidden-md-down{display:none!important}}@media (min-width:992px){.hidden-lg-up{display:none!important}}@media (max-width:1199px){.hidden-lg-down{display:none!important}}@media (min-width:1200px){.hidden-xl-up{display:none!important}}.hidden-xl-down{display:none!important}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}/*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/app/assets/stylesheets/buttons.css.scss b/app/assets/stylesheets/buttons.css.scss new file mode 100644 index 0000000..d155498 --- /dev/null +++ b/app/assets/stylesheets/buttons.css.scss @@ -0,0 +1,42 @@ +.btn-topbar { + margin:3px; + float:left; +} + +.form-buttons-container { + position: fixed; + width:150px; + top:auto; + left:auto; + margin-left:-180px; +} + +.form-button { + width:100%; + font-size: 18px; + line-height: 24px; + -webkit-box-shadow: -1px 1px 2px 1px #888888; + -moz-box-shadow: -1px 1px 2px 1px #888888; + box-shadow: -1px 1px 2px 1px #888888; +} + +.figure-button { + margin: 3px; + display: inline-block; +} + + +.select-button { + color: #fff; + text-shadow: 1px 1px 0 #fff; + text-shadow: -1px -1px 0 rgba(0,0,0,0.3); + border-color: #B33030; + border-bottom-color: #A02A2A; + background-color: #CF3C3C; + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#E45D5D), to(#B54C4C)); + background-image: -moz-linear-gradient(#E45D5D, #B54C4C); + background-image: -ms-linear-gradient(#E45D5D, #B54C4C); + background-image: -o-linear-gradient(#E45D5D, #B54C4C); + background-image: linear-gradient(#E45D5D, #B54C4C); +} + diff --git a/app/assets/stylesheets/errors.css.scss b/app/assets/stylesheets/errors.css.scss new file mode 100644 index 0000000..3d20d96 --- /dev/null +++ b/app/assets/stylesheets/errors.css.scss @@ -0,0 +1,72 @@ +.error-page { + position: static; + margin: 0; + margin-right: auto; + margin-left: auto; + display: table; + color: #fff; + text-align:center; + max-width:75%; + width:auto; + height:100%; + transform:translate(0, 50%); + + .error-text { + width:auto; + height:auto; + transform:translate(0, -50%); + text-shadow: 3px 3px 3px #333; + text-align:center; + } + + h1 { + font-size: 40px; + line-height: 90px; + } + + p { + font-size: 20px; + line-height: 20px; + } + +} + +#notice { + color: #fff; + text-align:center; +} + +.field_with_errors { + height:100%; + padding: 2px; + box-shadow: #ff3385 0px 0px 2px, inset #ff3385 0px 0px 2px; +} + +#error_explanation { + display: table; + width: 100%; + border: 2px solid #ff3385; + padding: 7px; + padding-bottom: 0; + background-color: #eee; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 16px; + margin: -7px; + margin-bottom: 0px; + background-color: #ff3385; + color: #fff; + } + + ul li { + font-size: 16px; + list-style: circle; + } + +} diff --git a/app/assets/stylesheets/examples.scss b/app/assets/stylesheets/examples.scss new file mode 100644 index 0000000..293d481 --- /dev/null +++ b/app/assets/stylesheets/examples.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Examples controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/figures.css.scss b/app/assets/stylesheets/figures.css.scss new file mode 100644 index 0000000..05f3796 --- /dev/null +++ b/app/assets/stylesheets/figures.css.scss @@ -0,0 +1,293 @@ +.figure-layout { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + flex-wrap: wrap; + min-height:100vh; +} + +.fig-container { + min-height:600px; + max-height:90vh; + height:auto; + padding:10px; + + #fig { + width:100%; + height:100%; + overflow:auto; + } + + .svg-figure { + display:table; + margin:auto; + } +} + +.btn-container { + margin-top:10px; + margin-bottom:10px; +} + +.btn-sep { + border-top:1px solid #666; + display:inline-block; + width:100%; + margin-bottom:5px +} + +.fig-legend-container { + max-height:90vh; + height:auto; + padding:10px; + + #fig-legend { + width:100%; + height:100%; + overflow:auto; + } + + .columns-1 { + columns: 1; + -webkit-columns: 1; + -moz-columns: 1; + } + + .columns-2 { + columns: 2; + -webkit-columns: 2; + -moz-columns: 2; + } + + .columns-4 { + columns: 4; + -webkit-columns: 4; + -moz-columns: 4; + } +} + +// BAR FOR FIGURE MODIFICATION/SEARCH TOOLS +.figure-btn-container { + padding-top:7px; + padding-bottom:7px; + background-color:#333; + color:#fff; + display: flex; + flex-direction: column; + overflow:visible; + + .export .dropdown>ul>li { + cursor:pointer; + } + + .figure-btn { + border-top:1px solid #666; + display:inline-block; + + .search-input{ + margin-top:5px; + + i { + color:#666; + } + } + + .icon-sim-link { + float:left; + margin-right:5px; + } + + .icon-dissim-link { + float:left; + margin-right:5px; + } + } + + +} + +// TOP AND BOTTOM SCROLL BARS +.scrollbar-top-container, .double-scrolled-container { + overflow-x: auto; + overflow-y:hidden; +} + +.scrollbar-top-container { + height: 20px; +} +.double-scrolled-container { + height: 100%; +} + +.scrollbar-top { + height: 100%; +} + +.double-scrolled { + height:100%; + overflow: visible; +} + +// THE FIGURE ITSELF (PLOTS, LEGEND, ETC...) +.figure-container { + background-color:#fff; + display: flex; + flex-direction: column; + overflow:hidden; + height:auto; + min-height:100vh; + + .figure-container-margin { + padding-bottom:25px; + margin-right:5px; + margin-left:-15px; + } + + .label-sm { + font-size:12px; + } + + .svg-figure { + display:table; + margin:auto; + + } + + .network-well { + padding:0; + } + + + // FIGURE LEGEND + .legend-container { + padding:5px; + padding-bottom:15px; + height:100%; + overflow:hidden; + overflow-y:auto; + overflow-x:auto; + + .legend-container-margin { + padding:10px; + } + + p { + margin-top:10px; + font-weight:bold; + } + + .sidebar-sub-title { + font-style: italic; + } + + .svg-legend { + padding:0; + padding-left:20px; + margin:5px; + height:100%; + } + + ul { + padding:5px; + &>li { + cursor:pointer; + padding:5px; + } + + .legend-no-interaction { + cursor:auto; + } + } + + .svg-legend-with-title { + margin-top:40px; + } + + ul>li>span { + width:100%; + display:inline; + white-space:nowrap; + + i { + margin-right:5px; + } + } + } +} + +// MOVING SIDEBAR FOR THE DESCRIPTION OF THE FIGURE +.sidebar { + position:absolute; + right:0px; + width:20px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + background-color:rgba(255, 0, 102, 0.9); + padding:2px; + -webkit-transition:width .3s ease-out, padding .3s ease; + -moz-transition:width .3s ease-out, padding .3s ease; + -ms-transition:width .3s ease-out, padding .3s ease; + transition:width .3s ease-out, padding .3s ease; + overflow:hidden; + border-left:4px solid #e6005c; + -webkit-box-shadow: -2px 0px 3px -2px #888888; + -moz-box-shadow: -2px 0px 3px -2px #888888; + box-shadow: -2px 0px 3px -2px #888888; + + .sidebar-icon { + transform:rotate(0deg); + color:#fff; + -webkit-transition:transform .3s linear .4s; + -moz-transition:transform .3s linear .4s; + -ms-transition:transform .3s linear .4s; + transition:transform .3s linear .4s; + } + + .description{ + visibility:hidden; + opacity:0; + -webkit-transition-delay:visibility .3s; + -moz-transition-delay:visibility .3s; + -ms-transition-delay:visibility .3s; + transition-delay:visibility .3s; + -webkit-transition:opacity .1s ease .3s; + -moz-transition:opacity .1s ease .3s; + -ms-transition:opacity .1s ease .3s; + transition:opacity .1s ease .3s; + word-break: normal; + text-align:justify; + h3 { + margin-top:4px; + color:#fff; + small { + color:#fff; + } + } + } +} + +// LOGS +.log-title { + text-align:center; + color:#fff; + margin-right: auto; + margin-left: auto; + display: table; + text-shadow: 3px 3px 3px #333; + + small { + color:#fff; + } +} + +.log-pre { + background-color: rgba(0, 0, 0, 0.4); + color:#fff; + width:75%; + margin-left:auto; + margin-right:auto; +} + + diff --git a/app/assets/stylesheets/form.css.scss b/app/assets/stylesheets/form.css.scss new file mode 100644 index 0000000..7999b27 --- /dev/null +++ b/app/assets/stylesheets/form.css.scss @@ -0,0 +1,476 @@ +.important { +color:red +} + +div.missing_field input{ +border-color:red +} +div.missing_field span.custom-file-control{ +border-color:red +} + +.margin_addon { +margin-right:10px +} + +.card-text { + margin-bottom:15px; +} + +label { + margin-bottom:2px; +} + +.form-bool { + margin-left:1.25rem; +} + +.group_field { +margin-bottom:5px; +} + +.field_group_content{ +padding:5px +} + +.fold_bar { +background-color:grey; +text-align:right +} + +.fold_bar i { +color:white; +text-align:right +} + +.form-number-field { + max-width:80px; + overflow:hidden; + text-align:right; +} + +.form_card { + height:50vh; + overflow-y:auto; +} + +#customFile .custom-file-control:lang(en)::after { + content: "Select file..."; +} + +#customFile .custom-file-control:lang(en)::before { + content: "Click me"; +} + +/*when a value is selected, this class removes the content */ +.custom-file-control.selected:lang(en)::after { + content: "" !important; +} + +.custom-file { + overflow: hidden; +} + +.custom-file-control { + white-space: nowrap; +} + +.form-categ { + position:absolute; + border: solid 1px #999; + border-bottom: solid 4px #999; + padding:7px; + background-color:#f2f2f2; + color:#666; + height:90vh; + width:93%; + overflow:hidden; + overflow-y:auto; + -webkit-transition:max-height .6s ease, background-color .2s ease, border .2s ease; + -moz-transition:max-height .6s ease, background-color .2s ease, border .2s ease; + -ms-transition:max-height .6s ease, background-color .2s ease, border .2s ease; + transition:max-height .6s ease, background-color .2s ease, border .2s ease; + + + .indent-field { + margin-left:10px; + } + + .bordered-top-field { + border-top:solid 1px #ccc; + padding-top:5px; + } + + .form-tabs-selector { + margin-bottom:15px; + } + + &:hover { + z-index:100; + height:auto; + max-height:110%; + overflow-y:auto; + border: solid 1px #333; + background-color: #fff; + color:#333; + -webkit-box-shadow: -1px 1px 2px 1px #B3B3B3; + -moz-box-shadow: -1px 1px 2px 1px #B3B3B3; + box-shadow: -1px 1px 2px 1px #B3B3B3; + + } +} + +.file_download { + margin-bottom:32px; + display:none; + + &:hover, + &:focus, + &:active, + &.active { + color:#333; + } +} + +.help-button { + + .label-text, + .help-icon { + cursor:pointer; + color:#666; + + &:hover, + &:focus, + &:active, + &.active { + color:#333; + } + } + + .help-icon { + float:right; + margin-right:10px; + } +} + +.help-block { + padding-top:8px; + max-height:0; + height:auto; + opacity:0; + visibility: hidden; + -webkit-transition: max-height 0.4s ease, opacity 0.4s ease, visibility 0.8s; + -moz-transition: max-height 0.4s ease, opacity 0.4s ease, visibility 0.8s; + -ms-transition: max-height 0.4s ease, opacity 0.4s ease, visibility 0.8s; + transition: max-height 0.4s ease, opacity 0.4s ease, visibility 0.8s; + + .file_example { + margin-top:2px; + display:block; + } +} + +.expandable { + display:block; + border: 1px solid #333; + border-radius:5px; + padding: 3px; + margin-top:2px; + margin-bottom:5px; + max-height:100%; + height:auto; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + -webkit-transition:max-height 1.5s ease; + -moz-transition:max-height 1.5s ease; + -ms-transition:max-height 1.5s ease; + transition:max-height 1.5s ease; + + + +.expandable-image { + position:absolute; + right:90px; + margin-left:2px; + background-color:#fff; + border: 1px solid #d9d9d9; + height:auto; + max-height:40px; + -webkit-box-shadow: 0; + -moz-box-shadow: 0; + box-shadow: 0; + -webkit-transition:max-height 0.7s ease, transform 0.7s ease, webkit-box-shadow 0.7s ease; + -moz-transition:max-height 0.7s ease, transform 0.7s ease, -moz-box-shadow 0.7s ease; + -ms-transition:max-height 0.7s ease, transform 0.7s ease, box-shadow 0.7s ease; + transition:max-height 0.7s ease, transform 0.7s ease, box-shadow 0.7s ease; + + &:hover { + z-index:200; + max-height:120px; + transform:translate(0, -40px); + -webkit-box-shadow: -1px 1px 2px 1px #B3B3B3; + -moz-box-shadow: -1px 1px 2px 1px #B3B3B3; + box-shadow: -1px 1px 2px 1px #B3B3B3; + } + +} + + a { + color: #666; + + &:hover, + &:focus, + &:active, + &.active { + color:#333; + } + } + + .slide-open, .slide-close { + padding:3px; + float:right; + margin-right:7px; + } + + .slider { + cursor: pointer; + } + + .slide { + display: block; + max-height:0; + height:auto; + opacity:0; + visibility: hidden; + margin-top: -15px; + width: 98%; + padding:7px; + -webkit-transition:max-height 0.5s ease, opacity 0.5s ease, visibility 0.9s; + -moz-transition:max-height 0.5s ease, opacity 0.5s ease, visibility 0.9s; + -ms-transition:max-height 0.5s ease, opacity 0.5s ease, visibility 0.9s; + transition:max-height 0.5s ease, opacity 0.5s ease, visibility 0.9s; + } +} + +.multiselect-btn-container { + height:35px; + position:relative; + display:block; +} + +.form-text { + width:auto; + max-width:90%; +} + +.input-group-number { + width:auto; + max-width:110px; +} + +.form-number { + text-align:right; +} + +.input-group-addon { + width:auto; + max-width:50%; +} + + +.text-field { + color:#333; + overflow: hidden; + display: inline-block; + padding: 0.4em 0.4em; + border:solid #999999 1px; + border-radius: 5px; + background-color: rgba(255, 255, 255, 1); + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + text-decoration: none; + font-weight: normal; + font-family: verdana, arial, helvetica, sans-serif; + white-space: nowrap; + outline: none; +} + +.big-text-field { + width: 100%; + font-size: 20px; + line-height: 22px; +} + +input[type=number] {-moz-appearance: textfield;} +::-webkit-inner-spin-button { -webkit-appearance: none;} + +.form-text-field { + width:80%; +} + + + +.form-select-field, .select-from-file-row, .select-from-field { + width:50%; +} + + +.text-field:hover, +.text-field:focus, +.text-field:active, +.text-field.active { + -webkit-box-shadow: 0px 0px 1px 1px #999999; + -moz-box-shadow: 0px 0px 1px 1px #999999; + box-shadow: 0px 0px 1px 1px #999999; +} + +.parameters { + width: 400px; + border: solid 2px #599bdc; + margin:0; + padding: 7px; + background-color: #fff; + -webkit-box-shadow: -1px 3px 3px -1px #888888; + -moz-box-shadow: -1px 3px 3px -1px #888888; + box-shadow: -1px 3px 3px -1px #888888; + + h1 { + color: #fff; + background-color:#599bdc; + padding: 1px 1px 1px 15px; + font-size: 22px; + margin-left: -7px; + margin-right: -7px; + margin-top: -7px; + margin-bottom: 15px; + } + + p { + padding: 1px 15px 1px 15px; + font-size: 14px; + margin: -7px; + margin-bottom: 0px; + } + + .select-from-file-category { + overflow: hidden; + padding:0; + border:none; + background-color:#fff; + width:100%; + height:38px; + option { + line-height: 25px; + border:1px solid #666666; + border-radius:50%; + padding:0px; + margin:3px; + width:25px; + height:25px; + text-align:center; + display:inline-block; + float:right; + background-color:#F4F4F4; + } + option:checked, + option:hover { + color:#fff; + background-color:#599bdc; + } + } + + .select-from-file-category:disabled { + option { + width:auto; + border:none; + border-radius:0px; + color:#8F9499; + background-color:#fff; + background-image: none; + } + } + + + + + .field_categ { + padding: 7px; + + .field { + margin:0px; + width: 100%; + overflow: hidden; + display: inline-block; + padding: 7px; + border:solid #999999 1px; + border-radius: 5px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + } + + h1 { + padding: 0px 0px 0px 15px; + font-size: 18px; + margin: -7px; + background-color: #808080; + color: #fff; + -webkit-box-shadow: -1px 1px 2px 1px #B3B3B3; + -moz-box-shadow: -1px 1px 2px 1px #B3B3B3; + box-shadow: -1px 1px 2px 1px #B3B3B3; + + + + + + +} + + + + .legend { + display:inline-block; + max-width:200px; + width:auto; + vertical-align:top; + background-color: #F3F4FF; + font-size:12px; + line-height: 18px; + border-radius:2px; + padding:2px; + overflow:hidden; + + .legend-text { + display:inline; + + } + } + + .test-file { + display:inline-block; + width:250px; + line-height: 22px; + margin-top:5px; + margin-left:2px; + + .test-select { + font-size:14px; + float:right; + } + + .test-text { + display:inline-block; + font-size:14px; + float:left; + } + } + + .comment { + margin-top:10px; + } + + + } +} diff --git a/app/assets/stylesheets/general.css.scss b/app/assets/stylesheets/general.css.scss new file mode 100644 index 0000000..0743c24 --- /dev/null +++ b/app/assets/stylesheets/general.css.scss @@ -0,0 +1,269 @@ +#main{ + margin:5px; + height:100vh; +} + +.main-container { + min-height:100vh; + height:auto; + padding-top:3px; +} + +.doc-table tr td, +.doc-table tr th, +.version-table tr td { + text-align:left +} + + +.border-left { + border-left:2px solid grey; +} + +.border-right { + border-right:2px solid grey; +} + +.navbar-bottom { + display:table; + width:100%; + margin: 0; +} +.navbar-bottom > li { + float:none; + display:table-cell; + text-align:center; +} + +.topbar-padding { + padding-top:56px +} + +.topbar-margin { + margin-top:56px +} + +.dropdown-item { + cursor:pointer; +} +.step-card{ + margin-bottom:5px +} + +.float-right { + float:right +} + +.align-center { + text-align:center; +} + +.align-left { + text-align:left; +} + +.align-right { + text-align:right; +} +ul.no-bullets li { + list-style-type: none +} + +.full-width { + width:100%; +} + +.full-height { + height:100%; +} + +.align-items-center { + align-items:center; +} + +.hidden { + display:none; +} + +#popup_window { + position: absolute; + z-index:100000; + padding:15px; + display: none; + background: #ccc; + border: 1px solid; +} +#popup_window_close { + float:right; + font-size:20px; + margin-top:-19px; + margin-right:-10px; + cursor:pointer; +} + +.tip_window { + position: fixed; + z-index:100000; + padding:15px; + display: none; + background: #e6e6e6; + border: 1px solid; +} + +.tip_window_close { + padding-top:10px; + cursor:pointer; +} + + +.title_popup {white-space:nowrap;font-weight:bold} + +.infos { + position:static; +} + +.background-col-1 { + background-color:#e6e6e6; +} + +.background-col-2 { + color:#fff; + background-color:#999; +} + +.row-padding-10 { + padding-top:10vh; + padding-bottom:10vh; +} + +.row-padding-5 { + padding-top:5vh; + padding-bottom:5vh; +} + +.row-padding-2 { + padding-top:2vh; + padding-bottom:2vh; +} + +.row-padding-bottom-20 { + padding-bottom:20vh; +} + +.v-align-middle { + height:100%; + transform:translate(0, 35%); +} + +.back-to-top { + color:#999; + text-decoration: none; + + &:hover, + &:focus, + &:active { + color:#ccc; + text-decoration: none; + } +} + +.content { + position:static; + display: table; + margin-right: auto; + margin-left: auto; + margin-top: 60px; + margin-bottom: 60px; + + .left { + float:left; + margin-right:20px; + } + + .right { + float:right; + margin-left:20px; + } + + .align-left { + text-align:left; + } + + .align-right { + text-align:right; + } + + .centered-div { + width: 260px; + line-height: 300px; + } + + .centered-span { + display: inline-block; + vertical-align: middle; + line-height: normal; + } + +} + +a { + color: #333; + + &.white { + color: #fff; + } + + &:link, &:visited { + text-decoration: none; + } + + &:hover { + text-decoration: underline; + + &.button, + &.tab, + &.subtab, + &.help-button, + &.slider, + &.slide-open, + &.slide-close, + &.test-button { + text-decoration: none; + } + &.slide-open, &.slide-close { + text-shadow: 1px 0px #333, -1px -0px #333, 0px -1px #333, 0px 1px #333; + } + } +} + + + +h1 { + font-weight: 500; + font-size: 28px; + line-height: 34px; +} + +h2 { + font-weight: 500; + font-size: 24px; + line-height: 30px; +} + +h3 { + font-weight: 500; + font-size: 20px; + line-height: 25px; +} + +pre { + font-family: verdana, arial, helvetica, sans-serif; + background-color: #eee; + padding: 10px; + font-size: 12px; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + white-space: -o-pre-wrap; + word-wrap: break-word; +} diff --git a/app/assets/stylesheets/home.css.scss b/app/assets/stylesheets/home.css.scss new file mode 100644 index 0000000..7cf9753 --- /dev/null +++ b/app/assets/stylesheets/home.css.scss @@ -0,0 +1,25 @@ +.doc-index-container { + max-height:100vh; + height:auto; + overflow:auto; +} +.doc-index { + + padding-bottom:50vh; + + .index-link-1 { + padding:.3em .5em; + font-size:1.2em; + } + .index-link-2 { + padding:.3em 1em; + } + .index-link-3 { + padding:.3em 1.2em; + } + .index-link-4 { + padding:.3em 1.8em; + font-size:0.9em; + font-weight:bold; + } +} diff --git a/app/assets/stylesheets/index.css.scss b/app/assets/stylesheets/index.css.scss new file mode 100644 index 0000000..89d77de --- /dev/null +++ b/app/assets/stylesheets/index.css.scss @@ -0,0 +1,131 @@ +.index { + width:100%; + display:table; + margin:auto; +} + +.filter-table { + max-width:30vw; + width:auto; + float:right; + margin-bottom:3px; +} + +.table { + margin-top:5px; + background-color:#fff; +} + +.icon-info { + color:blue; +} + +.icon-success { + color:green; +} + +.icon-warning { + color:orange; +} + +.icon-danger { + color:#dc3545; +} + + +.icon-inactive { + color:gray; +} + +.table-responsive table th { + cursor:pointer; +} + +table { + + th { + font-weight: normal; + text-align:center; + + .th-content-container { + display:table; + margin:auto; + } + + .fa-sort-with-filter { + display:table; + float:left; + margin-left:5px; + line-height:200%; + } + + .form-group { + display:table; + float:left; + margin:auto; + width:auto; + max-width:150px; + min-width:60px; + + .form-control { + text-align:center; + } + + i { + color:#b3b3b3; + } + } + + .form-group-sm { + max-width:70px; + } + + .form-group-md { + max-width:100px; + } + + } + + td { + text-align:center; + + a { + &:hover, + &:focus, + &:active{ + text-decoration:none; + } + + .fa-times { + color:#ff4d4d; + + &:hover, + &:focus, + &:active{ + color:#e60000 + } + } + + .td-content-container { + color:#333; + width:100%; + height:100%; + cursor:pointer; + } + + .td-icon-container { + display:table; + width:100%; + height:100%; + cursor:pointer; + + } + + .test-name { + color:#ff3385; + } + + } + + } +} diff --git a/app/assets/stylesheets/infos.scss b/app/assets/stylesheets/infos.scss new file mode 100644 index 0000000..63b2497 --- /dev/null +++ b/app/assets/stylesheets/infos.scss @@ -0,0 +1,22 @@ +.info-container { + display:table; + margin:auto; +} + +.terms { + margin:20px; + width:800px; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + white-space: -pre-wrap; + background-color: #fff; + color: #333; + font-family: "Lucida Console", monospace; + font-weight: normal; + font-size: 16px; + line-height: 20px; +} + +.credits { + border:solid 1px #333; +} diff --git a/app/assets/stylesheets/main.scss b/app/assets/stylesheets/main.scss new file mode 100644 index 0000000..d4e6ecb --- /dev/null +++ b/app/assets/stylesheets/main.scss @@ -0,0 +1,7 @@ +@import "font-awesome"; +/* +@import "bootstrap-multiselect"; +@import "bootstrap-sprockets"; +@import "bootstrap"; +*/ +@import "jquery.dataTables.min"; diff --git a/app/assets/stylesheets/navbar.css.scss b/app/assets/stylesheets/navbar.css.scss new file mode 100644 index 0000000..3e4a284 --- /dev/null +++ b/app/assets/stylesheets/navbar.css.scss @@ -0,0 +1,83 @@ +.navbar { + margin-bottom: 0; + border:0; +} + +.git-link { + font-size: 1.5em +} + +.topbar { + -webkit-box-shadow: 0px 3px 3px 2px #888888; + -moz-box-shadow: 0px 3px 3px 2px #888888; + box-shadow: 0px 3px 3px -2px #888888; + + .topbar-title { + color:#999; + } + + .topbar2 { + background-color:#333; + color:#fff; + + .topbar2-btns>li>a, + .topbar2-tabs>li>a { + &, + &:hover, + &:focus, + &:active { + color:#fff; + } + } + + .topbar2-btns>li>a { + &, + &:hover, + &:focus, + &:active { + background-color:#333; + } + } + + .topbar2-tabs { + float:left; + + &>li { + &:hover, + &:focus, + &:active, + &.active { + background-color:#3C65A9; + } + } + } + + .input-group>.dropdown>.dropdown-toggle, + .input-group>.input-group-addon>a { + &, + &:hover, + &:focus, + &:active { + color: #333; + text-decoration: none; + background-color: #00000000; + } + } + + .topbar2-title>li, + .topbar2-tabs>li.input-group-container { + padding:8px; + } + + .topbar2-title>li { + padding-left:0; + } + } +} + +.bottombar { + color:#999; + small { + color:#999; + } +} diff --git a/app/assets/stylesheets/show.css.scss b/app/assets/stylesheets/show.css.scss new file mode 100644 index 0000000..e88d012 --- /dev/null +++ b/app/assets/stylesheets/show.css.scss @@ -0,0 +1,18 @@ +.menu {width:100%;margin:5px} +.menu a {width:100%;} +.sub-menu {width:100%;margin:5px} +.sub-menu a {width:100%} +#job_header_right {position:absolute; top:0px; right:10px} +.status_image {height:15px} + +.job-description { + max-height:100px; + height:auto; + overflow:auto; +} + +.show-element { + margin:10px; + padding:10px; +} + diff --git a/app/assets/stylesheets/versions.scss b/app/assets/stylesheets/versions.scss new file mode 100644 index 0000000..aec9088 --- /dev/null +++ b/app/assets/stylesheets/versions.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Versions controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/welcome.css.scss b/app/assets/stylesheets/welcome.css.scss new file mode 100644 index 0000000..8431a8e --- /dev/null +++ b/app/assets/stylesheets/welcome.css.scss @@ -0,0 +1,49 @@ +.welcome-text { + color:#fff; + text-shadow: 3px 3px 3px #333; + + h1 { + font-size: 10vh; + line-height: 15vh; + } + + p { + font-size: 20px; + line-height: 20px; + } +} + +.signin { + color:#fff; + padding:2%; + background-color: rgba(0, 0, 0, 0.5); + + a { + color: #fff; + } + +} + +.move-down, +.move-up { + a { + display:table; + font-size:60px; + margin-left:auto; + margin-right:auto; + + &.dark { + color:#333; + } + + &.light { + color:#fff; + } + + &:hover, + &:focus, + &:active { + color:#ccc; + } + } +} diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb new file mode 100644 index 0000000..c17c577 --- /dev/null +++ b/app/controllers/admins_controller.rb @@ -0,0 +1,8 @@ +class AdminsController < ApplicationController + before_action :authenticate_user! + + def show + render template: "/admins/#{params[:page]}" + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..6600f65 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,54 @@ +class ApplicationController < ActionController::Base + # Prevent CSRF attacks by raising an exception. + # For APIs, you may want to use :null_session instead. + protect_from_forgery with: :exception + + before_action :configure_permitted_parameters, if: :devise_controller? + before_filter :start_timer, :init_session + helper_method :readable?, :superadmin?, :admin? + + def init_session + session[:current_key]||= create_key() + session[:status_vector]||=[] + session[:selected_view]||='primary_dataset' + session[:current_level]||= 1 + end + + def superadmin? + current_user and current_user.role == 'superadmin' + end + + def admin? + current_user and current_user.role == 'admin' + end + + def readable? job + (job and superadmin? or admin? or + (current_user and current_user.id == job.user_id) or + (!current_user and job.key == session[:current_key] and job.user_id == 1)) + end + + def start_timer + @start_time = Time.now.to_f + end + + def create_key + tmp_key = Array.new(8){[*'0'..'9', *'a'..'z'].sample}.join + while Job.where(:key => tmp_key).count > 0 + tmp_key = Array.new(8){[*'0'..'9', *'a'..'z'].sample}.join + end + return tmp_key + end + + + protected + + def configure_permitted_parameters + + devise_parameter_sanitizer.permit(:sign_in, keys: [:username]) + devise_parameter_sanitizer.permit(:sign_up, keys: [:username, :email]) + devise_parameter_sanitizer.permit(:account_update, keys: [:username, :email]) + end + + +end diff --git a/app/controllers/examples_controller.rb b/app/controllers/examples_controller.rb new file mode 100644 index 0000000..e14091d --- /dev/null +++ b/app/controllers/examples_controller.rb @@ -0,0 +1,75 @@ +class ExamplesController < ApplicationController + before_action :set_example, only: [:show, :edit, :update, :destroy] + + # GET /examples + # GET /examples.json + def index + @examples = Example.all + @jobs = Job.where(:key => @examples.map{|e| e.job_key}).all + end + + # GET /examples/1 + # GET /examples/1.json + def show + end + + # GET /examples/new + def new + @example = Example.new + end + + # GET /examples/1/edit + def edit + end + + # POST /examples + # POST /examples.json + def create + @example = Example.new(example_params) + + respond_to do |format| + if @example.save + format.html { redirect_to @example, notice: 'Example was successfully created.' } + format.json { render :show, status: :created, location: @example } + else + format.html { render :new } + format.json { render json: @example.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /examples/1 + # PATCH/PUT /examples/1.json + def update + respond_to do |format| + if @example.update(example_params) + format.html { redirect_to @example, notice: 'Example was successfully updated.' } + format.json { render :show, status: :ok, location: @example } + else + format.html { render :edit } + format.json { render json: @example.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /examples/1 + # DELETE /examples/1.json + def destroy + @example.destroy + respond_to do |format| + format.html { redirect_to examples_url, notice: 'Example was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_example + @example = Example.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def example_params + params.require(:example).permit(:job_key) #:nam + end +end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb new file mode 100644 index 0000000..acbb33e --- /dev/null +++ b/app/controllers/home_controller.rb @@ -0,0 +1,26 @@ +class HomeController < ApplicationController + + + def index + end + + def doc + end + + def about + end + + def data_format + end + + def tutorial + end + + def reference + end + + def faq + end + + +end diff --git a/app/controllers/jobs_controller.rb b/app/controllers/jobs_controller.rb new file mode 100644 index 0000000..d964c93 --- /dev/null +++ b/app/controllers/jobs_controller.rb @@ -0,0 +1,771 @@ +class JobsController < ApplicationController + before_action :set_job, only: [:show, :edit, :update, :destroy, :serve, :view, :refresh, :clone] + # before_action :authenticate_user!, only: [:show, :edit, :update, :destroy, :serve] + + def kill_job j + + tmp_dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + j.user_id.to_s + j.key + main_pid = File.read(tmp_dir + ".pid") if File.exist?(tmp_dir + ".pid") + + # kill job if one already running + existing_main_job = `ps -ef | grep #{j.key} | grep #{main_pid} | grep -v 'grep'` + puts "Existing main job: " + existing_main_job.to_json + + # pids=[] + if main_pid and !existing_main_job.empty? + lines = `ps -ef | grep #{j.key} | grep -v 'grep'`.split("\n").select{|l| !l.empty?} + + pids = lines.map{|l| t= l.split(/\s+/); t[1]} + pids.unshift(main_pid) + + if pids.size > 0 + pids.each do |pid| + cmd = "kill #{pid}" + `#{cmd}` + end + end + end + + # delete log.json + log_json = tmp_dir + "output" + "log.json" + logger.debug("DEBUG: " + log_json.to_s) + File.delete log_json if File.exist? log_json + + end + + def clone + #if current_user + + tmp_dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + @job.user_id.to_s + @job.key + + @new_job = nil + + if current_user + @new_job = @job.dup + session[:current_key] = create_key() + @new_job.key = session[:current_key] + else + current_job = Job.where(:key => session[:current_key]).first + if current_job + + ### kill current job + kill_job(current_job) + + @new_job = current_job + @new_job.update_attributes(:form_json => @job.form_json, + :output_json => @job.output_json, + :name => @job.name, + :description => @job.description, + :status => @job.status) + + else + @new_job = @job.dup + @new_job.key = session[:current_key] + # @new_job.read_access = true + # @new_job.write_access = true + end + end + + # kill_job(@job) + + ### clone + + @new_job.sandbox = (current_user) ? false : true + @new_job.user_id = (current_user) ? current_user.id : 1 + new_tmp_dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + @new_job.user_id.to_s + Dir.mkdir(new_tmp_dir) if !File.exist? new_tmp_dir + new_tmp_dir += @new_job.key + if File.exist? new_tmp_dir + FileUtils.rm_r new_tmp_dir + Dir.mkdir new_tmp_dir + end + FileUtils.cp_r tmp_dir.to_s + "/.", new_tmp_dir + + ### rename cloned tar.gz + FileUtils.mv (new_tmp_dir + (@job.key + ".tar.gz")), (new_tmp_dir + (@new_job.key + ".tar.gz")) if File.exist?(new_tmp_dir + (@job.key + ".tar.gz")) + + ### change filepaths in the result file + cmd = "perl -i -pe 's/\\/#{@job.user_id}\\/#{@job.key}\\//\\/#{@new_job.user_id}\\/#{@new_job.key}\\//g' #{new_tmp_dir}/output/log.json" + logger.debug("CMD: #{cmd}") + `#{cmd}` + + ### delete tmp_dir + FileUtils.rm_r new_tmp_dir + "tmp" if File.exist?(new_tmp_dir + "tmp") + + @new_job.name += " cloned" + if @new_job.save + redirect_to job_path(@new_job.key) + end + #else + # render :nothing => true + # end + end + + def serve + if readable? @job + tmp_dir = Pathname.new(APP_CONFIG[:data_dir]) + 'users' + @job.user.id.to_s + @job.key #params[:key] # + params[:step] + # tmp_dir += params[:item_id].to_s if params[:item_id] + filename = params[:filename] #|| 'dl_output.tab' + filepath = tmp_dir + filename + send_file filepath.to_s, type: params[:content_type] || 'text', disposition: (!params[:display]) ? ("attachment; filename=" + filename.gsub("/", "_")) : '' + end + end + + + def read_file_header + if params[:file_key] + + new_data = [] + + user_id = (current_user) ? current_user.id.to_s : "1" + dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + user_id + session[:current_key] + 'input' + filename = params[:file_key] + ".txt" + filepath = dir + filename + + test = "" + if File.exist? filepath + lines = [] + File.open(filepath, "r") do |f| + while(l = f.gets) do + lines.push(l.chomp) + end + end + + j=0 + (0 .. lines.size-1).to_a.each do |i| + if !lines[i].match(/^#/) + j=i + break + end + end + + i = (j > 1) ? j-1 : 0 + + headers = lines[i].split("\t") + + if params[:add_blank] + new_data.push({label:"", value:""}); + end + if headers.size > 0 + headers.each do |header| + new_data.push({:label => header, :value => header}); + end + + end + end + render :text => new_data.to_json + else + render :nothing => true + end + end + + def read_file_column + if params[:file_key] + + new_data = [] + + user_id = (current_user) ? current_user.id.to_s : "1" + dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + user_id + session[:current_key] + 'input' + filename = params[:file_key] + ".txt" + filepath = dir + filename + + + if File.exist? filepath + lines = File.readlines(filepath) + + j=0 + (0 .. lines.size-1).each do |i| + if !lines[i].match(/^#/) + j = i + break + end + end + i = (j > 1) ? j-1 : 0 + + header_els = lines[i].chomp.split("\t") + + pos_col = 0 + (0 .. header_els.size).each do |k| + if header_els[k] == params[:col_name] + pos_col = k + break + end + end + + max_val=0 + (i .. lines.size-1).each do |j| + t = lines[j].split("\t") + n = t[pos_col].split(";").size + if n > max_val + max_val = n + end + end + + if max_val > 0 + (1 .. max_val+1).each do |j| + new_data.push({:label => j, :value => j}); + end + end + end + render :text => new_data.to_json + else + render :nothing => true + end + end + + # BUILD THE INDEX OF JOBS FROM CURRENT USER OR ALL (ADMIN ONLY) + def index + if current_user + if current_user.role == "admin" #and session[:which] == "all" + @jobs = Job.all + @users = User.all + else#if current_user + @jobs = current_user.jobs.all + end + end + + respond_to do |format| + format.html{ + if !current_user + redirect_to "/welcome" + end + } + end + end + + def get_views + + @h_views = {} + View.all.each do |v| + @h_views[v.name]= v + end + + end + + def get_statuses + + log_file = Pathname.new(APP_CONFIG[:data_dir]) + "users" + @user.id.to_s + @job.key + 'output' + 'log.json' + #stdout_file = Pathname.new(APP_CONFIG[:data_dir]) + "users" + @user.id.to_s + @job.key + 'output' + 'stdout.log' + #stderr_file = Pathname.new(APP_CONFIG[:data_dir]) + "users" + @user.id.to_s + @job.key + 'output' + 'stderr.log' + + #if File.exist? stdout_file and !File.size(stdout_file) != 0 + # @stdout_log = JSON.parse(File.read(stdout_file)).select {|e| e[0] != nil} + #end + #if File.exist? stderr_file and !File.size(stderr_file) != 0 + # @stderr_log = JSON.parse(File.read(stderr_file)).select {|e| e[0] != nil} + #end + + @h_statuses = {}#'completed' => 1, 'pending' => 2, 'running' => 3, 'failed' => 4} + Status.all.each do |s| + @h_statuses[s.name]= s + end + + + if File.exist? log_file and !File.size(log_file) != 0 + + @log_json = JSON.parse(File.read(log_file)) + + @final_json = { + :global_status => nil, + :status_by_step => {}, + :status_by_substep => {}, + :global_status_by_step => {}, + :messages_by_step => {} + } + + @h_icons={ + 'description' => 'circle-o fa-xs', + 'output' => 'file-text-o', + 'warning' => 'exclamation-triangle icon-warning', + 'error' => 'exclamation-triangle icon-danger' + } + + @test = "" + + + ### datasets + + @log_json.select{|e| ['dataset', 'map'].include?(e['type'])}.each do |cat| + @final_json[:status_by_step][cat['name']] ||= [] + @final_json[:global_status_by_step][cat['name']] ||= nil + # @final_json[:messages_by_step][cat['name']] ||= [] + cat['log'].select{|e| e['type'] != 'file'}.each do |el| + @final_json[:status_by_substep][el['name']]=[] + step_key = cat['name'] + "_" + el['name'] + @final_json[:messages_by_step][step_key] ||= [] + tmp_global_status = nil + # @test += el['operations'].to_json + + if el['operations'] + + el['operations'].each do |el2| + # @test += el2.to_json + @final_json[:status_by_substep][el['name']].push({:name => el2['name'], :status => el2['status'], :execution_time => el2['execution_time'], :cat => cat['name']}) + @final_json[:messages_by_step][step_key].push({:name => el2['name'], :messages => el2['messages'], :cat => cat['name']}) + if !tmp_global_status or (@h_statuses[el2['status']] and @h_statuses[el2['status']].precedence > @h_statuses[tmp_global_status].precedence) + tmp_global_status = el2['status'] + # @test += el2['status'].to_json + end + end + end + @final_json[:status_by_step][cat['name']].push({:name => el['name'], :status => (el['status'] || tmp_global_status), :execution_time => el['execution_time']}) + + if !@final_json[:global_status_by_step][cat['name']] or (@h_statuses[el['status']] and @h_statuses[el['status']].precedence > @h_statuses[@final_json[:global_status_by_step][cat['name']]].precedence) + @final_json[:global_status_by_step][cat['name']] = el['status'] + end + if !@final_json[:global_status] or (@h_statuses[el['status']] and @h_statuses[el['status']].precedence > @h_statuses[@final_json[:global_status]].precedence) + @final_json[:global_status] = el['status'] + end + @final_json[:messages_by_step][step_key].push({:name => el['name'], :messages => el['messages']}) + end + # @final_json[:messages_by_step][cat['name']].push({:name => cat['name'], :messages => cat['messages']}) + end + + + ### analyses + analyses = @log_json.select{|e| e['type'] == 'analysis'} + if analyses.size > 0 + analyses.first['log'].each do |el| + @final_json[:global_status_by_step][el['name']] ||= nil + @final_json[:status_by_step][el['name']] ||= [] + # @final_json[:status_by_substep][el['name']]=[] + @final_json[:messages_by_step][el['name']] ||= [] + + tmp_global_status = nil + + if el['levels'] + el['levels'].each do |el2| + @final_json[:status_by_step][el['name']].push({:name => el2['name'], :status => el2['status'], :execution_time => el2['execution_time']}) + @final_json[:messages_by_step][el['name']].push({:name => el2['name'], :messages => el2['messages']}) + if !tmp_global_status or (@h_statuses[el2['status']] and @h_statuses[el2['status']].precedence > @h_statuses[tmp_global_status].precedence) + tmp_global_status = el2['status'] + end + end + end + if !@final_json[:global_status_by_step][el['name']] or (@h_statuses[el['status']] and @h_statuses[el['status']].precedence > @h_statuses[@final_json[:global_status_by_step][cat['name']]].precedence) + @final_json[:global_status_by_step][el['name']] = el['status'] + end + if !@final_json[:global_status] or (@h_statuses[el['status']] and @h_statuses[el['status']].precedence > @h_statuses[@final_json[:global_status]].precedence) + @final_json[:global_status] = el['status'] + end + + @final_json[:status_by_step][el['name']].push({:name => el['name'], :status => (el['status'] || tmp_global_status), :execution_time => el['execution_time']}) + @final_json[:messages_by_step][el['name']].push({:name => el['name'], :messages => el['messages']}) + end + end + + + @update = 0 + + @status_vector = [] + if @final_json + if ['primary_dataset', 'map', 'secondary_dataset'].include? session[:selected_view] + if @final_json[:status_by_step][session[:selected_view]] + @status_vector += @final_json[:status_by_step][session[:selected_view]].map{|e| @h_statuses[e[:status]].id} + @final_json[:status_by_substep].keys.sort.each do |k| + @status_vector += @final_json[:status_by_substep][k].select{|e| e[:cat] == session[:selected_view]}.map{|e| @h_statuses[e[:status]].id} + end + end + @final_json[:messages_by_step].keys.sort.each do |k| + @status_vector += @final_json[:messages_by_step][k].select{|e| e[:cat] == session[:selected_view] and e[:messages]}.map{|e| e[:messages].size} + end + else + if @final_json[:status_by_step][session[:selected_view]] + @status_vector += @final_json[:status_by_step][session[:selected_view]].map{|e| @h_statuses[e[:status]].id} + end + if @final_json[:messages_by_step][session[:selected_view]] + @status_vector += @final_json[:messages_by_step][session[:selected_view]].select{|e| e[:messages]}.map{|e| e[:messages].size} + end + end + + end + # @final_json[:status_by_step].keys.sort.each do |name| + # @status_vector.push(@final_json[:status_by_step][name].map{|e| @h_statuses[e[:status]].id}) + # end + # @final_json[:messages_by_step].keys.sort.each do |name| + # @status_vector.push(@final_json[:messages_by_step][name].select{|e| e[:messages]}.map{|e| e[:messages].size}) + # end + + if session[:status_vector] != @status_vector + @update = 1 + end + + end ## end file exist + end + + + def refresh + +# @user = (current_user) ? current_user : User.where(:username => 'guest').first +# @user = @project. + get_basic_info() + get_statuses() + + render :partial => "refresh" + end + + def view +# @user = (current_user) ? current_user : User.where(:username => 'guest').first + + get_basic_info() + get_statuses() + get_views() + + @form_json = JSON.parse @job.form_json if @job.form_json and !@job.form_json.empty? + if !session[:current_level] + session[:current_level] = (@form_json['bin_levels'] and @form_json['bin_levels'].size > 0) ? @form_json['bin_levels'][0] : nil + end + session[:current_level] = params[:current_level].to_i if params[:current_level] + + @data_json = nil + @filename = nil + @imagename = nil + @description = '' + @i = 0 + @log = '' + + if !['primary_dataset', 'secondary_dataset', 'map'].include?(params[:partial]) + + e = @log_json.select{|el| el['type'] == 'analysis'}.first['log'].select{|el| el['name'] == params[:partial]}.first + if e['levels'] #and @form_json['bin_levels'] + @i = @form_json['bin_levels'].index(session[:current_level].to_s) + @form_json['bin_levels'].each_index do |li| + l = @form_json['bin_levels'][li] + @log += ">" + l.to_s + ";" + if l == session[:current_level].to_s + @i = li + @log += '!!' + end + end + e2 = e['levels'][@i] + @filename = e2['messages'].select{|el| el['output'] and el['output'].match(/#{@h_views[params[:partial]].data_format}$/)}.map{|el| el['output']}.first + @imagename = e2['messages'].select{|el| el['output'] and el['output'].match(/pdf|png|jpg$/)}.map{|el| el['output']}.first + @description = e2['messages'].select{|el| el['description']}.map{|el| el['description']} + else + @i = -1 + @filename = e['messages'].select{|el| el['output'] and el['output'].match(/#{@h_views[params[:partial]].data_format}$/)}.map{|el| el['output']}.first + @imagename = e['messages'].select{|el| el['output'] and el['output'].match(/pdf|png|jpg$/)}.map{|el| el['output']}.first + @description = e['messages'].select{|el| el['description']}.map{|el| el['description']} + end + end + + #end + + if @filename and File.exist? @filename + @data_json = File.read(@filename) + end + session[:selected_view] = params[:partial] + + json_file = Rails.root.join('lib', 'genocrunch_console', 'etc', 'genocrunchlib.json') + file_content = File.read(json_file) + h = JSON.parse(file_content) + @h_form_choices = h['choices'] + + render :partial => "view_" + params[:partial] + end + + # ALLOW SESSION VARIABLE UPDATE ON SHOW + # THIS ALLOW CONDITIONAL RENDERING OF EITHER FIGURES OR EDIT FORM PARTIALS + def show + # @user = (current_user) ? current_user : User.where(:username => 'guest').first + # if current_user.role == "admin" and session[:which] == "all" + # @jobs = Job.all + # @users = User.all + # else#if current_user + # @jobs = current_user.jobs.all + # end + # session[:context] = params[:context] + # logger.debug("JOB: " + @job.to_json) + #check_box belongs_to + get_basic_info() + get_statuses() + get_views() + + @analyses = [] + @inputs = [] + + @h_job_form = JSON.parse(@job.form_json) + + session[:current_level] = (@h_job_form['bin_levels'] && @h_job_form['bin_levels'][0]) || nil + + @h_form['fields']['Inputs'].select{|f| f['type'] == 'file'}.each do |f| + if @h_job_form[f['id']] + @inputs.push({:id => f['id'], :label => f['label'] || f['id'].gsub(/_/, ' ').capitalize}) + end + end + @h_form['fields']['Analysis'].select{|f| f['type'] == 'check_box' and !f['belongs_to']}.each do |f| + if @h_job_form[f['id']] #and @h_job_form[f['id']]==true + @analyses.push({:id => f['id'], :label => f['label'] || f['id'].gsub(/_/, ' ').capitalize}) + end + end + if !readable? @job + redirect_to root_path, notice: 'Cannot access this resource.' + end + end + + # ALLOW UPDATE OF INDEX UPON CHANGE AND ADMIN TO ACCESS ALL JOBS AND USERS + def manage +# session[:which] = params[:which] +# session[:what] = params[:what] + @previous_jobs = nil + if current_user.role == "admin" #and params[:which] == "all" + @jobs = Job.all + @users = User.all + @partial = "/admins/index" + if defined?(params[:previous_jobs]) + @previous_jobs = params[:previous_jobs] + end + else + @jobs = current_user.jobs + @users = current_user + @partial = "/jobs/index" + if defined?(params[:previous_jobs]) + @previous_jobs = params[:previous_jobs] + end + end + @id = "indexPartial" + respond_to do |format| + format.js + end + end + + def get_basic_info + json_file = Rails.root.join('lib', 'genocrunch_console', 'etc', 'genocrunchlib.json') + file_content = File.read(json_file) + @h_form = JSON.parse(file_content) + @h_tips = JSON.parse(File.read(Rails.root.join('public', 'app', 'tips.json'))) + @h_help = {} + @h_field_groups = {} + @h_form['fields'].keys.map{|card| @h_form['fields'][card]}.flatten.map{|field| + @h_help[field['id']]=field['help'] + ### add the fields with belongs_to to the @h_field_groups + if field['belongs_to'] + @h_field_groups[field['belongs_to']]||=[] + @h_field_groups[field['belongs_to']].push(field) + end + } + end + + # SET A NEW JOB + def new + + @user = (current_user) ? current_user : User.where(:username => 'guest').first + @job = @user.jobs.new +# if current_user + session[:current_key] = create_key() + @job.key = session[:current_key] #(Job.where(:key => session[:current_key])) ? create_key() : session[:current_key] +# end + get_basic_info + @default = {} + + end + + + def set_fields p + @missing_fields = [] + @present_fields = [] + get_basic_info + @h_fields ={} + @h_form['fields'].each_key do |card_title| + @h_form['fields'][card_title].each do |f| + @h_fields[f['id']]=f + end + end + @log = '' + + flag=0 + + if p + flag=1 + + ### check if some parameters are not allowed + p.each_key do |k| + if !@h_fields[k] + flag = 0 + # @log += k + " is missing!!!! FUCK" + break + end + end + + ### check if all parameters required are submitted + @h_fields.keys.select{|k| @h_fields[k]['optional'] == false}.each do |k| + logger.debug("EXPLORE field " + k) + if (@h_fields[k]['type']== 'file' and ((p[k] and !p[k].blank? #p[k].original_filename and !p[k].original_filename.empty? + ) or !params[:p2][k].empty?) ) or (p[k] and !p[k].empty?) + @present_fields.push(@h_fields[k]) + else + @missing_fields.push(@h_fields[k]) + flag = 0 + # @log += k + " is missing!!!! FUCK2" + end + + end + end + + return flag + end + + def create_dirs + + user_id = (current_user) ? current_user.id.to_s : "1" + + dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + user_id + Dir.mkdir dir if !File.exist? dir + dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + user_id + @job.key + Dir.mkdir dir if !File.exist? dir + + end + + def write_files p + + user_id = (current_user) ? current_user.id.to_s : "1" + dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + user_id + @job.key + 'input' + Dir.mkdir dir if !File.exist? dir + + form_json = JSON.parse @job.form_json if @job.form_json and !@job.form_json.empty? + +# fields = @h_fields.keys.select{|k| @h_fields[k]['type'] == 'file' and p[k]} + fields = ['primary_dataset', 'map', 'secondary_dataset'] + fields.each do |k| + logger.debug("Key:" + k) + filepath = dir + ( k + '.txt') + content = (p[k]) ? p[k].read : nil + if content and !content.empty? + params[:p2][k] = p[k].original_filename + File.open(filepath, 'w') do |f| + logger.debug("CONTENT FILE: " + content) + f.write content + end + `dos2unix #{filepath}` + `mac2unix #{filepath}` + elsif form_json and form_json[k.to_s] + params[:p2][k] = form_json[k.to_s]['original_filename'] + params[:p][k] = form_json[k.to_s] + end + end + + end + + # CREATE A NEW JOB + def create + +# @job = Job.new(job_params) + @job = Job.new(:key => params[:tmp_key]) + + create_dirs() + write_files(params[:p]) + + @valid_job = set_fields params[:p] + @job.name = params[:p][:name] + @job.description = params[:p][:description] + @job.form_json = params[:p].to_json + + @default = params[:p] + + if current_user + @job.user_id = current_user.id + @job.sandbox = false + end + + ## not possible to create a new job with an existing key + @valid_job = 0 if !@job.key.empty? and Job.where(:key => @job.key).first + + respond_to do |format| + + if @valid_job==1 and @job.save + + @job.delay.perform # run analysis as delayed job + session[:agree_with_terms] = true + session[:current_key]=create_key() if current_user + + format.html { redirect_to job_path(@job.key) } + format.json { render action: 'new', status: :created, location: @job } + else + format.html { render action: 'new' } + format.json { render json: @job.errors, status: :unprocessable_entity } + end + end + end + + def edit + get_basic_info + @default = JSON.parse(@job.form_json) + params[:p2]={} + + ['primary_dataset', 'map', 'secondary_dataset'].select{|k| @default[k]}.each do |k| + params[:p2][k] = @default[k]['original_filename'] + end + + end + + # PATCH/PUT /jobs/1 + # PATCH/PUT /jobs/1.json + def update + + write_files(params[:p]) + + valid_job = set_fields params[:p] + + h_job = { + :name => params[:p][:name], + :description => params[:p][:description], + :form_json => params[:p].to_json + } + + @default = JSON.parse(@job.form_json) + @default = params[:p] + + respond_to do |format| + if valid_job==1 and @job.update_attributes(h_job) + + #kill job + kill_job(@job) + + @job.delay.perform # run analysis as delayed job + session[:agree_with_terms] = true + # if current_user.role == "admin" and @job.user.role != "admin" + # format.html { redirect_to jobs_url } + # else +# format.html { redirect_to jobs_url } + format.html {redirect_to job_path(@job.key)} + # end + format.json { head :no_content } + else + format.html { render action: 'edit'} + format.json { render json: @job.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /jobs/1 + # DELETE /jobs/1.json + def destroy + @job.destroy + respond_to do |format| + format.html { redirect_to jobs_url } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_job +# if current_user.role == "admin" +# @job = Job.all.find(params[:key]) +# else +# @job = current_user.jobs.find(params[:key]) +# end + @job = Job.where(:key => params[:key] || params[:tmp_key]).first + session[:current_key] = @job.key if action_name != 'clone' #if current_user + @user = @job.user + logger.debug("JOB: " + @job.to_json) + # @job = nil if !readable? @job + end + + # Never trust parameters from the scary internet, only allow the white list through. + def job_params + params.require(:job).permit() + end +end + diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 0000000..1126e23 --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,28 @@ +class Users::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + # def new + # super + # end + + # POST /resource/confirmation + # def create + # super + # end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + # protected + + # The path used after resending confirmation instructions. + # def after_resending_confirmation_instructions_path_for(resource_name) + # super(resource_name) + # end + + # The path used after confirmation. + # def after_confirmation_path_for(resource_name, resource) + # super(resource_name, resource) + # end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 0000000..1907e5b --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,28 @@ +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/plataformatec/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when OmniAuth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 0000000..53cc34e --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,32 @@ +class Users::PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 0000000..9db80f0 --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,88 @@ +class Users::RegistrationsController < Devise::RegistrationsController + + def create + + build_resource(sign_up_params) + if verify_rucaptcha?(resource) && resource.save + yield resource if block_given? + if resource.persisted? + if resource.active_for_authentication? + set_flash_message! :notice, :signed_up + sign_up(resource_name, resource) + respond_with resource, location: after_sign_up_path_for(resource) + else + set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}" + expire_data_after_sign_in! + respond_with resource, location: after_inactive_sign_up_path_for(resource) + end + else + clean_up_passwords resource + set_minimum_password_length + respond_with resource + end + else + clean_up_passwords resource + respond_with resource + end + end + + +# before_filter :configure_sign_up_params, only: [:create] +# before_filter :configure_account_update_params, only: [:update] + + # GET /resource/sign_up + # def new + # super + # end + + # POST /resource + # def create + # super + # end + + # GET /resource/edit + # def edit + # super + # end + + # PUT /resource + # def update + # super + # end + + # DELETE /resource + # def destroy + # super + # end + + # GET /resource/cancel + # Forces the session data which is usually expired after sign + # in to be expired now. This is useful if the user wants to + # cancel oauth signing in/up in the middle of the process, + # removing all OAuth session data. + # def cancel + # super + # end + + #protected + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_up_params + # devise_parameter_sanitizer.for(:sign_up) << :attribute + # end + + # If you have extra params to permit, append them to the sanitizer. + # def configure_account_update_params + # devise_parameter_sanitizer.for(:account_update) << :attribute + # end + + # The path used after sign up. + #def after_sign_up_path_for(resource) + # new_user_session_path + #end + + # The path used after sign up for inactive accounts. + #def after_inactive_sign_up_path_for(resource) + # new_user_session_path + #end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 0000000..8ed869c --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,26 @@ +class Users::SessionsController < Devise::SessionsController +# before_filter :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + # protected + + # If you have extra params to permit, append them to the sanitizer. + # def configure_sign_in_params + # devise_parameter_sanitizer.for(:sign_in) << :attribute + # end + +end diff --git a/app/controllers/users/unlocks_controller.rb b/app/controllers/users/unlocks_controller.rb new file mode 100644 index 0000000..8b9ef86 --- /dev/null +++ b/app/controllers/users/unlocks_controller.rb @@ -0,0 +1,28 @@ +class Users::UnlocksController < Devise::UnlocksController + # GET /resource/unlock/new + # def new + # super + # end + + # POST /resource/unlock + # def create + # super + # end + + # GET /resource/unlock?unlock_token=abcdef + # def show + # super + # end + + # protected + + # The path used after sending unlock password instructions + # def after_sending_unlock_instructions_path_for(resource) + # super(resource) + # end + + # The path used after unlocking the resource + # def after_unlock_path_for(resource) + # super(resource) + # end +end diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb new file mode 100644 index 0000000..281bd63 --- /dev/null +++ b/app/controllers/versions_controller.rb @@ -0,0 +1,83 @@ +class VersionsController < ApplicationController + before_action :set_version, only: [:show, :edit, :update, :destroy] + + # GET /versions + # GET /versions.json + def index + @versions = Version.all + + # packages_R.json + filename = Rails.root.join('lib', 'genocrunch_console', 'etc', 'packages_R.json') + json = File.read(filename) + list_tools = JSON.parse(json) + @h_tools = {} + list_tools.each do |t| + @h_tools[t['name']] = t + end + end + + # GET /versions/1 + # GET /versions/1.json + def show + end + + # GET /versions/new + def new + @version = Version.new + end + + # GET /versions/1/edit + def edit + end + + # POST /versions + # POST /versions.json + def create + @version = Version.new(version_params) + + respond_to do |format| + if @version.save + format.html { redirect_to @version, notice: 'Version was successfully created.' } + format.json { render :show, status: :created, location: @version } + else + format.html { render :new } + format.json { render json: @version.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /versions/1 + # PATCH/PUT /versions/1.json + def update + respond_to do |format| + if @version.update(version_params) + format.html { redirect_to @version, notice: 'Version was successfully updated.' } + format.json { render :show, status: :ok, location: @version } + else + format.html { render :edit } + format.json { render json: @version.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /versions/1 + # DELETE /versions/1.json + def destroy + @version.destroy + respond_to do |format| + format.html { redirect_to versions_url, notice: 'Version was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_version + @version = Version.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def version_params + params.require(:version).permit(:description, :tools_json, :release_date) + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/examples_helper.rb b/app/helpers/examples_helper.rb new file mode 100644 index 0000000..d206554 --- /dev/null +++ b/app/helpers/examples_helper.rb @@ -0,0 +1,2 @@ +module ExamplesHelper +end diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb new file mode 100644 index 0000000..23de56a --- /dev/null +++ b/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/app/helpers/jobs_helper.rb b/app/helpers/jobs_helper.rb new file mode 100644 index 0000000..adcbb90 --- /dev/null +++ b/app/helpers/jobs_helper.rb @@ -0,0 +1,160 @@ +module JobsHelper + + def display_duration t + parts = [] + tab = [] + if t + tab = t.split(":") + parts.push(tab[0] + "h") if tab[0].to_i > 0 + parts.push(tab[1] + "m") if tab[1].to_i > 0 + sec = tab[2].to_f + parts.push(sec.round(1).to_s + "s") if sec > 0.0 + end + return (parts.size > 0) ? "" + parts.join(" ") + "" : '' + end + + def help_button f + html = "" + if f['trigger'] && f['trigger'] == 'drop_down' + html += "" + end + return html + end + + def field_label f + label = (f['label']) ? f['label'] : f['id'].gsub(/_/, ' ').capitalize + css_classes = [] + css_classes.push('form-check-label') if f['type'] == 'check_box' #) ? 'form-check-label' : '' + html = "" + return [html, html2] + end + + def field_check_box f + val = '' + + if f['trigger'] && f['trigger'] == 'drop_down' + val = (@default[f['db_field']] && @default[f['db_field']].include?(f['id'])) || @default[f['id']] || false + # val = @default[f['id']] || f['default'] + else + val = @default[f['id']] || false + end +# id = (f['trigger'] && f['trigger'] == 'drop_down') ? "p[#{f['db_field']}][#{f['id']}]" : "p[#{f['id']}]" + id = "p[#{f['id']}]" + label = field_label f + html = "
    " + html += label.first + html += check_box_tag id, 1, val, {:class => 'form-check-input'} + html += label.second + html += "
    " + +# if f['trigger'] && f['trigger'] == 'drop_down' +# html += "" +# end + + return html + end + + def field_bool f + checked = @default[f['id']] || f['default'] + label = field_label f + html = "
    " + html += label.first + html += hidden_field_tag "p[#{f['id']}]", 'FALSE' + html += check_box_tag "p[#{f['id']}]", 'TRUE', checked, {:class => 'form-check-input'} + html += label.second + html += "
    " + return html + end + + def field_integer f + val = @default[f['id']] || f['default'] + return text_field_tag "p[#{f['id']}]", val, {:placeholder => (f['placeholder'] || ''), :class => 'form-control' } + end + + def field_hidden f + val = @default[f['id']] || f['default'] + return hidden_field_tag "p[#{f['id']}]", val + end + + def field_text f + val = @default[f['id']] || f['default'] + css_classes = ["form-control full-width"] + css_classes.push('belongs_to') if f['belongs_to'] + return text_field_tag "p[#{f['id']}]", val, {:placeholder => (f['placeholder'] || ''), :class => css_classes.join(' ') } + end + + def field_textarea f + val = @default[f['id']] || f['default'] + css_classes = ["form-control full-width"] + css_classes.push('belongs_to') if f['belongs_to'] + return text_area_tag "p[#{f['id']}]", val, {:placeholder => (f['placeholder'] || ''), :class => css_classes.join(' ') } + end + + def field_select f, h_c + val = @default[f['id']] || f['default'] + l = (h_c) ? h_c.map{|e| [e['label'], e['value']]} : [] + css_class=(f['belongs_to']) ? 'belongs_to' : '' + html = "
    " + html += select_tag "p[#{f['id']}]", options_for_select(l, val), {:placeholder => (f['placeholder'] || ''), multiple: (f["multiple"] && f["multiple"] == true), :class => "form-control full-width multiselect #{css_class}" } + html += "
    " + + return html + end + + def toto + +# +# <%= f.text_field(key.to_sym, value: default_value, placeholder: ((field["options"].key?("placeholder"))? field["options"]["plac#eholder"] : ''), class:"form-control form-text") %> + + end + + + # Build a hash of paramters (sort of 'flatten' the app JSON) + # element_h (hash) A hash containing fields + # params_h (hash) Parameter hash to build + # type (array) Type of field to search for + def set_params(element_h, params_h, type) + for t in type + if element_h.keys.include? t + element_h[t].each do |key, field| + # Check if field represents a parameter to be set in the form + if field.keys.include? "param" and ["all", "web"].include? field["scope"] + # If field is not included in the parameter hash already, ad it + if !params_h.keys.include? field["param"] + params_h[field["param"].to_sym] = Hash.new + params_h[field["param"].to_sym][:optional] = field["optional"] + params_h[field["param"].to_sym][:scope] = field["scope"] + if field.keys.include? "type" + params_h[field["param"].to_sym][:type] = field["type"] + end + if field.keys.include? "options" + if field["options"].keys.include? "default" + params_h[field["param"].to_sym][:default] = field["options"]["default"] + end + if field["options"].keys.include? "hidden_if" + params_h[field["param"].to_sym][:hidden_if] = field["options"]["hidden_if"] + end + end + if ["text-if-file", "bool-if-file", "select-from-field", "select-from-file-row", "select-from-file-category", "select-heatmap-sidebar"].include? field["type"] + params_h[field["param"].to_sym][:options] = field["options"] + end + end + end + end + end + end + end +end diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb new file mode 100644 index 0000000..c9d0dd8 --- /dev/null +++ b/app/helpers/versions_helper.rb @@ -0,0 +1,2 @@ +module VersionsHelper +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..a009ace --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..b7ec5f5 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,5 @@ +class ApplicationMailer < Devise::Mailer + def headers_for(action, opts) + super.merge!({template_path: '/users/mailer'}) + end +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/example.rb b/app/models/example.rb new file mode 100644 index 0000000..8b11c39 --- /dev/null +++ b/app/models/example.rb @@ -0,0 +1,3 @@ +class Example < ApplicationRecord + +end diff --git a/app/models/info_link.rb b/app/models/info_link.rb new file mode 100644 index 0000000..4b9f9be --- /dev/null +++ b/app/models/info_link.rb @@ -0,0 +1,2 @@ +class Info_link < ApplicationRecord +end diff --git a/app/models/job.rb b/app/models/job.rb new file mode 100644 index 0000000..8edc2a6 --- /dev/null +++ b/app/models/job.rb @@ -0,0 +1,28 @@ +class Job < ActiveRecord::Base + + belongs_to :user + + # START THE JOB + def perform + self.update_attribute("status", 'running') + + `rails run_job[#{self.key}]` + + end + + def success + self.status = "completed" + self.save + end + + def failure + self.status = "failed" + self.output = "unavailable" + self.save + end + + def abort + self.kill + end + +end diff --git a/app/models/status.rb b/app/models/status.rb new file mode 100644 index 0000000..3a33854 --- /dev/null +++ b/app/models/status.rb @@ -0,0 +1,2 @@ +class Status < ApplicationRecord +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..704fee3 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,42 @@ +class User < ActiveRecord::Base + + has_many :jobs + + # Include default devise modules. Others available are: + # :token_authenticatable, :confirmable, + # :lockable, :timeoutable and :omniauthable + #devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable + + if APP_CONFIG[:user_confirmable] + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :confirmable + else + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable + end + + validates :username, + :presence => true, + :uniqueness => { + :case_sensitive => false + } + + validates :email, + :presence => true, + :uniqueness => { case_sensitive: false }, + :format => { + :with => Devise.email_regexp, :allow_blank => true, :if => :email_changed? + } + + validates :password, + :presence => { + :on=>:create + }, + :confirmation => { + :on=>:create + }, + :length => { + :within => Devise.password_length + } + + protected + +end diff --git a/app/models/version.rb b/app/models/version.rb new file mode 100644 index 0000000..ae11fe3 --- /dev/null +++ b/app/models/version.rb @@ -0,0 +1,2 @@ +class Version < ApplicationRecord +end diff --git a/app/models/view.rb b/app/models/view.rb new file mode 100644 index 0000000..5c99669 --- /dev/null +++ b/app/models/view.rb @@ -0,0 +1,2 @@ +class View < ApplicationRecord +end diff --git a/app/views/admins/_index.html.erb b/app/views/admins/_index.html.erb new file mode 100644 index 0000000..96385b5 --- /dev/null +++ b/app/views/admins/_index.html.erb @@ -0,0 +1,18 @@ + + +<%= link_to 'RefreshIndex', {:controller => "jobs", :action => 'manage', :which => session[:which], :what => session[:what], :id => "refreshIndex", :previous_jobs => @jobs.map {|e| e.status}.to_a}, :remote => true, id: "refreshIndex", class: "hidden" %> + +
    + <% if session[:what] == "users" %> + <%= render partial: '/admins/users' %> + <% else %> + <%= render partial: '/jobs/jobs' %> + <% end %> +
    + diff --git a/app/views/admins/_users.html.erb b/app/views/admins/_users.html.erb new file mode 100644 index 0000000..0fd07fb --- /dev/null +++ b/app/views/admins/_users.html.erb @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + <% @users.each do |user| %> + + + + + + + + + + + + <% end %> + + +
    +
    +
    + + + +
    + +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    + + + +
    + +
    +
    +
    +
    + + + +
    + +
    +
    +
    completed + +
    +
    +
    pending + +
    +
    +
    failed + +
    +
    + <%= user.id %> + + <%= user.username %> + + <%= user.role %> + + <%= user.email %> + + <%= user.updated_at %> + + <%= user.jobs.select{ |i| i.status == "completed" }.length %> + + <%= user.jobs.select{ |i| i.status == "pending" }.length %> + + <%= user.jobs.select{ |i| i.status == "failed" }.length %> +
    + + + + + + + + + + + + + + + + + + +
    userscompletedpendingfailed
    <%= @users.length %><%= Job.all.select{ |i| i.status == "completed" }.length %><%= Job.all.select{ |i| i.status == "pending" }.length %><%= Job.all.select{ |i| i.status == "failed" }.length %>
    diff --git a/app/views/examples/_example.json.jbuilder b/app/views/examples/_example.json.jbuilder new file mode 100644 index 0000000..9ded881 --- /dev/null +++ b/app/views/examples/_example.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! example, :id, :created_at, :updated_at +json.url example_url(example, format: :json) \ No newline at end of file diff --git a/app/views/examples/_form.html.erb b/app/views/examples/_form.html.erb new file mode 100644 index 0000000..3b51e81 --- /dev/null +++ b/app/views/examples/_form.html.erb @@ -0,0 +1,22 @@ +<%= form_for(example) do |f| %> + <% if example.errors.any? %> +
    +

    <%= pluralize(example.errors.count, "error") %> prohibited this example from being saved:

    + +
      + <% example.errors.full_messages.each do |message| %> +
    • <%= message %>
    • + <% end %> +
    +
    + <% end %> + +Job Key +<%= f.text_field 'job_key' %> + +
    + + + <%= f.submit %> +
    +<% end %> diff --git a/app/views/examples/edit.html.erb b/app/views/examples/edit.html.erb new file mode 100644 index 0000000..f4b411f --- /dev/null +++ b/app/views/examples/edit.html.erb @@ -0,0 +1,6 @@ +

    Editing Example

    + +<%= render 'form', example: @example %> + +<%= link_to 'Show', @example %> | +<%= link_to 'Back', examples_path %> diff --git a/app/views/examples/index.html.erb b/app/views/examples/index.html.erb new file mode 100644 index 0000000..b345fb4 --- /dev/null +++ b/app/views/examples/index.html.erb @@ -0,0 +1,27 @@ +
    +

    <%= notice %>

    + +

    Examples

    + +
    +<% @jobs.each do |j| %> + +
    +
    +
    +Load +
    + <%= j.name %> +
    +
    + +

    <%= j.description %>

    +
    +
    +<% end %> +
    +<%= link_to 'New Example', new_example_path if admin? %> +
    +<%= javascript_tag do %> + +<% end %> diff --git a/app/views/examples/index.json.jbuilder b/app/views/examples/index.json.jbuilder new file mode 100644 index 0000000..5935093 --- /dev/null +++ b/app/views/examples/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @examples, partial: 'examples/example', as: :example \ No newline at end of file diff --git a/app/views/examples/new.html.erb b/app/views/examples/new.html.erb new file mode 100644 index 0000000..ad31608 --- /dev/null +++ b/app/views/examples/new.html.erb @@ -0,0 +1,5 @@ +

    New Example

    + +<%= render 'form', example: @example %> + +<%= link_to 'Back', examples_path %> diff --git a/app/views/examples/show.html.erb b/app/views/examples/show.html.erb new file mode 100644 index 0000000..5917896 --- /dev/null +++ b/app/views/examples/show.html.erb @@ -0,0 +1,4 @@ +

    <%= notice %>

    + +<%= link_to 'Edit', edit_example_path(@example) %> | +<%= link_to 'Back', examples_path %> diff --git a/app/views/examples/show.json.jbuilder b/app/views/examples/show.json.jbuilder new file mode 100644 index 0000000..4cc9c48 --- /dev/null +++ b/app/views/examples/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "examples/example", example: @example \ No newline at end of file diff --git a/app/views/home/_doc.html.erb b/app/views/home/_doc.html.erb new file mode 100644 index 0000000..b4996c6 --- /dev/null +++ b/app/views/home/_doc.html.erb @@ -0,0 +1,496 @@ +

    Documentation

    + +

    Version 1.1

    + +

    Overview

    +

    Genocrunch is a web-based data mining platform dedicated to metagenomics and metataxonomics. It provides data mining tools for data pre-processing and transformation, diversity analysis, multivariate statistics, dimensionality reduction, differential analysis, clustering as well as correlation and similarity network analysis. Interactive visualization is offered for all figures.

    + +

    Pipeline

    +

    The general organization of the tools is described in the figure below. + +

    +<%= image_tag 'genocrunch_pipeline.png' %> +

    + +

    New users and registration

    + +

    Running an analysis session does not require any registration, however, managing multiple analysis (recovering/editing previous work) is done through a personnal index that requires users to sign in. Before signing in, new users need to register.

    + +

    Registering

    +

    Registering allows users to recover and edit their previous work via a personal index page. Registering is done via the Register button of the top-bar. The registration process requires new users to chose a password and provide an email address. The email address is only used to recover forgotten passwords. +<% if APP_CONFIG[:user_confirmable] %> + In order to complete the registration process, new users need to follow a confirmation link which is automatically sent to the provided email address. In case of issue, this confirmation link can be resent to the provided email address via the Resend confirmation instruction link on the welcome page. +<% end %> +

    + +

    Signing in

    +

    Signing in is done from the welcome page, which is accessible via the Sign In button of the top-bar. Before signing in, new users need to complete the registration process.

    + +

    Trying Genocrunch

    +

    The best way to try Genocrunch is to load an example and edit it.

    + +

    Examples

    +

    Examples of analyses are provided via the Examples link of the top-bar. Loading Example data into a new analysis session is possible via the Load button. This allows to visualize and edit Examples without restriction. For signed-in users, it will also clone the example into their personal job index.

    + +

    Analyzing data

    + +

    Running a new analysis

    +

    New analyses can be submitted via the New Job button of the topbar. If the user is signed out, the analysis session will be associated to the browser session and remain available as long as the browser is not closed. If the user had been signed-in, the job will be stored in his personal index. Jobs parameters are set by filling a single form comprising four parts for Inputs, Pre-processing, Transformation and Analysis. Jobs are then submitted via the Create Job button located at the bottom the form. See the Inputs and Tools section below for details.

    + +

    Editing an analysis

    +

    Analyses can be edited via the associated Edit button present on the analysis page and via the edit icons () of the analysis index (for signed-in users only). Editing is performed by updating the job form and submiting the modifications via the Update Job button located at the bottom of the form.

    + +

    Cloning an analysis

    +

    Analyses can be cloned (copied) via the clone icons () of the analysis index (for signed-in users only).

    + +

    Deleting an analysis

    +

    Analyses can be deleted via the delete icons () of the analysis index (for signed-in users only). This process is irreversible.

    + +

    Inputs and Tools

    +

    The analyses are set from a single form composed of four sections: Inputs, Pre-processing, Transformation, Analysis. These sections and their associated parameters are described below.

    + +

    Inputs

    +

    The Inputs section of the job creation form allows to upload data files.

    +
      +
    • General information +
        +
      • Name (mandatory) +

        The name will appear in the job index. It should be informative enough to differentiate from other jobs.

        +
      • +
      • Description +

        The description can be used to provide additional information about the job.

        +
      • +
      +
    • +
    • Data files +
        + +
      • Primary dataset (mandatory) +

        The primary dataset is the principal data file. It must contain a data table in the tab-delimited text format with columns representing samples and rows representing observations. The first row must contain the names of samples. The first column must contain the names of the observations. Both columns and rows names must be unique. A column containing a description of each observation, in the form of semi-column-delimited categories, can be added. This format can be obtained from the BIOM format using the biom_convert command.

        +

        Format example:

        +
        +#OTU ID	Spl1	Spl2	Spl3	Spl4	taxonomy
        +1602	1	2	4	4	k__Bacteria; p__Actinobacteria; c__Actinobacteria; o__Actinomycetales; f__Propionibacteriaceae; g__Propionibacterium; s__acnes
        +1603	0	0	24	0	k__Bacteria; p__Firmicutes; c__Clostridia; o__Clostridiales; f__Clostridiaceae; g__Clostridium; s__difficile
        +1604	0	12	23	0	k__Bacteria; p__Firmicutes; c__Bacilli; o__Lactobacillales; f__Enterococcaceae; g__Enterococcus; s__casseliflavus
        +1605	23	4	2	14	k__Bacteria; p__Firmicutes; c__Clostridia; o__Clostridiales; f__Veillonellaceae; g__Veillonella; s__parvula
        +1606	45	10	42	12	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Bacteroidaceae; g__Bacteroides; s__fragilis
        +1607	8	15	20	13	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Prevotellaceae; g__Prevotella; s__copri
        +
        +
      • +
      • Category column (mandatory) +

        To increase compatibility, the category column specifies which column in the pimary dataset contains a categorical description of the observations.

        +
      • + + +
      • Map (mandatory) +

        The map contains information about the experimental design. It must contain a table in the tab-delimited text format with columns representing experimental variables and rows representing samples. The first column must contain the names of the samples as they appear in the primary dataset. The first row must contain the names of experimental variables. Both columns and rows names must be unique. This format is compatible with the mapping file used by the Qiime pipeline.

        +

        Format example:

        +
        +ID	Sex	Treatment
        +Spl1	M	Treated
        +Spl2	F	Treated
        +Spl3	M	Control
        +Spl4	F	Control
        +
        +
      • + +
      • Secondary dataset +

        The secondary dataset is an optional data file containing additional observations. It must contain a data table in the tab-delimited text format with columns representing samples and rows representing observations. The first row must contain the names of samples as they appear in the primary dataset and the map. The first column must contain the names of the observations. Both columns and rows names must be unique.

        +

        Format example:

        +
        +Metabolites	Spl1	Spl2	Spl3	Spl4
        +metabolite1	0.24	0.41	1.02	0.92
        +metabolite2	0.98	0.82	1.12	0.99
        +metabolite3	0.05	0.11	0.03	0.02
        +
        +
      • +
      +
    • +
    + +

    Pre-processing

    +

    The Pre-processing section of the job creation form specifies modifications that will be applied to the primary dataset prior to analysis.

    +
      +
    • Filtering +

      Data in the primary dataset can be filtered based on Relative or Absolute abundance and presence.

      +
        +
      • Abundance threshold +

        Minimal abundance per sample to be retained.

        +
      • +
      • Presence thresholds +

        Minimal presence among samples to be retained.

        +
      • +
      +

      Example: Filtering data with an absolute abundance threshold of 5 and and an absolute presence threshold of 2.

      +
      +#Before filtering
      +#OTU ID	Spl1	Spl2	Spl3	Spl4	taxonomy
      +1602	1	2	4	4	k__Bacteria; p__Actinobacteria; c__Actinobacteria; o__Actinomycetales; f__Propionibacteriaceae; g__Propionibacterium; s__acnes
      +1603	0	0	24	0	k__Bacteria; p__Firmicutes; c__Clostridia; o__Clostridiales; f__Clostridiaceae; g__Clostridium; s__difficile
      +1604	0	12	23	0	k__Bacteria; p__Firmicutes; c__Bacilli; o__Lactobacillales; f__Enterococcaceae; g__Enterococcus; s__casseliflavus
      +1605	23	4	2	14	k__Bacteria; p__Firmicutes; c__Clostridia; o__Clostridiales; f__Veillonellaceae; g__Veillonella; s__parvula
      +1606	45	10	42	12	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Bacteroidaceae; g__Bacteroides; s__fragilis
      +1607	8	15	20	13	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Prevotellaceae; g__Prevotella; s__copri
      +
      +
      +#After filtering
      +#OTU ID	Spl1	Spl2	Spl3	Spl4	taxonomy
      +1604	0	12	23	0	k__Bacteria; p__Firmicutes; c__Bacilli; o__Lactobacillales; f__Enterococcaceae; g__Enterococcus; s__casseliflavus
      +1605	23	4	2	14	k__Bacteria; p__Firmicutes; c__Clostridia; o__Clostridiales; f__Veillonellaceae; g__Veillonella; s__parvula
      +1606	45	10	42	12	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Bacteroidaceae; g__Bacteroides; s__fragilis
      +1607	8	15	20	13	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Prevotellaceae; g__Prevotella; s__copri
      +
      +
    • +
    • Category binning +

      Data in primary dataset can be binned by category thanks to the category column.

      +
        +
      • Binning levels +

        Category levels at which data should be binned.

        +
      • +
      • Category binning function +

        Chose whether binning should be performed by summing or averaging values.

        +
      • +
      +

      Example: Binning data at level 2 with sum.

      +
      +#Before binning
      +#OTU ID	Spl1	Spl2	Spl3	Spl4	taxonomy
      +1604	0	12	23	0	k__Bacteria; p__Firmicutes; c__Bacilli; o__Lactobacillales; f__Enterococcaceae; g__Enterococcus; s__casseliflavus
      +1605	23	4	2	14	k__Bacteria; p__Firmicutes; c__Clostridia; o__Clostridiales; f__Veillonellaceae; g__Veillonella; s__parvula
      +1606	45	10	42	12	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Bacteroidaceae; g__Bacteroides; s__fragilis
      +1607	8	15	20	13	k__Bacteria; p__Bacteroidetes; c__Bacteroidia; o__Bacteroidales; f__Prevotellaceae; g__Prevotella; s__copri
      +
      +
      +#After binning
      +	ID	Spl1	Spl2	Spl3	Spl4	taxonomy
      +1	23	16	25	14	k__Bacteria; p__Firmicutes
      +2	53	25	62	25	k__Bacteria; p__Bacteroidetes
      +
      +
    • +
    + +

    Transformation

    +

    The Transformation section of the job creation form propose additional modifications for both primary and secondary datasets.

    +
      +
    • Rarefaction +

      Although controversial (McMurdie and Holmes, 2014), rarefaction is a popular method amongst microbial ecologists used to correct for variations in sequencing depth, inherent to high-throughput sequencing methods. It consists in random sampling (without replacement) of a fixed number of count from each sample to be compared. The same number of count being drawn out of each sample, this corrects for differences in sequencing depth while conserving the original count distribution among observations. Note that the analysis of diversity is particularly sensitive to variations in sequencing depth.

      + +
        +
      • Sampling depth +

        This specifies the number of count to be drawn from each sample.

        +

        Tip: The maximal sampling depth corresponds to the minimal sequencing depth. First check the sequencing depth of your data by suming the counts in each sample. Then chose a sampling depth accordingly.

        +
      • +
      • N samplings +

        This specifies how many times the random drawing should be repeated. If more than 1 random sampling is done, the result is the average of all random samplings.

        +
      • +
      +
    • +
    • Transformation +

      Whether it is to correct for sequencing depth, to stabilize the variance or simply to improve the visualization of skewed data, a transformation step may be needed. Common methods include transforming counts into proportions (percent or counts per million) and applying a log. We are working to also propose more advanced transformations, including those developed for RNA sequencing (RNA-seq) as part of the DESeq2 (Love et al., 2014) and the Voom (Law et al., 2014) pipelines. To limit the effect of known experimental bias (or batch-effect), the Combat method can be applied.

      + +
        +
      • Transformation +

        Specifies which transformation method should be applied.

        +
      • +
      +
    • +
    • Batch-effect suppression Under construction +
        +
      • Batch-effect suppression +

        Specifies which column from the map should be used to suppress batch effect.

        +
      • +
      +
    • +
    + +

    Analysis

    +

    The Analysis section of the job creation form sets which statistics to apply and which figures to generate.

    +
      +
    • Experimental design +

      This sets the default statistical method to apply for the analysis. Two options are available: Basic and Advanced.

      +
        +
      • Statistics (Advanced only) +

        Chose a statistical method to compare groups of samples. If Basic is chosen, this defaults to ANOVA.

        +
      • +
      • Model +

        Chose a model defining groups of samples. If Basic is chosen, a model can be picked among headers of the map. If Advanced is chosen, the model must be typed by the user in the form of an R formula. The formula must be compatible with the specified statistics. All terms of the formula must refer to column headers of the map.

        +
      • +
      + + Examples: +
        +
      1. Simple design: + +
        +ID	Subject	Site
        +Spl1	subject1	Hand
        +Spl2	subject2	Hand
        +Spl3	subject3	Hand
        +Spl4	subject4	Foot
        +Spl5	subject5	Foot
        +Spl6	subject6	Foot
        +
        + + + + + + + + + + + + + + + + + + + + +
        StatisticsModel
        Comparing sites (parametric)T-testSite
        Comparing sites (non-parametric)Wilcoxon rank sum testSite
        + +
      2. +
      3. Simple paired design: + +
        +ID	Subject	Site
        +Spl1	subject1	Hand
        +Spl2	subject2	Hand
        +Spl3	subject3	Hand
        +Spl4	subject1	Foot
        +Spl5	subject2	Foot
        +Spl6	subject3	Foot
        +
        + + + + + + + + + + + + + + + + + + + + +
        StatisticsModel
        Comparing sites (parametric)Paired t-testSite
        Comparing sites (non-parametric)Wilcoxon signed rank testSite
        + +
      4. +
      5. Multiple comparisons: + +
        +ID	Subject	Site
        +Spl1	subject1	Hand
        +Spl2	subject2	Hand
        +Spl3	subject3	Hand
        +Spl4	subject4	Foot
        +Spl5	subject5	Foot
        +Spl6	subject6	Foot
        +Spl7	subject7	Mouth
        +Spl8	subject8	Mouth
        +Spl9	subject9	Mouth
        +
        + + + + + + + + + + + + + + + + + + + + +
        StatisticsModel
        Comparing sites (parametric)ANOVASite
        Comparing sites (non-parametric)Kruskal-Wallis rank sum testSite
        + +
      6. +
      7. Multiple comparisons with nesting: + +
        +ID	Subject	Site
        +Spl1	subject1	Hand
        +Spl2	subject2	Hand
        +Spl3	subject3	Hand
        +Spl4	subject1	Foot
        +Spl5	subject2	Foot
        +Spl6	subject3	Foot
        +Spl7	subject1	Mouth
        +Spl8	subject2	Mouth
        +Spl9	subject3	Mouth
        +
        + + + + + + + + + + + + + + + +
        StatisticsModel
        Comparing sitesFriedman testSite | Subject
        + +
      8. +
      9. Additive model: + +
        +ID	Treatment	Gender
        +Spl1	Treated	M
        +Spl2	Treated	M
        +Spl3	Treated	M
        +Spl4	Treated	F
        +Spl5	Treated	F
        +Spl6	Treated	F
        +Spl7	Control	M
        +Spl8	Control	M
        +Spl9	Control	M
        +Spl10	Control	F
        +Spl11	Control	F
        +Spl12	Control	F
        +
        + + + + + + + + + + + + + + + +
        StatisticsModel
        Assessing effects of treatment and genderANOVATreatment+Gender
        + +
      10. +
      + +
    • + +
    • Proportions +

      For each sample, display the proportions of each observation (as percent) in the form of a stacked barchart. This gives an overview of the primary dataset. This analysis is performed before applying any selected transformation.

      +
    • + +
    • Diversity +

      Perform a diversity analysis of the primary dataset. This will display rarefaction curves for the selected diversity metric(s). Rarefaction curves are used to estimate the diversity in function of the sampling depth. A rarefaction curve showing an asymptotic behavior is considered as an indicator of sufficient sampling depth to observe the sample diversity. This analysis is performed before applying any selected transformation.

      +
        +
      • metric +

        Chose a diversity metric. Choices include the richness as well as metrics included in the R vegan, ineq and fossil packages. The richness simply represents the number of different observations that are seen within a particular sample.

        +
      • +
      • Compare groups +

        If selected, diversity between groups will be assessed using statistics and model specified in the experimental design.

        +
      • +
      +
    • + +
    • perMANOVA +

      Perform a permutational multivariate analysis of the variance using the Adonis method from R package vegan. This method uses a distance matrix based on the primary dataset.

      +
        +
      • Distance metric +

        Chose a distance metric. Choices include metrics available in the R vegan package as well as distances based on correlation coefficients.

        +
      • +
      • Model +

        For compatibility purpose, this model may differ from the model specified in the experimental design section. This model must be in the form of an R formula compatible with the Adonis function of the R vegan package. All terms of the formula must refer to column headers of the map.

        +
      • +
      • Strata +

        This is used to specify any nesting in the model. the strata must be compatible with the Adonis function of the R vegan package.

        +
      • +
      +
    • + +
    • PCA +

      Perform a principal component analysis (PCA) on the primary dataset. PCA is a commonly used dimensionality reduction method that projects a set of observations onto a smaller set of components that capture most of the variance between samples.

      +
    • + +
    • PCoA +

      Perform a principal coordinate analysis (PCoA) on the primary dataset. The PCoA is a form of dimensionality reduction based on a distance matrix.

      +
        +
      • Distance metric +

        Chose a distance metric. Choices include metrics available in the R vegan package as well as distances based on correlation coefficients.

        +
      • +
      +
    • + +
    • Heatmap +

      This displays the primary dataset on a heatmap of proportions. The columns (samples) and rows (observations) of the heatmap are re-ordered using a hierarchical clustering. Each observation will be individually compared between groups of samples based on the specified experimental design and associated p-values will be displayed on the side of the heatmap. Individual correlations between observations from the primary dataset and observations from the secondary dataset will also be displayed on the side of the heatmap when available. Samples will be color-coded according to the experimental design.

      +
    • + +
    • Changes +

      This performs a differential analysis. Each observation will be individually compared between groups of samples based on the specified experimental design. Fold-change between groups as well as the associated p-values and mean abundance will be displayed on MA plots and volcano plots.

      +
    • + +
    • Correlation network +

      This builds a network with nodes representing observations and edges representing the correlations. The network will be based on the primary dataset and will include the secondary dataset if available. Observations belonging to the primary and the secondary dataset will be represented using different node shapes. Nodes will be colored to include a number of additional information.

      +
        +
      • Correlation method +

        Chose a correlation method. Choices include the Pearson correlation and the Spearman correlation.

        +
      • +
      +
    • + +
    • Clustering +

      This applies a clustering algorithm to separate samples into categories. The categories will be added as a new column to the map. Categories will automatically be compared with ANOVA in relevant analysis.

      +
        +
      • Algorithm +

        Chose a clustering algorithm. Choices include the k-means algorithm, the k-medoids algorithm (also known as the partitioning around medoids of pam algorithm) and versions of the k-medoids based on distance matrices.

        +
      • +
      +
    • + +
    • Similarity network +

      This builds a network with nodes representing samples and edges representing a similarity score. If a secondary dataset is available, three networks will be build: one based on the primary dataset, one based on the secondary dataset and a third based on the fusion of these two networks. The similarity network fusion (SNF) is based on the R SNFtool package. A categorical clustering based on each network is applied.

      +
        +
      • Metric for primary dataset +

        Chose the similarity metric to apply on the primary dataset.

        +
      • +
      • Metric for secondary dataset +

        Chose the similarity metric to apply on the secondary dataset.

        +
      • +
      • Clustering algorithm +

        Chose the clustering algorithm to assign samples to categories based on each similarity network.

        +
      • +
      +
    • +
    + +

    Versions

    +

    Genocrunch data analysis tools are based on R libraries. Used libraries are mentionned in the analysis report. The version of the libraries are available on the Versions page via the Infos menu of the topbar).

    + + +

    Analysis report

    +

    A detailed analysis report is generated for each job. It is available directly after creating/updating a new job via the My Job button of the topbar or via the corresponding icon () in the job index (for signed-in users only). The analysis report includes detailed descriptions of the tools and methods used, possible warning and error logs as well as intermediate files, final files and interactive figures as they become available. Information for the pre-processing and data transformation are available for the primary dataset and the optional secondary dataset in their respective sections. Figures and descriptions for each analysis are available in separate sections. If multiple binning levels were specified, figures for each levels can be accessed via the associated Level button.

    + +

    Exporting figures

    +

    Figures can be exported via the associated Export menu. All figures can be exported as a vector image in the SVG format. Some figures are also available in the PDF format, but PDFs are generated in the same time as the figure raw data and cannot be edited with the interactive visualization tools. Raw figure data are also available either in the JSON format, the TXT format or both. Figure legends are currently only available in the HTML format.

    + +

    Archive

    +

    Files containing initial, intermediate and final data as well as the analysis log and logs for standard output and standard error are automatically included into a .tar.gz archive. This archive can be downloaded via the button of the analysis report page or via the corresponding icon () in the job index (for signed-in users only). Although some figures may be automatically included in the archive in the form of a minimal PDF file, the archive does generally not contain figure images. These should be exported separately via the Export button.

    diff --git a/app/views/home/_doc_index.html.erb b/app/views/home/_doc_index.html.erb new file mode 100644 index 0000000..4851784 --- /dev/null +++ b/app/views/home/_doc_index.html.erb @@ -0,0 +1,277 @@ + + + + + diff --git a/app/views/home/doc.html.erb b/app/views/home/doc.html.erb new file mode 100644 index 0000000..b8f5edb --- /dev/null +++ b/app/views/home/doc.html.erb @@ -0,0 +1,23 @@ +
    +
    + +
    + <%= render :partial => 'doc' %> +
    + + + + + + + + + +
    +
    diff --git a/app/views/home/reference.html.erb b/app/views/home/reference.html.erb new file mode 100644 index 0000000..a916979 --- /dev/null +++ b/app/views/home/reference.html.erb @@ -0,0 +1,21 @@ +
    +
    + +
    +

    Manuscript in preparation.

    +
    + +
    +

    + How to cite <%= Rails.application.class.parent_name %>: +
    + + Data mining was performed on <%= Rails.application.class.parent_name %> (genocrunch.epfl.ch). +
    + R libraries used in the analysis should also be cited. +
    +

    +
    + +
    +
    diff --git a/app/views/home/terms.html.erb b/app/views/home/terms.html.erb new file mode 100644 index 0000000..e34f6d4 --- /dev/null +++ b/app/views/home/terms.html.erb @@ -0,0 +1,9 @@ +
    +
    +
    +
    <%= File.read('public/app/TERMS_OF_SERVICE.txt') %>
    +
    +
    +
    + + diff --git a/app/views/home/tutorial.html.erb b/app/views/home/tutorial.html.erb new file mode 100644 index 0000000..eb239d1 --- /dev/null +++ b/app/views/home/tutorial.html.erb @@ -0,0 +1,11 @@ +
    +
    +
    +

    Coming soon! +
    + See the Doc in the Infos menu for help. +

    + +
    +
    +
    diff --git a/app/views/jobs/_card_form.html.erb b/app/views/jobs/_card_form.html.erb new file mode 100644 index 0000000..277c0bd --- /dev/null +++ b/app/views/jobs/_card_form.html.erb @@ -0,0 +1,20 @@ +<% list_fields.each do |field| %> +<% css_classes = [] %> +<% css_classes.push('hidden') if (field['type'] == 'select' and !field['values'] and !@default[field['id']]) %> +<% css_classes.push('missing_field') if @missing_fields and @missing_fields.map{|e| e['id']}.include?(field['id']) %> + +
    ' class='card-text <%= css_classes.join(" ") %>'> + <% label = field_label(field).join("") if !['check_box', 'model_type', 'bool'].include?field['type'] and !field['hidden_field'] %> + <%= raw label %> + <% if field['type'] %> + <%= render :partial => "field_" + field['type'], :locals => {:field => field} %> + <% else %> + <%= field.to_json %> + <% end %> +
    + +<% end %> + + + + diff --git a/app/views/jobs/_edit.html.erb b/app/views/jobs/_edit.html.erb new file mode 100644 index 0000000..63419f1 --- /dev/null +++ b/app/views/jobs/_edit.html.erb @@ -0,0 +1,24 @@ +
    + +
    + + <%= render partial: 'form', :locals => {default: @default, context: "main"} %> +
    + + + diff --git a/app/views/jobs/_export_btn.html.erb b/app/views/jobs/_export_btn.html.erb new file mode 100644 index 0000000..50c28ca --- /dev/null +++ b/app/views/jobs/_export_btn.html.erb @@ -0,0 +1,36 @@ + + +<% if @filename %> +<%= javascript_tag do %> + + $("#exportSVG").on("click", function(){ + exportFigure('#svg-figure', 'svg', '<%= Pathname(@filename).basename.to_s.split('.').first() %>') + }) + $("#exportLegend").on("click", function(){ + exportFigure('#svg-legend', 'html', 'legend-<%= Pathname(@filename).basename.to_s.split('.').first() %>') + }) + +<% end %> +<% end %> diff --git a/app/views/jobs/_failure.html.erb b/app/views/jobs/_failure.html.erb new file mode 100644 index 0000000..fffc3f8 --- /dev/null +++ b/app/views/jobs/_failure.html.erb @@ -0,0 +1,21 @@ +
    + +
    + +
    +

    Sorry, something went wrong.
    + If the following output log does not help you, please contact us. +

    + +
    <%= render :file => @job.send('log'.to_sym).url %>
    +
    +
    + + diff --git a/app/views/jobs/_field_basic_model.html.erb b/app/views/jobs/_field_basic_model.html.erb new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/views/jobs/_field_basic_model.html.erb @@ -0,0 +1 @@ + diff --git a/app/views/jobs/_field_batch_effect_suppression.html.erb b/app/views/jobs/_field_batch_effect_suppression.html.erb new file mode 100644 index 0000000..d7364e6 --- /dev/null +++ b/app/views/jobs/_field_batch_effect_suppression.html.erb @@ -0,0 +1 @@ +bl diff --git a/app/views/jobs/_field_bool.html.erb b/app/views/jobs/_field_bool.html.erb new file mode 100644 index 0000000..70898ca --- /dev/null +++ b/app/views/jobs/_field_bool.html.erb @@ -0,0 +1,2 @@ +<%= raw field_bool(field) %> + diff --git a/app/views/jobs/_field_category_column.html.erb b/app/views/jobs/_field_category_column.html.erb new file mode 100644 index 0000000..ef07ddc --- /dev/null +++ b/app/views/jobs/_field_category_column.html.erb @@ -0,0 +1 @@ +cat diff --git a/app/views/jobs/_field_check_box.html.erb b/app/views/jobs/_field_check_box.html.erb new file mode 100644 index 0000000..3bae263 --- /dev/null +++ b/app/views/jobs/_field_check_box.html.erb @@ -0,0 +1,15 @@ +
    +
    +
    + <%= raw field_check_box(field) %> +
    + + <% if field['trigger'] && field['trigger'] == 'drop_down' && @h_field_groups[field['id']] %> + + <% end %> +
    +
    diff --git a/app/views/jobs/_field_drop_down_select.html.erb b/app/views/jobs/_field_drop_down_select.html.erb new file mode 100644 index 0000000..a7f8d9e --- /dev/null +++ b/app/views/jobs/_field_drop_down_select.html.erb @@ -0,0 +1 @@ +bla diff --git a/app/views/jobs/_field_file.html.erb b/app/views/jobs/_field_file.html.erb new file mode 100644 index 0000000..91af0f5 --- /dev/null +++ b/app/views/jobs/_field_file.html.erb @@ -0,0 +1,57 @@ + + +<%= javascript_tag do %> + + $(document).ready(function() { + +//var default_values = <%= raw @default.to_json %>; + +function change_<%= field['id'] %>(where){ +//alert("WHERE: " + where) +<% if field['id'] == 'primary_dataset' %> + + var url = (where == 'server') ? '<%= read_file_header_jobs_path() %>?file_key=<%= field['id'] %>' : null; + setSelectFromFileRow("category_column", $("#p_primary_dataset")[0].files[0], '<%= @default['category_column'] || '' -%>', null, url); + +<% elsif field['id'] == 'map' %> + + var url = (where == 'server') ? '<%= read_file_header_jobs_path() %>?file_key=<%= field['id'] %>' : null; + setSelectFromFileRow("basic_model", $("#p_map")[0].files[0], '<%= @default['basic_model'] || '' -%>', null, url); + setSelectFromFileRow("sample_name", $("#p_map")[0].files[0], '<%= @default['sample_name'] || '' -%>', null, url); + var basic_model_help = $("#field-basic_model_help") + + basic_model_help.addClass("hidden") + +<% end %> +} + +$("#p_<%= field['id'] %>").on('change',function(){ + var filename = $(this).val().replace(/.*?fakepath\\/, ''); + $(this).next('.form-control-file').addClass("selected").html(filename); + // the file on the server is obsolete: + $("#p2_<%= field['id'] %>").val("") + // reset the default value of bin_levels + $("#default_bin_levels").val("[]"); + // refresh the fields depending on the file field + change_<%= field['id'] %>("client"); +//alert("t"); +}) + +<%# if @default and @default[field['id']] %> + +var ori_filename = $("#p2_<%= field['id'] %>").val(); + if (ori_filename != ''){ + $("#p_<%= field['id'] %>").next('.form-control-file').addClass("selected").html(ori_filename); //'<%# @default[field['id']].original_filename %>'); + change_<%= field['id'] %>("server"); + <% if field['id'] == 'map' %> + $("#field-basic_model_help").addClass("hidden"); + <% end %> + } +<%# end %> + +}); + +<% end %> diff --git a/app/views/jobs/_field_integer.html.erb b/app/views/jobs/_field_integer.html.erb new file mode 100644 index 0000000..c8e506b --- /dev/null +++ b/app/views/jobs/_field_integer.html.erb @@ -0,0 +1,2 @@ +<%= raw field_integer(field) %> + diff --git a/app/views/jobs/_field_model_type.html.erb b/app/views/jobs/_field_model_type.html.erb new file mode 100644 index 0000000..37160cd --- /dev/null +++ b/app/views/jobs/_field_model_type.html.erb @@ -0,0 +1,65 @@ +<% val = @default[field['id']] || field['default'] %> +<%# val %> + +
    +
    +
    + <%= field['label'] %> +
    + + +
    +
    +
    + +
    +
    Select a map file
    + <%= render :partial => 'card_form', :locals => {:list_fields => @h_field_groups['model_type_basic']} %> +
    + + +
    +
    + +<%= javascript_tag do %> +/* + +$("a.nav-link").click(function(){ +var radio = $(this).children().first().children().first(); +radio.prop('checked', true); +$(this).addClass("active"); +alert("active: " + this.id) +var inactive = (this.id == 'p_model_type_basic') ? 'p_model_type_advanced' : 'model_type_basic'; +$("#" + inactive).parent().parent().removeClass("active"); +alert("inactive : " + inactive) +}); + +*/ + +function set_tab(e){ + if (e == 'advanced'){ + $("#model-basic").addClass("hidden"); + $("#model-advanced").removeClass("hidden"); + }else{ + $("#model-basic").removeClass("hidden"); + $("#model-advanced").addClass("hidden"); + } + +} + +$(".model_radio_button").change(function(){ +set_tab($(this).val()); +}); + +set_tab($(".model_radio_button:checked").val()); + + +<% end %> diff --git a/app/views/jobs/_field_sample_name.html.erb b/app/views/jobs/_field_sample_name.html.erb new file mode 100644 index 0000000..d64a3d9 --- /dev/null +++ b/app/views/jobs/_field_sample_name.html.erb @@ -0,0 +1 @@ +sample diff --git a/app/views/jobs/_field_select.html.erb b/app/views/jobs/_field_select.html.erb new file mode 100644 index 0000000..3962a87 --- /dev/null +++ b/app/views/jobs/_field_select.html.erb @@ -0,0 +1 @@ +<%= raw field_select field, @h_form['choices'][field['values']] %> diff --git a/app/views/jobs/_field_text.html.erb b/app/views/jobs/_field_text.html.erb new file mode 100644 index 0000000..fbe9679 --- /dev/null +++ b/app/views/jobs/_field_text.html.erb @@ -0,0 +1 @@ +<%= raw field_text(field) %> diff --git a/app/views/jobs/_field_textarea.html.erb b/app/views/jobs/_field_textarea.html.erb new file mode 100644 index 0000000..4844622 --- /dev/null +++ b/app/views/jobs/_field_textarea.html.erb @@ -0,0 +1 @@ +<%= raw field_textarea(field) %> diff --git a/app/views/jobs/_field_threshold.html.erb b/app/views/jobs/_field_threshold.html.erb new file mode 100644 index 0000000..035ff51 --- /dev/null +++ b/app/views/jobs/_field_threshold.html.erb @@ -0,0 +1,49 @@ +
    + <%# field.to_json %> + <% field['default']= (field['default_type'] == 'percent') ? field['default_percent'] : field['default_integer'] %> + <% field['placeholder']= (field['default_type'] == 'percent') ? field['placeholder_percent'] : field['placeholder_integer'] %> + <%= field_integer field %> +
    + + +
    ' class="dropdown-menu dropdown-menu-right"> + Percent + Absolute value +
    +
    +
    + + + +<%= javascript_tag do %> + +$("#select-<%= field['id'] %> a").click(function(){ + name = $(this).html(); + $("#btn-<%= field['id'] %>").html(name); + var default_val = { + 'Percent' : "<%= field['default_percent'] %>", + 'Absolute value' : "<%= field['default_integer'] %>" + }; + var default_placeholder = { + 'Percent' : "<%= field['placeholder_percent'] %>", + 'Absolute value' : "<%= field['placeholder_integer'] %>" + }; + var type_val = { + 'Percent' : 'percent', + 'Absolute value' : 'int' + }; + + $("#p_<%= field['id'] %>").attr('placeholder', default_placeholder[name]); + $("#p_<%= field['id'] %>").val(default_val[name]); + $("#p_<%= field['id'] %>_type").val(type_val[name]); +}); + +<% end %> + diff --git a/app/views/jobs/_field_threshold_type.erb b/app/views/jobs/_field_threshold_type.erb new file mode 100644 index 0000000..d649c9e --- /dev/null +++ b/app/views/jobs/_field_threshold_type.erb @@ -0,0 +1,2 @@ +<%= raw field_hidden(field) %> + diff --git a/app/views/jobs/_fig_browser.html.erb b/app/views/jobs/_fig_browser.html.erb new file mode 100644 index 0000000..d6c2b07 --- /dev/null +++ b/app/views/jobs/_fig_browser.html.erb @@ -0,0 +1,148 @@ +<%# RETRIEVE INDEXES OF DATA CONRESPONDING TO CONTENT TO BE DISPLAYED %> +<% indexes = @job.figtypes.size.times.select {|i| @job.figtypes[i] == content} %> + +
    + +
    + +
    + +
    + +
    +
    + <%# LEVEL %> +
    +
    + + <% if content != Job.defaultView["param"] or Job.defaultView["level"] != 'none' %> + <% levels = @job.figlvls.values_at(*indexes) %> +
    + <%= select_tag("level", options_for_select(levels, selected=levels[0]), :id => "select_level", :onchange => "displayFigure('plot_wrapper', #{content.to_json.html_safe}, '#{@job.key}', #{ @job.figures.to_json.html_safe}, #{ @job.data.to_json.html_safe}, #{indexes.to_json.html_safe}, #{levels.to_json.html_safe}, #{Job.applicationJson["Rfunctions"].to_json.html_safe}, false);", multiple: false, class:"form-control fig-browser-multiselect") %> +
    + <% else %> + <% levels = nil %> +
    + <%= select_tag("level", options_for_select([["NA", nil]], selected="none"), :id => "select_level", disabled: true, multiple: false, class:"form-control fig-browser-multiselect") %> +
    + <% end %> +
    +
    + + <%# EXPORT %> +
    +
    + +
    + +
    +
    +
    +
    + +
    + + <%# FIGURE OPTIONS %> +
    +
    +
    +
    +
    +
    + +
    + <%# FIGURE %> +
    +
    +
    +
    +
    +
    + <% if content != Job.defaultView["param"] or Job.defaultView["type"] == 'figure' %> +
    +
    +
    + <% else %> + <%= render :partial => 'overview', :locals => {data: @job.data[indexes[0]]} %> + <% end %> +
    +
    +
    + +
    +
    +
    + + diff --git a/app/views/jobs/_form.html.erb b/app/views/jobs/_form.html.erb new file mode 100644 index 0000000..cc14339 --- /dev/null +++ b/app/views/jobs/_form.html.erb @@ -0,0 +1,98 @@ + +<% @h_tips['form'].each_with_index do |tip, index| %> + + + +<% end %> + +<%= form_for(@job, :url => ((action_name == 'edit') ? job_path(@job.key) : jobs_path), :html => {:multipart => true}) do |f| %> + + <%# @default['primary_dataset']['original_filename'] %> + <%# @default.to_json %> + +<% ['primary_dataset', 'map', 'secondary_dataset'].each do |k| %> + <%= hidden_field_tag "p2[" + k + "]", (params[:p2] && params[:p2][k]) || '' %> +<% end %> + +<%= hidden_field_tag "default_bin_levels", @default['bin_levels'].to_json %> +<%= hidden_field_tag "url_read_file_column", read_file_column_jobs_path() %> + +<%= hidden_field_tag 'tmp_key', @job.key %> + +
    + + <% @h_form['fields'].each_key do |card_title| %> + +
    +
    +
    +

    <%= card_title %> + <% if card_title == 'Inputs' %> + <% l = ['', ''] %> + <%# select_tag 'input_test', {:class => 'form-inline'} %> + <% end %> +

    +
    + <%= render :partial => 'card_form', :locals => {:list_fields => @h_form['fields'][card_title].reject{|e| e['belongs_to']}} %> + <%# @h_form['fields'][card_title].reject{|e| e['belongs_to']}.to_json %> +
    +
    +
    +
    + + <% end %> + +
    + +
    + <% type_job = (action_name == 'edit') ? 'Update' : 'Create' %> + <% if !current_user and !session[:agree_with_terms] %> + <%= f.submit type_job + " Job", data: { confirm: "Before using Genocrunch, you must agree with the following Terms of Service:\n\n"+File.read('public/app/TERMS_OF_SERVICE.txt') }, :class => "btn btn-success btn-lg col-md-12", :style=>'margin-top:10px' %> + <% else %> + <%= f.submit type_job + " Job", :class => "btn btn-success btn-lg col-md-12", :style=>'margin-top:10px' %> + <% end %> +
    + + + <%= javascript_tag do %> + + $(document).ready(function() { + + //var default_values = <%= raw @default.to_json %>; + + var l = ['category_column', 'sample_name', 'bin_levels', 'basic_model']; + for (var i =0; i< l.length; i++){ + $("#p_" + l[i]).change(function(){ + if ($("#p_" + l[i] + " option").length == 0) + $("#field-" + l[i]).addClass("hidden"); + }); + } + + $("#p_category_column").change(function(){ + var ori_filename = $("#p2_primary_dataset").val(); + var url = (ori_filename != '') ? "<%= read_file_column_jobs_path() + '?file_key=primary_dataset' %>" : null; + var val = <%= raw (@default["bin_levels"]) ? @default["bin_levels"].to_json : "[]" %> + update_bin_levels($(this).val(), val, url) + }); + + $("select.belongs_to").change(function(){ + var d = $(this).parent().parent().parent().parent().parent().parent().children().filter('.card-header').first().children().filter('.form-check').first().children().filter('.form-check-label').first().children().filter('.form-check-input').first(); + d.prop("checked", "checked"); + }); + + $(".belongs_to").keyup(function(){ + var d = $(this).parent().parent().parent().parent().children().filter('.card-header').first().children().filter('.form-check').first().children().filter('.form-check-label').first().children().filter('.form-check-input').first(); + d.prop("checked", "checked"); + }); + + }); + + <% end %> + +<% end %> diff --git a/app/views/jobs/_form_fields.html.erb b/app/views/jobs/_form_fields.html.erb new file mode 100644 index 0000000..2ef574a --- /dev/null +++ b/app/views/jobs/_form_fields.html.erb @@ -0,0 +1,120 @@ +<%# SET DEFAULT VALUE %> +<% if default.keys.include? key.to_sym %> + <% default_value = default[key.to_sym] %> +<% else %> + <% default_value = nil %> +<% end %> + +<%# BOOL %> +<% if field["type"] == "bool" %> + + <%= f.radio_button(key.to_sym, "True", checked: (default_value.to_s.downcase == "true"), id: 'job_'+key+'_true') %> + <%= f.label("Yes") %> + <%= f.radio_button(key.to_sym, "False", checked: (default_value.to_s.downcase == "false"), id: 'job_'+key+'_false') %> + <%= f.label("No") %> + +<%# TEXT %> +<% elsif field["type"] == 'text' %> + <%= f.text_field(key.to_sym, value: default_value, placeholder: ((field["options"].key?("placeholder"))? field["options"]["placeholder"] : ''), class:"form-control form-text") %> + + +<%# CHOICE %> +<% elsif field["type"] == "choice" %> + + +<%# PERCENT %> +<% elsif field["type"] == "percent" %> +
    + <%= f.number_field(key.to_sym, value: default_value, min: 0, max: 100, step: 'any', class:"form-control form-number") %> +
    %
    +
    + +<%# NUMBER %> +<% elsif field["type"] == "number" %> + + <%= f.number_field(key.to_sym, value: default_value, min: field["options"]["min"], max: field["options"]["max"], step: field["options"]["step"], class:"form-control form-number input-group-number") %> + +<%# CHECKBOX %> +<% elsif field["type"] == "checkbox" %> + + <%= f.check_box(key.to_sym, {:multiple => false, :disabled => false, :checked => (default_value == 'true')}, checked_value = field["options"]["checked_value"], unchecked_value = field["options"]["unchecked_value"]) %> + <%= f.label(field["options"]["label"]) %> + +<%# SELECT %> +<% elsif field["type"] == "select" %> + + <% if Job.applicationJson["Rfunctions"].keys().include? field["options"]["choices"] %> + <% choices = Job.applicationJson["Rfunctions"][field["options"]["choices"]] %> + <% else %> + <% choices = field["options"]["choices"] %> + <% end %> + +
    + <%= f.select(key.to_sym, options_for_select(choices.map {|c| [c["label"], c["value"]]}, selected=default_value), {}, multiple: (field["options"]["multiple"].to_s.downcase == "true"), include_blank: (field["options"]["include_blank"].to_s.downcase == "true"), class:"form-control multiselect") %> +
    + +<% elsif ["select-from-file-category", "select-from-file-row"].include? field["type"] %> + +
    + <%= f.select(key.to_sym, options_for_select(field["options"]["init"]), {}, multiple: (field["options"]["multiple"].to_s.downcase == "true"), include_blank: (field["options"]["include_blank"].to_s.downcase == "true"), class:"form-control") %> +
    + +<%# FILE UPLOAD (WITH MANDATORY TEST FILE) %> +<% elsif field["type"] == "file" %> + + <%= f.file_field key, :id => 'job_'+key %> +
    + <%= link_to "", download: "", target: "_blank", id: "link_"+key+"_test_file" do %> + Test file + + <% end %> +
    +<% end %> + + + diff --git a/app/views/jobs/_form_from_json.html.erb b/app/views/jobs/_form_from_json.html.erb new file mode 100644 index 0000000..f1a3bf9 --- /dev/null +++ b/app/views/jobs/_form_from_json.html.erb @@ -0,0 +1,121 @@ +<%# BUILD A FORM BASED ON A JSON (recursively through expandable_fields and fields) %> + + + +<% if json.keys.include? "title" %> +
    +
    +

    <%= json["title"] %>

    +<% end %> + +<%# FIELDS %> +<% if json.keys.include? "fields" %> + <% json["fields"].each do |key, field| %> + <% if ["all", "web"].include? field["scope"] %> +
    + <% if field.key?("help") %> + + <% else %> + + <% end %> + <%= render partial: 'form_fields', locals: {f: f, context: context, default: default, field: field, key: key} %> + <% if field.key?("help") %> + + <%= field["help"] %> + <%= field["type"] == "file" ? link_to('See a format example', root_url+"helps/"+key, target: "_blank", class: "file_example") : '' %> + + <% end %> +
    + <% end %> + <% end %> + +<% end %> + + +<%# EXPANDABLE FIELDS %> +<% if json.keys.include? "expandable_fields" %> + + <% json["expandable_fields"].each do |key, expandable_field| %> + + <% if ["all", "web"].include? expandable_field["scope"] %> + + <% end %> + <% end %> +<% end %> + +<%# TABS %> +<% if json.keys.include? "Tabs" %> + <% key = json["Tabs"]["Selection"].keys[0] %> + <%= render partial: 'form_fields', locals: {f: f, context: context, default: default, field: json["Tabs"]["Selection"][key], key: key} %> + + <% json["Tabs"]["Content"].each_with_index do |content, index| %> +
    + <%= render partial: 'form_from_json', locals: {f: f, json: content, context: context, default: default} %> +
    + <% end %> + +<% end %> + +<% if json.keys.include? "title" %> + +
    +
    +<% end %> diff --git a/app/views/jobs/_form_js.html.erb b/app/views/jobs/_form_js.html.erb new file mode 100644 index 0000000..bd6c5b4 --- /dev/null +++ b/app/views/jobs/_form_js.html.erb @@ -0,0 +1,37 @@ +<%= javascript_tag do %> + + var h_help = <%= raw @h_help.to_json %>; + + $(document).ready(function() { + + var window_width = $(window).width(); + + <% @h_tips['form'].each_with_index do |tip, index| %> + <% if !current_user and !session["tip#{tip['id']}".to_sym] %> + $("#tip_window<%= index %>").removeClass('hidden'); + $("#tip_window<%= index %>").css({<%= raw tip['css_string'] %>}).stop().show(100); + $('#tip_window<%= index %>_text').html("<%= raw tip['html'] %>") + <% session["tip#{tip['id']}".to_sym] = true %> + <% end %> + <% end %> + + $('.help').click(function(e) { + + var t = this.id.split("-") + $('#popup_window').removeClass('hidden'); + var help = h_help[t[1]]; + $('#popup_container').html(help); + $("#popup_window").css({ + left: (window_width -e.pageX < window_width/2) ? e.pageX-310 : e.pageX + 10 , + width: 300, + top: e.pageY + 10 + }).stop().show(100); + }); + + fold_section('.foldbar-icon'); + + setMultiselect('.multiselect'); + + }); + +<% end %> diff --git a/app/views/jobs/_index.html.erb b/app/views/jobs/_index.html.erb new file mode 100644 index 0000000..5a373dd --- /dev/null +++ b/app/views/jobs/_index.html.erb @@ -0,0 +1,10 @@ +
    +<%# link_to 'New Job', new_job_path, :class => "btn btn-primary" %> +
    +

    List of analyses

    +<%= link_to 'RefreshIndex', {:controller => "jobs", :action => 'manage', :which => "mine", :what => "jobs", :previous_jobs => @jobs.map {|e| e.status}.to_a}, :remote => true, id: "refreshIndex", class: "hidden" %> + +
    + <%= render partial: '/jobs/jobs' %> +
    + diff --git a/app/views/jobs/_jobs.html.erb b/app/views/jobs/_jobs.html.erb new file mode 100644 index 0000000..5a1982b --- /dev/null +++ b/app/views/jobs/_jobs.html.erb @@ -0,0 +1,141 @@ + + + + + + + + + <% if current_user.role == "admin" %> + + + <% end %> + + + + + + + + + <% @jobs.compact.each do |job| %> + <% date = job.updated_at.year.to_s[-2..-1].to_s + "-" + job.updated_at.month.to_s.rjust(2, '0') + "-" + job.updated_at.day.to_s.rjust(2, '0') + " " + job.updated_at.hour.to_s.rjust(2, '0') + ":" + job.updated_at.min.to_s.rjust(2, '0') + " " + job.updated_at.zone.to_s %> + + + + + <% if current_user.role == "admin" %> + + + <% end %> + + + + + <% destroy_text = (['pending', 'running'].include? job.status) ? 'abort' : 'delete' %> + + + <% end %> + +
    KeyNameDateUserTypeStatusDataMapResults
    +
    + <%= link_to job.key, job_path(job.key) %> +
    +
    +
    + <%= link_to job.name, job_path(job.key) %> +
    +
    + <%= link_to date, job_path(job.key) %> + + <%= job.user.username %> + + <%= (Example.where(:job_key => job.key).all.size > 0) ? 'Example' : 'Std' %> + + <% if job.status %> + + <% end %> + + <%= link_to raw(''), serve_job_path(job.key, :filename => 'input/primary_dataset.txt'), title: "primary dataset" %> + <% if File.exist? Pathname.new(APP_CONFIG[:data_dir]) + 'users' + job.user_id.to_s + job.key + 'input' + 'secondary_dataset.txt' %> + | <%= link_to raw(''), serve_job_path(job.key, :filename => 'input/secondary_dataset.txt'), title: "secondary dataset" %> + <% end %> + + <%= link_to raw(''), serve_job_path(job.key, :filename => 'input/map.txt') %> + + <%= link_to raw(''), job_path(job.key), :title => 'show' %> + <% if (File.exist? Pathname.new(APP_CONFIG[:data_dir]) + 'users' + job.user_id.to_s + job.key + (job.key + '.tar.gz').to_s) and (!['pending', 'running'].include? job.status) %> + | <%= link_to raw(''), serve_job_path(job.key, :filename => job.key + '.tar.gz'), :title => 'download archive' %> + <% end %> + + <%= link_to raw(''), clone_job_path(job.key) %> + | +<%= link_to raw(''), edit_job_path(job.key) %> + | + <% destroy_text = (['pending', 'running'].include? job.status) ? 'abort' : 'delete' %> + <%= link_to(job_path(job.key), method: :delete, data: { confirm: 'Are you sure you want to ' + destroy_text + ' "' + (job.name || 'NA') + '" ?'}) do %> + + <% end %> +
    + + + + + + + + + + + + + + + + + + + + +
    jobs
    <%= @jobs.length %><%= @jobs.select{ |i| i.status == "completed" }.length %><%= @jobs.select{ |i| i.status == "pending" }.length %><%= @jobs.select{ |i| i.status == "running" }.length %><%= @jobs.select{ |i| i.status == "failed" }.length %>
    + + +<%= javascript_tag do %> +$(document).ready(function(){ + var table = $('#myTable').DataTable({ + sDom: 'ltp', + "aLengthMenu": [[10, 25, 50, 100, -1], + [10, 25, 50, 100, "All"]], + "iDisplayLength" : 10, + "order": [[ 2, 'desc' ], [ 1, 'asc' ]] + }); + + + + $('#filter-input').insertAfter('#myTable_length') + +}); + + function filterTable() { + var input = document.getElementById("filter-input"), + filter = input.value.toUpperCase(), + table = document.getElementById("myTable"), + tr = table.getElementsByTagName("tr"), + td, kept; + for (var i = 1; i < tr.length; i++) { + td = tr[i].getElementsByTagName("td"); + kept = false; + for (var j = 0; j < td.length; j++) { + if (td[j].innerHTML.toUpperCase().indexOf(filter) > -1) { + tr[i].style.display = ""; + kept = true; + break; + } + } + if (kept == false) { + tr[i].style.display = "none"; + } + } + } + +<% end %> diff --git a/app/views/jobs/_list_messages.html.erb b/app/views/jobs/_list_messages.html.erb new file mode 100644 index 0000000..313acbd --- /dev/null +++ b/app/views/jobs/_list_messages.html.erb @@ -0,0 +1,52 @@ +<% if @final_json[:status_by_step][k] %> +<% @final_json[:status_by_step][k].each do |e| %> +
    +
    +
    + <%= raw display_duration(e[:execution_time]) %> +
    + + <%# image_tag @h_statuses[e[:status] || 'pending'].filename, :alt => '', :id => "status-#{e[:id]}", :class => 'status_image' %> + <%= e[:name].capitalize %> +
    + +<% step_key = "primary_dataset_" + e[:name] %> +<% if @final_json[:messages_by_step][step_key] and @final_json[:messages_by_step][step_key].select{|e| e[:messages] and e[:messages].select{|h| h.keys.size > 0}.compact.size > 0}.size > 0 %> +
    + +<%# @final_json[:messages_by_step][step_key].to_json %> +
      +<% @final_json[:messages_by_step][step_key].select{|e| e[:messages] and e[:messages].select{|h| h.keys.size > 0}.compact.size > 0}.each do |e2| %> +
    • +<% status_substep = (@final_json[:status_by_substep][e[:name]]) ? @final_json[:status_by_substep][e[:name]].select{|e3| e3[:name] == e2[:name]}.first : nil %> +
      + <%= raw display_duration(status_substep[:execution_time]) if status_substep %> +
      + <%if status_substep %> + + <% end %> + <%# image_tag(@h_statuses[status_substep[:status] || 'pending'].filename, :alt => '', :id => "status-#{e[:id]}", :class => 'status_image') if status_substep %> +<%= e2[:name].capitalize %> + +<% e2[:messages].each do |e3| %> +<% k = e3.keys.first %> +

      + +<% if k != 'output' %> +<%= e3[k] %> +<% else %> +<% t = e3[k].gsub(/.+?\/#{@job.key}\//, '') %> +<%= link_to 'Download file', serve_job_path(@job.key, :filename => t), :class => 'btn btn-secondary btn-sm' %> +<% end %> +

      +<% end %> + +
    • +<% end %> +
    +
    +<% end %> + +
    +<% end %> +<% end %> diff --git a/app/views/jobs/_overview.html.erb b/app/views/jobs/_overview.html.erb new file mode 100644 index 0000000..23392df --- /dev/null +++ b/app/views/jobs/_overview.html.erb @@ -0,0 +1,57 @@ +<% data_parser = File.open(Rails.root.to_s+data.to_s, "r").read.split(/\r\n|\r|\n/) %> + + + + + <% data_parser[0].split("\t").each_with_index do |h, i| %> + + <% end %> + + + + <% data_parser.drop(1).each do |l| %> + + <% l.split("\t").each_with_index do |e, i| %> + + <% end %> + <% end %> + +
    +
    +
    + + +
    + +
    +
    <%= e %>
    + + diff --git a/app/views/jobs/_refresh.html.erb b/app/views/jobs/_refresh.html.erb new file mode 100644 index 0000000..6be0d01 --- /dev/null +++ b/app/views/jobs/_refresh.html.erb @@ -0,0 +1,40 @@ +<% if @final_json %> +$("#global_status").attr('class', '<% (@final_json[:global_status] && @h_statuses[@final_json[:global_status]].icon) || @h_statuses['pending'].icon %>'); + +<% @final_json[:global_status_by_step].each_key do |name| %> +$("#status-<%= name %>").attr('class', '<%= (@h_statuses[@final_json[:global_status_by_step][name]]) ? @h_statuses[@final_json[:global_status_by_step][name]].icon : @h_statuses['pending'].icon %>'); +<% end %> + +//$("#log_text").html('<%= raw @log_json.to_json %>'); +//alert("<%= session[:status_vector] %> = <%= @status_vector %>"); + +<% if @update == 1 %> + + +//alert("I want to update!"); +var div = $("#show_content"); +var view = $("#selected_view").val(); + var url = '<%= view_job_path(:key => @job.key) %>?' + "partial=" + view; + $.ajax({ + url: url, + type: "get", + dataType: "html", + beforeSend: function(){ + }, + success: function(returnData){ + div.empty(); + div.html(returnData); + }, + error: function(e){ + } + }); + +<% session[:status_vector] = @status_vector %> +<% end %> + +<% end %> + +<% tarfile = Pathname.new(APP_CONFIG[:data_dir]) + "users" + @user.id.to_s + @job.key + "#{@job.key}.tar.gz" %> +<% if File.exist?(tarfile) %> + $("#archive-btn").removeClass('hidden'); +<% end %> diff --git a/app/views/jobs/_show_map.html.erb b/app/views/jobs/_show_map.html.erb new file mode 100644 index 0000000..9fef816 --- /dev/null +++ b/app/views/jobs/_show_map.html.erb @@ -0,0 +1 @@ +Map diff --git a/app/views/jobs/_show_primary_dataset.html.erb b/app/views/jobs/_show_primary_dataset.html.erb new file mode 100644 index 0000000..24c6916 --- /dev/null +++ b/app/views/jobs/_show_primary_dataset.html.erb @@ -0,0 +1,8 @@ +

    Primary dataset

    +ldksjflskdjf +<%= @final_json[:status_by_step].to_json %> +<% @final_json[:status_by_step]['primary_dataset'].each do |e| %> +
    +<%= e['name'] %> <%= image_tag @h_status[e['status']].filename %> +
    +<% end %> diff --git a/app/views/jobs/_standard_fig_layout.html.erb b/app/views/jobs/_standard_fig_layout.html.erb new file mode 100644 index 0000000..e7ba508 --- /dev/null +++ b/app/views/jobs/_standard_fig_layout.html.erb @@ -0,0 +1,69 @@ +
    +
    +
    +
    + + <% if @form_json['bin_levels'] %> +
    + + + +
    + <% l = @form_json['bin_levels'].map{|e| [e, e]} %> + <%= select_tag 'current_level', options_for_select(l, session[:current_level].to_s), {:class => 'form-control multiselect figtool-multiselect'} %> +
    + +
    + + <% end %> + <%= render :partial => 'export_btn' %> + + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +<%= javascript_tag do %> + +$("#current_level").change(function(){ +var div = $('#show_content'); +var url = '<%= view_job_path(:key => @job.key) %>?partial=<%= params[:partial] %>¤t_level=' + $(this).val(); + + $.ajax({ + url: url, + type: "get", + beforeSend: function(){ + }, + success: function(returnData){ + div.empty(); + div.html(returnData); + }, + error: function(e){ + } + }); + +}); + +setMultiselect('#current_level'); + +<% end %> diff --git a/app/views/jobs/_summary.html.erb b/app/views/jobs/_summary.html.erb new file mode 100644 index 0000000..d7235f3 --- /dev/null +++ b/app/views/jobs/_summary.html.erb @@ -0,0 +1 @@ +Summary diff --git a/app/views/jobs/_view_adonis.html.erb b/app/views/jobs/_view_adonis.html.erb new file mode 100644 index 0000000..e26f259 --- /dev/null +++ b/app/views/jobs/_view_adonis.html.erb @@ -0,0 +1,11 @@ +

    perMANOVA

    +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + + adonisPieChart('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif"); + +<% end %> diff --git a/app/views/jobs/_view_change.html.erb b/app/views/jobs/_view_change.html.erb new file mode 100644 index 0000000..5756072 --- /dev/null +++ b/app/views/jobs/_view_change.html.erb @@ -0,0 +1,11 @@ +

    Changes

    +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + + foldChange('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> diff --git a/app/views/jobs/_view_clustering.html.erb b/app/views/jobs/_view_clustering.html.erb new file mode 100644 index 0000000..3618b7c --- /dev/null +++ b/app/views/jobs/_view_clustering.html.erb @@ -0,0 +1,2 @@ +

    Clustering

    +<%= render :partial => 'view_description' %> diff --git a/app/views/jobs/_view_correlation_network.html.erb b/app/views/jobs/_view_correlation_network.html.erb new file mode 100644 index 0000000..ad6a1a3 --- /dev/null +++ b/app/views/jobs/_view_correlation_network.html.erb @@ -0,0 +1,12 @@ +

    Correlation network

    + +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + + correlationNetwork('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> diff --git a/app/views/jobs/_view_description.html.erb b/app/views/jobs/_view_description.html.erb new file mode 100644 index 0000000..96ac535 --- /dev/null +++ b/app/views/jobs/_view_description.html.erb @@ -0,0 +1,16 @@ +<% if @description and !@description.empty? %> +
    +
    + + <% if @description.length() > 1 %> +
      + <% @description.each do |d| %> +
    • <%= d %>
    • + <% end %> +
    + <% else %> + <%= @description.first %> + <% end %> +
    +
    +<% end %> diff --git a/app/views/jobs/_view_diversity.html.erb b/app/views/jobs/_view_diversity.html.erb new file mode 100644 index 0000000..421648f --- /dev/null +++ b/app/views/jobs/_view_diversity.html.erb @@ -0,0 +1,13 @@ +

    Diversity

    +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>, + RfunctionsDiversity = <%= @h_form_choices['diversity'].to_json.html_safe %>; + +diversity('fig', 'fig-legend', fig_data, RfunctionsDiversity, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> + diff --git a/app/views/jobs/_view_heatmap.html.erb b/app/views/jobs/_view_heatmap.html.erb new file mode 100644 index 0000000..1494b13 --- /dev/null +++ b/app/views/jobs/_view_heatmap.html.erb @@ -0,0 +1,13 @@ +

    Heatmap

    + +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + +heatMap('fig', 'fig-legend', fig_data, W = 750, H = 750, font_family = "verdana, arial, helvetica, sans-serif"); + +<% end %> + diff --git a/app/views/jobs/_view_map.html.erb b/app/views/jobs/_view_map.html.erb new file mode 100644 index 0000000..2f73199 --- /dev/null +++ b/app/views/jobs/_view_map.html.erb @@ -0,0 +1,7 @@ +<% if @final_json and @final_json[:status_by_step]['map'] %> + +Map +<%= render :partial => 'view_description' %> +<%= render :partial => 'list_messages', :locals => {:k => 'map'} %> + +<% end %> diff --git a/app/views/jobs/_view_pca.html.erb b/app/views/jobs/_view_pca.html.erb new file mode 100644 index 0000000..2946e96 --- /dev/null +++ b/app/views/jobs/_view_pca.html.erb @@ -0,0 +1,11 @@ +

    PCA

    +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + + pca('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> diff --git a/app/views/jobs/_view_pcoa.html.erb b/app/views/jobs/_view_pcoa.html.erb new file mode 100644 index 0000000..4f39501 --- /dev/null +++ b/app/views/jobs/_view_pcoa.html.erb @@ -0,0 +1,14 @@ +<%# @form_json['bin_levels'].to_json %> +<%# session[:current_level].to_json %> + +

    PCoA

    +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + + pcoa('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> diff --git a/app/views/jobs/_view_primary_dataset.html.erb b/app/views/jobs/_view_primary_dataset.html.erb new file mode 100644 index 0000000..73ee87c --- /dev/null +++ b/app/views/jobs/_view_primary_dataset.html.erb @@ -0,0 +1,7 @@ +<% if @final_json and @final_json[:status_by_step]['primary_dataset'] %> + +Primary dataset +<%= render :partial => 'view_description' %> +<%= render :partial => 'list_messages', :locals => {:k => 'primary_dataset'} %> + +<% end %> diff --git a/app/views/jobs/_view_proportions.html.erb b/app/views/jobs/_view_proportions.html.erb new file mode 100644 index 0000000..ec28b27 --- /dev/null +++ b/app/views/jobs/_view_proportions.html.erb @@ -0,0 +1,13 @@ +

    Proportions

    + +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + +barchart('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> + diff --git a/app/views/jobs/_view_secondary_dataset.html.erb b/app/views/jobs/_view_secondary_dataset.html.erb new file mode 100644 index 0000000..a913e76 --- /dev/null +++ b/app/views/jobs/_view_secondary_dataset.html.erb @@ -0,0 +1,9 @@ +<% if @final_json and @final_json[:status_by_step]['secondary_dataset'] %> + +Secondary dataset +<%= render :partial => 'view_description' %> + +<%= render :partial => 'list_messages', :locals => {:k => 'secondary_dataset'} %> + +<% end %> + diff --git a/app/views/jobs/_view_similarity_network.html.erb b/app/views/jobs/_view_similarity_network.html.erb new file mode 100644 index 0000000..6a415b8 --- /dev/null +++ b/app/views/jobs/_view_similarity_network.html.erb @@ -0,0 +1,12 @@ +

    Similarity network

    + +<%= render :partial => 'view_description' %> +<%= render :partial => 'standard_fig_layout' %> + +<%= javascript_tag do %> + + var fig_data = <%= raw @data_json %>; + + similarityNetwork('fig', 'fig-legend', fig_data, W = 600, H = 600, font_family = "verdana, arial, helvetica, sans-serif", color_palette = d3.schemeCategory10); + +<% end %> diff --git a/app/views/jobs/edit.html.erb b/app/views/jobs/edit.html.erb new file mode 100644 index 0000000..ab1ad49 --- /dev/null +++ b/app/views/jobs/edit.html.erb @@ -0,0 +1,13 @@ +
    +

    Edit job

    + + <% if @missing_fields and @missing_fields.size > 0 %> +
    These fields are missing: <%= @missing_fields.map{|e| e['label'] || e['id'].gsub("_", " ").capitalize}.join(", ") %>.
    + <% end %> + + <%= render partial: 'form', :locals => {default: @default, context: "main"} %> + +
    + +<%= render :partial => 'form_js' %> + diff --git a/app/views/jobs/index.html.erb b/app/views/jobs/index.html.erb new file mode 100644 index 0000000..b26ca85 --- /dev/null +++ b/app/views/jobs/index.html.erb @@ -0,0 +1,29 @@ +
    + +
    + +
    + + <%# if session[:which] == "all" %> + <%# render partial: '/admins/index' %> + <%# else %> + <%= render partial: '/jobs/index' %> + <%# end %> + +
    +
    + + diff --git a/app/views/jobs/index.json.jbuilder b/app/views/jobs/index.json.jbuilder new file mode 100644 index 0000000..8a0c528 --- /dev/null +++ b/app/views/jobs/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@jobs) do |job| + json.extract! job, :id, :name, :input, :map, :params, :output, :status + json.url job_url(job, format: :json) +end diff --git a/app/views/jobs/manage.js.erb b/app/views/jobs/manage.js.erb new file mode 100644 index 0000000..26f49d8 --- /dev/null +++ b/app/views/jobs/manage.js.erb @@ -0,0 +1,7 @@ +<% if @previous_jobs.nil? == false %> + <% if (@previous_jobs.to_a - @jobs.map {|e| e.status}).blank? == false %> + $("#<%= @id %>").html("<%=j render(:partial => @partial, :locals => {:jobs => @jobs, :users => @users}) %>"); + <% end %> +<% else %> + $("#<%= @id %>").html("<%=j render(:partial => @partial, :locals => {:jobs => @jobs, :users => @users}) %>"); +<% end %> diff --git a/app/views/jobs/new.html.erb b/app/views/jobs/new.html.erb new file mode 100644 index 0000000..fac3dd9 --- /dev/null +++ b/app/views/jobs/new.html.erb @@ -0,0 +1,16 @@ +
    + + + +

    New job

    + + <% if @missing_fields and @missing_fields.size > 0 %> +
    These fields are missing: <%= @missing_fields.map{|e| e['label'] || e['id'].gsub("_", " ").capitalize}.join(", ") %>.
    + <% end %> + + <%= render partial: 'form', :locals => {default: @default, context: "main"} %> + +
    + +<%= render :partial => 'form_js' %> + diff --git a/app/views/jobs/show.html.erb b/app/views/jobs/show.html.erb new file mode 100644 index 0000000..27d3471 --- /dev/null +++ b/app/views/jobs/show.html.erb @@ -0,0 +1,151 @@ +
    +
    + + +
    +

    + Project '<%= @job.name %>' + + + <%# image_tag @h_statuses[(@final_json) ? @final_json[:global_status] : 'pending'].filename, :id => 'global_status', :class => 'status_image' %> +

    +
    + + <%= link_to serve_job_path(@job.key, :filename => @job.key + '.tar.gz'), :id => 'archive-btn', :title => 'Download archive', :class => 'btn btn-secondary hidden' do %> + + + <% end %> + <%= link_to 'Edit', edit_job_path(:key => @job.key), :class => 'btn btn-primary' %> +
    +
    + <%# if @final_json and @final_json[:global_status_by_step]['primary_dataset'] %> + <%= render :partial => 'view_primary_dataset' %> + <%# end %> +
    +
    + + +<% @h_tips['show'].each_with_index do |tip, index| %> + +<% end %> + +
    +
    + +<%= javascript_tag do %> + + + $(document).ready(function() { + + var window_width = $(window).width(); + + <% @h_tips['show'].each_with_index do |tip, index| %> + <% if !current_user and !session["tip#{tip['id']}".to_sym] %> + $("#tip_window<%= index %>").removeClass('hidden'); + $("#tip_window<%= index %>").css({<%= raw tip['css_string'] %>}).stop().show(100); + $('#tip_window<%= index %>_text').html("<%= raw tip['html'] %>") + <% session["tip#{tip['id']}".to_sym] = true %> + <% end %> + <% end %> + +}); + + +$(".menu").click(function(){ +var name = this.id.split("-")[1]; +$("#selected_view").val(name); +}); + +var timer = setInterval(function(){ +refresh_data() +}, 5000); + +function refresh_data(){ + var url = '<%= refresh_job_path(:key => @job.key) %>'; + $.ajax({ + url: url, + type: "get", + dataType: "script", + beforeSend: function(){ + }, + success: function(returnData){ +// div.empty(); +// div.html(returnData); + }, + error: function(e){ + } + }); + +} + +$(".menu").click(function(){ + var t = this.id.split("-"); + var div = $("#show_content"); + var url = '<%= view_job_path(:key => @job.key) %>?partial=' + t[1]; + $.ajax({ + url: url, + type: "get", + beforeSend: function(){ + }, + success: function(returnData){ + div.empty(); + div.html(returnData); + }, + error: function(e){ + } + }); + +}); + + +refresh_data(); + +<% end %> diff --git a/app/views/jobs/show.json.jbuilder b/app/views/jobs/show.json.jbuilder new file mode 100644 index 0000000..a3b44d8 --- /dev/null +++ b/app/views/jobs/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @job, :id, :name, :input, :map, :params, :output, :status, :created_at, :updated_at diff --git a/app/views/jobs/update.html.erb b/app/views/jobs/update.html.erb new file mode 100644 index 0000000..a7f8d9e --- /dev/null +++ b/app/views/jobs/update.html.erb @@ -0,0 +1 @@ +bla diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..5510caa --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,138 @@ + + + + <%= Rails.application.class.parent_name %> + <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %> + <%= javascript_include_tag "application", "data-turbolinks-track" => true %> + <%= csrf_meta_tags %> + + + + + + + + + + + + + + + + + + + +
    + <%= yield %> +
    + + + + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/users/confirmations/new.html.erb b/app/views/users/confirmations/new.html.erb new file mode 100644 index 0000000..cbf2d2a --- /dev/null +++ b/app/views/users/confirmations/new.html.erb @@ -0,0 +1,22 @@ +
    +
    +
    + +

    Resend confirmation instructions

    + +<%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
    + + <%= f.email_field :email, :autofocus => true, class: "form-control input-lg", tabindex: 1, placeholder: "Your email address" %> +
    + +
    + <%= f.submit "Resend instructions", :class => "btn btn-success btn-lg full-width", tabindex: 2 %> +
    +<% end %> + +
    +
    +
    diff --git a/app/views/users/mailer/confirmation_instructions.html.erb b/app/views/users/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..ddd94f0 --- /dev/null +++ b/app/views/users/mailer/confirmation_instructions.html.erb @@ -0,0 +1,7 @@ +

    Welcome to <%= Rails.application.class.parent_name %>, <%= @resource.username %>!

    + +

    You can confirm your account through the link below:

    + +

    <%= link_to 'Confirm my account', confirmation_url(@resource, :confirmation_token => @token) %>

    + +

    You will then be able to login to <%= Rails.application.class.parent_name %> using your login name and password.

    diff --git a/app/views/users/mailer/reset_password_instructions.html.erb b/app/views/users/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..93de6d0 --- /dev/null +++ b/app/views/users/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

    Hello <%= @resource.email %>!

    + +

    Someone has requested a link to change your password. You can do this through the link below.

    + +

    <%= link_to 'Change my password', edit_password_url(@resource, :reset_password_token => @token) %>

    + +

    If you didn't request this, please ignore this email.

    +

    Your password won't change until you access the link above and create a new one.

    diff --git a/app/views/users/mailer/unlock_instructions.html.erb b/app/views/users/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..f59615f --- /dev/null +++ b/app/views/users/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

    Hello <%= @resource.email %>!

    + +

    Your account has been locked due to an excessive number of unsuccessful sign in attempts.

    + +

    Click the link below to unlock your account:

    + +

    <%= link_to 'Unlock my account', unlock_url(@resource, :unlock_token => @token) %>

    diff --git a/app/views/users/passwords/edit.html.erb b/app/views/users/passwords/edit.html.erb new file mode 100644 index 0000000..a4e44a1 --- /dev/null +++ b/app/views/users/passwords/edit.html.erb @@ -0,0 +1,28 @@ +
    +
    +
    + +

    Change your password

    + + <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put }) do |f| %> + <%= devise_error_messages! %> + <%= f.hidden_field :reset_password_token %> + +
    + + <%= f.password_field :password, :autofocus => true, class: "form-control input-lg", tabindex: 1, placeholder: "Your new password" %> +
    + +
    + <%= f.password_field :password_confirmation, class: "form-control input-lg", tabindex: 2, placeholder: "Repeat your password" %> +
    + +
    + <%= f.submit "Change my password", :class => "btn btn-success btn-lg full-width" %> +
    + + <% end %> + +
    +
    +
    diff --git a/app/views/users/passwords/new.html.erb b/app/views/users/passwords/new.html.erb new file mode 100644 index 0000000..4ea7d0d --- /dev/null +++ b/app/views/users/passwords/new.html.erb @@ -0,0 +1,30 @@ +
    +
    +
    + +

    Renew password

    + + <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
    + + <%= f.email_field :email, :autofocus => true, class: "form-control input-lg", tabindex: 1, placeholder: "Your email address" %> +
    + +
    + <%= f.submit "Send instructions", :class => "btn btn-success btn-lg full-width" %> +
    + + <% end %> + +
    +
    +
    + + + + + + + diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/registrations/edit.html.erb new file mode 100644 index 0000000..1d451d3 --- /dev/null +++ b/app/views/users/registrations/edit.html.erb @@ -0,0 +1,50 @@ +
    +
    +
    + +

    <%= resource_name.to_s.humanize %> details

    + +<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> + <%= devise_error_messages! %> + +
    + + <%= f.text_field :username, :autofocus => true, class: "form-control input-lg", tabindex: 1, placeholder: "New username" %> +
    + +
    + + <%= f.email_field :email, class: "form-control input-lg", tabindex: 2, placeholder: "Your email address" %> +
    + + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> +
    Currently waiting confirmation for: <%= resource.unconfirmed_email %>
    + <% end %> + +
    + + <%= f.password_field :password, :autocomplete => "off", class: "form-control input-lg", tabindex: 3, placeholder: "Define a new password" %> +
    + +
    + <%= f.password_field :password_confirmation, class: "form-control input-lg", tabindex: 4, placeholder: "Repeat new password" %> +
    + +
    + + <%= f.password_field :current_password, class: "form-control input-lg", tabindex: 5, placeholder: "Your current password" %> +
    + +
    + <%= f.submit "Update", :class => "btn btn-success btn-lg full-width", tabindex: 6 %> +
    + +<% end %> + +
    + <%= button_to "Cancel my account", registration_path(resource_name), :data => { :confirm => "Are you sure you want to cancel your account?" }, :method => :delete, :class => "btn btn-danger btn-lg full-width", tabindex: 7 %> +
    + +
    +
    +
    diff --git a/app/views/users/registrations/new.html.erb b/app/views/users/registrations/new.html.erb new file mode 100644 index 0000000..fa6be7e --- /dev/null +++ b/app/views/users/registrations/new.html.erb @@ -0,0 +1,45 @@ +
    +
    +
    + +

    Join <%= Rails.application.class.parent_name %>

    + +<%= form_for(resource, :as => resource_name, :url => registration_path(resource_name)) do |f| %> + + <%= devise_error_messages! %> + +
    + + <%= f.text_field :username, :autofocus => true, class: "form-control input-lg", tabindex: 1, placeholder: "Choose a username" %> +
    + +
    + + <%= f.email_field :email, class: "form-control input-lg", tabindex: 2, placeholder: "Your email address" %> +
    + +
    + + <%= f.password_field :password, class: "form-control input-lg", tabindex: 3, placeholder: "Define a password" %> +
    + +
    + <%= f.password_field :password_confirmation, class: "form-control input-lg", tabindex: 4, placeholder: "Repeat your password" %> +
    + +
    + + <%= rucaptcha_input_tag(class: 'form-control input-lg', tabindex: 5, placeholder: 'Solve the captcha below') %> + <%= rucaptcha_image_tag(alt: 'Captcha') %> +
    + +
    + +
    + +
    <%= f.submit "Register", data: { confirm: File.read('public/app/TERMS_OF_SERVICE.txt') }, :class => "btn btn-success btn-lg full-width", tabindex: 5 %>
    +<% end %> + +
    +
    +
    diff --git a/app/views/users/sessions/_infos.html.erb b/app/views/users/sessions/_infos.html.erb new file mode 100644 index 0000000..7f5bfcf --- /dev/null +++ b/app/views/users/sessions/_infos.html.erb @@ -0,0 +1,30 @@ +
    + + <%# PIPELINE %> +
    +

    Pipeline

    + <%= image_tag('genocrunch_pipeline.png', alt: 'Genocrunch pipeline') %> +
    + + <%# TUTORIAL VIDEO %> +
    +

    Tutorial

    + +
    + +
    +
    + +
    +

    + How to cite <%= Rails.application.class.parent_name %>: +
    + + Data mining was performed on <%= Rails.application.class.parent_name %> (genocrunch.epfl.ch). +
    + R libraries used in the analysis should also be cited. +
    +

    +
    + +
    diff --git a/app/views/users/sessions/new.html.erb b/app/views/users/sessions/new.html.erb new file mode 100644 index 0000000..ffa18b3 --- /dev/null +++ b/app/views/users/sessions/new.html.erb @@ -0,0 +1,81 @@ +
    + +
    + + <%# WELCOME TEXT %> + +
    +
    +

    Analyse your data online

    +

    We offer a user-friendly data mining pipeline for metagenomics and metataxonomics

    +
    + +
    + + <%# SIGNIN %> + + + <%# GO DOWN %> +
    + + + +
    + +
    +
    + +
    + + <%# INFOS %> + <%= render :partial => "infos" %> + +
    + +
    + + diff --git a/app/views/users/shared/_links.erb b/app/views/users/shared/_links.erb new file mode 100644 index 0000000..4f8f56b --- /dev/null +++ b/app/views/users/shared/_links.erb @@ -0,0 +1,15 @@ +
  • +
    + + <%- if controller_name != 'sessions' %> + <%= link_to "Sign in", new_session_path(resource_name), class: "btn btn-default navbar-btn" %> + <% end -%> + + <%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to 'Sign up', new_registration_path(resource_name), class: "btn btn-default navbar-btn" %> + <% end -%> + + <%= link_to 'Try', new_job_path, class: "btn btn-default navbar-btn" %> + +
    +
  • diff --git a/app/views/users/unlocks/new.html.erb b/app/views/users/unlocks/new.html.erb new file mode 100644 index 0000000..0765d02 --- /dev/null +++ b/app/views/users/unlocks/new.html.erb @@ -0,0 +1,22 @@ +
    +
    +
    + +

    Resend unlock instructions

    + +<%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post }) do |f| %> + <%= devise_error_messages! %> + +
    + + <%= f.email_field :email, :autofocus => true, class: "form-control input-lg", tabindex: 1, placeholder: "Your email address" %> +
    + +
    + <%= f.submit "Resend instructions", :class => "btn btn-success btn-lg full-width", tabindex: 2 %> +
    +<% end %> + +
    +
    +
    diff --git a/app/views/versions/_form.html.erb b/app/views/versions/_form.html.erb new file mode 100644 index 0000000..cebc0a0 --- /dev/null +++ b/app/views/versions/_form.html.erb @@ -0,0 +1,25 @@ +<%= form_for(version) do |f| %> + <% if version.errors.any? %> +
    +

    <%= pluralize(version.errors.count, "error") %> prohibited this version from being saved:

    + +
      + <% version.errors.full_messages.each do |message| %> +
    • <%= message %>
    • + <% end %> +
    +
    + <% end %> + +Description +<%= f.text_area :description %> +
    +Date +<%= f.date_field :release_date %> +JSON +<%= f.text_area :tools_json %> + +
    + <%= f.submit %> +
    +<% end %> diff --git a/app/views/versions/_version.json.jbuilder b/app/views/versions/_version.json.jbuilder new file mode 100644 index 0000000..5c185c5 --- /dev/null +++ b/app/views/versions/_version.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! version, :id, :created_at, :updated_at +json.url version_url(version, format: :json) \ No newline at end of file diff --git a/app/views/versions/edit.html.erb b/app/views/versions/edit.html.erb new file mode 100644 index 0000000..f41d8a5 --- /dev/null +++ b/app/views/versions/edit.html.erb @@ -0,0 +1,6 @@ +

    Editing Version

    + +<%= render 'form', version: @version %> + +<%= link_to 'Show', @version %> | +<%= link_to 'Back', versions_path %> diff --git a/app/views/versions/index.html.erb b/app/views/versions/index.html.erb new file mode 100644 index 0000000..344e7e0 --- /dev/null +++ b/app/views/versions/index.html.erb @@ -0,0 +1,59 @@ +
    + +

    <%= notice %>

    + +

    Versions

    + + <% @versions.each do |version| %> +
    +
    Version <%= version.id %>
    +
    +

    <%= version.description %>

    +<% list_tools = JSON.parse(version.tools_json).select { |e| e['version'] != 'NA' } %> + + + + + + + + + + +<% list_tools.each do |t| %> + + + + + + +<% end %> + +
    R packageCategoryDescriptionVersion
    ' target='_blank'><%= t['name'] %><%= @h_tools[t['name']]['categ'] %><%= @h_tools[t['name']]['description'] %><%= t['version'] %>
    +<%= link_to "Edit", edit_version_path(version) if admin? %> +
    +
    + + + <% end %> + +
    + +<%= link_to('New Version', new_version_path) if admin? %> +
    + + +<%= javascript_tag do %> + + <% @versions.each do |v| %> + + table = $('#tools_<%= v.id %>').dataTable({ + sDom: 't', + "iDisplayLength" : -1 + }); + + table.fnSort( [ [1,'asc'], [0,'asc'] ] ); + + <% end %> + +<% end %> diff --git a/app/views/versions/index.json.jbuilder b/app/views/versions/index.json.jbuilder new file mode 100644 index 0000000..47dadfb --- /dev/null +++ b/app/views/versions/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @versions, partial: 'versions/version', as: :version \ No newline at end of file diff --git a/app/views/versions/new.html.erb b/app/views/versions/new.html.erb new file mode 100644 index 0000000..a139f01 --- /dev/null +++ b/app/views/versions/new.html.erb @@ -0,0 +1,5 @@ +

    New Version

    + +<%= render 'form', version: @version %> + +<%= link_to 'Back', versions_path %> diff --git a/app/views/versions/show.html.erb b/app/views/versions/show.html.erb new file mode 100644 index 0000000..732e4cd --- /dev/null +++ b/app/views/versions/show.html.erb @@ -0,0 +1,4 @@ +

    <%= notice %>

    + +<%= link_to 'Edit', edit_version_path(@version) %> | +<%= link_to 'Back', versions_path %> diff --git a/app/views/versions/show.json.jbuilder b/app/views/versions/show.json.jbuilder new file mode 100644 index 0000000..3c1e884 --- /dev/null +++ b/app/views/versions/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "versions/version", version: @version \ No newline at end of file diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..66e9889 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/delayed_job b/bin/delayed_job new file mode 100755 index 0000000..edf1959 --- /dev/null +++ b/bin/delayed_job @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) +require 'delayed/command' +Delayed::Command.new(ARGV).daemonize diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..0739660 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..1724048 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..e620b4d --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/update b/bin/update new file mode 100755 index 0000000..a8e4462 --- /dev/null +++ b/bin/update @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/yarn b/bin/yarn new file mode 100755 index 0000000..c2bacef --- /dev/null +++ b/bin/yarn @@ -0,0 +1,11 @@ +#!/usr/bin/env ruby +VENDOR_PATH = File.expand_path('..', __dir__) +Dir.chdir(VENDOR_PATH) do + begin + exec "yarnpkg #{ARGV.join(" ")}" + rescue Errno::ENOENT + $stderr.puts "Yarn executable was not detected in the system." + $stderr.puts "Download Yarn at https://yarnpkg.com/en/docs/install" + exit 1 + end +end diff --git a/cable.js b/cable.js new file mode 100644 index 0000000..739aa5f --- /dev/null +++ b/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/config.ru b/config.ru new file mode 100755 index 0000000..f7ba0b5 --- /dev/null +++ b/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/config/application.rb b/config/application.rb new file mode 100755 index 0000000..5e971f6 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,15 @@ +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Genocrunch + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100755 index 0000000..30f5120 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..0bbde6f --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,9 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 diff --git a/config/config.yml.keep b/config/config.yml.keep new file mode 100644 index 0000000..5d0b6a8 --- /dev/null +++ b/config/config.yml.keep @@ -0,0 +1,18 @@ +development: + # Genocrunch main directory + data_dir: /path/to/genocrunch + + # Additional link(s) that should be included in the Infos menu of the topbar + info_links: [{name: 'link_name', href: 'link_url', target: '_blank'}] + + # Webmaster email + webmaster_email: 'webmaster_email' + + # Send a validation link to user email to confirm registration? + user_confirmable: false + +production: + data_dir: /path/to/genocrunch + info_links: [{name: 'link_name', href: 'link_url', target: '_blank'}] + webmaster_email: 'webmaster_email' + user_confirmable: false diff --git a/config/database.yml.keep b/config/database.yml.keep new file mode 100644 index 0000000..1f734b7 --- /dev/null +++ b/config/database.yml.keep @@ -0,0 +1,85 @@ +# PostgreSQL. Versions 9.1 and up are supported. +# +# Install the pg driver: +# gem install pg +# On OS X with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On OS X with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + # For details on connection pooling, see Rails configuration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling + pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> + +development: + <<: *default + database: my_application_db_name + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + username: myusername + + # The password associated with the postgres role (username). + password: mypassword + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + #host: localhost + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: my_application_db_name + +# As with config/secrets.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password as a unix environment variable when you boot +# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full rundown on how to provide these environment variables in a +# production deployment. +# +# On Heroku and other platform providers, you may have a full connection URL +# available as an environment variable. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# You can use this database configuration with: +# +# production: +# url: <%= ENV['DATABASE_URL'] %> +# +production: + <<: *default + database: my_application_db_name + username: myusername + password: <%= ENV['GENOCRUNCH_DATABASE_PASSWORD'] %> diff --git a/config/environment.rb b/config/environment.rb new file mode 100755 index 0000000..426333b --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb.keep b/config/environments/development.rb.keep new file mode 100755 index 0000000..50756a8 --- /dev/null +++ b/config/environments/development.rb.keep @@ -0,0 +1,70 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + config.force_ssl = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + config.perform_caching = true + # Enable/disable caching. By default caching is disabled. + #if Rails.root.join('tmp/caching-dev.txt').exist? + # config.action_controller.perform_caching = true + + # config.cache_store = :memory_store + # config.public_file_server.headers = { + # 'Cache-Control' => 'public, max-age=172800' + # } + #else + # config.action_controller.perform_caching = false + + # config.cache_store = :null_store + #end + #config.cache_store = :mem_cache_store + config.cache_store = :file_store, Rails.root.join('tmp/cache') + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = true + config.action_mailer.delivery_method = :smtp + config.action_mailer.perform_deliveries = true + config.action_mailer.perform_caching = false + config.action_mailer.default_url_options = { :host => 'localhost:3000' } + config.action_mailer.smtp_settings = { + :address => "smtp.gmail.com", + :port => 587, + :domain => "mail.google.com", + :user_name => "app_email@gmail.com", + :password => "app_email_password", + :authentication => :plain, + :enable_starttls_auto => true + } + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..51639b6 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100755 index 0000000..01ef3e6 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100755 index 0000000..59385cd --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..5a6a32d --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb new file mode 100755 index 0000000..aec592f --- /dev/null +++ b/config/initializers/delayed_job_config.rb @@ -0,0 +1,10 @@ +# config/initializers/delayed_job_config.rb +Delayed::Worker.destroy_failed_jobs = true +Delayed::Worker.sleep_delay = 1 +Delayed::Worker.max_attempts = 1 +Delayed::Worker.max_run_time = 7.days +Delayed::Worker.read_ahead = 10 +Delayed::Worker.default_queue_name = 'default' +Delayed::Worker.delay_jobs = !Rails.env.test? +Delayed::Worker.raise_signal_exceptions = :term +Delayed::Worker.logger = Logger.new(File.join(Rails.root, 'log', 'delayed_job.log')) diff --git a/config/initializers/delayed_web.rb b/config/initializers/delayed_web.rb new file mode 100755 index 0000000..4e3d302 --- /dev/null +++ b/config/initializers/delayed_web.rb @@ -0,0 +1,4 @@ +# Tell Delayed::Web that we're using ActiveRecord as our backend. +Rails.application.config.to_prepare do + Delayed::Web::Job.backend = 'active_record' +end diff --git a/config/initializers/devise.rb.keep b/config/initializers/devise.rb.keep new file mode 100755 index 0000000..add7ada --- /dev/null +++ b/config/initializers/devise.rb.keep @@ -0,0 +1,252 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class with default "from" parameter. + config.mailer_sender = "app_email@gmail.com" + + # Configure the class responsible to send e-mails. + config.mailer = "ApplicationMailer" + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + config.authentication_keys = [ :username ] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [ :email ] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [ :email ] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:token]` will + # enable it only for token authentication. The supported strategies are: + # :database = Support basic authentication with authentication key + password + # :token = Support basic authentication with token authentication key + # :token_options = Support token authentication with options as defined in + # http://api.rubyonrails.org/classes/ActionController/HttpAuthentication/Token.html + # config.http_authenticatable = false + + # If http headers should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. "Application" by default. + # config.http_authentication_realm = "Application" + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # :http_auth and :token_auth by adding those symbols to the array below. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing :skip => :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 10. If + # using other encryptors, it sets how many times you want the password re-encrypted. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. + config.stretches = Rails.env.test? ? 1 : 10 + + # Setup a pepper to generate the encrypted password. + # config.pepper = "ceca052ac08e9990c0541e47c112de15c1072dfcbfed6bb5ba7c208613333d3f05b6033ee9179bb178bfba2359d1cec2477f8126f79d7dedc24ef8420f0c8004" + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming his account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming his account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming his account. + #config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + #config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed new email is stored in + # unconfirmed email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + config.confirmation_keys = [ :email ] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + config.remember_for = 2.weeks + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # :secure => true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. Default is 8..128. + config.password_length = 5..128 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@]+@[^@]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + config.timeout_in = 30.minutes + + # If true, expires auth token on session timeout. + # config.expire_auth_token_on_timeout = false + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [ :email ] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + config.reset_password_keys = [ :email ] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # ==> Configuration for :encryptable + # Allow you to use another encryption algorithm besides bcrypt (default). You can use + # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, + # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) + # and :restful_authentication_sha1 (then you should set stretches to 10, and copy + # REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Configuration for :token_authenticatable + # Defines name of the authentication token params key + # config.token_authentication_key = :auth_token + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ["*/*", :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(:scope => :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: "/my_engine" + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using omniauth, Devise cannot automatically set Omniauth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = "/my_engine/users/auth" +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100755 index 0000000..4a994e1 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100755 index 0000000..ac033bf --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/load_config.rb b/config/initializers/load_config.rb new file mode 100644 index 0000000..386dd55 --- /dev/null +++ b/config/initializers/load_config.rb @@ -0,0 +1,4 @@ +raw_config = File.read("#{Rails.root}/config/config.yml") +APP_CONFIG = YAML.load(raw_config)[Rails.env].symbolize_keys if YAML.load(raw_config)[Rails.env] + + diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100755 index 0000000..dc18996 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb new file mode 100644 index 0000000..671abb6 --- /dev/null +++ b/config/initializers/new_framework_defaults.rb @@ -0,0 +1,24 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = true + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = true + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = true + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = true + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = false + +# Configure SSL options to enable HSTS with subdomains. Previous versions had false. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } diff --git a/config/initializers/rucaptcha.rb b/config/initializers/rucaptcha.rb new file mode 100755 index 0000000..4f12d06 --- /dev/null +++ b/config/initializers/rucaptcha.rb @@ -0,0 +1,15 @@ +RuCaptcha.configure do + # Using mem_cache_store is actually problematic + self.cache_store = :file_store + # Number of chars, default: 4 + # slef.len = 4 + # Image font size, default: 45 + # self.font_size = 45 + # Cache generated images in file store, this is config files limit, default: 100 + # set 0 to disable file cache. + # self.cache_limit = 100 + # Custom captcha code expire time if you need, default: 2 minutes + # self.expires_in = 120 + # Color style, default: :colorful, allows: [:colorful, :black_white] + # self.style = :colorful +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100755 index 0000000..656d368 --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.session_store :cookie_store, key: '_genocrunch_session' diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100755 index 0000000..bbfc396 --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100755 index 0000000..f3bf171 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,59 @@ +# Additional translations at https://github.com/plataformatec/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your account was successfully confirmed. You can now sign in." + send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account was not activated yet." + invalid: "Invalid username or password." + invalid_token: "Invalid authentication token." + locked: "Your account is locked." + not_found_in_database: "Invalid username or password." + timeout: "Your session expired, please sign in again to continue." + unauthenticated: "" + unconfirmed: "You have to confirm your account before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock Instructions" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions about how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password was changed successfully." + updated_not_active: "Your password was changed successfully." + registrations: + destroyed: "Your account was successfully cancelled." + signed_up: "You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A confirmation link has been sent to your email address. Please check your emails and follow the confirmation link to confirm your account (you may have to check your spam folder). If this does not work, use the Resend confirmation instructions link." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." + updated: "You updated your account successfully." + sessions: + signed_in: "" + signed_out: "" + unlocks: + send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100755 index 0000000..0653957 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,23 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/locales/rucaptcha.en.yml b/config/locales/rucaptcha.en.yml new file mode 100755 index 0000000..79ca4ea --- /dev/null +++ b/config/locales/rucaptcha.en.yml @@ -0,0 +1,3 @@ +en: + rucaptcha: + invalid: "Captcha is invalid." diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..c7f311f --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,47 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted this block will be run, if you are using `preload_app!` +# option you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100755 index 0000000..a28d9d9 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,114 @@ +Genocrunch::Application.routes.draw do + + resources :versions + resources :examples + resources :home do + collection do + get :about ## for references + get :data_format + get :tutorial + get :reference + get :doc + get :faq + get :version + get :terms + end + end + + # Manage roles + authenticated :user, -> user { user.role == "admin" } do + mount Delayed::Web::Engine, at: '/delayed-jobs' + get "/admins/:page" => "admins#show" + end + + # creates routes for users + devise_for :users, controllers: {sessions: "users/sessions", + confirmations: "users/confirmations", + mailer: "users/mailer", + passwords: "users/passwords", + registrations: "users/registrations", + shared: "users/shared", + unlocks: "users/unlocks"} + + resources :jobs, :param => :key do # creates routes for jobs # '$ rake routes' to see them (or go tohttp://localhost:3000/rails/info/routes) + member do + get :tab + get :serve + get :view + get :refresh + get :clone + end + collection do + get :manage + get :read_file_header + get :read_file_column + end + end + + get "jobs/:id/refresh" => "jobs#refresh", :as => :refresh_jobs + + get "/helps/:page" => "helps#show" + + # error pages + + # The priority is based upon order of creation: first created -> highest priority. + # See how all your routes lay out with "rake routes". + + # match '/home', to: 'jobs#index', via: [:get] + devise_scope :user do + get '/welcome' => 'users/sessions#new', as: 'sign_in' + end + # You can have the root of your site routed with "root" + root 'jobs#index' + + #get '/jobs/:id(.:format)', to 'jobs#copy', as 'jobs_copy' + + # Example of regular route: + # get 'products/:id' => 'catalog#view' + + # Example of named route that can be invoked with purchase_url(id: product.id) + # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase + + # Example resource route (maps HTTP verbs to controller actions automatically): + # resources :products + + # Example resource route with options: + # resources :products do + # member do + # get 'short' + # post 'toggle' + # end + # + # collection do + # get 'sold' + # end + # end + + # Example resource route with sub-resources: + # resources :products do + # resources :comments, :sales + # resource :seller + # end + + # Example resource route with more complex sub-resources: + # resources :products do + # resources :comments + # resources :sales do + # get 'recent', on: :collection + # end + # end + + # Example resource route with concerns: + # concern :toggleable do + # post 'toggle' + # end + # resources :posts, concerns: :toggleable + # resources :photos, concerns: :toggleable + + # Example resource route within a namespace: + # namespace :admin do + # # Directs /admin/products/* to Admin::ProductsController + # # (app/controllers/admin/products_controller.rb) + # resources :products + # end +end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..c9119b4 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/config/unicorn.rb b/config/unicorn.rb new file mode 100755 index 0000000..b6f8c28 --- /dev/null +++ b/config/unicorn.rb @@ -0,0 +1,21 @@ +# set path to application +app_dir = File.expand_path("../..", __FILE__) +shared_dir = "#{app_dir}/shared" +working_directory app_dir + + +# Set unicorn options +worker_processes 2 +preload_app true +timeout 30 + +# Set up socket location +listen "#{shared_dir}/sockets/unicorn.sock", :backlog => 64 + +# Logging +stderr_path "#{shared_dir}/log/unicorn.stderr.log" +stdout_path "#{shared_dir}/log/unicorn.stdout.log" + +# Set master PID location +pid "#{shared_dir}/pids/unicorn.pid" + diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..ba3c67a --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,100 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20170815081748) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "delayed_jobs", force: :cascade do |t| + t.integer "priority", default: 0, null: false + t.integer "attempts", default: 0, null: false + t.text "handler", null: false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + end + + create_table "jobs", force: :cascade do |t| + t.boolean "sandbox", default: true + t.integer "user_id", default: 1 + t.string "name" + t.string "key" + t.datetime "created_at" + t.datetime "updated_at" + t.string "status", default:'pending' + t.integer "pmid" + t.text "description" + t.text "read_access" + t.text "write_access" + t.text "form_json" + t.text "output_json" + end + + create_table "users", force: :cascade do |t| + t.string "username", default: "", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.string "confirmation_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.datetime "created_at" + t.datetime "updated_at" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.integer "sign_in_count", default: 0 + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.string "role", default: "user" + t.index ["username"], name: "index_users_on_username", unique: true, using: :btree + end + + create_table "examples", force: :cascade do |t| + t.text "job_key" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "statuses", force: :cascade do |t| + t.text "name" + t.text "icon" + t.integer "precedence" + end + + create_table "views", force: :cascade do |t| + t.text "name" + t.text "category" + t.text "icon" + t.integer "position" + t.boolean "graphical" + t.text "data_format" + end + + create_table "versions", force: :cascade do |t| + t.text "description" + t.datetime "release_date" + t.text "tools_json" + t.datetime "created_at" + t.datetime "updated_at" + end + +end diff --git a/db/seeds.rb.keep b/db/seeds.rb.keep new file mode 100755 index 0000000..4a70490 --- /dev/null +++ b/db/seeds.rb.keep @@ -0,0 +1,101 @@ +User.create!([{username: 'guest', + role: 'guest', + email: 'guest@guestmailbox.com', # <- HERE + confirmed_at: '2017-01-01 00:00:00.000000', + password: 'guest_account_password'}, # <- HERE + {username: 'admin', + role: 'admin', + email: 'admin@adminmailbox.com', # <- HERE + confirmed_at: '2017-01-01 00:00:00.000000', + password: 'admin_account_password'}]) # <- AND THERE + +Status.create!([{name: 'completed', + icon: 'fa fa-check icon-success', + precedence: '1'}, + {name: 'pending', + icon: 'fa fa-ellipsis-h icon-inactive', + precedence: '2'}, + {name: 'running', + icon: 'fa fa-circle-o-notch fa-pulse', + precedence: '3'}, + {name: 'failed', + icon: 'fa fa-exclamation-triangle icon-warning', + precedence: '4'}, + {name: 'skipped', + icon: 'fa fa-check icon-success', + precedence: '5'}]) + +View.create!([{name: 'map', + category: 'input_data_preparation', + icon: 'fa fa-align-justify', + position: 1, + graphical: false}, + {name: 'primary_dataset', + category: 'input_data_preparation', + icon: 'fa fa-align-justify', + position: 2, + graphical: false}, + {name: 'secondary_dataset', + category: 'input_data_preparation', + icon: 'fa fa-align-justify', + position: 3, + graphical: false}, + {name: 'proportions', + category: 'analysis', + icon: 'fa fa-bar-chart', + position: 1, + graphical: true, + data_format: 'json'}, + {name: 'diversity', + category: 'analysis', + icon: 'fa fa-line-chart', + position: 2, + graphical: true, + data_format: 'json'}, + {name: 'adonis', + category: 'analysis', + icon: 'fa fa-pie-chart', + position: 3, + graphical: true, + data_format: 'json'}, + {name: 'pca', + category: 'analysis', + icon: 'fa fa-bar-chart', + position: 4, + graphical: true, + data_format: 'json'}, + {name: 'pcoa', + category: 'analysis', + icon: 'fa fa-bar-chart', + position: 5, + graphical: true, + data_format: 'json'}, + {name: 'heatmap', + category: 'analysis', + icon: 'fa fa-th', + position: 6, + graphical: true, + data_format: 'json'}, + {name: 'change', + category: 'analysis', + icon: 'fa fa-bar-chart', + position: 7, + graphical: true, + data_format: 'json'}, + {name: 'correlation_network', + category: 'analysis', + icon: 'fa fa-share-alt', + position: 8, + graphical: true, + data_format: 'json'}, + {name: 'similarity_network', + category: 'analysis', + icon: 'fa fa-share-alt', + position: 9, + graphical: true, + data_format: 'json'}, + {name: 'clustering', + category: 'analysis', + icon: 'fa fa-align-justify', + position: 10, + graphical: false}]) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..6b906ac --- /dev/null +++ b/install.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +#install.sh +# Generic script to install through bash profile + +################ +# Error function +################ +err() { + echo "Error [${__base}]: $@" >&2 + exit 1 +} + +################ +# Main +# Optional args: +# (1) action Valid choices: 'install', 'uninstall'. Default: 'install'. +################ +main(){ + local funcname + funcname='main' + + # Set action (install or uninstall) + local value + value=('install' 'uninstall') + local action + action="${value[0]}" + if [[ "${#@}" -gt 0 ]]; then + [[ ! " ${value[@]} " =~ " ${1} " ]] && err "in ${funcname}() (line ${LINENO}): unknown value to action (${1}). Valid choice: ${value[@]}. Default:${value[0]}." + [[ "${1}" == "${value[1]}" ]] && action="${1}" + fi + + local bash_profile + if [[ -e "${HOME}"/.bashrc ]]; then + bash_profile="${HOME}"/.bashrc + elif [[ -e "${HOME}"/.bash_profile ]]; then + bash_profile="${HOME}"/.bash_profile + else + err "in ${funcname}() (line ${LINENO}): no .bashrc or .bash_profile found in \$HOME." + fi + [[ ! -w "${bash_profile}" ]] && err "in ${funcname}() (line ${LINENO}): write permission denied on ${bash_profile}." + + local append_str + append_str="export PATH=\"${__installdir}:\$PATH\"" + + if [[ "${action}" == 'install' ]]; then + if [[ $(grep -c "${append_str}" "${bash_profile}") -eq 0 ]]; then + echo "${append_str}" >> "${bash_profile}" + echo "Appending '${append_str}' to ${bash_profile}..." + else + echo "'${append_str}' alredy found in ${bash_profile}. Nothing to do." + fi + else + if [[ $(grep -c "${append_str}" "${bash_profile}") -ne 0 ]]; then + # !!! Hard coded !!! + sed -i -e '\&^'"export PATH=\\\"${__installdir}:\\\$PATH\\\""'$&d' "${bash_profile}" + echo "Removed '${append_str}' from ${bash_profile}." + else + echo "'${append_str}' not found in ${bash_profile}. Nothing to do." + fi + fi + + return 0 +} + +set -o nounset +set -o pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__basedir="$(basename ${__dir})" +#__basedir="$(echo "${__basedir}" | sed -e "s/-[0-9]*\.[0-9]*$//g")" +__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" +__base="$(basename ${__file})" +__installdir="${__dir}"/lib/genocrunch_console/bin + +main "${@}" + +exit 0 diff --git a/lib/custom_public_exceptions.rb b/lib/custom_public_exceptions.rb new file mode 100644 index 0000000..d5d4a4a --- /dev/null +++ b/lib/custom_public_exceptions.rb @@ -0,0 +1,12 @@ +class CustomPublicExceptions < ActionDispatch::PublicExceptions + def call(env) + status = env["PATH_INFO"][1..-1] + + if status == "404" + Rails.application.routes.call(env) + else + super + end + end +end + diff --git a/lib/genocrunch_console/.DS_Store b/lib/genocrunch_console/.DS_Store new file mode 100644 index 0000000..b70ffea Binary files /dev/null and b/lib/genocrunch_console/.DS_Store differ diff --git a/lib/genocrunch_console/bin/.DS_Store b/lib/genocrunch_console/bin/.DS_Store new file mode 100755 index 0000000..8cf9fc6 Binary files /dev/null and b/lib/genocrunch_console/bin/.DS_Store differ diff --git a/lib/genocrunch_console/bin/.Rhistory b/lib/genocrunch_console/bin/.Rhistory new file mode 100644 index 0000000..e69de29 diff --git a/lib/genocrunch_console/bin/analyse_count_table.py b/lib/genocrunch_console/bin/analyse_count_table.py new file mode 100755 index 0000000..160d16f --- /dev/null +++ b/lib/genocrunch_console/bin/analyse_count_table.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +#analyse_count_table.py + +import os, json, errno, copy, sys, inspect, shutil + +# Import genocrunch library +__dir__ = os.path.dirname(__file__) +main_dir = '/'.join(__dir__.split('/')[0:len(__dir__.split('/'))-1]) + +genocrunchlib = main_dir+'/lib' +if genocrunchlib not in sys.path: + sys.path.insert(0, genocrunchlib) + +import genocrunchlib as gc + +analysis = gc.Analysis(main_dir+'/etc/genocrunchlib.json') + +try: + analysis.run() +except: + analysis.cleanup() + print("Unexpected error:", sys.exc_info()[0]) + raise + +exit() diff --git a/lib/genocrunch_console/bin/analyse_table.R b/lib/genocrunch_console/bin/analyse_table.R new file mode 100755 index 0000000..146f3fc --- /dev/null +++ b/lib/genocrunch_console/bin/analyse_table.R @@ -0,0 +1,326 @@ +#!/usr/bin/env Rscript +#analyse_table.R +# Generic script to analyse a table + +# Set environment +libname <- 'genocrunchlib' + +args <- commandArgs(trailingOnly=FALSE) +dir <- gsub('--file=', '', dirname(args[4])) +file <- gsub('--file=', '', args[4]) +lib <- normalizePath(path=paste(dir, + '/../lib/', + libname, + '.R', + sep=''), + mustWork=TRUE) + + +holynetlib <- normalizePath(path=paste(dir,'/../lib/holynetlib.R', sep=''), mustWork=FALSE) +friedmanlib <- normalizePath(path=paste(dir,'/../lib/friedman_test_with_post_hoc.R', sep=''), mustWork=FALSE) +source(lib) +source(friedmanlib) +suppressMessages(library('optparse')) +suppressMessages(library('rjson')) +options(scipen=999) +set.seed(2) + +# Set whether output can be graphical or not +graphical <- list(clustering= TRUE, + proportions= TRUE, + diversity= TRUE, + adonis= TRUE, + pca= TRUE, + ca= TRUE, + pcoa= TRUE, + cca= TRUE, + change= TRUE, + heatmap= TRUE, + correlation_network=FALSE, + similarity_network= FALSE) + +# Set options. Note: there is a small bug with '-g', do not use it +option_list <- list(make_option(c('-t', '--table'), # General options + type='character', + default=NULL, + help='Path to data table'), + make_option(c('-o', '--output'), + type='character', + default=paste(file, '_out', sep=''), + help='Path to output file'), + make_option(c('-v', '--verbose'), + type='character', + default='TRUE', + help='Print a description?'), + make_option('--graphical', + type='logical', + default=TRUE, + help='Generate graphics?'), + make_option('--json', + type='character', + default=ReadJson(file=normalizePath(paste(dir, + '/../etc/', + libname, + '.json', + sep=''), + mustWork=TRUE), + rel=FALSE), + help='A json string of characters with function-specific information'), + make_option(c('-m', '--method'), + type='character', + default=NULL, + help=paste('Analysis method. Valid choices: ', + paste(names(graphical), collapse=', '), + sep='')), + make_option('--category', + type='character', + default='taxonomy', + help='Name of the category column in table'), + make_option('--width', + type='numeric', + default=5, + help='Figure width in cm'), + make_option('--height', + type='numeric', + default=5, + help='Figure height in cm'), + make_option('--fun', + type='character', + default=NULL, + help='Function to use (generic)'), + make_option('--map', + type='character', + default=NULL, + help='Path to mapping file'), + make_option('--stats', + type='character', + default='anova', + help='Statistycal test to use.'), + make_option('--model', + type='character', + default=NULL, + help='An R formula representing the model for stats. Terms must refer to names(map). Multiple comma-separated formulae are accepted.'), + make_option('--adonis_model', + type='character', + default=NULL, + help='An R formula representing the model for adonis. Terms must refer to names(map). Multiple comma-separated formulae are accepted.'), + make_option('--strata', + type='character', + default=NULL, + help='Strata parameter for adonis. IF multiple models are specified, then multiple comma-separated strata are accepted.'), + make_option('--metadata', + type='character', + default=NULL, + help='Path to metadata table'), + make_option('--nrar', + type='numeric', + default=50, + help='Number of rarefaction to perform'), + make_option('--clust', + type='character', + default="walktrap", + help='Graph-based clustering algorithm'), + make_option('--column', + type='character', + default=NULL, + help='Name of a column'), + make_option('--compare_diversity', + type='logical', + default=FALSE, + help='Compare diversity between groups?'), + make_option('--log', + type='character', + default=NULL, + help='Path to log file')) + +opt_parser <- OptionParser(option_list=option_list) +opt <- parse_args(opt_parser) +log_fp <- opt$log + +# Convert the library parameters from json string to list +json <- fromJSON(json_str=opt$json) + +# Set general inputs +table <- read.table(file=opt$table, sep='\t', header=1, row.names=1) + +if (opt$category %in% names(table)) { + if (length(unique(unlist(table[, opt$category]))) == nrow(table)) { + row.names(table) <- table[, opt$category] + } else { + row.names(table) <- paste(table[, opt$category], '(', row.names(table) ,')', sep='') + } + table <- table[, names(table) != opt$category] +} + +if (!is.null(opt$map)) { + map <- read.table(file=opt$map, sep='\t', header=1, row.names=1) +} else { + map <- NULL +} + +if (!is.null(opt$stats) && opt$stats != '') { + stats <- strsplit(opt$stats, ',')[[1]] +} else { + stats <- NULL +} + +if (!is.null(opt$model) && opt$model != '') { + model <- strsplit(opt$model, ',')[[1]] +} else { + model <- NULL +} + +if (!is.null(opt$fun)) { + fun <- unlist(strsplit(opt$fun, ',')) +} else { + fun <- NULL +} + +if (!is.null(opt$metadata)) { + metadata <- read.table(file=opt$metadata, sep='\t', header=1, row.names=1) +} else { + metadata <- NULL +} + +# Set graphical output +if (graphical[[opt$method]] == TRUE && opt$graphical == TRUE) { + fig.fp <- paste(opt$output, '.pdf', sep='') + pdf(file=fig.fp, width=opt$width, height=opt$height) +} + +# Call the analysis function +################### +# Analysis function output should be one of the following element or a list +# containing one or more of the following elements: +# 'txt' A matrix or data-frame to be written in a tab-delimited file +# 'json' A json string to be written in a json file +# 'csv' A matrix or data-frame to be written in a comma-delimited file +################### +data <- list() +if (opt$method == 'clustering') { + + data[['txt']] <- PerformClustering(table=table, + fun=fun, + json=json, + verbose=opt$verbose, + graphical=opt$graphical) + if (!is.null(data[['txt']])) { + names(data[['txt']]) <- basename(opt$output) + } +} else if (opt$method == 'proportions') { + + data[['json']] <- AnalyseProportions(table=table, + verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'diversity') { + data[['json']] <- AnalyseDiversity(table=table, map=map, fun=fun, + nrar=opt$nrar, + compare_diversity=opt$compare_diversity, + stats=stats, + model=model, + json=json, verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'adonis') { + if (!is.null(opt$adonis_model) && opt$adonis_model != '') { + adonis_model <- unlist(strsplit(opt$adonis_model, ',')) + } else { + adonis_model <- model + } + if (!is.null(opt$strata) && opt$strata != '') { + strata <- unlist(strsplit(opt$strata, ',')) + } else { + strata <- NULL + } + data[['json']] <- PerformAdonis(table=table, map=map, fun=fun, + model=adonis_model, strata=strata, + json=json, verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'pca') { + + data[['json']] <- PerformPCA(table=table, map=map, + verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'ca') { + + data[['json']] <- PerformCA(table=table, map=map, + verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'pcoa') { + + data[['json']] <- PerformPCoA(table=table, map=map, + fun=fun, json=json, verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'cca') { + + data[['json']] <- PerformCCA(table=table, map=map, column=opt$column, + verbose=opt$verbose, + graphical=opt$graphical) + +} else if (opt$method == 'change') { + + data[['json']] <- AnalyseChange(table=table, map=map, + stats=stats, + model=model, json=json, + verbose=opt$verbose, graphical=opt$graphical) + +} else if (opt$method == 'heatmap') { + + data[['json']] <- BuildHeatMap(table=table, map=map, + stats=stats, + model=model, metadata=metadata, fun=fun, json=json, + verbose=opt$verbose, graphical=opt$graphical) + +} else if (opt$method == 'correlation_network') { + + data[['json']] <- BuildCorrelationNetwork(table=table, map=map, + stats=stats, + model=model, + json=json, metadata=metadata, fun=fun, + verbose=opt$verbose) + +} else if (opt$method == 'similarity_network') { + if (!is.null(metadata)) { + tables <- list(table, metadata) + clust.names <- c('data', 'metadata', 'fusion') + } else { + tables <- list(table) + clust.names <- 'data' + } + data <- BuildSimilarityNetwork(table=tables, map=map, + clust=opt$clust, + clust.names=paste(basename(opt$output), + clust.names, sep='_'), + funs=fun, json=json, + verbose=opt$verbose, lib=holynetlib) +} + +# Write data +if (graphical[[opt$method]] == TRUE && opt$graphical == TRUE) { + PrintMsg(paste('"output":"', fig.fp, '"', sep=''), opt$verbose) + graphics.off() +} + +f.type <- names(data) +for (i in 1:length(f.type)) { + + if (!is.null(data[[f.type[i]]])) { + output.fp <- paste(opt$output, '.', f.type[i], sep='') + PrintMsg(paste('"output":"', output.fp, '"', sep=''), opt$verbose) + + if (f.type[i] == 'json') { + write(x=data[[f.type[i]]], file=output.fp, append=FALSE) + } else if (f.type[i] == 'txt') { + WriteTable(data[[f.type[i]]], output.fp, name='', sep='\t') + } else if (f.type[i] == 'csv') { + WriteTable(data[[f.type[i]]], output.fp, name='name', sep=',') + } + } +} + +rm(list=ls()) diff --git a/lib/genocrunch_console/bin/get_version.R b/lib/genocrunch_console/bin/get_version.R new file mode 100755 index 0000000..25d415b --- /dev/null +++ b/lib/genocrunch_console/bin/get_version.R @@ -0,0 +1,23 @@ +#!/usr/bin/env Rscript +#get_version.R +# Get version of packages installed +args <- commandArgs(trailingOnly=FALSE) +pkgs <- unlist(strsplit(args[6], ',')) + +output <- c(1:length(pkgs)) +for (i in 1:length(pkgs)) { + if (!require(pkgs[i], character.only=TRUE)) { + version <- 'NA' + } else { + version <- packageVersion(pkgs[i]) + } + output[i] <- paste('{"name":"', + pkgs[i], + '","version":"', + version, + '"}', + sep='') +} +cat(paste('[', paste(output, collapse=','), ']', sep='')) + +rm(list=ls()) diff --git a/lib/genocrunch_console/bin/get_version.py b/lib/genocrunch_console/bin/get_version.py new file mode 100755 index 0000000..444c6e1 --- /dev/null +++ b/lib/genocrunch_console/bin/get_version.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python2.7 +# -*- coding: utf-8 -*- +#get_version.py + +import os +from subprocess import Popen, PIPE +from json import load, dump +from datetime import datetime + +__dir__ = os.path.dirname(__file__) + +pkg_list = [] + +# Get packages hard-coded in R scripts +sub = Popen([__dir__+'/get_version.sh'], stdout=PIPE, stderr=PIPE) +sub.wait() +pkg_list.extend([e for e in sub.stdout.read().split('\n') if e not in [None, '', ' ']]) + +# Get packages passed in etc json +json_fp = __dir__+'/../etc/genocrunchlib.json' +if os.path.exists(json_fp): + data = load(open(json_fp)) + +for key, val in data['choices'].iteritems(): + pkg = [e['pkg'] for e in val if 'pkg' in e.keys() and e['pkg'] not in [None, '', ' ']] + if len(pkg) > 0: + pkg_list.extend(pkg) +pkg_list = list(set(pkg_list)) + +# Get version installed +sub = Popen([__dir__+'/get_version.R', ','.join(pkg_list)], stdout=PIPE, stderr=PIPE) +sub.wait() +versions = sub.stdout.read() + +with open('version_'+str(datetime.now()).replace(' ', '_')+'.json', 'w') as f: + f.write(versions) + +exit() diff --git a/lib/genocrunch_console/bin/get_version.sh b/lib/genocrunch_console/bin/get_version.sh new file mode 100755 index 0000000..6a6fb9c --- /dev/null +++ b/lib/genocrunch_console/bin/get_version.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +#get_version.sh +# Get version of R packages used in R scripts + +################ +# Main +################ +main(){ + local funcname + funcname='main' + + local fps + fps=($(cd .. && git ls-files '*.R' | sed 's/^/..\//g')) + + # Get packages hard-coded in R scripts + grep -hE "(require|library)\([a-zA-Z0-9\']*\)" "${fps[@]}" | \ + sed '/^[[:space:]]*#/d' | \ + sed -r "s/.*(require|library)\(([a-zA-Z0-9\']*)\).*/\2/" | \ + sed "s/[\']*//g" | \ + sort -u + + return 0 +} + +set -o nounset +set -o pipefail + +main "${@}" + +exit 0 diff --git a/lib/genocrunch_console/bin/modify_table.R b/lib/genocrunch_console/bin/modify_table.R new file mode 100755 index 0000000..8226d58 --- /dev/null +++ b/lib/genocrunch_console/bin/modify_table.R @@ -0,0 +1,178 @@ +#!/usr/bin/env Rscript +#modify_table.R +# Generic script to modify a table + +# Set environment +args <- commandArgs(trailingOnly=FALSE) +dir <- gsub('--file=', '', dirname(args[4])) +file <- gsub('--file=', '', args[4]) +lib <- paste(dir, '/../lib', sep='') +source(file.path(lib, 'genocrunchlib.R')) +suppressMessages(library('optparse')) +options(scipen=999) +set.seed(2) + +methods <- c('sorting', 'filtering', 'log2cpm', 'percent', 'VST', 'anova_adjustment', 'rarefaction', 'binning', 'transpose', 'div', 'none') + +# Set options. Note: there is a small bug with '-g', do not use it for any option +option_list <- list(make_option(c('-t', '--table'), + type='character', + default=NULL, + help='Path to data file'), + make_option(c('-o', '--output'), + type='character', + default=paste(file, '_out', sep=''), + help='Path to output file'), + make_option(c('-m', '--method'), + type='character', + default=NULL, + help=paste('Modification type. Valid choices: ', paste(methods, collapse=', '), sep='')), + make_option(c('-v', '--verbose'), + type='logical', + default=TRUE, + help='Print a description?'), + make_option('--map', + type='character', + default=NULL, + help='Path to mapping file'), + make_option('--effect', + type='character', + default=NULL, + help='comma-delimited list of random effects to \ +suppress. Elements must be names of columns in mapping file'), + make_option('--ignore', + type='character', + default='none', + help='Comma-delimited list of columns to ignore'), + make_option('--sample', + type='character', + default='max', + help='Sampling depth (sample size)'), + make_option('--nsampling', + type='numeric', + default=3, + help='Number of random sampling (without replacement) to perform'), + make_option('--level', + type='character', + default='NA', + help='Aggragation level'), + make_option('--fun', + type='character', + default='sum', + help='Function to use for aggregation'), + make_option('--column', + type='character', + default=NULL, + help='Column name'), + make_option('--abundance_threshold', + type='numeric', + default=0, + help='Abundance threshold'), + make_option('--abundance_threshold_type', + type='character', + default='percent', + help='Percent or absolute value? Valid choices: percent,int.'), + make_option('--presence_threshold', + type='numeric', + default=0, + help='Presence threshold'), + make_option('--presence_threshold_type', + type='character', + default='int', + help='Percent or absolute value? Valid choices: percent,int.'), + make_option('--vect', + type='character', + default=NULL, + help='Path to vector file'), + make_option('--log', + type='character', + default=NULL, + help='Path to log file')) + +opt_parser <- OptionParser(option_list=option_list) +opt <- parse_args(opt_parser) +log_fp <- opt$log + +# Set general inputs +table <- read.table(file=opt$table, sep='\t', header=1, row.names=1) +ignore <- as.vector(unlist(strsplit(opt$ignore, split=','))) +table.ignored <- as.data.frame(table[, names(table) %in% ignore]) +names(table.ignored) <- names(table)[names(table) %in% ignore] +table <- table[, !(names(table) %in% ignore)] + +if (! is.null(opt$map)) { + map <- read.table(file=opt$map, sep='\t', header=1, row.names=1) +} + +if (opt$method == 'none') { + + table.modified <- table + +} else if (opt$method == 'sorting') { +# column <- opt$column +# if (is.null(column)) { +# column <- 1 +# } +# print(column) + table.modified <- SortTable(table=table, map=map, verbose=opt$verbose) + +} else if (opt$method == 'filtering') { + + table.modified <- FilterTable(table=table, + category=opt$column, + abundance_threshold=opt$abundance_threshold, + abundance_threshold_type=opt$abundance_threshold_type, + presence_threshold=opt$presence_threshold, + presence_threshold_type=opt$presence_threshold_type, + verbose=opt$verbose) + +} else if (opt$method == 'VST') { + + table.modified <- ApplyVST(table=table, map=map, verbose=opt$verbose) + +} else if (opt$method == 'log2cpm') { + + table.modified <- ApplyLog2Cpm(table=table, verbose=opt$verbose) + +} else if (opt$method == 'log2') { + + table.modified <- ApplyLog2(table=table, verbose=opt$verbose) + +} else if (opt$method == 'percent') { + + table.modified <- ApplyCount2Percent(table=table, verbose=opt$verbose) + +} else if (opt$method == 'batch_effect_suppression') { + + table.modified <- SuppressBatchEffect(table=table, map=map, effect=opt$effect, + fun=opt$fun, verbose=opt$verbose) + +} else if (opt$method == 'rarefaction') { + + table.modified <- RarefyTable(table=table, sample=opt$sample, nsampling=opt$nsampling, verbose=opt$verbose) + +} else if (opt$method == 'binning') { + + table.modified <- BinTableByCategory(table, opt$column, opt$fun, opt$level, opt$verbose) + +} else if (opt$method == 'transpose') { + + table.modified <- Transpose(table=table, verbose=opt$verbose) + +} else if (opt$method == 'div') { + + vect <- read.table(file=opt$vect, sep='\t', header=1, row.names=1) + table.modified <- DivTable(table, vect, verbose=opt$verbose) + +} + +# Append optional ignored column(s) if any +if (ncol(as.matrix(table.ignored)) != 0) { + table.modified <- data.frame(table.modified, table.ignored) +} + +# Write modified table to output location +WriteTable(table.modified, opt$output, name='', sep='\t') +PrintMsg(paste('"output":"', opt$output, '"', sep=''), opt$verbose) + +rm(list=ls()) diff --git a/lib/genocrunch_console/bin/modify_table.sh b/lib/genocrunch_console/bin/modify_table.sh new file mode 100755 index 0000000..6312f87 --- /dev/null +++ b/lib/genocrunch_console/bin/modify_table.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +#modify_table.sh +# Generic script to modify a table + +################ +# Main +# Args: +# (1) table_fp Path to comma-delimited txt table (1st row as header) +# (2) method Method tu apply for table modification +# (3) category Name of the column in 'table_fp' that contains data category +# (4) map_fp Path to mapping file +# (5) factor Name of the column in 'map_fp' that contains sorting factor +# (6) output_fp Path to output file +################ +main() { + + local funcname + funcname='main' + + local table_fp + table_fp="${1}" + [[ ! -e "${table_fp}" ]] && err "in ${funcname}() cannot find ${table_fp}" + + local method + method="${2}" + + if [[ "${method}" == 'check_dataset' ]]; then + local category_column + category_column="${3}" + local map_fp + map_fp="${4}" + local sample_name + sample_name="${5}" + elif [[ "${method}" == 'table2R' ]]; then + local output_fp + output_fp="${3}" + elif [[ "${method}" == 'aggregate' ]]; then + local category_column + category_column="${3}" + local level + level="${4}" + local fun + fun="${5}" + local output_fp + output_fp="${6}" + else + err "in ${funcname}() wrong argument to method (2): ${method}" + fi + + # Modify table + if [[ "${method}" == 'check_dataset' ]]; then + verify_format "${table_fp}" "${category_column}" "${map_fp}" "${sample_name}" + elif [[ "${method}" == 'check_map' ]]; then + verify_map_format "${table_fp}" + elif [[ "${method}" == 'table2R' ]]; then + remove_comments "${table_fp}" "${output_fp}" # remove any comment lines at the begining of the table and the 1st element of the 1st line + elif [[ "${method}" == 'aggregate' ]]; then + aggregate_by_category_level "${table_fp}" "${category}" "${level}" "${fun}" "${output_fp}" + fi + + return 0 +} + +set -o nounset +set -o pipefail +#set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" +__base="$(basename ${__file})" +__lib="${__dir}"/../lib +source "${__lib}"/genocrunchlib.sh +__user=$(basename "${HOME}") +__workingdir="$(pwd)" +[[ ! -w "${__workingdir}" ]] && err "(line ${LINENO}) write permission denied on ${__workingdir}" +__roottmp=/tmp +if ([[ ! -d "${__roottmp}" ]] || [[ ! -w "${__roottmp}" ]]) ; then + __roottmp="${__dir}"/../tmp + [[ ! -d "${__roottmp}" ]] && { mkdir "${__roottmp}" ; [[ "$?" -ne 0 ]] && \ + err "(line ${LINENO}) cannot make ${__roottmp}" ; } +fi +tmpdir=$(mktemp -d "${__roottmp}"/"$(basename ${__file} .sh)"XXX) +trap 'rm -rf "${tmpdir}"' EXIT # makes sure tmpdir is deleted when exit +[[ ! -d "${tmpdir}" ]] && { mkdir "${tmpdir}" ; [[ "$?" -ne 0 ]] && \ +err "(line ${LINENO}) cannot make ${tmpdir}" ; } +[[ ! -w "${tmpdir}" ]] && err "(line ${LINENO}) write permission denied on ${tmpdir}" +chown "${__user}" "${tmpdir}" + +main "${@}" + +exit 0 diff --git a/lib/genocrunch_console/bin/validate_format.R b/lib/genocrunch_console/bin/validate_format.R new file mode 100755 index 0000000..4de13ab --- /dev/null +++ b/lib/genocrunch_console/bin/validate_format.R @@ -0,0 +1,70 @@ +#!/usr/bin/env Rscript +#validate_format.R +# Generic script to validate dataset format + +# Set environment +suppressMessages(library('optparse')) + +methods <- c('validate_dataset', 'validate_map') + +# Set options. Note: there is a small bug with '-g', do not use it for any option +option_list <- list(make_option(c('-t', '--table'), + type='character', + default=NULL, + help='Path to data table'), + make_option(c('-m', '--method'), + type='character', + default=NULL, + help=paste('Valid choices: ', paste(methods, collapse=', '), sep='')), + make_option('--map', + type='character', + default=NULL, + help='Path to mapping file'), + make_option('--category_column', + type='character', + default=NULL, + help='Name of the category column in dataset')) + +opt <- parse_args(OptionParser(option_list=option_list)) +msg <- NULL +if (opt$method == 'validate_dataset') { + table <- read.table(file=opt$table, sep='\t', header=1, row.names=1) + map <- read.table(file=opt$map, sep='\t', header=1) + + if (!is.null(opt$category_column) && opt$category_column != '') { + if (! opt$category_column %in% names(table)) { + msg <- append(msg, paste('"error":"', opt$category_column, ' not found in dataset headers"', sep='')) + } + } + + data.sample <- names(table[, names(table) != opt$category_column]) + intersect <- intersect(data.sample, map[, 1]) + if (length(intersect) < nrow(map)) { + not.in.data <- as.vector(map[, 1][! map[, 1] %in% intersect]) + msg <- append(msg, paste('"error":"The following samples were found in map but not in dataset: ', paste(not.in.data, collapse=' ,'), '. This can happen if the wrong samples names column was selected in map or if samples names in map do not match samples names in dataset(s)."', sep='')) + } + + if (length(data.sample) > nrow(map)) { + not.in.map <- as.vector(data.sample[! data.sample %in% intersect]) + msg <- append(msg, paste('"comment":"The following samples were found in dataset but not in map: ', paste(not.in.map, collapse=' ,'), '. These samples will not be included in the analysis. Please make sure that this is what you expect."', sep='')) + } + + n.missing <- length(table[table == '']) + if (n.missing) { + msg <- append(msg, '"warning":"Missing values were found (', n.missing, ' detected)."') + } + +} else if (opt$method == 'validate_map') { + map <- read.table(file=opt$map, sep='\t', header=1) + + if (length(unique(map[, 1])) != nrow(map)) { + msg <- append(msg, '"error":"Some sample names were found multiple times. Please fix the map."') + } +} + +if (is.null(msg)) { + msg <- append(msg, '"description":"No issue detected."') +} +cat(paste(msg, collapse=',')) + +rm(list=ls()) diff --git a/lib/genocrunch_console/etc/.DS_Store b/lib/genocrunch_console/etc/.DS_Store new file mode 100644 index 0000000..ffd6ed0 Binary files /dev/null and b/lib/genocrunch_console/etc/.DS_Store differ diff --git a/lib/genocrunch_console/etc/genocrunchlib.json b/lib/genocrunch_console/etc/genocrunchlib.json new file mode 100644 index 0000000..d1dc103 --- /dev/null +++ b/lib/genocrunch_console/etc/genocrunchlib.json @@ -0,0 +1,442 @@ +{ +"choices":{ + "statistics":[ + {"value":"anova", "label":"ANOVA"}, + {"value":"ttest", "label":"t-test"}, + {"value":"ttest_paired", "label":"Paired t-test"}, + {"value":"kruskal", "label":"Kruskal-Wallis rank sum test"}, + {"value":"friedman", "label":"Friedman test"}, + {"value":"wilcox", "label":"Wilcoxon rank sum test"}, + {"value":"wilcox_paired", "label":"Wilcoxon signed rank test"} + ], + "binning":[ + {"value":"sum", "label":"Sum values within categories"}, + {"value":"mean", "label":"Average values within categories"} + ], + "transformation":[ + {"value":"none", "label":"No transformation"}, + {"value":"log2cpm", "label":"log2 of count per million"}, + {"value":"log2", "label":"Log2"}, + {"value":"percent", "label":"Percent"} + ], + "diversity":[ + {"value":"richness", "label":"Richness", "pkg":""}, + {"value":"shannon", "label":"Shannon Diversity Index", "pkg":"vegan"}, + {"value":"simpson", "label":"Simpson Diversity Index", "pkg":"vegan"}, + {"value":"invsimpson", "label":"inverse Simpson Diversity Index", "pkg":"vegan"}, + {"value":"Gini", "label":"Gini coefficient", "pkg":"ineq"}, + {"value":"chao1", "label":"chao1 estimator", "pkg":"fossil"}, + {"value":"RS", "label":"Ricci-Schutz coefficient", "pkg":"ineq"}, + {"value":"Atkinson", "label":"Atkinson measure", "pkg":"ineq"}, + {"value":"Theil", "label":"Theil entropy measure", "pkg":"ineq"}, + {"value":"Kolm", "label":"Kolm measure", "pkg":"ineq"}, + {"value":"var", "label":"Coefficient of variation", "pkg":"ineq"} + ], + "correlation":[ + {"value":"spearman", "label":"Spearman Correlation Coefficient", "pkg":""}, + {"value":"pearson", "label":"Pearson Correlation Coefficient", "pkg":""} + ], + "distance":[ + {"value":"jaccard", "label":"Jaccard Index", "pkg":"vegan"}, + {"value":"bray", "label":"Bray-Curtis Distance", "pkg":"vegan"}, + {"value":"euclidean", "label":"Euclidean Distance", "pkg":"vegan"}, + {"value":"manhattan", "label":"Manhattan Distance", "pkg":"vegan"}, + {"value":"spearman", "label":"Spearman Correlation Coefficient", "pkg":""}, + {"value":"pearson", "label":"Pearson Correlation Coefficient", "pkg":""}, + {"value":"canberra", "label":"Canberra Distance", "pkg":"vegan"}, + {"value":"kulczynski", "label":"Kulczynski Similarity Measure", "pkg":"vegan"}, + {"value":"gower", "label":"Gower Similarity Coefficient", "pkg":"vegan"}, + {"value":"morisita", "label":"Morisita Overlap Index", "pkg":"vegan"}, + {"value":"horn", "label":"Horn-Morisita Index", "pkg":"vegan"}, + {"value":"mountford", "label":"Mountford Index", "pkg":"vegan"}, + {"value":"raup", "label":"Raup-Crick dissimilarity", "pkg":"vegan"}, + {"value":"binomial", "label":"Binomial distance", "pkg":"vegan"}, + {"value":"chao", "label":"Chao Index", "pkg":"vegan"} + ], + "clustering":[ + {"value":"none", + "label":"", + "pkg":""}, + {"value":"pam", + "label":"k-medoids", + "pkg":"fpc"}, + {"value":"pam-bray", + "label":"k-medoids on Bray-Curtis dissimilarity", + "pkg":"fpc"}, + {"value":"pam-jaccard", + "label":"k-medoids on Jaccard distance", + "pkg":"fpc"}, + {"value":"kmeans", + "label":"k-means", + "pkg":"fpc"} + ], + "graph_clustering":[ + {"value":"none", "label":"", "pkg":""}, + {"value":"walktrap", "label":"Short random walks algorithm", "pkg":"igraph"}, + {"value":"fastgreedy", "label":"Greedy optimization of modularity algorithm", "pkg":"igraph"}, + {"value":"louvain", "label":"Multi-level optimization of modularity algorithm", "pkg":"igraph"}, + {"value":"labelpropagation", "label":"Label propagation method of Raghavan et al.", "pkg":"igraph"} + ] +}, +"fields":{ + "Inputs":[ + {"id":"name", + "scope":"form_only", + "help":"This will be the name of the new job. Choose something relevent and avoid special characters. Ex: Exp01_Seq01.", + "type":"text", + "optional":false, + "placeholder":"New job name" + }, + {"id":"description", + "scope":"form_only", + "help":"This will be the description of the new job. Give enough details for your analysis to be understood by someone else (if you want to share).", + "type":"textarea", + "optional":true, + "placeholder":"Some details" + }, + {"id":"primary_dataset", + "help":"Data file. Format must be tab-delimited text with one column per sample and one row per OTU/gene. Last column must be 'taxonomy' and contain ';'-delimited categories. No row or column names duplicate are allowed. First row must include samples names. Comments can be included at the beginning of the file and start with '#'. [File format]", + "type":"file", + "optional":false + }, + {"id":"category_column", + "help":"This must be the name of the column in your input file that contains the category (i.e. taxonomy, pathways, etc.).", + "type":"select", + "optional":false, + "depends_on":"primary_dataset" + }, + {"id":"map", + "help":"Mapping file. It describes the experimental design. Columns represent factors (ex: treatment,sex,etc.) and rows indicates samples. Format must be tab-delimited text. First row must contain column names. At least one column must contain the sample names,matching the first row of data file (Input). [File format]", + "type":"file", + "optional":false + }, + {"id":"secondary_dataset", + "help":"Optional metadata file. It can include related data from a different type of analysis that was performed on the same samples. Format must be the same as data file (Input) but without any category column and without any comment. Column names must match column names in data file (Input) in spelling and order. [File format]", + "type":"file" + } +], +"Pre-processing":[ + {"id":"abundance_threshold", + "help":"Abundance threshold for filtering.", + "type":"threshold", + "default_type":"percent", + "default_integer":"10", + "default_percent":"0.03", + "placeholder_integer":">0", + "placeholder_percent":"0-100" + }, + {"id":"abundance_threshold_type", + "type":"threshold_type", + "default":"percent", + "hidden_field":1 + }, + {"id":"presence_threshold", + "help":"Presence threshold for filtering.", + "type":"threshold", + "default_type":"integer", + "default_integer":"2", + "default_percent":"10", + "placeholder_integer":">0", + "placeholder_percent":"0-100" + }, + {"id":"presence_threshold_type", + "type":"threshold_type", + "default":"int", + "hidden_field":1 + }, + {"id":"bin_levels", + "label":"Category binning levels", + "help":"The level(s) at which the categories will be binned.", + "type":"select", + "multiple":true + }, + {"id":"bin_fun", + "label":"Category binning function", + "help":"Function to apply for category binning.", + "type":"select", + "values":"binning", + "default":"sum" + } +], +"Transformations":[ + {"id":"prim_rarefaction", + "label":"Rarefaction (Primary dataset)", + "help":"Sub-sampling (random drawings without replacement).", + "type":"check_box", + "db_field":"prim_rarefaction", + "trigger":"drop_down", + "default":false + }, + {"id":"prim_sampling_depth", + "label":"Sampling depth", + "help":"Number of elements (counts) to draw in each sample. Must be a positive integer. The value cannot exceed the minimum count per sample in the data set (it will be corrected automatically if needed).", + "belongs_to":"prim_rarefaction", + "type":"integer", + "default":"500000", + "placeholder":">0" + }, + {"id":"prim_nsampling", + "label":"N samplings", + "help":"Number of repeated rarefactions to perform (the result will be an average of these rarefactions).", + "belongs_to":"prim_rarefaction", + "type":"integer", + "default":"1", + "placeholder":">0" + }, + {"id":"prim_trans_method", + "label":"Transformation (Primary dataset)", + "help":"Choose a data transformation method.", + "type":"select", + "values":"transformation", + "default":"none" + }, + {"id":"prim_batch_effect_suppression", + "label":"Batch effect suppression (Primary dataset)", + "help":"Batch effect(s) can be suppressed to facilitate statistical tests. Example: In an experiment where 20 samples from experimental group A and 20 samples from experimental group B where distributed into two batches for processing,the batch-effect can be supressed. Make sure it is consitent with Model,in the Experimental design section.", + "type":"select", + "multiple":true + }, + {"id":"sec_rarefaction", + "label":"Rarefaction (Secondary dataset)", + "help":"Sub-sampling (random drawings without replacement).", + "type":"check_box", + "db_field":"sec_rarefaction", + "trigger":"drop_down", + "default":false + }, + {"id":"sec_sampling_depth", + "label":"Sampling depth", + "help":"Number of elements (counts) to draw in each sample. Must be a positive integer. The value cannot exceed the minimum count per sample in the data set (it will be corrected automatically if needed).", + "belongs_to":"sec_rarefaction", + "type":"integer", + "default":"500000", + "placeholder":">0" + }, + {"id":"sec_nsampling", + "label":"N samplings", + "help":"Number of repeated rarefactions to perform (the result will be an average of these rarefactions).", + "belongs_to":"sec_rarefaction", + "type":"integer", + "default":"1", + "placeholder":">0" + }, + {"id":"sec_trans_method", + "label":"Transformation (Secondary dataset)", + "help":"Choose a data transformation method.", + "type":"select", + "values":"transformation", + "default":"none" + }, + {"id":"sec_batch_effect_suppression", + "label":"Batch effect suppression (Secondary dataset)", + "help":"Batch effect(s) can be suppressed to facilitate statistical tests. Example: In an experiment where 20 samples from experimental group A and 20 samples from experimental group B where distributed into two batches for processing,the batch-effect can be supressed. Make sure it is consitent with Model,in the Experimental design section.", + "type":"select", + "multiple":true + } +], +"Analysis":[ + {"id":"model_type", + "label":"Experimental design", + "help":"Choose a basic or advanced model.", + "type":"model_type", + "default":"basic" + }, + {"id":"basic_model", + "label":"Model", + "help":"This will define the statistical model to use for the analysis. An Anova will be used to compare groups.", + "type":"select", + "belongs_to":"model_type_basic" + }, + {"id":"advanced_stats", + "label":"Statistics", + "help":"Define which statistics will be used to compare groups. You need to make sure that the syntax of the Model formula fits the statistics requirements.", + "type":"select", + "belongs_to":"model_type_advanced", + "values":"statistics", + "default":"anova" + }, + {"id":"advanced_model", + "label":"Model", + "help":"This will define the statistical model to use for the analysis. It should be a formula with terms refering to columns in the mapping file. Simple example: In an experiment where column A defines the subjects genders and column B defines subjects diets,a model testing both the effect of gender and diet and their interactions could be writen A*B. Example with nested variables: In an experiment where column A defines individuals and column B defines replicated sampling within each individuals,a model testing the effect of B could be writen A/B.", + "type":"text", + "placeholder":"ex:ExperimentalGroup*Gender", + "belongs_to":"model_type_advanced" + }, + {"id":"proportions", + "scope":"form_only", + "label":"Proportions", + "help":"Display proportions in a stacked bar-chart.", + "type":"check_box", + "db_field":"analysis", + "default":true + }, + {"id":"diversity", + "scope":"form_only", + "help":"This will assess the diversity within each sample. It will generate rarefaction curves for each selected diversity metric that will allow you to compare diversity between samples and between the groups defined in your model.", + "type":"check_box", + "color":"tomato", + "db_field":"analysis", + "trigger":"drop_down", + "default":false + }, + {"id":"diversity_metric", + "label":"Metric", + "belongs_to":"diversity", + "help":"Choose metric(s).", + "type":"select", + "multiple":true, + "values":"diversity", + "default":"richness" + }, + {"id":"compare_diversity", + "label":"Compare groups", + "belongs_to":"diversity", + "help":"Compare experimental groups using model and statistical test?", + "type":"bool", + "default":false + }, + {"id":"adonis", + "scope":"form_only", + "label":"perMANOVA", + "help":"This will perform a Permutational Multivariate Analysis of Variance using a distance matrix (Adonis method). It will show how your model explains differences based on a similarity matrix that is computed using the chosen similarity metric.", + "type":"check_box", + "db_field":"analysis", + "trigger":"drop_down", + "default":false + }, + {"id":"adonis_distfun", + "label":"Distance metric", + "belongs_to":"adonis", + "help":"Distance metric.", + "type":"select", + "multiple":true, + "values":"distance", + "default":"jaccard" + }, + {"id":"adonis_model", + "label":"Model", + "belongs_to":"adonis", + "help":"Model for Adonis.", + "type":"text", + "placeholder":"ex:ExperimentalGroup*Gender" + }, + {"id":"adonis_strata", + "label":"Strata", + "belongs_to":"adonis", + "help":"Adonis strata parameter.", + "type":"text", + "placeholder":"ex:Subject" + }, + {"id":"pca", + "scope":"form_only", + "label":"PCA", + "help":"Principal component analysis (PCA).", + "type":"check_box", + "db_field":"analysis", + "default":false + }, + {"id":"pcoa", + "scope":"form_only", + "label":"PCoA", + "help":"Principal coordinate analysis (PCoA).", + "type":"check_box", + "db_field":"analysis", + "trigger":"drop_down", + "default":false + }, + {"id":"pcoa_distfun", + "label":"Distance metric", + "belongs_to":"pcoa", + "help":"Distance metric.", + "type":"select", + "multiple":true, + "values":"distance", + "default":"jaccard" + }, + {"id":"heatmap", + "scope":"form_only", + "label":"Heatmap", + "help":"Abundance heatmap.", + "type":"check_box", + "db_field":"analysis", + "default":false + }, + {"id":"change", + "scope":"form_only", + "label":"Changes", + "help":"Differential analysis including fold-change and visualization with MA/volcano plots.", + "type":"check_box", + "db_field":"analysis", + "default":false + }, + {"id":"correlation_network", + "scope":"form_only", + "label":"Correlation network", + "help":"Correlation network.", + "type":"check_box", + "db_field":"analysis", + "trigger":"drop_down", + "default":false + }, + {"id":"correlation_network_fun", + "label":"Correlation method", + "belongs_to":"correlation_network", + "help":"Correlation method.", + "type":"select", + "values":"correlation", + "default":"spearman" + }, + {"id":"clustering", + "scope":"form_only", + "label":"Clustering", + "help":"Perform categorical clustering on the primary dataset.", + "type":"check_box", + "db_field":"analysis", + "trigger":"drop_down", + "default":false + }, + {"id":"clustering_fun", + "label":"Algorithm", + "belongs_to":"clustering", + "help":"Choose clustering algorithm(s).", + "type":"select", + "multiple":true, + "values":"clustering", + "default":"pam" + }, + {"id":"similarity_network", + "scope":"form_only", + "label":"Similarity network", + "help":"Similarity network, graph-based clustering and similarity network fusion (SNF).", + "type":"check_box", + "db_field":"analysis", + "trigger":"drop_down", + "default":false + }, + {"id":"similarity_network_fun1", + "label":"Metric for primary dataset", + "belongs_to":"similarity_network", + "help":"Similarity metric for primary dataset.", + "type":"select", + "values":"distance", + "default":"jaccard" + }, + {"id":"similarity_network_fun2", + "label":"Metric for secondary dataset", + "belongs_to":"similarity_network", + "help":"Similarity metric for secondary dataset.", + "type":"select", + "values":"distance", + "default":"manhattan" + }, + {"id":"similarity_network_clust", + "label":"Clustering algorithm", + "belongs_to":"similarity_network", + "help":"Graph-based clustering algorithm.", + "type":"select", + "values":"graph_clustering", + "default":"walktrap" + } +] +} +} diff --git a/lib/genocrunch_console/etc/packages_R.json b/lib/genocrunch_console/etc/packages_R.json new file mode 100644 index 0000000..c11b673 --- /dev/null +++ b/lib/genocrunch_console/etc/packages_R.json @@ -0,0 +1,58 @@ +[ +{"name":"ineq", + "description":"Provides metrics for inequality, concentration and poverty", + "man":"https://cran.r-project.org/web/packages/ineq/ineq.pdf", + "categ":"analysis"}, +{"name":"rjson", + "description":"Handles JSON objects", + "man":"https://cran.r-project.org/web/packages/rjson/rjson.pdf", + "categ":"miscellaneous"}, +{"name":"fpc", + "description":"Provides clustering and cluster validation methods", + "man":"https://cran.r-project.org/web/packages/fpc/fpc.pdf", + "categ":"analysis"}, +{"name":"multcomp", + "description":"Provides simultaneous tests and confidence intervals for general linear hypotheses in parametric models, including linear, generalized linear, linear mixed effects, and survival models.", + "man":"https://cran.r-project.org/web/packages/multcomp/multcomp.pdf", + "categ":"analysis"}, +{"name":"FactoMineR", + "description":"Provides dimmentionality reduction methods including pincipal component analysis (PCA)", + "man":"https://cran.r-project.org/web/packages/FactoMineR/FactoMineR.pdf", + "categ":"analysis"}, +{"name":"colorspace", + "description":"Handles colors", + "man":"https://cran.r-project.org/web/packages/colorspace/colorspace.pdf", + "categ":"miscellaneous"}, +{"name":"vegan", + "description":"Provides ordination methods, diversity analysis and other tools for microbial ecology", + "man":"https://cran.r-project.org/web/packages/vegan/vegan.pdf", + "categ":"analysis"}, +{"name":"sva", + "description":"Provides functions for removing batch effects and other unwanted variation in high-throughput experiment", + "man":"http://bioconductor.org/packages/release/bioc/manuals/sva/man/sva.pdf", + "categ":"pre-processing"}, +{"name":"optparse", + "description":"Provides a command line option parser", + "man":"https://cran.r-project.org/web/packages/optparse/optparse.pdf", + "categ":"miscellaneous"}, +{"name":"gplots", + "description":"Provides various data plotting tools", + "man":"https://cran.r-project.org/web/packages/gplots/gplots.pdf", + "categ":"graphics"}, +{"name":"igraph", + "description":"Provides tools for graphs and network analysis", + "man":"https://cran.r-project.org/web/packages/igraph/igraph.pdf", + "categ":"analysis"}, +{"name":"fossil", + "description":"Provides functions for estimating species richness in microbial ecology", + "man":"https://cran.r-project.org/web/packages/fossil/fossil.pdf", + "categ":"analysis"}, +{"name":"coin", + "description":"Provides conditional inference procedures for the general independence problem including two-sample, K-sample (non-parametric ANOVA), correlation, censored, ordered and multivariate problems", + "man":"https://cran.r-project.org/web/packages/coin/coin.pdf", + "categ":"analysis"}, +{"name":"SNFtool", + "description":"Provides tolls for similarity network fusion (SNF)", + "man":"https://cran.r-project.org/web/packages/SNFtool/SNFtool.pdf", + "categ":"analysis"} +] diff --git a/lib/genocrunch_console/lib/.DS_Store b/lib/genocrunch_console/lib/.DS_Store new file mode 100644 index 0000000..50610c0 Binary files /dev/null and b/lib/genocrunch_console/lib/.DS_Store differ diff --git a/lib/genocrunch_console/lib/__init__.py b/lib/genocrunch_console/lib/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/lib/genocrunch_console/lib/friedman_test_with_post_hoc.R b/lib/genocrunch_console/lib/friedman_test_with_post_hoc.R new file mode 100755 index 0000000..4119903 --- /dev/null +++ b/lib/genocrunch_console/lib/friedman_test_with_post_hoc.R @@ -0,0 +1,156 @@ +# Originally published on: +# http://www.r-statistics.com/2010/02/post-hoc-analysis-for-friedmans-test-r-code + +# good read: http://en.wikipedia.org/wiki/Friedman_test +# http://finzi.psych.upenn.edu/R/library/coin/html/SymmetryTests.html +# could have used: block = with(data, eval(parse(text = all.vars(formu)[3]))) + +friedman.test.with.post.hoc <- function(formula, data, to.print.friedman = T, to.post.hoc.if.signif = T, to.plot.parallel = F, to.plot.boxplot = F, signif.P = .05, color.blocks.in.cor.plot = F, jitter.Y.in.cor.plot =F) +{ + # formu is a formula of the shape: Y ~ X | block + # data is a long data.frame with three columns: [[ Y (numeric), X (factor), block (factor) ]] + + # Note: This function doesn't handle NA's! In case of NA in Y in one of the blocks, then that entire block should be removed. + + suppressMessages(library('coin')) + # Loading needed packages + #if(!require(coin)) + #{ + # print("You are missing the package 'coin', we will now try to install it...") + # install.packages("coin") + # library(coin) + #} + + suppressMessages(library('multcomp')) + #if(!require(multcomp)) + #{ + # print("You are missing the package 'multcomp', we will now try to install it...") + # install.packages("multcomp") + # library(multcomp) + #} + + #if(!require(colorspace)) + #{ + # print("You are missing the package 'colorspace', we will now try to install it...") + # install.packages("colorspace") + # library(colorspace) + #} + + + # get the names out of the formula + formu.names <- all.vars(formula) + Y.name <- formu.names[1] + X.name <- formu.names[2] + block.name <- formu.names[3] + + if(dim(data)[2] >3) data <- data[,c(Y.name,X.name,block.name)] # In case we have a "data" data frame with more then the three columns we need. This code will clean it from them... + + # Note: the function doesn't handle NA's. In case of NA in one of the block T outcomes, that entire block should be removed. + + # stopping in case there is NA in the Y vector + if(sum(is.na(data[,Y.name])) > 0) stop("Function stopped: This function doesn't handle NA's. In case of NA in Y in one of the blocks, then that entire block should be removed.") + + # make sure that the number of factors goes with the actual values present in the data: + data[,X.name ] <- factor(data[,X.name ]) + data[,block.name ] <- factor(data[,block.name ]) + number.of.X.levels <- length(levels(data[,X.name ])) + if(number.of.X.levels == 2) { warning(paste("'",X.name,"'", "has only two levels. Consider using paired wilcox.test instead of friedman test"))} + + # making the object that will hold the friedman test and the other. + the.sym.test <- symmetry_test(formula, data = data, ### all pairwise comparisons + teststat = "max", + xtrafo = function(Y.data) { trafo( Y.data, factor_trafo = function(x) { model.matrix(~ x - 1) %*% t(contrMat(table(x), "Tukey")) } ) }, + ytrafo = function(Y.data){ trafo(Y.data, numeric_trafo = rank, block = data[,block.name] ) } + ) + # if(to.print.friedman) { print(the.sym.test) } + + + if(to.post.hoc.if.signif) + { + #if(pvalue(the.sym.test) < signif.P) + #{ + # the post hoc test + The.post.hoc.P.values <- pvalue(the.sym.test, method = "single-step") # this is the post hoc of the friedman test + + + # plotting + if(to.plot.parallel & to.plot.boxplot) par(mfrow = c(1,2)) # if we are plotting two plots, let's make sure we'll be able to see both + if(to.plot.parallel | to.plot.boxplot) library(colorspace) + if(to.plot.parallel) + { + X.names <- levels(data[, X.name]) + X.for.plot <- seq_along(X.names) + plot.xlim <- c(.7 , length(X.for.plot)+.3) # adding some spacing from both sides of the plot + + if(color.blocks.in.cor.plot) + { + blocks.col <- rainbow_hcl(length(levels(data[,block.name]))) + } else { + blocks.col <- 1 # black + } + + data2 <- data + if(jitter.Y.in.cor.plot) { + data2[,Y.name] <- jitter(data2[,Y.name]) + par.cor.plot.text <- "Parallel coordinates plot (with Jitter)" + } else { + par.cor.plot.text <- "Parallel coordinates plot" + } + + # adding a Parallel coordinates plot + matplot(as.matrix(reshape(data2, idvar=X.name, timevar=block.name, + direction="wide")[,-1]) , + type = "l", lty = 1, axes = FALSE, ylab = Y.name, + xlim = plot.xlim, + col = blocks.col, + main = par.cor.plot.text) + axis(1, at = X.for.plot , labels = X.names) # plot X axis + axis(2) # plot Y axis + points(tapply(data[,Y.name], data[,X.name], median) ~ X.for.plot, col = "red",pch = 4, cex = 2, lwd = 5) + } + + if(to.plot.boxplot) + { + # first we create a function to create a new Y, by substracting different combinations of X levels from each other. + subtract.a.from.b <- function(a.b , the.data) + { + the.data[,a.b[2]] - the.data[,a.b[1]] + } + + temp.wide <- reshape(data, idvar=X.name, timevar=block.name, + direction="wide") #[,-1] + wide.data <- as.matrix(t(temp.wide[,-1])) + colnames(wide.data) <- temp.wide[,1] + + Y.b.minus.a.combos <- apply(with(data,combn(levels(data[,X.name]), 2)), 2, subtract.a.from.b, the.data =wide.data) + names.b.minus.a.combos <- apply(with(data,combn(levels(data[,X.name]), 2)), 2, function(a.b) {paste(a.b[2],a.b[1],sep=" - ")}) + + the.ylim <- range(Y.b.minus.a.combos) + the.ylim[2] <- the.ylim[2] + max(sd(Y.b.minus.a.combos)) # adding some space for the labels + is.signif.color <- ifelse(The.post.hoc.P.values < .05 , "green", "grey") + + boxplot(Y.b.minus.a.combos, + names = names.b.minus.a.combos , + col = is.signif.color, + main = "Boxplots (of the differences)", + ylim = the.ylim + ) + legend("topright", legend = paste(names.b.minus.a.combos, rep(" ; PostHoc P.value:", number.of.X.levels),round(The.post.hoc.P.values,5)) , fill = is.signif.color ) + abline(h = 0, col = "blue") + + } + + list.to.return <- list(Friedman.Test = pvalue(the.sym.test), PostHoc.Test = The.post.hoc.P.values) + if(to.print.friedman) {print(list.to.return)} + return(list.to.return) + + #} else { + # print("The results where not significant, There is no need for a post hoc test") + # return(the.sym.test) + # } + } + +# Original credit (for linking online, to the package that performs the post hoc test) goes to "David Winsemius", see: +# http://tolstoy.newcastle.edu.au/R/e8/help/09/10/1416.html +} + diff --git a/lib/genocrunch_console/lib/genocrunchlib.R b/lib/genocrunch_console/lib/genocrunchlib.R new file mode 100755 index 0000000..ac6d21d --- /dev/null +++ b/lib/genocrunch_console/lib/genocrunchlib.R @@ -0,0 +1,2864 @@ +#genocrunchlib.R + +################ +# Print message in a formatted way so it can be decomposed into categories of +# logs. Can also be used to issue errors +# Args: +# (1) label (character) A label for the message (ex: title, log, warning...) +# (2) text (character) Message to print +# (3) verbose (character) A statement: 'TRUE' or same value as 'label':print +# to stdout; 'FALSE':don't print; +# Prints to stdout: +# Tab-separated: Path to executed script, date and time, 'label' and 'text' +################ +log_fp <- NULL +PrintMsg <- function(text='default message', verbose=TRUE, stop=FALSE, log=log_fp) { + + if (!is.null(log)) { + f <- file(log, open="at") + sink(file=f, append=TRUE) + sink(type="message") + } + if (as.logical(verbose) == TRUE) { + cat(text) + } + if (!is.null(log)) { + sink() + sink(type="message") + close(f) + } + if (as.logical(stop) == TRUE) { + stop(text) + } +} + +################ +# Write a table to an output location, with the option of having indented +# columns name if 'name'=''. +# Args: +# (1) table Table object +# (2) output.fp Path to the output file +# (3) name Name of first column +# (4) sep Separator character +# (5) na na +# (6) dec dec +# (7) quote quote +# Outputs: +# Writes 'table' to 'output.fp', starting first line with an indentation ('\t') +################ +WriteTable <- function(table=NULL, output.fp='', name='name', sep='\t', na='', + dec='.', quote=FALSE) { + write.table(t(c(name, names(table))), file=output.fp, append=FALSE, + row.names=FALSE, col.names=FALSE, sep=sep, na=na, dec=dec, + quote=quote) + write.table(table, file=output.fp, append=TRUE, row.names=TRUE, + col.names=FALSE, sep=sep, na=na, dec=dec, quote=quote) +} + +################ +# Reads the "Rfunctions" section of a json file as a json string +# Args: +# (1) json Path to the json file +# Outputs: +# A json string as found in the "Rfunctions" section of the json file +################ +ReadJson <- function(file='/../etc/genocrunchlib.json', rel=TRUE) { + suppressMessages(library('rjson')) + + if (rel) { + # Consider file as a relative path with respect to this library + args <- commandArgs(trailingOnly=FALSE) + dir <- dirname(sub('--file=', '', args[grep('--file=', args)])) + + file <- normalizePath(paste(dir, + file, + sep=''), + mustWork=TRUE) + } + + if (!file.exists(file)) { + PrintMsg(paste('"error":"File not found: ', + file, + '."', + sep=''), + verbose=TRUE, stop=TRUE) + } + + json_str <- fromJSON(file=file) + + return (toJSON(json_str$choices)) +} + +################################################################################ +################################################################################ +# MANIPULATION ################################################################# +# MANIPULATION ################################################################# +# MANIPULATION ################################################################# +# MANIPULATION ################################################################# +################################################################################ +################################################################################ + +################ +# Convert a data frame of factors to a numeric data frame. Unlike data.matrix, +# it will force characters to numeric. +# Args: +# (1) x A data frame +# Returns: +# A numeric version of 'x' +################ +ForceNumericDataFrame <- function(x) { + x <- as.matrix(x) + for (i in 1:ncol(x)) { + x[, i] <- as.numeric(as.vector(unlist(x[, i]))) + } + return (as.data.frame(x)) +} + +################ +# Convert values of a numeric vector to percents of its sum +# Args: +# (1) x A numeric vector +# Returns: +# A percent version of 'x' +################ +Numeric2Percent <- function(x){ + return (100*x/sum(x)) +} + +################ +# Scale a numeric vector on a range of values +# Args: +# (1) x A numeric vector +# (2) range A numeric vector containing 2 elements +# (3) force A logical indicating wheter infinite values should be forced into +# 'range' +# Returns: +# A scaled version of 'x', within 'range' +################ +RangeScale <- function(x=NULL, range=c(0, 1), force=TRUE) { + scaled <- range[1]*(1-((x-min(x, na.rm=TRUE))/(max(x, na.rm=TRUE)-min(x, + na.rm=TRUE))))+range[2]*((x-min(x, na.rm=TRUE))/(max(x, + na.rm=TRUE)-min(x, na.rm=TRUE))) + if (force == TRUE) { + scaled[scaled == -Inf] <- min(range) + scaled[scaled == Inf] <- max(range) + } + return (scaled) +} + +################ +# Scale by standard deviation and center on mean +# Args: +# (1) x A numeric vector +# Returns: +# A scaled version of 'x' +################ +cScale <- function(x=NULL) { + sd <- sd(x, na.rm=TRUE) + mean <- mean(x, na.rm = TRUE) + if (is.na(sd)) { + sd <- 1 + } else if (sd == 0) { + sd <- 1 + } + if (is.na(mean)) { + mean <- 0 + } + return (x/sd-mean) +} + +################################################################################ +################################################################################ +# JSON HANDELING ############################################################### +# JSON HANDELING ############################################################### +# JSON HANDELING ############################################################### +# JSON HANDELING ############################################################### +################################################################################ +################################################################################ + +################ +# Build a json string to store figure data +# Args: +# (1) data (data-frame) +# (2) xmetadata (data-frame) With nrow=ncol(data) +# Returns: +# A json string +################ +Dataframe2DataJson <- function(data=NULL, xmetadata=NULL){ + + # Rules: + # colname(data) -> [{'name'}] + # rowname(data) and data -> [{'data':['rowname(data)':'data']}] + # xmetadata -> [{'rowname(xmetadata)'}] + + json <- c(1:ncol(data)) + for (i in 1:ncol(data)) { + xmetadata.json <- NULL + if (!is.null(xmetadata)) { + xmetadata.json <- c(1:ncol(xmetadata)) + for (j in 1:ncol(xmetadata)) { + xmetadata.json[j] <- paste('"', + names(xmetadata)[j], + '":"', + xmetadata[i, j], + '"', + sep='') + } + xmetadata.json <- paste(xmetadata.json, collapse=',') + } + + data.json.el <- c(1:nrow(data)) + for (j in 1:nrow(data)) { + data.json.el[j] <- paste('"', + row.names(data)[j], + '":"', + data[j, i], + '"', + sep='') + } + data.json <- paste('{', paste(data.json.el, collapse=','), '}',sep='') + + json[i] <- paste('{', + paste(c(paste('"name":"', + names(data)[i], + '"', + sep=''), + xmetadata.json, + paste('"data":', + data.json, + sep='')), + collapse=','), + '}', + sep='') + } + + return (paste('[', paste(json, collapse=','), ']', sep='')) +} + + +################ +# Build a json string to store diversity values +# Args: +# (1) diversity (list) +# (2) p.value (list) +# (3) map (matrix) +# Returns: +# A json string +################ +diversity2json <- function(diversity=NULL, p.value=NULL, map=NULL){ + rar <- row.names(diversity) + nsample <- ncol(diversity) + sample <- c(1:nsample) + for (j in 1:nsample) { + ndata <- nrow(diversity) + data <- c(1:ndata) + for (k in 1:ndata) { + data[k] <- paste('"', + rar[k], + '":', + diversity[k, j], + sep='') + } + metadata <- c(1:ncol(map)) + for (k in 1:ncol(map)) { + metadata[k] <- paste('"', + names(map[k]), + '":"', + map[j, k], + '"', + sep='') + } + sample[j] <- paste('{"name":"', + row.names(map)[j], + '",', + paste(metadata, collapse=','), + ',"data":{', + paste(data, collapse=','), + '}}', + sep='') + } + if (!is.null(p.value)) { + stat <- c(1:length(p.value)) + for (j in 1:length(p.value)) { + s <- c(1:length(p.value[[j]])) + for (k in 1:length(p.value[[j]])) { + s[k] <- paste('{"name":"', + names(p.value[[j]])[k], + '", "p-value":"', + p.value[[j]][1, k], + '"}', + sep='') + } + stat[j] <- paste(s, collapse=',') + } + json <- paste('{"data":[', + paste(sample, collapse=','), + '],"stat":[', + paste(stat, collapse=','), + ']}', + sep='') + } else { + json <- paste('{"data":[', + paste(sample, collapse=','), + ']}', + sep='') + } + return (json) +} + + +################ +# Build a json tree from dendrogram +# Args: +# (1) dendrogram (list) A dendrogram (nested lists) +# Returns: +# A json string +################ +dendrogram2json <- function(dendrogram=NULL, json=NULL, it=0) { + start <- FALSE + if (is.null(json)) { + start <- TRUE + json <- paste('{"name":"node', + it, + '", "children":[', + sep='') + } + + json.vect <- c(1:length(dendrogram)) + for (i in 1:length(dendrogram)) { + if (length(dendrogram[[i]]) > 1) { + it <- it+1 + json.vect[i] <- paste('{"name":"node', + it, + '", "children":[', + sep='') + dendrogram2json.out <- dendrogram2json(dendrogram[[i]], json.vect[i], it) + + json.vect[i] <- dendrogram2json.out[[1]] + it <- dendrogram2json.out[[2]] + json.vect[i] <- paste(json.vect[i], ']}', sep='') + } else { + json.vect[i] <- paste('{"name":"', + dendrogram[[i]], + '"}', + sep='') + + } + } + + if (start) { + json <- paste(json, paste(json.vect, collapse=','), ']}', sep='') + } else { + json <- paste(json, paste(json.vect, collapse=','), sep='') + } + + out <- list() + out[1] <- json + out[2] <- it + return (out) +} + +################ +# Build a json network from similarity matrix +# Args: +# (1) mat list of numeric matrix +# Returns: +# A json string +################ +buildFusionNetworkJson <- function(mat=NULL, map=NULL){ + names <- colnames(mat[[1]]) + ncol <- ncol(mat[[1]]) + if (!is.null(map)) { + ncol.map <- ncol(map) + } + nodes <- c(1:ncol) + for (i in 1:ncol){ + if (!is.null(map)) { + map.row <- c(1:ncol.map) + for (j in 1:ncol.map) { + map.row[j] <- paste('"', + names(map)[j], + '":"', + map[i, j], + '"', + sep='') + } + } else { + map.row <- '"none":"none"' + } + nodes[i] <- paste('{"id":', + i-1, + ',"name":"', + names[i], + '",', + paste(map.row, collapse=','), + '}', + sep='') + } + links <- c(1:(ncol*(ncol/2-1))) + k <- 1 + for (i in 1:(ncol-1)) { + for (j in (i+1):ncol) { + weights <- c(1:length(mat)) + for (l in 1:length(mat)) { + weights[l] <- mat[[l]][i,j] + } + links[k] <- paste('{"source":', + i-1, + ',"target":', + j-1, + ',"weight":[', + paste(weights,collapse = ","), + ']}', + sep='') + k <- k+1 + } + } + return(paste('{"nodes":[', paste(nodes, collapse = ","),'],"links":[', paste(links, collapse = ","), ']}', sep='')) +} + +################################################################################ +################################################################################ +# MODIFICATION ################################################################# +# MODIFICATION ################################################################# +# MODIFICATION ################################################################# +# MODIFICATION ################################################################# +################################################################################ +################################################################################ + +################ +# Sort a data-frame columns according to a column of another +# Args: +# (1) table (matrix-like object) Table to be sorted +# (2) map (matrix-like object) Map to extract the sorting column from +# (3) column (numeric) Index of the column in map to use for sorting +# (4) verbose (bool) Print messages? see PrintMsg() options +# Returns: +# A sorted version of table +# Print to stdout: +# A description +################ +SortTable <- function(table=NULL, map=NULL, verbose=TRUE) { +# print(row.names(map)) +# print(map) +# print(map[, column]) + PrintMsg('"description":"Sort table columns according to map"', verbose) + return(as.data.frame(table[, as.vector(row.names(map))])) +} + +################ +# Transpose a data frame (or a matrix-like object) +# Args: +# (1) x (matrix-like object) +# Returns: +# A transposed version of 'x' (numeric dataframe) +################ +Transpose <- function(x=NULL) { + if (is.data.frame(x)) { + names <- names(x) + row.names <- row.names(x) + x <- as.data.frame(t(x)) + names(x) <- row.names + row.names(x) <- names + } else { + x <- t(x) + } + return (x) +} + +################ +# Filter numeric table (with category-specific options for OTU-type tables) +# Args: +# (1) table (numeric dataframe) Count table +# (2) category (character) Name of the (optional) category column +# (3) abundance_threshold (numeric) Min % abundance per column [0,100] +# (4) presence_threshold (numeric) Min % presence per row [0,100] +# (5) rm_unassigned (logical) Remove unassigned category ? +# (6) verbose (character) Print messages? see PrintMsg() options +# Returns: +# An filtered version of 'table' (numeric dataframe) +# Prints to stdout: +# A description of the function +################ +FilterTable <- function(table=NULL, category='taxonomy', + abundance_threshold=0.03, + abundance_threshold_type='percent', + presence_threshold=1, + presence_threshold_type='int', + verbose=TRUE) { + + nrow.init <- nrow(table) + + # First filter based on minimal abundance + msg <- paste('"description":"Removed rows not exceeding ', abundance_threshold, sep='') + filter <- table[, names(table) != category] + minmax.a <- paste(min(filter), max(filter), sep='-') + if (abundance_threshold_type == 'percent') { + filter <- as.data.frame(apply(filter, 2, Numeric2Percent)) + msg <- paste(msg, '% ', sep='') + minmax.a <- paste(min(filter), '-', max(filter), '%', sep='') + } + max.abundance <- apply(filter, 1, max) + table.filtered <- table[max.abundance >= abundance_threshold, ] + nrow.filtered.a <- nrow(table.filtered) + msg <- paste(msg, + 'in at least one column: ', + nrow.init-nrow.filtered.a, + ' rows were removed out of ', + nrow.init, + '. Then, removed rows with presence (number of values not equal to zero) lower than ', + presence_threshold, + sep='') + + # Then filter based on minimal presence + filter <- table.filtered[, names(table.filtered) != category] + + presence <- rowSums(filter != 0) + minmax.p <- paste(min(presence), max(presence), sep='-') + if (presence_threshold_type == 'percent') { + presence <- Numeric2Percent(presence) + msg <- paste(msg, '%', sep='') + minmax.p <- paste(min(presence), '-', max(presence), '%', sep='') + } + table.filtered <- table.filtered[presence >= presence_threshold, ] + nrow.filtered.p <- nrow(table.filtered) + + nstop <- nrow(table.filtered) + ab.min <- round(min(filter), digits=4) + ab.max <- round(max.abundance, digits=4) + msg <- paste(msg, + ': ', + nrow.filtered.a-nrow.filtered.p, + ' rows were removed out of ', + nrow.filtered.a, + ' (final number of rows = ', + nrow.filtered.p, + '). Min-max abundance found was ', + minmax.a, + '. Min-max presence found was ', + minmax.p, + '."', + sep='') + + PrintMsg(msg, verbose) + + if (nrow.filtered.p == 0) { + PrintMsg(paste('"error":"All the rows were filtered out. Please chose an abundance threshold within ', minmax.a, ' and a presence threshold within ', minmax.p, '."', sep=''), verbose, TRUE) + } + + return (table.filtered) +} + +################ +# Bin numeric table by category +# Args: +# (1) table (numeric dataframe) Count table +# (2) aggr.col (character) Name of the column in 'table' to consider for data +# aggregation +# (3) fun (character) Function to use for data aggregation. Valid +# choices: 'mean', 'sum' +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# An aggregated version of 'table' (numeric dataframe) +# Prints to stdout: +# A description of the function +################ +BinTableByCategory <- function(table=NULL, category_col='taxonomy', fun='sum', + level='NA', verbose=TRUE) { + PrintMsg(paste('"description":"Bin table rows by category (column=', + category_col, + ', function=', + fun, + ', level=', + level, + ')."', + sep=''), verbose) + + category_is_row_names <- FALSE + if ((category_col == '') || !(category_col %in% names(table)) ) { + category_is_row_names <- TRUE + category <- row.names(table) + } else { + category <- paste(table[, names(table) == category_col], + '; (', + row.names(table), + ')', + sep='') + table <- table[, names(table) != category_col] + } + category <- strsplit(category, ';') + + for (i in 1:length(category)) { + category[[i]] <- gsub('^ *.__ *$', '', category[[i]]) + category[[i]] <- gsub('^ *', '', category[[i]]) + category[[i]] <- gsub(' *$', '', category[[i]]) + category[[i]] <- category[[i]][! category[[i]] %in% c('', ' ')] + if (length(category[[i]]) >= level) { + category[[i]] <- paste(category[[i]][1:level], collapse='; ') + } else { + category[[i]] <- paste(category[[i]], collapse='; ') + } + } + category <- list(unlist(category)) + name <- 'category' + while (name %in% names(table)) { + name <- paste(name,paste(sample(c(0:9, letters, LETTERS), 3), collapse=''),sep='_') + } + names(category) <- name + + table.binned <- aggregate(table, + by=category, FUN=fun) + + row.names(table.binned) <- table.binned[, name] + table.binned <- table.binned[, names(table.binned) != name] + + return(table.binned) +} + +################ +# Divide a data frame (or a matrix-like object) by a vector +# Args: +# (1) table (numeric dataframe) Count table +# (2) vect (vector) Vector to divide 'table' +# (3) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A divided (per row) version of 'table' (numeric dataframe) +# Prints to stdout: +# A description of the function +################ +DivTable <- function(table=NULL, vect=NULL, verbose='TRUE') { + PrintMsg('log', 'Divide tables rows by a vector.', verbose=verbose) + + return (as.matrix(table) / unlist(vect)) +} + +################ +# Transform to log2 count per milion +# Args: +# (1) table (numeric dataframe) Count table +# (2) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A transformed version of 'table' (numeric dataframe) +# Prints to stdout: +# A description of the function +################ +ApplyLog2Cpm <- function(table=NULL, verbose='TRUE') { + if (length(table[table < 0]) > 0) { + PrintMsg('"error":"Log2Cpm was not applied because negative values were detected."', + verbose, TRUE) + return () + } + if (length(table[table%%1 > 0]) > 0) { + PrintMsg('"warning":"Non-integer values were detected."', verbose) + table <- round(table, digits=0) + } + + PrintMsg('"description":"Converted values into per-million per column and applied log2(x+1)."', + verbose) + + transformed.table <- as.data.frame(log2(1+(apply(as.matrix(table), + 2, + Numeric2Percent))*10000)) + names(transformed.table) <- names(table) + row.names(transformed.table) <- row.names(table) + + return (transformed.table) +} + +################ +# Transform to log2 +# Args: +# (1) table (numeric dataframe) Count table +# (2) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A transformed version of 'table' (numeric dataframe) +# Prints to stdout: +# A description of the function +################ +ApplyLog2 <- function(table=NULL, verbose=TRUE) { + if (length(table[table < 0]) > 0) { + PrintMsg('"error":"log2(x+1) was not applied because negative values were detected."', + verbose, TRUE) + return () + } + PrintMsg('"description":"Applied log2(x+1)."', + verbose) + + transformed.table <- as.data.frame(log2(1+(as.matrix(table)))) + names(transformed.table) <- names(table) + row.names(transformed.table) <- row.names(table) + + return (transformed.table) +} + +################ +# Transform to percent (per column) +# Args: +# (1) table (numeric dataframe) Count table +# (2) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A transformed version of 'table' (numeric dataframe) +# Prints to stdout: +# A description of the function +################ +ApplyCount2Percent <- function(table, verbose=TRUE) { + PrintMsg('"description":"Transformed values into percentages per column."', verbose=verbose) + transformed.table <- as.data.frame(apply(as.matrix(table), 2, Numeric2Percent)) + names(transformed.table) <- names(table) + row.names(transformed.table) <- row.names(table) + return (transformed.table) +} + +################ +# Apply rarefaction +# Args: +# (1) table (numeric dataframe) Count table +# (2) sample (positive integer or 'max') Sampling depth +# (3) nsampling (positive integer) Number of repeated sampling to performe +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A rarefied version of 'table' (a numeric dataframe) +# Prints to stdout: +# A description of the function +################ +RarefyTable <- function(table=NULL, sample='max', nsampling=1, verbose=TRUE) { + suppressMessages(library('vegan')) + + # Round counts if not integers + if (length(table[table < 0]) > 0) { + PrintMsg('"error":"Rarefaction is not applicable because negative values were detected."', verbose, TRUE) + return () + } + if (length(table[table%%1 > 0]) > 0) { + PrintMsg('"warning":"Non-integer values were detected. In order to permit rarefaction, these values were rounded to the nearest integer."', verbose) + table <- round(table, digits=0) + } + + # Define/check rarefaction depth + min.sample <- min(colSums(table)) + if (sample == 'max' || as.numeric(sample) > min.sample) { + sample <- min.sample + } + + if (sample < 100) { + PrintMsg(paste('"warning":"Very low counts detected in the following sample(s): ', + paste(names(table)[which(colSums(table) < 100)], collapse=','), + '."', + sep=''), verbose) + } else if (sample < 1000) { + PrintMsg(paste('"warning":"Low counts detected in the following sample(s): ', + paste(names(table)[which(colSums(table) < 1000)], collapse=','), + '."', + sep=''), verbose) + } + + # Apply rarefaction + table <- t(table) + table.list <- list() + for (i in 1:as.numeric(nsampling)) { + table.list[[i]] <- rrarefy(table, sample=sample) + } + rarefied.table <- apply(simplify2array(table.list), c(1, 2), mean) + # element-wise mean across rarefied tables + + PrintMsg(paste('"description":"Equalized number of observations per samples using the rarefaction method with sampling depth=', + sample, + ' (random sampling without replacement) (R package vegan). New values were calculated as the means of ', + nsampling, + ' independent samplings that were rounded to the nearest integer."', + sep=''), verbose=verbose) + + return (data.frame(t(round(rarefied.table, digits=0)))) +} + + +################ +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (dataframe) Design +# (3) effect (character) comma-delimited list of random effects to +# suppress. Elements must be names of columns in mapping file +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A adjusted version of 'table' (a numeric dataframe) +# Prints to stdout: +# A description of the function +################ +SuppressBatchEffect <- function(table=NULL, map=NULL, effect=NULL, + fun='combat', verbose=TRUE) { + method <- list(combat='COMBAT', mean_centering='mean centering') + PrintMsg(paste('"description":"Batch effect suppression on ', + effect, + ' using', + method[[fun]], + '"'), + verbose) + + effect <- unlist(strsplit(effect, split=',')) + + if (fun == 'mean_centering') { + table <- t(table) + + for (i in 1:length(effect)) { + fact <- map[effect[i]] + fact.name <- as.vector(unique(unlist(fact))) + nfact <- length(fact.name) + + for (j in 1:ncol(table)) { + data <- data.frame(d=table[, j], fact=fact) + means <- NULL + for (k in 1:nfact) { + means <- append(means, mean(data[fact == fact.name[k], 'd'])) + } + for (k in 1:nfact) { + adj <- max(means)-means[k] + data[fact == fact.name[k], 'd'] <- data[fact == fact.name[k], 'd']+adj + } + table[, j] <- data$d + } + } + table <- t(table) + } else if (fun == 'combat') { + suppressMessages(library('sva')) + table <- ComBat(table, map[, effect]) + } + return (table) +} + + +################ +# Filter mapping file to keep only terms in model +# Args: +# (1) map (dataframe) +# (2) model (formula vector) +# Returns: +# A filtered map +################ +FilterMap <- function(map=NULL, model=NULL) { + + # keep only columns of map that are used in the model + if (!is.null(model)) { + term <- list() + for (i in 1:length(model)) { + term[[i]] <-as.vector(unique(unlist(strsplit(model[i], split=c('[^A-Za-z0-9_]'))))) + term[[i]] <- term[[i]][! term[[i]] %in% c("", " ", "1", "Error")] + } + term <- as.vector(unlist(term)) + row.names <- row.names(map) + if (length(term) > 1) { + map <- as.data.frame(map[, names(map) %in% term]) + } else { + map <- as.data.frame(map[, names(map) == term]) + names(map) <- term + } + if (ncol(map) < length(unique(term))) { + PrintMsg(paste('"error":"Some terms in model ', + model, + ' were not found in the map. Please make sure that all model terms refer to columns headers in the map."', + sep=''), + verbose=TRUE, TRUE) + } + } + + return (map) +} + +################ +# Remove constant rows from table +# Args: +# (1) table (dataframe) +# Returns: +# A filtered table +################ +RemoveConstantRows <- function(table=NULL, verbose=TRUE) { + + filter <- rep(TRUE, nrow(table)) + for (i in 1:nrow(table)) { + if (length(as.vector(unique(unlist(table[i, ])))) < 2) { + filter[i] <- FALSE + PrintMsg(paste('"warning":"Row ', i, ' (', row.names(table)[i], ') contained essentially constant data and was removed."', sep=''), + verbose=verbose) + } + } + return (table[filter == TRUE, ]) +} + +################################################################################ +################################################################################ +# ANALYSIS ##################################################################### +# ANALYSIS ##################################################################### +# ANALYSIS ##################################################################### +# ANALYSIS ##################################################################### +################################################################################ +################################################################################ + +################ +# Apply a clustering algorithm +# Args: +# (1) table (numeric dataframe) Count table +# (2) fun (character) Clustering function (algorithme) +# (3) json (character) Json string specifying packages +# (4) verbose (character) Print messages? see PrintMsg() options +# (5) graphical (logical) Generate graphics? +# Returns: +# A table of clusters +# Output to device: +# Depending on 'fun', a figure +# Prints to stdout: +# A description of the function +################ +PerformClustering <- function(table=NULL, fun=c('kmeans', 'pam', 'pam-bray', 'pam-jaccard'), json=libJson, + verbose=TRUE, graphical=TRUE) { + + if (ncol(table) < 10 || nrow(table) < 3) { + PrintMsg('"warning":"Not enough data to perform clustering (min 10 column and 3 rows)."', + verbose) + return (NULL) + } else if (ncol(table) > 65536) { + PrintMsg('"warning":"Sorry, the dataset is too large for clustering algorithms (max 65536 columns)."', + verbose) + return (NULL) + } + + spec <- as.data.frame(do.call('rbind', json$clustering)) + spec <- spec[spec$value == fun, ] + package <- '' + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + PrintMsg(paste('"description":"Performed clustering using ', spec$label, + package, '."', sep=''), + verbose) + + table <- t(table) + + if (fun == 'kmeans') { + + + if (graphical == TRUE) { + kmean.out <- kmeansruns(table, criterion='asw', plot=TRUE)$cluster + } else { + kmean.out <- kmeansruns(table, criterion='asw')$cluster + } + cluster <- paste('kmean', + kmean.out, + sep ='') + + } else if (fun == 'pam') { + + pam.out <- pamk(table, usepam=FALSE) + if (graphical == TRUE) { + plot(pam.out$pamobject, which.plots=2) + } + + cluster <- paste('pam', + pam.out$pamobject$cluster, + sep ='') + + } else if (fun == 'pam-bray') { + + dist.data <- ComputeDistance(table=table, fun='bray', json=json, + verbose=verbose) + + pam.out <- pamk(dist.data, usepam=TRUE) + if (graphical == TRUE) { + plot(pam.out$pamobject, which.plots=2) + } + + cluster <- paste('pam', + pam.out$pamobject$cluster, + sep ='') + + } else if (fun == 'pam-jaccard') { + + dist.data <- ComputeDistance(table=table, fun='jaccard', json=json, + verbose=verbose) + + pam.out <- pamk(dist.data, usepam=TRUE) + if (graphical == TRUE) { + plot(pam.out$pamobject, which.plots=2) + } + + cluster <- paste('pam', + pam.out$pamobject$cluster, + sep ='') + } else { + return(NULL) + } + + + names(cluster) <- 'Clustering' + return (cluster) +} + +################ +# Build a relative abundance stacked barplot +# Args: +# (1) table (numeric dataframe) Count table +# (2) threshold (numeric) Abundance cutoff for display in percent [0;100] +# (3) verbose (character) Print messages? see PrintMsg() options +# (4) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +AnalyseProportions <- function(table=NULL, + map=NULL, + verbose=TRUE, graphical=TRUE) { + + # Convert to relative abundances per column + PrintMsg('"description":"Converted data to relative abundance (%) per sample."', + verbose) + table <- as.data.frame(apply(table, 2, Numeric2Percent)) + + # Order by decreasing values + table <- as.data.frame(table[order(rowMeans(table), decreasing=TRUE), ]) + + # Build stacked barchart + if (as.logical(graphical) == TRUE) { + col <- rainbow(nrow(table)) + par(mar=c(4, 4, 2, 4), xpd=TRUE) + barplot(as.matrix(table), + ylim=c(0, max(colSums(table))), + space=0, + border=NA, + names.arg=names(table), + ylab='%', + col=col, + las=2, + bty="n", + cex.names=1.84906*ncol(table)^-0.2898) + # Build legend in a separate figure + plot.new() + legend("topleft", row.names(table), cex=0.8, + fill=col, horiz=FALSE) + } + + return (Dataframe2DataJson(data=table, xmetadata=map)) +} + +################ +# Calculate diversity per column +# Args: +# (1) table (numeric dataframe) Count table +# (2) fun (character) The diversity function to use +# (3) json (character) Json string specifying packages +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A vector of length=ncol(table) +# Prints to stdout: +# A description of the function +################ +ComputeDiversity <- function(table=NULL, fun='shannon', json=libJson, + verbose=TRUE) { + + spec <- as.data.frame(do.call('rbind', json$diversity)) + spec <- spec[spec$value == fun, ] + package <- '' + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } else { + pkg <- '' + } + + PrintMsg(paste('"description":"Calculated ', + spec$label, + package, + ' per sample."', sep=''), verbose) + + diversity <- NULL + if (pkg == 'vegan') { + for (i in 1:ncol(table)) { + diversity <- append(diversity, diversity(table[, i], index=fun, MARGIN=0, + base=2), after=length(diversity)) + } + } else if (pkg == 'ineq') { + for (i in 1:ncol(table)) { + diversity <- append(diversity, ineq(table[, i], parameter=NULL, type=fun, + na.rm=TRUE), after=length(diversity)) + } + } else if (fun == 'richness') { + for (i in 1:ncol(table)) { + diversity <- append(diversity, sum(table[, i] != 0), + after=length(diversity)) + } + } else if (fun == 'chao1') { + diversity <- chao1(table, taxa.row = TRUE) + } else { + PrintMsg(paste('"error":"Unknown argument to fun (2):', fun, '"', sep=' '), + verbose, TRUE) + } + + return (diversity) +} + +################ +# Compute correlations +# Args: +# (1) table (numeric dataframe) Count table +# (2) fun (character) The correlation methode to use +# (3) json (character) Json string specifying packages +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A vector of length=ncol(table) +# Prints to stdout: +# A description of the function +################ +ComputeCorrelation <- function(table=NULL, fun='pearson', test=FALSE, + json=libJson, verbose=TRUE) { + + spec <- as.data.frame(do.call('rbind', json$correlation)) + spec <- spec[spec$value == fun, ] + package <- '' + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + PrintMsg(paste('"description":"Calculate ', spec$label, package, '."', sep=''), + verbose=verbose) + + if (as.logical(test) == TRUE) { + cor.output <- list() + cor.output[['estimate']] <- matrix(ncol=nrow(table), nrow=nrow(table)) + cor.output[['p.value']] <- matrix(ncol=nrow(table), nrow=nrow(table)) + for (i in 2:nrow(table)) { + for (j in 1:(i-1)) { + out <- cor.test(unlist(table[i, ]), unlist(table[j, ]), method=fun) + cor.output[['estimate']][i, j] <- out$estimate + cor.output[['p.value']][i, j] <- out$p.value + } + } + cor.output[['estimate']][upper.tri(cor.output[['estimate']])] <- t(cor.output[['estimate']])[upper.tri(cor.output[['estimate']])] + diag(cor.output[['estimate']]) <- 1 + cor.output[['p.value']][upper.tri(cor.output[['p.value']])] <- t(cor.output[['p.value']])[upper.tri(cor.output[['p.value']])] + diag(cor.output[['p.value']]) <- 0 + return (cor.output) + } + + if (fun == 'pearson') { + return (cor(t(table), method=fun, use='pairwise.complete.obs')) + } else { + return (cor(t(table), method=fun, use='na.or.complete')) + } +} + +################ +# Compute distances +# Args: +# (1) table (numeric dataframe) Count table +# (2) fun (character) The distance function to use +# (3) json (character) Json string specifying packages +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A vector of length=ncol(table) +# Prints to stdout: +# A description of the function +################ +ComputeDistance <- function(table=NULL, fun='bray', json=libJson, + verbose=TRUE) { + + spec <- as.data.frame(do.call('rbind', json$distance)) + spec <- spec[spec$value == fun, ] + package <- '' + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + + if (fun %in% c('pearson', 'spearman')) { + + PrintMsg(paste('"description":"Calculate distance matrix based on ', + spec$label, package, '."', sep=''), verbose=verbose) + return (as.dist(as.matrix(ComputeCorrelation(table, fun=fun, json=json, + verbose=FALSE)))) + + } else { + # Correct for negative entries by adding a scalar (Caillez correction method) + if (min(table) < 0) { + + table <- table+abs(min(table)) + + PrintMsg(paste('"description":"Apply a Caillez correction and calculate distance matrix based on ', + spec$label, package, '."', sep=''), verbose=verbose) + + } else { + PrintMsg(paste('"description":"Calculate distance matrix based on ', + spec$label, package, '."', sep=''), verbose=verbose) + } + + if ('vegan' %in% pkg) { + return (vegdist(table, method=fun, binary=FALSE, diag=TRUE, upper=TRUE)) + } + } + +} + +################ +# Test if model is balanced +# Args: +# (1) map (data-frame) Experimental design table +# (2) model (character) A formula representing the model for stats. Terms +# must refer to names(map) +# (3) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A bool indicating whether the design is balanced or not +################ +ModelIsBalanced <- function(map=NULL, model=NULL, verbose='TRUE') { + # keep only columns of map that are used in the model + map <- FilterMap(map=map, model=model) + for (i in 1:ncol(map)) { + fact <- as.vector(unique(unlist(map[, i]))) + n <- length(map[map[, i] == fact[1], i]) + if (length(fact) > 1) { + for (j in 2:length(fact)) { + if (length(map[map[, i] == fact[j], i]) != n) { + PrintMsg('log', paste('Model ',model , ' is not balanced.', sep=''), verbose=verbose) + return (FALSE) + } + } + } + } + + PrintMsg('note', paste('Model ',model , ' is balanced.', sep=''), verbose=verbose) + return (TRUE) +} + +################ +# Test if model's variables could be nested +# Args: +# (1) map (data-frame) Experimental design table +# (2) model (character) A formula representing the model for stats. Terms +# must refer to names(map) +# (3) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A list of nested variables +################ +ModelIsNested <- function(map=NULL, model=NULL, verbose='TRUE') { + + # keep only columns of map that are used in the model + map <- FilterMap(map=map, model=model) + if (ncol(map) < 2) { + return (NULL) + } + + out <- list() + out.txt <- NULL + for (i in 1:ncol(map)) { + fact <- as.vector(unique(unlist(map[, i]))) + test <- as.data.frame(map[, -i]) + names(test) <- names(map)[-i] + out[[names(map)[i]]] <- NULL + for (j in 1:ncol(test)) { + group1 <- as.vector(test[map[, i] == fact[1], j]) + nested <- TRUE + if (length(as.vector(unique(map[, i]))) >= length(as.vector(unique(test[, j])))) { + nested <- FALSE + } else if (length(as.vector(unique(group1))) != length(group1)) { + nested <- FALSE + } else { + for (k in 2:length(fact)) { + if (!identical(group1, as.vector(test[map[, i] == fact[k], j]))) { + nested <- FALSE + break + } + } + } + if (nested == TRUE) { + out[[names(map)[i]]] <- append(out[[names(map)[i]]], names(test)[j], after=length(out[[names(map)[i]]])) + out.txt <- append(out.txt, paste(names(test)[i], names(map)[j], sep='/'), after=length(out.txt)) + } + } + } + if (length(out) == 0) { + return (NULL) + } + PrintMsg('log', paste('The following nesting was detected in model ', + model, + ': ', + paste(out.txt, collapse=', '), + ' Please control that the model is the one you want.', + sep=''), verbose=verbose) + return (out) +} + +################ +# Compute statistics +# Args: +# (1) response (numeric vector) +# (2) map (data-frame) Experimental design table with +# nrow=length(response) +# (3) method (character) Statistics to use +# (4) model (character) A formula representing the model for stats. Terms +# must refer to names(map) +# (5) pairwise (bool) Perform pairwise comparision? +# (6) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A data-frame +# Prints to stdout: +# A description of the function +################ +ComputeStats <- function(response=NULL, map=NULL, method='anova', + model=NULL, pairwise=TRUE, verbose=TRUE) { + + if (is.null(method)) { + method <- 'anova' + PrintMsg('"warning":"The statistical method was not specified. An ANOVA will be performed by default."', verbose) + } + + # If no model is specified, assume all columns in map are fixed variables + if (is.null(model)) { + model <- paste(names(map), collapse='+') + PrintMsg(paste('"warning":"Model was not specified. Based on the map, the following model will be used by default: ', + model, + '."', + sep=''), verbose) + } + # Test that each factor has >= 2 levels + term <- as.vector(unique(unlist(strsplit(model, split=c('[^A-Za-z0-9_]'))))) + term <- term[! term %in% c("", " ", "1", "Error")] + for (i in 1:length(term)) { + if (!term[i] %in% names(map)) { + PrintMsg(paste('"error":"The following model factor was not found in map: ', + term[i], + '. Please correct the model or correct the map."', + sep=''), verbose=TRUE, stop=TRUE) + } + levels <- as.vector(unique(unlist(map[, term[i]]))) + if (length(levels) < 2) { + PrintMsg(paste('"error":"The following model factor has less than 2 levels: ', + term[i], + '. Please correct the model or correct the map."', + sep=''), verbose=TRUE, stop=TRUE) + } + for (j in 1:length(levels)) { + if (length(map[map[, term[i]] == levels[j], ]) == 1) { + PrintMsg(paste('"warning":"The following group contains only one element (variance is null): ', + levels[j], + '. This will impact on statistics."', + sep=''), TRUE) + } + } + } + + response.name <- 'response' + while (response.name %in% names(map)) { + response.name <- paste('response', paste(sample(c(letters, LETTERS, c(0:9)), 4), collapse=''), sep='') + } + names(response) <- response.name + formula <- paste(response.name, ' ~ ', model, sep='') + + # Build the dataframe + data <- map + data <- cbind(map, response) + + pairwise.output <- NULL + if (method == 'anova' || is.null(method)) { + # PARAMETRIC, ACCEPTS MODELS WITH >2 FACTORS, MULTIPLE FIXED VARIABLES AND NESTING + # MODEL CAN BE OF THE FORM "a*b+c" + + PrintMsg(paste('"description":"Fit an analysis of variance (ANOVA) using model: ', model, + '."', sep=''), verbose) + + aov.output <- aov(formula=as.formula(formula), data=data) + + if (pairwise == TRUE) { + pairwise.output <- TukeyHSD(aov.output) + } + + summary <- as.data.frame(summary(aov.output)[[1]]) + if ('Pr(>F)' %in% names(summary)) { + p.value <- summary[, 'Pr(>F)'] + } else { + PrintMsg(paste('"warning":"Impossible to calculate a p-value. Please make sure that the model ', + model, ' is not saturated."', + sep=''), verbose) + p.value <- rep('NA', nrow(summary)) + } + + summary <- data.frame(row.names=gsub('[[:space:]]', '', row.names(summary)), + p.value=p.value) + #explained=summary[, 'Sum Sq']/sum(summary[, 'Sum Sq'])) + + } else if (method == 'friedman') { + # NON-PARAMETRIC, ACCEPTS MODELS WITH >2 FACTORS BUT REQUIRES EXACTLY 2 NESTED VARIABLE + # MODEL HAS TO BE OF THE FORM "a | b" + friedman.terms <- unlist(strsplit(model, split=' ')) + if (length(friedman.terms) != 3 || friedman.terms[2] != '|') { + PrintMsg(paste('"error":"Model must be of the form a | b when using Friedman test. Please provide an appropriate model. Current model: ', + model, '."', sep=''), verbose, TRUE) + } + + friedman.output <- friedman.test.with.post.hoc(formula=as.formula(formula), data=data) + + PrintMsg(paste('"description":"Performed Friedman test using model: ', model, + '."', sep=''), verbose) + + if (pairwise == TRUE) { + pairwise.output <- list() + pairwise.output[[term[1]]] <- data.frame(row.names=paste(unique(unlist(map[, term[1]])), collapse='-'), + p=friedman.output$PostHoc.Test) + PrintMsg('"description":"Performed post-hoc test with correction for multiple comparisions."', verbose) + } + + summary <- data.frame(row.names=term[1], + p.value=friedman.output$Friedman.Test) + + } else if (method == 'kruskal') { + # NON-PARAMETRIC, ACCEPTS MODELS WITH >2 FACTORS BUT ONLY 1 FIXED VARIABLE + # MODEL CAN BE OF THE FORM "a" + if (length(term) > 1) { + PrintMsg(paste('"error":"Model cannot include more than 1 variable when using Kruskal-Wallis rank sum test. Please provide an appropriate model. Current model: ', + model, ' (', length(term), ' variables)."', sep=''), verbose, TRUE) + } + + suppressMessages(library('dunn.test')) + dunn.output <- dunn.test(data[, response.name], g=map[, term[1]], kw=FALSE, table=FALSE, list=FALSE) + + PrintMsg(paste('"description":"Performed Kruskal-Wallis rank sum test using model: ', model, + '."', sep=''), verbose) + + if (pairwise == TRUE) { + pairwise.output <- list() + pairwise.output[[term[1]]] <- data.frame(row.names=paste(unique(unlist(map[, term[1]])), collapse='-'), + p=dunn.output$P) + PrintMsg('"description":"Performed post-hoc test with correction for multiple comparisions (R library {dunn.test})."', verbose) + } + + summary <- data.frame(row.names=term[1], + p.value=pchisq(dunn.output$chi2, nrow(data)-1, lower.tail=FALSE)) + + + } else if (method == 'wilcox') { + # NON-PARAMETRIC, ACCEPTS MODELS WITH 2 FACTORS ONLY AND ONE FIXED VARIABLES + # MODEL HAS TO BE OF THE FORM "a" + if (length(term) > 1) { + PrintMsg(paste('"error":"Model cannot include more than 1 variable when using Wilcoxon rank sum test. Please provide an appropriate model. Current model: ', + model, ' (', length(term), ' variables)."', sep=''), verbose, TRUE) + } + samples <- unique(unlist(data[, model])) + if (length(samples) != 2) { + PrintMsg(paste('"error":"Wilcoxon rank sum test can only perform two samples comparison. Please provide an appropriate model. Current model includes following samples: ', + sample, '."', sep=''), verbose, TRUE) + } + + wilcox.output <- wilcox.test(formula=as.formula(formula), data=data) + PrintMsg(paste('"description":"Performed Wilcoxon rank sum test using model: ', model, + '."', sep=''), verbose) + + summary <- data.frame(row.names=paste(samples, collapse='-'), + p.value=wilcox.output$p.value) + + pairwise.output <- list() + pairwise.output[[model]] <- as.data.frame(wilcox.output$p.value) + names(pairwise.output[[model]]) <- model + row.names(pairwise.output[[model]]) <-paste(samples, collapse='-') + + } else if (method == 'wilcox_paired') { + # NON-PARAMETRIC, ACCEPTS MODELS WITH 2 FACTORS ONLY AND ONE FIXED VARIABLES. NESTING IS ASSUMED FROM THE ORDER OF THE DATA! + # MODEL HAS TO BE OF THE FORM "a" + if (length(term) > 1) { + PrintMsg(paste('"error":"Model cannot include more than 1 variable when using Wilcoxon signed rank test. Please provide an appropriate model. Current model: ', + model, ' (', length(term), ' variables)."', sep=''), verbose, TRUE) + } + samples <- unique(unlist(data[, model])) + if (length(samples) != 2) { + PrintMsg(paste('"error":"Wilcoxon signed rank test can only perform two samples paired comparison. Please provide an appropriate model. Current model includes following samples: ', + paste(sample, collapse=', '), + '."', + sep=''), + verbose, TRUE) + } + x <- data[data[, model] == samples[1], model] + y <- data[data[, model] == samples[2], model] + if (length(x) != length(y)) { + PrintMsg(paste('"error":"Paired comparison in Wilcoxon signed rank test is possible only if samples have same size. Please provide an appropriate model, correct the map or use different statistics. Current samples: ', + samples[1], + '(n=', + length(x), + '), ', + samples[2], + '(n=', + length(y), + ')."', + sep=''), + verbose, TRUE) + } + + wilcox.output <- wilcox.test(formula=as.formula(formula), data=data, paired=TRUE) + PrintMsg(paste('"description":"Performed Wilcoxon signed rank test using model: ', + model, + '. Paires: ', + paste(paste(row.names(data)[data[, model] == samples[1]], row.names(data)[data[, model] == samples[2]], sep='-'), collapse=', '), + '."', sep=''), verbose) + + summary <- data.frame(row.names=paste(samples, collapse='-'), + p.value=wilcox.output$p.value) + + pairwise.output <- list() + pairwise.output[[model]] <- as.data.frame(wilcox.output$p.value) + names(pairwise.output[[model]]) <- model + row.names(pairwise.output[[model]]) <-paste(samples, collapse='-') + + } else if (method == 'ttest') { + # PARAMETRIC, ACCEPTS MODELS WITH 2 FACTORS ONLY AND ONE FIXED VARIABLES. + # MODEL HAS TO BE OF THE FORM "a" + if (length(term) > 1) { + PrintMsg(paste('"error":"Model cannot include more than 1 variable when using t-test. Please provide an appropriate model. Current model: ', + model, ' (', length(term), ' variables)."', sep=''), verbose, TRUE) + } + samples <- unique(unlist(data[, model])) + if (length(samples) != 2) { + PrintMsg(paste('"error":"T-test can only perform two samples comparison. Please provide an appropriate model. Current model includes following samples: ', + paste(sample, collapse=', '), + '."', + sep=''), + verbose, TRUE) + } + + ttest.output <- t.test(formula=as.formula(formula), data=data) + PrintMsg(paste('"description":"Performed paired t-test using model: ', + model, + '."', + sep=''), + verbose) + + summary <- data.frame(row.names=paste(samples, collapse='-'), + p.value=ttest.output$p.value) + + pairwise.output <- list() + pairwise.output[[model]] <- as.data.frame(ttest.output$p.value) + names(pairwise.output[[model]]) <- model + row.names(pairwise.output[[model]]) <-paste(samples, collapse='-') + + } else if (method == 'ttest_paired') { + # PARAMETRIC, ACCEPTS MODELS WITH 2 FACTORS ONLY AND ONE FIXED VARIABLES. NESTING IS ASSUMED FROM THE ORDER OF THE DATA! + # MODEL HAS TO BE OF THE FORM "a" + if (length(term) > 1) { + PrintMsg(paste('"error":"Model cannot include more than 1 variable when using t-test. Please provide an appropriate model. Current model: ', + model, ' (', length(term), ' variables)."', sep=''), verbose, TRUE) + } + samples <- unique(unlist(data[, model])) + if (length(samples) != 2) { + PrintMsg(paste('"error":"Paired t-test can only perform two samples comparison. Please provide an appropriate model. Current model includes following samples: ', + paste(sample, collapse=', '), + '."', + sep=''), + verbose, TRUE) + } + x <- data[data[, model] == samples[1], model] + y <- data[data[, model] == samples[2], model] + if (length(x) != length(y)) { + PrintMsg(paste('"error":"Paired comparison in t-test is possible only if samples have same size. Please provide an appropriate model, correct the map or use different statistics. Current samples: ', + samples[1], + '(n=', + length(x), + '), ', + samples[2], + '(n=', + length(y), + ')."', + sep=''), + verbose, TRUE) + } + + ttest.output <- t.test(formula=as.formula(formula), data=data, paired=TRUE) + PrintMsg(paste('"description":"Performed paired t-test using model: ', + model, + '. Paires: ', + paste(paste(row.names(data)[data[, model] == samples[1]], row.names(data)[data[, model] == samples[2]], sep='-'), collapse=', '), + '."', sep=''), verbose) + + summary <- data.frame(row.names=paste(samples, collapse='-'), + p.value=ttest.output$p.value) + + pairwise.output <- list() + pairwise.output[[model]] <- as.data.frame(ttest.output$p.value) + names(pairwise.output[[model]]) <- model + row.names(pairwise.output[[model]]) <-paste(samples, collapse='-') + } + + stat <- list(summary=summary, pairwise=pairwise.output) + return (stat) +} + +################ +# Plot rarefaction curves +# Args: +# (1) data (numeric matrix) rarefaction table +# Output to device: +# A figure +################ +PlotRarefactionCurves <- function(data=NULL, xlab='Rarefaction depth', ylab='') { + color.palette <- rainbow(ncol(data)) + plot(x=row.names(data), + y=c(1:nrow(data)), + type="n", + ylim=c(min(data), max(data)), + xlab=xlab, + ylab=ylab) + + for (i in 1:ncol(data)) { + lines(x=row.names(data), + y=data[, i], + col=color.palette[i]) + } + return () +} + +################ +# Perform a diversity analysis +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) fun (character) The diversity function to use +# (4) model (formula vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (5) json (character) Json string specifying packages +# (6) verbose (character) Print messages? see PrintMsg() options +# (7) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +AnalyseDiversity <- function(table=NULL, map=NULL, fun=c('richness', 'shannon'), + nrar=20, compare_diversity=FALSE, stats=NULL, model=NULL, + json=libJson, verbose=TRUE, graphical=TRUE) { + if (is.null(fun)) { + fun <- 'richness' + } + suppressMessages(library('vegan')) + + fun <- fun[fun != ''] + + # Compute rarefaction curves for diversity + colsums <- colSums(table) + min.colsum <- floor(min(colsums)) + ncol <- ncol(table) + sample <- matrix(rep(floor(seq(from=0, to=min.colsum, length.out=nrar)), ncol), ncol=ncol, byrow=FALSE) + diversity <- list() + map <- FilterMap(map=map, model=model) + + rarefied.table <- list() + for (i in 1:nrar) { + rarefied.table[[i]] <- rrarefy(t(table), sample=apply(rbind(sample[i, ], colsums), 2, min)) + } + + PrintMsg(paste('"description":"Generated rarefactions to compute diversity until ', min.colsum, ' counts per sample"', sep=''), verbose) + + data.json <- c(1:length(fun)) + for (i in 1:length(fun)) { + div <- matrix(ncol=ncol, nrow=0) + v <- TRUE + for (j in 1:nrar) { + div <- rbind(div, + ComputeDiversity(table=t(rarefied.table[[j]]), + fun=fun[i], + json=json, + verbose=v)) + v <- FALSE + } + colnames(div) <- colnames(table) + row.names(div) <- sample[, 1] + + diversity[[i]] <- as.matrix(div) + + # Build figures + if (graphical == TRUE) { + PlotRarefactionCurves(div, ylab=fun[i]) + } + + # Calculate statistics + if (compare_diversity == TRUE) { + PrintMsg(paste('"description":"Compared diversity between groups at a rarefaction depth of ', min.colsum, ' counts per sample"', sep=''), verbose) + p.value <- list() + for (j in 1:length(model)) { + if (length(stats) == length(model)) { + method <- stats[j] + } else { + method <- stats[1] + } + stat <- ComputeStats(response=div[nrar, ], map=map, method=method, + model=model[j], pairwise=FALSE, verbose=TRUE)$summary + p <- as.data.frame(t(as.matrix(stat$p.value))) + names(p) <- row.names(stat) + p <- as.data.frame(p[, names(p) != 'Residuals']) + # the following is usefull in case model contains only one element + names(p) <- row.names(stat)[row.names(stat) != 'Residuals'] + row.names(p) <- fun[i] + p.value[[j]] <- p + } + } else { + p.value <- NULL + } + data.json[i] <- paste('"', fun[i], '":', diversity2json(diversity[[i]], p.value, map), sep='') + } + + return (paste('{', paste(data.json, collapse=','), '}', sep='')) +} + + +################ +# Permutational Multivariate Analysis of Variance Using Distance Matrices +# Args: +# (1) model (character) +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) fun (character) The distance function to use +# (4) graphical (logical) Generate graphics? +# Returns: +# A summary (data-frame) +# Prints to stdout: +# A description of the function +################ +Adonis <- function(table=NULL, model=NULL, strata=NULL, map=NULL, + fun='bray', graphical=TRUE) { + + if (!is.null(model)) { + formula <- paste('table ~ ', model, sep='') + } else { + formula <- paste('table ~ ', names(map), sep='*') + } + + if (!is.null(strata)) { + if (strata == '') { + strata == NULL + } + } + + adonis.output <- adonis(formula=as.formula(formula), data=as.data.frame(map), + strata=strata, method=fun, + permutations=1000) + + summary <- data.frame(row.names=row.names(adonis.output$aov.tab)) + + summary <- data.frame(row.names=gsub('[[:space:]]', '', + row.names(adonis.output$aov.tab)), + p.value=adonis.output$aov.tab[, 'Pr(>F)'], + explained=adonis.output$aov.tab[, 'R2']) + + summary <- summary[row.names(summary) != 'Total', ] + + # Build the figure + if (graphical == TRUE) { + + pie(summary$explained, + labels=row.names(summary), + col=rainbow(length(summary$explained)), + init.angle=0, + radius=0.7, + cex=0.7, + xpd=TRUE) + legend(x=-1, y=-1, xjust=1, yjust=0.5, + title='p-value', + legend=round(summary$p.value, digits=4), + fill=rainbow(nrow(summary)), + cex=0.7, xpd=TRUE) + } + + return (summary) +} + + +################ +# Permutational Multivariate Analysis of Variance Using Distance Matrices +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) fun (character) The distance function to use +# (4) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# must refer to names(map) +# (5) verbose (character) Print messages? see PrintMsg() options +# (6) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +PerformAdonis <- function(table=NULL, map=NULL, fun='bray', + model=NULL, strata=NULL, + json=libJson, verbose=TRUE, graphical=TRUE) { + + suppressMessages(library('vegan')) + + #out.json <- c(1:length(fun)) + #for (k in 1:length(fun)) { + + spec <- as.data.frame(do.call('rbind', json$distance)) + spec <- spec[spec$value == fun, ] + package <- '' + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + if (length(pkg) > 0) { + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + PrintMsg(paste('"description":"Performed a permutational multivariate analysis of variance based on the ', + spec$label, + package, + ' using the Adonis method (R package {vegan})."', + sep=''), + verbose) + + # Correct for negative entries by adding a scalar (Caillez correction method) + if (min(table) < 0) { + c <- abs(min(table)) + table <- table+c + + PrintMsg(paste('"description":"Corrected for negative values using a Caillez correction (', + c, + ' added to all values)."', + sep=''), + verbose) + } + + if (is.null(strata) || strata == '') { + strata <- rep(NULL, length(model)) + } + if (!is.null(strata) && length(strata) != length(model)) { + PrintMsg(paste('"warning":"Number of strata (', + length(strata), + ') differs from number of models(', + length(model), + '). No strata will be used."', + sep=''), + verbose) + + strata <- rep(NULL, length(model)) + } + + # Perform perMANOVA + map <- FilterMap(map=map, model=model) + data.json <- c(1:length(model)) + for (i in 1:length(model)) { + stat <- Adonis(table=t(table), + model=model[i], strata=strata[i], map=map, + fun=fun, graphical=graphical) + + stat.json <- c(1:nrow(stat)) + for (j in 1:nrow(stat)) { + + stat.json[j] <- paste('{"name":"', + row.names(stat)[j], + '","p-value":"', + stat$p.value[j], + '","explained":"', + stat$explained[j], + '"}', + sep='') + } + data.json[i] <- paste('"', + model[i], + '":[', + paste(stat.json, collapse=','), + ']', + sep='') + } + + #out.json[k] <- paste('"', + # fun[k], + # '":"{', + # paste(data.json, collapse=','), + # '}', + # sep='') + #} + #return (paste('{',paste(out.json, collapse=','), '}', sep='')) + + return (paste('{',paste(data.json, collapse=','), '}', sep='')) +} + +################ +# Principal Component Analysis (PCA) +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) col (character) Name of a column in map to use to define colors +# (5) biplot (numeric) Show n=biplot factors with biggest norms on the +# biplot +# (6) verbose (character) Print messages? see PrintMsg() options +# (7) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +PerformPCA <- function(table=NULL, map=NULL, biplot=TRUE, + verbose=TRUE, graphical=TRUE) { + + suppressMessages(library('FactoMineR')) + + ncp <- min(max(2, ncol(table)), 5) + pca.output <- PCA(t(table), ncp=ncp, scale.unit=FALSE, graph=FALSE) + + if (graphical == TRUE) { + plot(pca.output, new.plot=FALSE) + } + + # Pack data into a json string + data.ind <- as.data.frame(t(as.data.frame(pca.output$ind$coord))) + row.names(data.ind) <- gsub('Dim.', 'PC', row.names(data.ind)) + + data.json <- Dataframe2DataJson(data=data.ind, xmetadata=map) + + eig.json <- c(1:ncp) + for (i in 1:ncp) { + eig.json[i] <- paste('"PC', + i, + '":"', + pca.output$eig[i, 2], + '"', + sep='') + } + + if (biplot == TRUE) { + data.var <- t(as.data.frame(pca.output$var$coord)) + row.names(data.var) <- gsub('Dim.', 'PC', row.names(data.var)) + + data.json <- paste('{"ind":', + data.json, + ',"var":', + Dataframe2DataJson(data=data.var), + ',"eig":{', + paste(eig.json, collapse=','), + '}}', + sep='') + } else { + data.json <- paste('{"data":', + data.json, + ',"eig":{', + paste(eig.json, collapse=','), + '}}', + sep='') + } + + PrintMsg('"description":"Performed principal component analysis (PCA) (R package {FactoMineR})."', + verbose) + + return (data.json) +} + +################ +# Correspondence Analysis (CA) +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) col (character) Name of a column in map to use to define colors +# (5) biplot (numeric) Show n=biplot factors with biggest norms on the +# biplot +# (6) verbose (character) Print messages? see PrintMsg() options +# (7) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +PerformCA <- function(table=NULL, map=NULL, + verbose=TRUE, graphical=TRUE) { + + suppressMessages(library('FactoMineR')) + + ncp <- min(max(2, ncol(table)), 5) + ca.output <- CA(table, ncp=ncp, graph=FALSE) + + if (graphical == TRUE) { + plot(ca.output, new.plot=FALSE) + } + + # Pack data into a json string + data.ind <- as.data.frame(t(as.data.frame(ca.output$col$coord))) + data.json <- Dataframe2DataJson(data=data.ind, xmetadata=map) + + eig.json <- c(1:ncp) + for (i in 1:ncp) { + eig.json[i] <- paste('"Dim', + i, + '":"', + ca.output$eig[i, 2], + '"', + sep='') + } + + data.var <- t(as.data.frame(ca.output$row$coord)) + + data.json <- paste('{"ind":', + data.json, + ',"var":', + Dataframe2DataJson(data=data.var), + ',"eig":{', + paste(eig.json, collapse=','), + '}}', + sep='') + + PrintMsg('"description":"Performed correspondence analysis (CA) (R package {FactoMineR})."', + verbose) + + return (data.json) +} + +################ +# Principal Coordinates Analysis (PCoA) +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) fun (character) The distance function to use +# (4) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (5) col (character) Name of a column in map to use to define colors +# (6) json (character) Json string specifying packages +# (7) verbose (character) Print messages? see PrintMsg() options +# (8) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +PerformPCoA <- function(table=NULL, map=NULL, fun='bray', + json=libJson, verbose=TRUE, graphical=TRUE) { + + spec <- as.data.frame(do.call('rbind', json$distance)) + spec <- spec[spec$value == fun, ] + package <- '' + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + + dist <- as.matrix(ComputeDistance(table=Transpose(table), + fun=fun, json=json, verbose=FALSE)) + row.names(dist) <- names(table) + + data.json <- PerformPCA(table=dist, map=map, biplot=FALSE, + verbose=FALSE, graphical=graphical) + + PrintMsg(paste('"description":"Performed principal coordinate analysis (PCoA) (R package {FactoMineR}) based on ', + spec$label, + package, + '."', + sep=''), + verbose) + + return (data.json) +} + +################ +# Canonical Correlation Analysis (CCA) +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) col (character) Name of a column in map to use to define colors +# (5) biplot (numeric) Show n=biplot factors with biggest norms on the +# biplot +# (6) verbose (character) Print messages? see PrintMsg() options +# (7) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +PerformCCA <- function(table=NULL, map=NULL, column=NULL, + verbose=TRUE, graphical=TRUE) { + + suppressMessages(library('vegan')) + + table <- as.data.frame(t(table)) + names <- unlist(strsplit(column, split=',')) + categs <- as.data.frame(map[, names]) + names(categs) <- names + row.names(categs) <- row.names(map) + + cca.output <- cca(table ~ ., data=categs) + + if (graphical == TRUE) { + plot(cca.output) + } + + # Pack data into a json string + data.ind <- as.data.frame(t(cca.output$CCA$u)) + data.var <- as.data.frame(t(cca.output$CCA$v)) + data.biplot <- as.data.frame(t(cca.output$CCA$biplot)) + data.json <- paste('{"ind":', + Dataframe2DataJson(data=data.ind, xmetadata=map), + ',"var":', + Dataframe2DataJson(data=data.var), + ',"biplot":', + Dataframe2DataJson(data=data.biplot), + '}', + sep='') + + PrintMsg(paste('"description":"Performed a canonical correspondence analysis (CCA) (R package {vegan}) with the following constraining variable(s):', + column, + '."', + sep=''), + verbose) + + return (data.json) +} + +################ +# Compute fold-change +# Args: +# (1) response (numeric vector) Data +# (2) factor (vector) Experimental design vector with +# nrow=length(response) +# (3) by (dataframe) A nested variable to use for calculating +# fold-changes +# (4) balanced (bool) assume that model balanced? If NULL, balance will be tested +# (5) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A data-frame containing fold-changes for each variable in model +# Prints to stdout: +# A description of the function +################ +ComputeFoldChange <- function(group1=NULL, group2=NULL, names=NULL, verbose=TRUE) { + + out <- list() + out[['logfc']] <- log2(mean(group2))-log2(mean(group1)) + out[['ab']] <- mean(c(group2, group1)) + + PrintMsg(paste('"description":"Calculated fold-change as log2(mean(', + names[2], + '))-log2(mean(', + names[1], + ')) and abundance as (mean(', + names[1], + ')+mean(', + names[2], + '))/2."', + sep=''), + verbose) + return(out) +} + + +################ +# Extract group of samples from map +# Args: +# (1) map (data-frame) Experimental design table with nrow=ncol(table) +# Returns: +# Prints to stdout: +################ +ExtractGroupFromMap <- function(map=NULL, column=NULL, group=NULL, verbose) { + map <- as.data.frame(FilterMap(map=map, model=column)) + group <- unlist(strsplit(group, split=':')) + if (ncol(map) != length(column)) { + PrintMsg('"error":"Unable to determine groups for fold-change analysis. Make sure no - or : character is present in the mapping file."' + , verbose) + } + keep <- 1 + for (i in 1:ncol(map)) { + keep <- keep*(map[, i] == group[i]) + } + return(keep) +} + +################ +# Analyse changes (fold-changes, p-values etc.) +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) json (character) Json string specifying packages +# (5) verbose (character) Print messages? see PrintMsg() options +# Returns: +# Figure data as a json string +# Prints to stdout: +# A description of the function +################ +AnalyseChange <- function(table=NULL, map=NULL, + stats=NULL, model=NULL, + json=libJson, verbose=TRUE, + graphical=FALSE) { + + PrintMsg('"description":"Analysed changes (fold-changes and p-values)."', + verbose) + # Calculate statistics + if (is.null(model)) { + model <- paste(names(map), sep='+') + } + nmodel <- length(model) + data.json <- c(1:nmodel) + for (i in 1:nmodel) { + if (length(stats) != length(model)) { + method = stats[1] + } else { + method = stats[i] + } + nrow <- nrow(table) + effect.json <- list() + v <- verbose + graphic.data <- list() + for (j in 1:nrow) { + stat <- ComputeStats(response=as.vector(unlist(table[j, ])), map=map, + method=method, model=model[i], pairwise=TRUE, + verbose=v)$pairwise + + effect.names <- names(stat) + neffect <- length(effect.names) + for (k in 1:neffect) { + if (j == 1 && k == 1) { + graphic.data[[effect.names[k]]] <- list() + } + p <- unlist(stat[[k]][, ncol(stat[[k]])]) + if (length(effect.json) == 0) { + effect.json[[effect.names[k]]] <- list() + } + comp.names <- row.names(stat[[k]]) + ncomp <- length(comp.names) + for (l in 1:ncomp) { + if (j == 1 && l == 1) { + graphic.data[[effect.names[k]]][[comp.names[l]]] <- data.frame(logfc=c(1:nrow), ab=c(1:nrow)) + row.names(graphic.data[[effect.names[k]]][[comp.names[l]]]) <- row.names(table) + } + if (length(effect.json[[effect.names[k]]]) == 0) { + effect.json[[effect.names[k]]][[comp.names[l]]] <- list() + effect.json[[effect.names[k]]][[comp.names[l]]] <- c(1:nrow) + } + group.names <- unlist(strsplit(row.names(stat[[k]])[l], split='-')) + if (length(group.names) != 2) { + PrintMsg('"error":"Unable to determine groups for fold-change analysis. Make sure no - or : character is present in the mapping file."', verbose) + } + + group1 <- ExtractGroupFromMap(map=map, column=names(stat)[k], group=group.names[1], verbose) + group2 <- ExtractGroupFromMap(map=map, column=names(stat)[k], group=group.names[2], verbose) + + fc <- ComputeFoldChange(as.vector(unlist(table[j, group1 == 1])), + as.vector(unlist(table[j, group2 == 1])), + names=group.names, + verbose=v) + + effect.json[[effect.names[k]]][[comp.names[l]]][j] <- paste('{"mean abundance":"', + fc$ab, + '","log2(fold-change)":"', + fc$logfc, + '","-log2(p-value)":"', + -log2(p[l]), + '"}', + sep='') + graphic.data[[effect.names[k]]][[comp.names[l]]]$logfc[j] <- fc$logfc + graphic.data[[effect.names[k]]][[comp.names[l]]]$ab[j] <- fc$ab + } + } + v <- FALSE + } + + for (k in 1:length(effect.json)) { + for (l in 1:length(effect.json[[k]])) { + effect.json[[k]][[l]] <- paste('"', + names(effect.json[[k]])[l], + '":[', + paste(effect.json[[k]][[l]], collapse=','), + ']', + sep='') + if (graphical == TRUE) { + plot(graphic.data[[k]][[l]]$ab, + graphic.data[[k]][[l]]$logfc, + main=names(effect.json[[k]])[l], + xlab='mean abundance', + ylab='log2(fold-change)') + } + } + effect.json[[k]] <- paste('"', + names(effect.json)[k], + '":{', + paste(effect.json[[k]], collapse=','), + '}', + sep='') + } + + + data.json[i] <- paste('"', + model[i], + '":{', + paste(effect.json, collapse=','), + '}', + sep='') + } + + return(paste('{"data":{', + paste(data.json, collapse=','), + '},"names":["', + paste(row.names(table), collapse='","'), + '"]}', + sep='')) + +} + +################ +# Build a heatmap of abundances +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character vector) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) metadata (numeric dataframe) Metadata table +# (5) fun (character) Function to use in hierarchical clustering +# (6) json (character) Json string specifying packages +# (7) verbose (character) Print messages? see PrintMsg() options +# (8) graphical (logical) Generate graphics? +# Returns: +# Figure data as a json string +# Output to device: +# A figure +# Prints to stdout: +# A description of the function +################ +BuildHeatMap <- function(table=NULL, map=NULL, stats='anova', + model=NULL, metadata=NULL, + fun='spearman', + json=libJson, verbose=TRUE, graphical=TRUE) { + + suppressMessages(library('gplots')) + + PrintMsg('"description":"Build a heatmap of proportions."', + verbose=verbose) + + # Check that there is no constant data + RemoveConstantRows(table=table, verbose=TRUE) + + if ((nrow(table) < 3) || (ncol(table) < 3)) { + PrintMsg('"error":"Not enough data to draw a heatmap (min 3 rows and 3 columns)."', TRUE, TRUE) + } + + # Center/scale by row + data <- as.data.frame(Transpose(apply(table, 1, cScale))) + + names(data) <- names(table) + row.names(data) <- row.names(table) + PrintMsg('"description":"Scale on standard deviation and center on mean abundance per row (variables)."', + verbose=verbose) + + # keep only columns of map that are used in the model + map <- FilterMap(map=map, model=model) + + # Set function for hierarchical clustering + DistFun <- function(x) { + return (ComputeDistance(table=x, + fun=fun, + json=json, + verbose=FALSE)) + } + + if (graphical == FALSE) { + pdf(file = NULL) + } + # Set the color palette + col <- colorRampPalette(c("navy", "seashell", "red2"))(n=51) + + heatmap.out <- heatmap.2(as.matrix(data), + scale='row', + na.rm=TRUE, + Rowv=TRUE, + Colv=TRUE, + dendrogram='both', + #distfun=DistFun, + col=col, + symm=FALSE, + symkey=FALSE, + key=TRUE, + keysize=1.5, + density.info='density', + trace='none', + labCol=names(data), + labRow=row.names(data), + cexCol=0.2+1/log10(ncol(data)), + mar=c(5, 10)) + + if (graphical == FALSE) { + dev.off() + } + + # Order data according to dendrogram + data <- data[heatmap.out$rowInd, ] + data <- data[, heatmap.out$colInd] + names <- names(map) + map <- as.data.frame(map[heatmap.out$colInd, ]) + names(map) <- names + if (!is.null(metadata)) { + metadata <- metadata[, heatmap.out$colInd] + } + + # Store heatmap data into a json string + ncol <- ncol(data) + heatmap.json <- c(1:ncol) + data.names <- names(data) + for (i in 1:ncol) { + heatmap.json[i] <- paste('{"name":"', + data.names[i], + '","value":["', + paste(data[, i], collapse='","'), + '"]}', + sep='') + } + + # Add categories to topbar + topbar.json <- list() + ncateg <- ncol(map) + category.json <- c(1:ncateg) + categ.names <- names(map) + + + for (i in 1:ncateg) { + category.json[i] <- paste('{"name":"', + categ.names[i], + '","value":["', + paste(map[, i], collapse='","'), + '"]}', + sep='') + } + topbar.json[['category']] <- paste('"category":[', + paste(category.json, collapse=','), + ']', + sep='') + + + # Add p-values to sidebar + sidebar.json <- list() + + if (is.null(model)) { + model <- paste(names(map), sep='+') + } + + nmodel <- length(model) + stat.json <- c(1:nmodel) + for (i in 1:nmodel) { + if (length(stats) != length(model)) { + method = stats[1] + } else { + method = stats[i] + } + nrow <- nrow(data) + v <- verbose + p.value <- list() + for (j in 1:nrow) { + stat <- ComputeStats(response=as.vector(unlist(data[j, ])), map=map, + method=method, model=model[i], pairwise=FALSE, + verbose=TRUE)$summary + + stat.rownames <- row.names(stat)[row.names(stat) != 'Residuals'] + stat <- as.data.frame(stat[row.names(stat) != 'Residuals', ]) + + nstat <- nrow(stat) + for (k in 1:nstat) { + if (is.null(p.value[[stat.rownames[k]]])) { + p.value[[stat.rownames[k]]] <- c(1:nrow) + } + p.value[[stat.rownames[k]]][j] <- stat[k, ] + } + v <- FALSE + } + for (j in 1:length(p.value)) { + p.value[[j]] <- paste('{"name":"', + names(p.value)[j], + '","value":["', + paste(p.value[[j]], collapse='","'), + '"]}', + sep='') + } + stat.json[i] <- paste('"', + model[i], + '":[', + paste(p.value, collapse=','), + ']', + sep='') + } + + sidebar.json[['p-values']] <- paste('"p-values":{', + paste(stat.json, collapse=','), + '}', + sep='') + + # Add correlations with metadata to sidebar + if (! is.null(metadata)) { + + PrintMsg('"description":"Calculate Spearman rank correlation with metadata."', + verbose=verbose) + + correlation <- as.data.frame(cor(x=t(data), y=t(metadata), use='na.or.complete', method ="spearman")) + + ncor <- ncol(correlation) + cor.json <- c(1:ncor) + for (i in 1:ncor) { + cor.json[i] <- paste('{"name":"', + names(correlation)[i], + '","value":["', + paste(correlation[, i], collapse='","'), + '"]}', + sep='') + } + sidebar.json[['correlations']] <- paste('"correlations":[', + paste(cor.json, collapse=','), + ']', + sep='') + } + + + data.json <- paste('{"heatmap":[', + paste(heatmap.json, collapse=','), + '],"colnames":["', + paste(names(data), collapse='","'), + '"],"rownames":["', + paste(row.names(data), collapse='","'), + '"],"topbar":{', + paste(topbar.json, collapse=','), + '},"sidebar":{', + paste(sidebar.json, collapse=','), + '},"colDendrogram":', + dendrogram2json(heatmap.out$colDendrogram)[[1]], + ',"rowDendrogram":', + dendrogram2json(heatmap.out$rowDendrogram)[[1]], + '}', + sep='') + + return (data.json) +} + +################ +# Get the groups with the highest mean +# Args: +# (1) data (numeric vector) The data +# (2) map (data-frame) Experimental design table with nrow=length(data) +# (3) model (character) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) verbose (character) Print messages? see PrintMsg() options +# Returns: +# A character vector with length=numer of terms in model +# Prints to stdout: +# A description of the function +################ +getGroupWithHighestMean <- function(data=NULL, map=NULL, column=NULL, verbose=TRUE) { + + PrintMsg('"description":"Determine groups with highest mean abundance."', + verbose=verbose) + + map <- as.data.frame(map[, unlist(strsplit(column, split=':'))]) + if (ncol(map) == 1) { + names(map) <- column + } + + group <- as.data.frame(unique(map)) + if (ncol(group) == 1) { + names(group) <- column + } + + mean <- c(1:nrow(group)) + for (i in 1:nrow(group)) { + keep <- 1 + for (j in 1:ncol(map)) { + keep <- keep*(map[, j] %in% group[i, names(map)[j]]) + } + mean[i] <- mean(unlist(data[as.logical(keep)])) + } + out <- list() + out$value <- max(mean) + out$id <- paste(unlist(group[which(mean == max(mean)), ]), collapse=':') + out$group <- apply(group, 1, paste, collapse=':') + + return (out) +} + +################ +# Build a network of correlations for variables +# Args: +# (1) table (numeric dataframe) Count table +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) metadata (numeric dataframe) Metadata table +# (5) fun (character) Distance, correlation or similarity function +# (6) json (character) Json string specifying packages +# (7) verbose (character) Print messages? see PrintMsg() options +# Returns: +# Figure data as a json string +# Prints to stdout: +# A description of the function +################ +BuildCorrelationNetwork <- function(table=NULL, map=NULL, stats='anova', + model=NULL, metadata=NULL, fun='spearman', + json=libJson, verbose=TRUE) { + + spec <- as.data.frame(do.call('rbind', json$correlation)) + spec <- spec[spec$value == fun, ] + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + package <- '' + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + PrintMsg(paste('"description":"Build network of variables based on ', + spec$label, package, '."', sep=''), + verbose=verbose) + + suppressMessages(library(igraph)) + + + # keep only columns of map that are used in the model + map <- FilterMap(map=map, model=model) + + # Combine with metadata + data <- rbind(table, metadata) + data.type <- rep('data', nrow(data)) + if (!is.null(metadata)) { + data.type[(nrow(table)+1):nrow(data)] <- "metadata" + } + + # Check that there is no constant data + RemoveConstantRows(table=data, verbose=TRUE) + + if ((nrow(data) < 3) || (ncol(data) < 3)) { + PrintMsg('"error":"Not enough data to compute a network (min 3 rows and 3 columns)."', TRUE, TRUE) + } + + # Calculate the correlation matrix + weight <- ComputeCorrelation(table=data, fun=fun, test=TRUE, + json=json, verbose=FALSE) + + nmodel <- length(model) + nnodes <- nrow(data) + node.json <- c(1:nnodes) + link.json <- c(1:(nnodes*(nnodes/2-1))) + l <- 1 + legend.json <- list() + for (i in 1:nnodes) { + model.json <- c(1:nmodel) + for (j in 1:nmodel) { + stat <- ComputeStats(response=as.vector(unlist(data[i, ])), map=map, + method=stats, model=model[j], pairwise=FALSE, + verbose=FALSE)$summary + neffect <- length(row.names(stat)[row.names(stat) != 'Residuals']) + effect.json <- c(1:neffect) + if (length(legend.json[[model[j]]]) == 0) { + legend.json[[model[j]]] <- c(1:neffect) + } + for (k in 1:neffect) { + gwhm <- getGroupWithHighestMean(data=data[i, ], map=map, column=row.names(stat)[k], verbose=FALSE) + effect.json[k] <- paste('"', + row.names(stat)[k], + '":{"p-value":"', + stat[k, ], + '","highest-mean":"', + gwhm$id, + '"}', + sep='') + if (i == 1) { + legend.json[[model[j]]][k] <- paste('"', + row.names(stat)[k], + '":["', + paste(gwhm$group, collapse='","'), + '"]', + sep='') + } + } + model.json[j] <- paste('"', + model[j], + '":{', + paste(effect.json, collapse=','), + '}', + sep='') + if (i == 1) { + legend.json[[model[j]]] <- paste('"', + model[j], + '":{', + paste(legend.json[[model[j]]], collapse=','), + '}', + sep='') + } + } + node.json[i] <- paste('{"id":"', + i-1, + '","name":"', + row.names(data)[i], + '","data-type":"', + data.type[i], + '","stat":{', + paste(model.json, collapse=','), + '},"mean":"', + mean(unlist(data[i, ])), + '","min":"', + min(unlist(data[i, ])), + '","max":"', + max(unlist(data[i, ])), + '"}', + sep='') + if (i < nnodes) { + for (j in (i+1):nnodes) { + link.json[l] <- paste('{"source":"', + i-1, + '","target":"', + j-1, + '","weight":"', + weight$estimate[i, j], + '","p-value":"', + weight$p.value[i, j], + '"}', + sep='') + + l <- l+1 + } + } + } + + return (paste('{"nodes":[', + paste(node.json, collapse=','), + '],"links":[', + paste(link.json, collapse=','), + '],"legend":{', + paste(unlist(legend.json), collapse=','), + '}}', + sep='')) + +} + +################ +# Build a network of similarity for samples +# Args: +# (1) tables (list of numeric dataframes) +# (2) map (data-frame) Experimental design table with nrow=ncol(table) +# (3) model (character) A formula representing the model for stats. Terms +# must refer to names(map) +# (4) funs (list of characters) Distance or similarity functions +# (5) clust (character) Network clustering algorithm +# (6) json (character) Json string specifying packages +# (7) verbose (character) Print messages? see PrintMsg() options +# (8) lib (character) Path to holynet library +# Returns: +# List [1] json string (fusion network) [2] data-frame (clusters) +# Prints to stdout: +# A description of the function +################ +BuildSimilarityNetwork <- function(table=NULL, map=NULL, funs=NULL, + clust="walktrap", clust.names=NULL, + json=libJson, verbose=TRUE, + lib='holynetlib.R') { + source(lib) + + spec <- as.data.frame(do.call('rbind', json$distance)) + for (i in 1:length(table)) { + fun <- funs[1] + if ((length(funs) == length(table)) && (length(funs) > 1)) { + fun <- funs[i] + } + spec <- spec[spec$value == fun, ] + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + package <- '' + if (length(pkg) > 0) { + for (j in 1:length(pkg)) { + suppressMessages(library(pkg[j], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + PrintMsg(paste('"description":"Build similarity network based on ', + spec$label, package, '."', sep=''), + verbose=verbose) + } + + # Calculate similarity matrices + mat <- list() + for (i in 1:length(table)) { + fun <- funs[1] + if (length(funs) == length(table)) { + fun <- funs[i] + } + mat[[i]] <- CreateAdjacencyMatrix(CreateSimilarityMatrix(table[[i]], fun)) + } + + # If multiple matrices exist, fuse them + if (length(mat) > 1) { + fusmat <- FuseMatrices(mat) + if (is.null(fusmat)) { + PrintMsg('"error":"Trying to fuse matrices but dimensions are not matching."', TRUE, TRUE) + } + mat[[(length(mat)+1)]] <- fusmat + } + + out <- list() + + # Compute clustering on similarity matrices + if (!is.null(clust) && (clust != 'none')) { + + spec <- as.data.frame(do.call('rbind', json$graph_clustering)) + spec <- spec[spec$value == clust, ] + pkg <- unlist(strsplit(as.character(unlist(spec$pkg)), split=',')) + package <- '' + if (length(pkg) > 0) { + for (i in 1:length(pkg)) { + suppressMessages(library(pkg[i], character.only=TRUE)) + } + package <- paste(' (R package {', unlist(spec$pkg), '})', sep='') + } + + PrintMsg(paste('"meassage":"Apply clustering using the ', + spec$label, package, '."', sep=''), + verbose=verbose) + + clusters <- matrix(ncol=length(mat), nrow=nrow(mat[[1]])) + for (i in 1:length(mat)) { + clusters[, i] <- paste(clust, + as.character(ApplyNetworkClustering(CreateNetwork(mat[[i]]), clust_method=clust)), + sep='') + } + clusters <- as.data.frame(clusters) + if (!is.null(clust.names)) { + names(clusters) <- clust.names + } + out[['txt']] <- clusters + map <- cbind(map, clusters) + } + + # Pack data into a json string + json.data <- buildFusionNetworkJson(mat, map) + + out[['json']] <- json.data + return (out) +} + +################################# +# (\/)(°,,,°)(\/) # +################################# +libJson <- ReadJson() + diff --git a/lib/genocrunch_console/lib/genocrunchlib.py b/lib/genocrunch_console/lib/genocrunchlib.py new file mode 100755 index 0000000..898a0c7 --- /dev/null +++ b/lib/genocrunch_console/lib/genocrunchlib.py @@ -0,0 +1,996 @@ +# -*- coding: utf-8 -*- +#genocrunchlib.py + +import os, sys, argparse, tarfile +from json import load, loads, dump +from subprocess import Popen, PIPE +from shutil import copyfile, move, rmtree +from itertools import izip +from collections import OrderedDict +from tempfile import mkdtemp, NamedTemporaryFile +from random import choice +from string import ascii_uppercase, digits +from copy import copy +from datetime import datetime + +__dir__ = os.path.dirname(__file__) +main_dir = '/'.join(__dir__.split('/')[0:len(__dir__.split('/'))-1]) + +def Error(f=__file__, msg='', to_rm=[]): + + print 'Error ['+ os.path.basename(f) +']: '+ msg + if len(to_rm) > 0: + for t in to_rm: + rmtree(t) + raise SystemExit(1) + + +def Warning(f=__file__, msg=''): + + print 'Warning ['+ os.path.basename(f) +']: '+ msg + + +def mkdir(dp=''): + """Create a directory if not already present + Args: + dp Directory path + """ + + if not os.path.exists(dp): + os.makedirs(dp) + return 0 + +def rand(n=1): + """Generate a random string + n Length of the random string + """ + + return ''.join(choice(ascii_uppercase + digits) for _ in range(n)) + +class Parameters(object): + """Parameters retrieved from genocrunchlib.json, from user's json and passed in command line""" + + def __init__(self, json_fp=''): + """Populate the object + json_fp Json file path + """ + + # Set authorized arguments for cli + argument_parser = argparse.ArgumentParser() + argument_parser._action_groups.pop() + required_arguments = argument_parser.add_argument_group('required arguments') + optional_arguments = argument_parser.add_argument_group('optional arguments') + optional_arguments.add_argument('--params_fp', + help='Path to a JSON file containing parameters.', + nargs=1, + type=str) + optional_arguments.add_argument('--analysis', + help='List of analysis to perform (comma-separated). Valid choices: proportions,diversity,adonis,pca,pcoa,heatmap,change,correlation_network,clustering,similarity_network', + nargs=1, + default='proportions,pcoa', + type=str) + required_arguments.add_argument('--output', + help='Path to output directory.', + nargs=1, + type=str) + + # Define parameters based on the json + self.json_fp = json_fp + with open(self.json_fp, 'r') as f: + self.json_s = load(f) + + self.fps = [] + required = [] + for key, value in self.json_s.get('fields').iteritems(): + for val in value: + if 'scope' not in val.keys() or val.get('scope') != 'form_only': + if 'default' in val.keys(): + default = val.get('default') + else: + default=None + # Add authorized cli arguments from the json + if 'optional' in val.keys() and val.get('optional') == False: + required.append(val.get('id')) + required_arguments.add_argument('--'+val.get('id'), + help=val.get('help'), + nargs=1, + default=default, + type=str) + else: + optional_arguments.add_argument('--'+val.get('id'), + help=val.get('help'), + nargs=1, + default=default, + type=str) + if val.get('type') == 'file': + self.fps.append(val.get('id')) + + # Retrieve cli arguments + self.params = argument_parser.parse_args().__dict__ + + # Set params values from user's json if provided (does not overwrite parameters passed in command line) + # Careful with booleans as form checkboxes return 1 or nothing... + for key, value in self.params.iteritems(): + if isinstance(value, list): + self.params[key] = value[0] + + if self.params['params_fp'] is not None and os.path.exists(self.params['params_fp']): + with open(self.params.get('params_fp'), 'r') as f: + params = load(f) + for key, value in params.items(): + if '--'+key not in sys.argv[1:]: + if value in ['', [], ['']]: + self.params[key] = None + else: + self.params[key] = value + + # Handle booleans + for e in ['prim_rarefaction', 'sec_rarefaction']: + if e not in params.keys(): + self.params[e] = False + elif params[e] in ['', ' ', '0']: + self.params[e] = False + else: + self.params[e] = bool(params[e]) + + if '--analysis' in sys.argv[1:]: + for a in self.params['analysis'].split(','): + self.params[a] = 1 + self.params.pop('analysis') + + for key, value in self.params.iteritems(): + if value is not None: + if isinstance(value, str) and len(value.split(',')) > 1: + self.params[key] = value.split(',') + elif key in required: + Error(msg='argument --'+key+' is requireds') + + # Check that all files exist + missing = [] + for fp in self.fps: + if self.params[fp] is not None and not os.path.exists(self.params[fp]): + missing.append('--'+fp+' '+self.params[fp]) + if len(missing) > 0: + Error(msg='The following file(s) were not found: '+' ,'.join(missing)) + + def write(self, fp=''): + """Write all parameters to a file + fp File path + """ + + with open(fp, 'w') as f: + dump(self.params, f, indent=2) + + +def table2R(fp): + """Adapt file format for easy loading in R: + Remove comments from the top of the file (also empty first element of first line) + """ + sub = Popen([main_dir+'/bin/modify_table.sh', + fp, + 'table2R', + fp]) + sub.wait() + + +def stdoutLog2json(stdout): + """Convert stdout to json string""" + + return loads('[{'+'"},{"'.join(stdout.read().split('""'))+'}]') + + +class Log(object): + """A log file""" + + def __init__(self, fp): + + self.fp = fp + self.data = [] + self.update() + + def update(self): + """Write log data to file""" + + with open(self.fp, 'w') as f: + dump(self.data, f, indent=2) + + self.secure(self.fp+'.bkp') + + + def secure(self, fp=None): + """Remove absolute paths from the log""" + + if fp is None: + fp = self.fp + + path_to_hide = os.path.dirname(self.fp) + f = open(self.fp, 'r') + content = f.read() + f.close() + new_content = content.replace(path_to_hide,'') + f = open(fp, 'w') + f.write(new_content) + f.close() + + def wrapup(self): + """Update pending and runing status to failed and update""" + + f = open(self.fp, 'r') + content = f.read() + f.close() + new_content = content.replace('running','failed').replace('pending','failed') + f = open(self.fp, 'w') + f.write(new_content) + f.close() + self.secure(self.fp+'.bkp') + +class Archive(object): + """An archive""" + + def __init__(self, target_fp='', name=None): + + self.fp = target_fp + self.source = [] + self.add(name) + + def add(self, name=None, update=False, generate=True): + """Add files to the archive""" + + if name is not None: + if isinstance(name, list): + self.source = list(set(self.source + name)) + else: + self.source = list(set(self.source + [name])) + + if update and generate: + self.update() + elif generate: + with tarfile.open(self.fp, "w:gz") as archive: + archive.add(name) + + + def update(self): + """Update files in archive""" + + if os.path.exists(self.fp): + tmp = NamedTemporaryFile(delete=False, dir=os.path.dirname(self.fp)) + os.rename(self.fp, tmp.name) + + with tarfile.open(self.fp, "w:gz") as archive: + for e in self.source: + archive.add(e) + + +class Map(object): + """Mapping file""" + + def __init__(self, fp, output): + + self.fp = output+'/map.txt' + copyfile(fp, self.fp) + self.log = [] + self.log.append({'name':'file', 'type':'file', 'path':self.fp}) + self.log.append({'name':'validation', 'type':'validation', 'status':'pending', 'messages':[]}) + + def validate(self): + + log = [e for e in self.log if e['name'] == 'validation'][0] + log['status'] = 'running' + start_time = datetime.now() + log['start_time'] = str(start_time) + sub = Popen([main_dir+'/bin/validate_format.R', + '-m', + 'validate_map', + '--map', + self.fp], + stdout=PIPE, + stderr=PIPE) + sub.wait() + log['status'] = 'completed' + log['messages'] = stdoutLog2json(sub.stdout) + + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + Error(msg='Error detected in '+self.fp) + log['status'] = 'completed' + log['execution_time'] = str(datetime.now() - start_time) + +class DataSet(object): + """Dataset""" + + _pipeline = [{'name':'pre-processing', 'fun':'preProcess', 'operations':[ + 'filtering', + 'binning' + ]}, + {'name':'transformation', 'fun':'transform', 'operations':[ + 'rarefaction', + 'transformation', + 'batch_effect_suppression' + ]} + ] + + def __init__(self, + json_fp = '', + pipeline = [], + data_fp = '', + output = '', + map = '', + category_column = '', + abundance_threshold = '', + abundance_threshold_type = '', + presence_threshold = '', + presence_threshold_type = '', + bin_levels = [], + bin_fun = '', + rarefy = False, + rarefaction_depth = '', + nsampling = '', + transformation = '', + batch_effect = ''): + + self.json_fp = json_fp + self.pipeline = self._pipeline + if pipeline != []: + self.pipeline = pipeline + + self.output = output + self.data_fp = [self.output+'/'+os.path.basename(data_fp)] + self.tmpdir = os.path.normpath(self.output+'/../'+mkdtemp()) + mkdir(self.tmpdir) + copyfile(data_fp, self.data_fp[0]) + table2R(self.data_fp[0]) + self.map = map + self.category_column = category_column + self.abundance_threshold = abundance_threshold + self.abundance_threshold_type = abundance_threshold_type + self.presence_threshold = presence_threshold + self.presence_threshold_type = presence_threshold_type + self.bin_levels = bin_levels + self.bin_fun = bin_fun + self.rarefy = rarefy + self.rarefaction_depth = rarefaction_depth + self.nsampling = nsampling + self.transformation = transformation + self.batch_effect = batch_effect + + self.log = [] + self.log.append({'name':'file', 'type':'file', 'path':self.data_fp[0]}) + self.log.append({'name':'validation', 'type':'validation', 'status':'pending', 'messages':[]}) + self.log.append({'name':'sorting', 'type':'operation', 'status':'pending', 'messages':[]}) + for p in self.pipeline: + op = [] + for e in p['operations']: + op.append({'name':e, 'status':'pending', 'messages':[]}) + self.log.append({'name':p['name'], 'type':'step', 'operations':op}) + op = [] + self.stdout = [] + self.stderr = [] + + def validate(self): + log = [e for e in self.log if e['name'] == 'validation'][0] + log['status'] = 'running' + start_time = datetime.now() + log['start_time'] = str(start_time) + for fp in self.data_fp: + sub = Popen([main_dir+'/bin/validate_format.R', + '-t', + fp, + '-m', + 'validate_dataset', + '--map', + self.map.fp, + '--category_column', + str(self.category_column)], + stdout=PIPE, + stderr=PIPE) + sub.wait() + log['messages'].extend(stdoutLog2json(sub.stdout)) + log['status'] = 'completed' + log['execution_time'] = str(datetime.now() - start_time) + + def sort(self): + + log = [e for e in self.log if e['name'] == 'sorting'][0] + log['status'] = 'running' + start_time = datetime.now() + log['start_time'] = str(start_time) + tmp = NamedTemporaryFile(delete=False, dir=self.tmpdir) + for fp in self.data_fp: + sub = Popen([main_dir+'/bin/modify_table.R', + '-t', + fp, + '-m', + 'sorting', + '--map', + self.map.fp, + '--ignore', + str(self.category_column), + '-o', + fp, + '--log', + str(tmp.name)], + stdout=PIPE, + stderr=PIPE) + sub.wait() + self.appendStdout(sub.stdout.read()) + self.appendStderr(sub.stderr.read()) + if os.path.exists(tmp.name): + with open(tmp.name, 'r') as t: + log['messages'].extend(stdoutLog2json(t)) + os.remove(tmp.name) + tmp.close() + log['status'] = 'completed' + log['execution_time'] = str(datetime.now() - start_time) + + def preProcess(self, fun=''): + + log = [e for e in [o for o in self.log if o['name'] == 'pre-processing'][0]['operations'] if e['name'] == fun][0] + log['status'] = 'running' + start_time = datetime.now() + log['start_time'] = str(start_time) + tmp = NamedTemporaryFile(delete=False, dir=self.tmpdir) + args = [main_dir+'/bin/modify_table.R', + '-m', + fun, + '--log', + str(tmp.name)] + + if fun == 'filtering': + if (self.abundance_threshold is None or self.abundance_threshold == 0) and (self.presence_threshold is None or self.presence_threshold == 0): + log['status'] = 'skipped' + return(0) + args.extend(['--column', + str(self.category_column), + '--abundance_threshold', + self.abundance_threshold, + '--abundance_threshold_type', + self.abundance_threshold_type, + '--presence_threshold', + self.presence_threshold, + '--presence_threshold_type', + self.presence_threshold_type]) + elif fun == 'binning': + if self.bin_levels is None: + log['status'] = 'skipped' + return(0) + args.extend(['--column', + str(self.category_column), + '--fun', + self.bin_fun]) + + output_fps = [] + for fp in self.data_fp: + args.extend(['-t', fp]) + if fun == 'filtering': + output_fp = fp +'_filtered.txt' + output_fps.append(output_fp) + args.extend(['-o', output_fp]) + sub = Popen(args, stdout=PIPE, stderr=PIPE) + sub.wait() + self.appendStdout(sub.stdout.read()) + self.appendStderr(sub.stderr.read()) + + elif fun == 'binning': + for level in self.bin_levels: + output_fp = fp+'_lvl'+level+'.txt' + output_fps.append(output_fp) + args.extend(['--level', level, '-o', output_fp]) + sub = Popen(args, stdout=PIPE, stderr=PIPE) + sub.wait() + self.appendStdout(sub.stdout.read()) + self.appendStderr(sub.stderr.read()) + if os.path.exists(tmp.name): + with open(tmp.name, 'r') as t: + log['messages'].extend(stdoutLog2json(t)) + os.remove(tmp.name) + tmp.close() + log['status'] = 'completed' + log['execution_time'] = str(datetime.now() - start_time) + self.data_fp = output_fps + + def transform(self, fun=''): + + log = [e for e in [o for o in self.log if o['name'] == 'transformation'][0]['operations'] if e['name'] == fun][0] + log['status'] = 'running' + start_time = datetime.now() + log['start_time'] = str(start_time) + tmp = NamedTemporaryFile(delete=False, dir=self.tmpdir) + args = [main_dir+'/bin/modify_table.R', + '--log', + str(tmp.name), + '--ignore', + str(self.category_column)] + + if fun == 'rarefaction': + if self.rarefy is None or self.rarefy == False: + log['status'] = 'skipped' + return(0) + args.extend(['-m', + 'rarefaction', + '--sample', + self.rarefaction_depth, + '--nsampling', + self.nsampling]) + + elif fun == 'transformation': + if self.transformation is None or self.transformation in ['none', '', ' ']: + log['status'] = 'skipped' + return(0) + args.extend(['-m', self.transformation]) + + elif fun == 'batch_effect_suppression': + if self.batch_effect is None: + log['status'] = 'skipped' + return(0) + args.extend(['-m', + 'batch_effect_suppression', + '--map', + self.map.fp, + '--effect', + self.batch_effect, + '--fun', + 'combat']) + + suffix = {'rarefaction':'rar', + 'transformation':'trans', + 'batch_effect_suppression':'bes'} + output_fps = [] + for fp in self.data_fp: + output_fp = ('.').join(fp.split('.')[:-1])+'_'+suffix[fun]+'.txt' + output_fps.append(output_fp) + args.extend(['-t', fp, '-o', output_fp]) + sub = Popen(args, stdout=PIPE, stderr=PIPE) + sub.wait() + self.appendStdout(sub.stdout.read()) + self.appendStderr(sub.stderr.read()) + if os.path.exists(tmp.name): + with open(tmp.name, 'r') as t: + log['messages'].extend(stdoutLog2json(t)) + os.remove(tmp.name) + tmp.close() + log['status'] = 'completed' + log['execution_time'] = str(datetime.now() - start_time) + self.data_fp = output_fps + + def appendStdout(self, stdout): + + if stdout is not None and stdout != '': + self.stdout.append(stdout) + + def appendStderr(self, stderr): + + if stderr is not None and stderr != '': + self.stderr.append(stderr) + + +class Analysis(object): + """THE ANALYSIS""" + + def __init__(self, json_fp): + """initialize the analysis pipeline + json_fp File path to json + """ + + # INITIALIZE THE PIPELINE + self.pipeline = [{'name':'diversity', 'before_step':'transformation', 'status':'pending', 'messages':[]}, + {'name':'clustering', 'before_step':'analysis', 'status':'pending', 'messages':[]}, + {'name':'proportions', 'status':'pending', 'messages':[]}, + {'name':'adonis', 'status':'pending', 'messages':[]}, + {'name':'pca', 'status':'pending', 'messages':[]}, + {'name':'ca', 'status':'pending', 'messages':[]}, + {'name':'pcoa', 'status':'pending', 'messages':[]}, + {'name':'cca', 'status':'pending', 'messages':[]}, + {'name':'heatmap', 'status':'pending', 'messages':[]}, + {'name':'change', 'status':'pending', 'messages':[]}, + {'name':'correlation_network', 'status':'pending', 'messages':[]}, + {'name':'similarity_network', 'status':'pending', 'messages':[]}] + + # Set parameters from json + self.parameters = Parameters(json_fp) + + # Make the output directory + self.output = self.parameters.params['output'] + if os.path.exists(self.output): + rmtree(self.output) + mkdir(self.output) + self.tmpdir = os.path.normpath(self.output+'/../'+mkdtemp()) + mkdir(self.tmpdir) + self.to_rm = [self.tmpdir] + + # Set the archive + #self.archive = Archive(self.output+'/output.tar.gz') + # Store parameters to output directory + self.parameters.write(self.output+'/params.json') + + # Start logging + self.log = Log(self.output+'/log.json') + self.stdout = Log(self.output+'/stdout.log') + self.stderr = Log(self.output+'/stderr.log') + + def run(self): + """Run the analysis""" + + # Create data sets + self.map = Map(self.parameters.params['map'], + self.output) + self.log.data.append({'name':'map', 'type':'map', 'log':self.map.log}) + self.log.update() + #self.archive.add(self.log.fp+'.bkp', update=False, generate=False) + log = [e for e in self.map.log if e['name'] == 'validation'][0] + log['status'] = 'running' + self.log.update() + self.map.validate() + + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + self.log.update() + Error(msg='Error detected in '+self.map.fp, to_rm=self.to_rm) + self.log.update() + + if self.parameters.params['bin_levels'] is None or isinstance(self.parameters.params['bin_levels'], list): + bin_levels = self.parameters.params['bin_levels'] + else: + bin_levels = self.parameters.params['bin_levels'].split(',') + + self.primary_dataset = DataSet(json_fp = self.parameters.json_fp, + data_fp = self.parameters.params['primary_dataset'], + output = self.output, + map = self.map, + category_column = self.parameters.params['category_column'], + abundance_threshold = self.parameters.params['abundance_threshold'], + abundance_threshold_type = self.parameters.params['abundance_threshold_type'], + presence_threshold = self.parameters.params['presence_threshold'], + presence_threshold_type = self.parameters.params['presence_threshold_type'], + bin_levels = bin_levels, + bin_fun = self.parameters.params['bin_fun'], + rarefy = bool(self.parameters.params['prim_rarefaction']), + rarefaction_depth = self.parameters.params['prim_sampling_depth'], + nsampling = self.parameters.params['prim_nsampling'], + transformation = self.parameters.params['prim_trans_method'], + batch_effect = self.parameters.params['prim_batch_effect_suppression']) + self.log.data.append({'name':'primary_dataset', 'type':'dataset', 'log':self.primary_dataset.log}) + self.log.update() + self.stdout.data.append({'primary_dataset':self.primary_dataset.stdout}) + self.stderr.data.append({'primary_dataset':self.primary_dataset.stderr}) + self.to_rm.append(self.primary_dataset.tmpdir) + #self.archive.add(self.primary_dataset.data_fp, update=False, generate=False) + + log = [e for e in self.primary_dataset.log if e['name'] == 'validation'][0] + log['status'] = 'running' + self.log.update() + self.primary_dataset.validate() + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + self.log.update() + Error(msg='Error detected in primary_dataset.validate()', to_rm=self.to_rm) + self.log.update() + + log = [e for e in self.primary_dataset.log if e['name'] == 'sorting'][0] + log['status'] = 'running' + self.log.update() + self.primary_dataset.sort() + self.stdout.update() + self.stderr.update() + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + self.log.update() + Error(msg='See'+self.log.fp+'.', to_rm=self.to_rm) + self.log.update() + + if self.parameters.params['secondary_dataset'] is not None: + self.secondary_dataset = DataSet(json_fp = self.parameters.json_fp, + pipeline = [{'name':'transformation', + 'fun':'transform', + 'operations':['rarefaction', + 'transformation', + 'batch_effect_suppression']}], + data_fp = self.parameters.params['secondary_dataset'], + output = self.output, + map = self.map, + category_column = '', + rarefy = bool(self.parameters.params['sec_rarefaction']), + rarefaction_depth = self.parameters.params['sec_sampling_depth'], + nsampling = self.parameters.params['sec_nsampling'], + transformation = self.parameters.params['sec_trans_method'], + batch_effect = self.parameters.params['sec_batch_effect_suppression']) + self.log.data.append({'name':'secondary_dataset', 'type':'dataset', 'log':self.secondary_dataset.log}) + self.log.update() + self.stdout.data.append({'secondary_dataset':self.secondary_dataset.stdout}) + self.stderr.data.append({'secondary_dataset':self.secondary_dataset.stderr}) + self.to_rm.append(self.secondary_dataset.tmpdir) + + log = [e for e in self.secondary_dataset.log if e['name'] == 'validation'][0] + log['status'] = 'running' + self.log.update() + self.secondary_dataset.validate() + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + self.log.update() + Error(msg='Error detected in secondary_dataset.validate()', to_rm=self.to_rm) + self.log.update() + + log = [e for e in self.secondary_dataset.log if e['name'] == 'sorting'][0] + log['status'] = 'running' + self.log.update() + self.secondary_dataset.sort() + self.stdout.update() + self.stderr.update() + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + self.log.update() + Error(msg='See'+self.log.fp+'.', to_rm=self.to_rm) + self.log.update() + + # RUN DATASET PIPELINE + self.log.data.append({'name':'analysis', 'type':'analysis', 'log':self.pipeline}) + self.stdout.data.append({'analysis':[]}) + self.stderr.data.append({'analysis':[]}) + + datasets = [self.primary_dataset] + if self.parameters.params['secondary_dataset'] is not None: + datasets.append(self.secondary_dataset) + + for dataset in datasets: + for step in dataset.pipeline: + if dataset == datasets[0]: + analyses = [a for a in [e for e in self.pipeline if 'before_step' in e.keys()] if a['before_step'] == step['name']] + for analysis in analyses: + eval('self.analysis("'+analysis['name']+'")') + + for operation in step['operations']: + log = [o for o in [ s for s in dataset.log if s['name'] == step['name']][0]['operations'] if o['name'] == operation][0] + log['status'] = 'running' + self.log.update() + eval('dataset.'+step['fun']+'("'+operation+'")') + self.stdout.update() + self.stderr.update() + if len([e for e in log['messages'] if 'error' in e.keys()]) > 0: + log['status'] = 'failed' + self.log.update() + Error(msg='See'+self.log.fp+'.', to_rm=self.to_rm) + self.log.update() + + # COMPLETE ANALYSIS PIPELINE + analyses = [a for a in [e for e in self.pipeline if 'before_step' in e.keys()] if a['before_step'] == 'analysis'] + for analysis in analyses: + eval('self.analysis("'+analysis['name']+'")') + analyses = [a for a in [e for e in self.pipeline if 'before_step' not in e.keys()]] + + for analysis in analyses: + eval('self.analysis("'+analysis['name']+'")') + + self.cleanup() + + + def cleanup(self): + + self.log.wrapup() + self.stdout.update() + self.stderr.update() + #self.archive.update() + + for t in self.to_rm: + rmtree(t) + + + def analysis(self, method=''): + """Run R scripts to generate figures and stats""" + + log = [e for e in [l for l in self.log.data if l['name'] == 'analysis'][0]['log'] if e['name'] == method][0] + stdout = [ e for e in self.stdout.data if e.keys()[0] == 'analysis'][0]['analysis'] + stderr = [ e for e in self.stderr.data if e.keys()[0] == 'analysis'][0]['analysis'] + + if method not in self.parameters.params.keys() or self.parameters.params[method] == 0: + log['status'] = 'skipped' + self.log.update() + return(0) + + log['status'] = 'running' + start_time = datetime.now() + log['start_time'] = str(start_time) + self.log.update() + append_to_map = False + output = self.output+'/'+method + mkdir(output) + tmp = NamedTemporaryFile(delete=False, dir=self.tmpdir) + + # Set general arguments + args = [main_dir+'/bin/analyse_table.R', + '--graphical', + 'TRUE', + '--log', + str(tmp.name), + '--method', + method, + '--map', + self.map.fp, + '--category', + self.primary_dataset.category_column] + + # Set analysis-specific arguments + if method == 'diversity': + if self.parameters.params['diversity_metric'] is not None: + args.extend(['--fun', + ','.join(self.parameters.params['diversity_metric'])]) + if self.parameters.params['compare_diversity'] is not None: + args.extend(['--compare_diversity', + self.parameters.params['compare_diversity']]) + + elif method == 'clustering': + append_to_map = True + if self.parameters.params['clustering_fun'] is not None: + clustering_fun = self.parameters.params['clustering_fun'] + if isinstance(clustering_fun, list): + clustering_fun = ','.join(clustering_fun) + args.extend(['--fun', + clustering_fun]) + + elif method == 'adonis': + if self.parameters.params['adonis_model'] is not None: + adonis_model = self.parameters.params['adonis_model'] + if isinstance(adonis_model, list): + adonis_model = ','.join(adonis_model) + args.extend(['--adonis_model', + adonis_model]) + if self.parameters.params['adonis_distfun'] is not None: + adonis_distfun = self.parameters.params['adonis_distfun'] + if isinstance(adonis_distfun, list): + adonis_distfun = ','.join(adonis_distfun) + args.extend(['--fun', + adonis_distfun]) + if self.parameters.params['adonis_strata'] is not None: + adonis_strata = self.parameters.params['adonis_strata'] + if isinstance(adonis_strata, list): + adonis_strata = ','.join(adonis_strata) + args.extend(['--strata', + adonis_strata]) + + elif method == 'pcoa': + pcoa_distfun = self.parameters.params['pcoa_distfun'] + if pcoa_distfun is not None: + if isinstance(pcoa_distfun, list): + pcoa_distfun = ','.join(pcoa_distfun) + args.extend(['--fun', + str(pcoa_distfun)]) + + elif method == 'cca': + if self.parameters.params['cca_categ'] is not None: + cca_categ = self.parameters.params['cca_categ'] + if isinstance(cca_categ, list): + cca_categ = ','.join(cca_categ) + args.extend(['--column', + cca_categ]) + else: + log['messages'].append({'warning':'No categorical data was given in CCA. Please provide the name(s) of column(s) in map that contain(s) categorical data to be used in CCA.'}) + log['status'] = 'skipped' + self.log.update() + return(0) + + elif method == 'heatmap' and self.parameters.params['secondary_dataset'] is not None: + args.extend(['--metadata', + self.secondary_dataset.data_fp[0]]) + + elif method == 'correlation_network': + if self.parameters.params['correlation_network_fun'] is not None: + correlation_network_fun = self.parameters.params['correlation_network_fun'] + if isinstance(correlation_network_fun, list): + correlation_network_fun = ','.join(correlation_network_fun) + args.extend(['--fun', + correlation_network_fun]) + if self.parameters.params['secondary_dataset'] is not None: + args.extend(['--metadata', + self.secondary_dataset.data_fp[0]]) + + elif method == 'similarity_network': + append_to_map = True + if self.parameters.params['similarity_network_fun1'] is not None: + fun = self.parameters.params['similarity_network_fun1'] + if self.parameters.params['secondary_dataset'] is not None and self.parameters.params['similarity_network_fun2'] is not None: + fun = fun+','+self.parameters.params['similarity_network_fun2'] + args.extend(['--fun', fun]) + if self.parameters.params['similarity_network_clust'] is not None: + args.extend(['--clust', + self.parameters.params['similarity_network_clust']]) + if self.parameters.params['secondary_dataset'] is not None: + args.extend(['--metadata', + self.secondary_dataset.data_fp[0]]) + + if len(self.primary_dataset.data_fp) > 1: + if len(self.primary_dataset.data_fp) != len(self.parameters.params['bin_levels']): + Error(msg='Analysis cannot be performed at specified binning levels because corresponding data files were not found.', + to_rm=self.to_rm) + + log['levels'] = log.pop('messages') + for i in range(0, len(self.parameters.params['bin_levels'])): + log['levels'].append({'level':self.parameters.params['bin_levels'][i], 'status':'pending', 'messages':[]}) + + for i in range(0, len(self.primary_dataset.data_fp)): + l = log + output_fp = output+'/'+method + if len(self.primary_dataset.data_fp) > 1: + l = [e for e in log['levels'] if e['level'] == self.parameters.params['bin_levels'][i]][0] + output_fp = output_fp+'_level_'+self.parameters.params['bin_levels'][i] + + l['status'] = 'running' + args_cp = copy(args) + + args_cp.extend(['-t', + self.primary_dataset.data_fp[i], + '-o', + output_fp]) + + if self.parameters.params['model_type'] == 'basic': + if isinstance(self.parameters.params['basic_model'], list): + m = ','.join(self.parameters.params['basic_model']) + else: + m = self.parameters.params['basic_model'] + args_cp.extend(['--model', + m]) + else: + if isinstance(self.parameters.params['advanced_model'], list): + m = ','.join(self.parameters.params['advanced_model']) + else: + m = self.parameters.params['advanced_model'] + args_cp.extend(['--stats', + self.parameters.params['advanced_stats'], + '--model', + m]) + + sub = Popen(args_cp, stdout=PIPE, stderr=PIPE) + sub.wait() + + s = sub.stdout.read() + print s + if s is not None and s != '': + stdout.append(s) + s = sub.stderr.read() + if s is not None and s != '': + stderr.append(s) + + if os.path.exists(tmp.name): + with open(tmp.name, 'r') as t: + l['messages'].extend(stdoutLog2json(t)) + os.remove(tmp.name) + tmp.close() + self.log.update() + + if append_to_map: + if os.path.exists(output_fp+'.txt') and os.stat(output_fp+'.txt').st_size != 1: + tmpmap = NamedTemporaryFile(delete=False, dir=self.tmpdir) + with open(tmpmap.name, 'w') as new, open(self.map.fp) as m, open(output_fp+'.txt') as c: + i = 0 + for line_m, line_c in zip(m, c): + to_add = line_c.rstrip().split('\t') + to_add.pop(0) + if i == 0: + while to_add in line_m.rstrip().split('\t'): + to_add = to_add+'_'+rand(3) + model_to_add = to_add + new.write('%s\t%s\n' % (line_m.rstrip(), '\t'.join(to_add))) + i = i+1 + move(tmpmap.name, self.map.fp) + + if self.parameters.params['model_type'] == 'basic': + if not isinstance(self.parameters.params['basic_model'], list): + self.parameters.params['basic_model'] = [self.parameters.params['basic_model']] + self.parameters.params['basic_model'].extend(model_to_add) + else: + if not isinstance(self.parameters.params['basic_model'], list): + self.parameters.params['advanced_model'] = [self.parameters.params['advanced_model']] + self.parameters.params['advanced_model'].extend(model_to_add) + else: + Warning(msg='In '+method+': output file not found ('+output_fp+'.txt'+').') + + if len([e for e in l['messages'] if 'error' in e.keys()]) > 0: + l['status'] = 'failed' + log['execution_time'] = str(datetime.now() - start_time) + log['status'] = 'failed' + self.log.update() + Error(msg='See'+self.log.fp+'.', to_rm=self.to_rm) + else: + l['status'] = 'completed' + self.log.update() + log['execution_time'] = str(datetime.now() - start_time) + log['status'] = 'completed' + self.log.update() + diff --git a/lib/genocrunch_console/lib/genocrunchlib.pyc b/lib/genocrunch_console/lib/genocrunchlib.pyc new file mode 100644 index 0000000..ea42a38 Binary files /dev/null and b/lib/genocrunch_console/lib/genocrunchlib.pyc differ diff --git a/lib/genocrunch_console/lib/genocrunchlib.sh b/lib/genocrunch_console/lib/genocrunchlib.sh new file mode 100755 index 0000000..c90a780 --- /dev/null +++ b/lib/genocrunch_console/lib/genocrunchlib.sh @@ -0,0 +1,240 @@ +#genocrunchlib.sh + +################ +# Error function +# Args: +# (1) Error message +# Global vars: +# __base script name +################ +err() { + echo "Error [${__base}]: $@" >&2 + exit 1 +} + +################ +# Message function +# Args: +# (1) categ Message category (ex: 'title', 'log', 'output', 'fig') +# (2) msg Message +# Global vars: +# __base script name +# Prints to stdout: +# 'msg' +################ +printmsg() { + local categ + categ="${1}" + + local msg + msg="${2}" + + local time + time=$(date +'%Y-%m-%d %X') + + printf "%s\t[%s]\t%s\t%s\n" "${__base}" "${time}" "${categ}" "${msg}" + + [[ "${msg}" == 'error' ]] && exit 1 + + return 0 +} + +################ +# Make a unique tmp directory +# Args: +# (1) Name to be used as a base for the creation of the new tmp directory +# Prints to stdout: +# The path to the created tmp diectory +################ +mktmpdir() { + local funcname + funcname='mktmpdir' + + local roottmp= + roottmp=/tmp + if ([[ ! -d "${roottmp}" ]] || [[ ! -w "${roottmp}" ]]) ; then + roottmp="$(pwd)" + fi + + local tmpdir + tmpdir=$(mktemp -d "${roottmp}"/"${1}"XXX) + [[ ! -d "${tmpdir}" ]] && { mkdir "${tmpdir}" ; [[ "$?" -ne 0 ]] && \ + err "in ${funcname}() cannot create ${tmpdir}" ; } + [[ ! -w "${tmpdir}" ]] && err "in ${funcname}() write permission denied on ${tmpdir}" + echo "${tmpdir}" + + return 0 +} + +################ +# Remove comments and 1st element of the 1st row in a BIOM-derived txt table +# Args: +# (1) input_fp Path to input file in txt format +# (2) output_fp Path to output file +# Outputs: +# A modified version of 'input_fp' +################ +remove_comments() { + local funcname + funcname='remove_comments' + + local input_fp + input_fp="${1}" + [[ ! -e "${input_fp}" ]] && err "in ${funcname}() cannot find ${input_fp}" + + local output_fp + output_fp="${2}" + + if [[ "${input_fp}" != "${output_fp}" ]]; then + cp "${input_fp}" "${output_fp}" + fi + + local second_line_char + second_line_char=$(awk < "${output_fp}" -F'\t' 'NR==2{print substr ($1, 1, 1)}') + + # Delete the first line until the second line does not start with "#" + while [[ "${second_line_char}" = '#' ]]; do + + sed -i -e '1d' "${output_fp}" + second_line_char=$(awk < "${output_fp}" -F'\t' 'NR==2{print substr ($1, 1, 1)}') + done + + # Remove the 1st element of the 1st line + awk < "${output_fp}" -F'\t' 'NR==1{for(i=2;i<=NF;i++)printf "\t%s",$i;print ""}NR>1{print $0}' > "${output_fp}".tmp && mv "${output_fp}".tmp "${output_fp}" + + return 0 +} + +################ +# Test if a header is present in the first row of a table +# Args: +# (1) Table in txt format +# (2) Name of the column to test +# Returns: +# 0 if not present, column index if present +################ +test_header() { + local funcname + funcname='test_header' + + local input_fp + input_fp="${1}" # set input table file path + [[ ! -e "${input_fp}" ]] && err "in ${funcname}() cannot find ${input_fp}" + + local header + header="${2}" + + awk < "${input_fp}" -v header="${header}" -F'\t' 'NR==1{coli=0;for(i=1;i<=NF;i++)if($i==header){coli=i;break};print coli}' + + return 0 +} + + +################ +# Aggregate TXT table (like BIOM-derived TXT table) by a specific column containing ;-delimited elements (such as taxonomic levels) +# Args: +# 1 TXT table (1st row as header) +# 2 Name of the column to use for aggregation +# 3 Level to use for aggregation (numeric value) +# 4 Function to use for data aggregation (sum, mean, ...) +# 5 Output file +################ +aggregate_by_category_level() { + local funcname + funcname='aggregate_by_category_level' + + local tmpdir + tmpdir=$(mktmpdir "${funcname}") + + local table_fp + table_fp="${1}" + [[ ! -e "${table_fp}" ]] && err "in ${funcname}() cannot find ${table_fp}" + + local category + category="${2}" + + local level + level="${3}" + + local fun + fun="${4}" + + local output_fp + output_fp="${5}" + + cp "${table_fp}" "${tmpdir}"/table.tmp + + if [[ "${category}" == 'false' ]]; then + cp "${tmpdir}"/table.tmp "${output_fp}" + rm -rf "${tmpdir}" + return 0 + fi + + local category_index + category_index=$(test_header "${tmpdir}"/table.tmp "${category}") # get column index of the category + + if [[ "${category_index}" -eq 0 ]]; then + printmsg 'warning' "in ${funcname}() no column named ${category} found in ${tmpdir}/table.tmp. First column will be used as category column by default."; + cut -f1 "${table_fp}" | paste -d'\t' "${table_fp}" - > "${tmpdir}"/table.tmp + awk < "${tmpdir}"/table.tmp -v category="${category}" -F'\t' 'NR==1{for(i=2;i1{print $0}' > "${tmpdir}"/table.tmp.tmp && mv "${tmpdir}"/table.tmp.tmp "${tmpdir}"/table.tmp + category_index=$(awk < "${tmpdir}"/table.tmp -F'\t' 'NR==1{print NF}'); + fi + + + # Get category, change to 'Unknown' when needed and prune down to specified level + cut -f1,"${category_index}" "${tmpdir}"/table.tmp \ + | sed 's/^$/Unknown/g;s/^ $/Unknown/g;s/^ _*$/Unknown/g;s/^ [a-zA-Z]__$/Unknown/g;s/\t/;/g' \ + | awk -v level=$((${level}+1)) -v category_index="${category_index}" -F';' 'NR==1{print $NF} + NR>1{ + if(NF == 1) { + printf "%s\n",$2; + } else { + if(category_index == 1) { + start=2; + printf "%s",$1; + } else { + start=3; + printf "%s",$2; + } + for(i=start;i<=level;i++){ + if($i=="" || $i==" " || $i~/^ _*$/ || $i~/^ [a-zA-Z]__$/){ + if (category_index != 1){ + printf "(%s)\n", $1; + } + break; + }else{ + if(i==level){ + printf ";%s\n", $level; + break; + }else{ + printf ";%s", $i; + } + } + } + } + }' \ + | paste -d'\t' "${tmpdir}"/table.tmp - \ + | awk -v category_index="${category_index}" -F'\t' 'NR==1{ + for(i=2;i<=NF;i++){ + if(i!=category_index){ + printf "\t%s",$i; + }; + }; + print ""; + }; + NR>1{ + printf "%s", NR; + for(i=2;i<=NF;i++){ + if(i!=category_index){ + printf "\t%s", $i; + }; + }; + print ""; + }' > "${tmpdir}"/table.tmp.tmp && mv "${tmpdir}"/table.tmp.tmp "${tmpdir}"/table.tmp + + # Aggregate by category + modify_table.R -t "${tmpdir}"/table.tmp --method 'aggregation' --aggr.col "${category}" --fun "${fun}" --level "${level}" -o "$(dirname "${output_fp}")"/"$(basename "${output_fp}")" # aggregate rows by row names + + rm -rf "${tmpdir}" + return 0 +} diff --git a/lib/genocrunch_console/lib/holynetlib.R b/lib/genocrunch_console/lib/holynetlib.R new file mode 100755 index 0000000..5f0e1c7 --- /dev/null +++ b/lib/genocrunch_console/lib/holynetlib.R @@ -0,0 +1,107 @@ +#holynetlib.R + +################################################# +# Create Similarity Matrix +# Args: +# (1) table (data.frame) +# (2) dist_metric (character) Possible options are "bray", "jaccard", "canberra", "manhattan", "gower" +# Returns: A similarity matrix of nxn dimension (n = number of samples) according to a distance metric defined by the user +################################################# +CreateSimilarityMatrix <- function(table, dist_metric) { + suppressMessages(library(vegan)) + sim_table <- as.matrix(1-vegdist(t(table), method = dist_metric, binary = FALSE, diag = TRUE, upper = TRUE)) + diag(sim_table) <- NA + return(sim_table) +} + +################################################# +# Create Adjacency Matrix +# Args: +# (1) sim_matrix (matrix) +# Returns: An adjacency matrix calculated by using linear transformation of the similarity matrix +################################################# +CreateAdjacencyMatrix <- function(sim_matrix) { + adj_matrix <- (sim_matrix-min(sim_matrix, na.rm = TRUE))/(max(sim_matrix, na.rm = TRUE)-min(sim_matrix, na.rm = TRUE)) + diag(adj_matrix) <- 1 # Diagonal is required + return(adj_matrix) +} + +################################################# +# Test for identical row-names in multiple data-frames +# Args: +# (1) mat (list of data-frames) +# Returns: A logical value TRUE if the headers and dimensions match, FALSE otherwise +################################################# +HasIdenticalRowNames <- function(mat) { + for (i in 2:length(mat)) { + if (identical(rownames(mat[[i]]), rownames(mat[[1]])) == FALSE) { + return(FALSE) + } + } + return(TRUE) +} + +################################################# +# Fuse Matrices +# Args: +# (1) mat (list of numeric matrices) Adjacency matrices +# (2) k (numeric) (OPTIONAL) Number of neighbors in K-nearest neighbors part of the algorithm +# (3) t (numeric) Number of iterations for the diffusion process +# Returns: A fusion matrix +################################################# +FuseMatrices <- function(mat, k = ncol(mat[[1]])^(1/2), t = 100) { + if (HasIdenticalRowNames(mat) == TRUE) { + suppressMessages(library(SNFtool)) + fusion.matrix <- SNF(mat, k, t) + rownames(fusion.matrix) <- rownames(mat[[1]]) + colnames(fusion.matrix) <- colnames(mat[[1]]) + fusion.matrix <- CreateAdjacencyMatrix(fusion.matrix) + diag(fusion.matrix) <- NA + return(fusion.matrix) + } + return(NULL) +} + +################################################# +# Create network from adjacency matrix +# Args: +# (1) mat (matrix) matrix generated by the CreateAdjacencyMatrix function +# Returns: A graph object-type (igraph) +################################################# +CreateNetwork <- function(mat) { + suppressMessages(library(igraph)) + network <- graph_from_adjacency_matrix(mat, mode = c("undirected"), weighted = TRUE, diag = FALSE, + add.colnames = FALSE, add.rownames = FALSE) + V(network)$name <- paste(rownames(mat)) + return(network) +} + +################################################# +# Apply network-based clustering +# Args: +# (1) network (igraph) Network object generated by the CreateNetwork function +# (2) clust_method (character) Possible options are: "fastgreedy","louvain","walktrap","labelpropagation" of the igraph package +# Returns: A numeric vector with cluster number for each sample +################################################# +ApplyNetworkClustering <- function(network, clust_method) { + suppressMessages(library(igraph)) + if (clust_method == "fastgreedy") { + network_cluster <- cluster_fast_greedy(network, merges = TRUE, modularity = TRUE, membership = TRUE, + weights = E(network)$weight)} + + if (clust_method == "louvain") { + network_cluster <- cluster_louvain(network, weights = E(network)$weight)} + + if (clust_method == "walktrap") { + network_cluster <- cluster_walktrap(network, weights = E(network)$weight, steps = 4, merges = TRUE, modularity = TRUE, + membership = TRUE)} + + if (clust_method == "labelpropagation") { + network_cluster <- label.propagation.community(network, weights = E(network)$weight, initial = NULL, fixed = NULL)} + return(network_cluster$membership) +} + + + + + diff --git a/lib/tasks/init_keys.rake b/lib/tasks/init_keys.rake new file mode 100644 index 0000000..9ffd342 --- /dev/null +++ b/lib/tasks/init_keys.rake @@ -0,0 +1,12 @@ +desc '####################### Clean' +task init_keys: :environment do + puts 'Executing...' + + now = Time.now + + controller = ApplicationController.new + Job.all.select{|j| !j.key}.each do |j| + j.update_attribute(:key, controller.create_key()) + end + +end diff --git a/lib/tasks/run_job.rake b/lib/tasks/run_job.rake new file mode 100644 index 0000000..399a411 --- /dev/null +++ b/lib/tasks/run_job.rake @@ -0,0 +1,106 @@ +desc '####################### Run a job' +task :run_job, [:key] => [:environment] do |t, args| + + puts "Executing run_job #{args[:key]}..." + + job = Job.where(:key => args[:key]).first + user_dir = Pathname.new(APP_CONFIG[:data_dir]) + "users" + job.user_id.to_s + data_dir = user_dir + job.key.to_s + stdout_file = data_dir + "stdout.log" + # f = File.open(data_dir + ".rake_task.log", 'a') + + # read existing pid + + main_pid = File.read(data_dir + ".pid") if File.exist?(data_dir + ".pid") + + # kill job if one already running + existing_main_job = `ps -ef | grep #{job.key} | grep #{main_pid} | grep -v 'grep'` + puts "Existing main job: " + existing_main_job.to_json + + # pids=[] + if main_pid and !existing_main_job.empty? + lines = `ps -ef | grep #{job.key} | grep -v 'grep'`.split("\n").select{|l| !l.empty?} + + puts "MAIN_PID " + main_pid + + pids = lines.map{|l| t= l.split(/\s+/); t[1]} + pids.unshift(main_pid) + puts "ALL_PIDS " + pids.to_json + if pids.size > 0 + pids.each do |pid| + cmd = "kill #{pid}" + puts cmd + `#{cmd}` + end + end + end + + # write current_pid + File.open( data_dir + ".pid", 'w') do |f| + f.write(Process.pid) + # f.write(pids.to_json) + end + + # start analysis + + input_dir = data_dir + 'input' + tmpdir = data_dir + 'tmp' + output_dir = data_dir + 'output' + Dir.mkdir tmpdir if !File.exist? tmpdir + Dir.mkdir output_dir if !File.exist? output_dir + + log_file = data_dir + 'log.txt' + primary_dataset = input_dir + 'primary_dataset.txt' + map = input_dir + 'map.txt' + secondary_dataset = input_dir + 'secondary_dataset.txt' + secondary_dataset = '' if !File.exist? secondary_dataset + params = input_dir + 'params.json' + + # read existing pid + + # create json file + File.open(params, 'w') do |f| + f.write job.form_json + end + + # delete log.json + log_json = data_dir + "output" + "log.json" + File.delete log_json if File.exist? log_json + + +# sleep(2) + + # delete tar + tar_file = data_dir + "#{job.key.to_s}.tar.gz" + puts "rm #{tar_file}" + `rm #{tar_file}` if File.exist?(tar_file) + + FileUtils.rm_r(data_dir + 'output') if File.exist?(data_dir + 'output') + FileUtils.rm_r(data_dir + 'tmp') if File.exist?(data_dir + 'tmp') + + # run genocrunch_console scripts +# hres = read_stdout() +# while(h_res ) +# f.write("BEFORE output:" + `ls -halt #{data_dir + 'output'}` + "\n") +# f.write("BEFORE input:" + `ls -halt #{data_dir + 'input'}` + "\n") + p = Rails.root.join('lib', 'genocrunch_console', 'bin', 'analyse_count_table.py') + cmd = "#{p} --primary_dataset #{primary_dataset} --map #{map} --output #{output_dir} --params #{params}" + cmd = cmd + " --secondary_dataset #{secondary_dataset}" if File.exist? secondary_dataset + puts cmd + + `#{cmd}` + #pid = spawn(cmd) + +# f.write(cmd) +# f.write("AFTER output:" + `ls -halt #{data_dir + 'output'}`) +# f.write("AFTER input:" + `ls -halt #{data_dir + 'input'}`) + + + # create archive + archive_cmd = "cd #{user_dir} && tar --exclude=log.json --exclude=stderr.log --exclude=stdout.log -czf #{job.key.to_s}.tar.gz #{job.key.to_s}" + puts archive_cmd + `#{archive_cmd}` + cmd = "cd #{user_dir} && mv #{job.key.to_s}.tar.gz #{job.key.to_s}" + `#{cmd}` +print cmd +end diff --git a/licenses.csv b/licenses.csv new file mode 100644 index 0000000..9616199 --- /dev/null +++ b/licenses.csv @@ -0,0 +1,100 @@ +name,version,homepage,summary,license +actioncable,5.0.1,http://rubyonrails.org,WebSocket framework for Rails.,MIT +actionmailer,5.0.1,http://rubyonrails.org,"Email composition, delivery, and receiving framework (part of Rails).",MIT +actionpack,5.0.1,http://rubyonrails.org,Web-flow and rendering framework putting the VC in MVC (part of Rails).,MIT +actionview,5.0.1,http://rubyonrails.org,Rendering framework putting the V in MVC (part of Rails).,MIT +activejob,5.0.1,http://rubyonrails.org,Job framework with pluggable queues.,MIT +activemodel,5.0.1,http://rubyonrails.org,A toolkit for building modeling frameworks (part of Rails).,MIT +activerecord,5.0.1,http://rubyonrails.org,Object-relational mapper framework (part of Rails).,MIT +activerecord-session_store,1.0.0,https://github.com/rails/activerecord-session_store,An Action Dispatch session store backed by an Active Record class.,MIT +activesupport,5.0.1,http://rubyonrails.org,A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.,MIT +arel,7.1.4,https://github.com/rails/arel,Arel Really Exasperates Logicians Arel is a SQL AST manager for Ruby,MIT +bcrypt,3.1.11,https://github.com/codahale/bcrypt-ruby,OpenBSD's bcrypt() password hashing algorithm.,MIT +bootstrap-sass,3.2.0.2,https://github.com/twbs/bootstrap-sass,"Twitter's Bootstrap, converted to Sass and ready to drop into Rails or Compass",MIT +builder,3.2.3,http://onestepback.org,Builders for MarkUp.,MIT +bundler,1.14.6,http://bundler.io,The best way to manage your application's dependencies,MIT +climate_control,0.0.3,https://github.com/thoughtbot/climate_control,Modify your ENV easily with ClimateControl,MIT +cocaine,0.5.8,https://github.com/thoughtbot/cocaine,A small library for doing (command) lines,MIT +coffee-rails,4.2.1,https://github.com/rails/coffee-rails,CoffeeScript adapter for the Rails asset pipeline.,MIT +coffee-script,2.4.1,http://github.com/josh/ruby-coffee-script,Ruby CoffeeScript Compiler,MIT +coffee-script-source,1.12.2,http://coffeescript.org,The CoffeeScript Compiler,MIT +concurrent-ruby,1.0.4,http://www.concurrent-ruby.com,"Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell, F#, C#, Java, and classic concurrency patterns.",MIT +d3-rails,4.9.1,https://github.com/iblue/d3-rails,D3 for Rails Asset Pipeline,MIT +daemons,1.2.4,https://github.com/thuehlinger/daemons,A toolkit to create and control daemons in different ways,MIT +dalli,2.7.6,https://github.com/petergoldstein/dalli,High performance memcached client for Ruby,MIT +debug_inspector,0.0.2,https://github.com/banister/debug_inspector,A Ruby wrapper for the MRI 2.0 debug_inspector API,MIT +delayed-web,0.4.4,http://github.com/tatey/delayed-web,A rails engine that provides a simple web interface for exposing the Delayed::Job queue.,MIT +delayed_job,4.1.2,http://github.com/collectiveidea/delayed_job,Database-backed asynchronous priority queue system -- Extracted from Shopify,MIT +delayed_job_active_record,4.1.1,http://github.com/collectiveidea/delayed_job_active_record,ActiveRecord backend for DelayedJob,MIT +descriptive_statistics,2.5.1,https://github.com/thirtysixthspan/descriptive_statistics,Descriptive Statistics,MIT +devise,4.2.0,https://github.com/plataformatec/devise,Flexible authentication solution for Rails with Warden,MIT +did_you_mean,1.0.0,https://github.com/yuki24/did_you_mean,"""Did you mean?"" experience in Ruby",MIT +erubis,2.7.0,http://www.kuwata-lab.com/erubis/,a fast and extensible eRuby implementation which supports multi-language,MIT +execjs,2.7.0,https://github.com/rails/execjs,Run JavaScript code from Ruby,MIT +font-awesome-rails,4.7.0.2,https://github.com/bokmann/font-awesome-rails,an asset gemification of the font-awesome icon font library,MIT +gem-licenses,0.2.1,http://github.com/dblock/gem-licenses,Attempts to figure out what licenses various gems use.,MIT +globalid,0.3.7,http://www.rubyonrails.org,Refer to any model with a URI: gid://app/class/id,MIT +haml,4.0.7,http://haml.info/,"An elegant, structured (X)HTML/XML templating engine.",MIT +i18n,0.8.0,http://github.com/svenfuchs/i18n,New wave Internationalization support for Ruby,MIT +jbuilder,2.6.1,https://github.com/rails/jbuilder,Create JSON structures via a Builder-style DSL,MIT +jquery-fileupload-rails,0.4.7,https://github.com/tors/jquery-fileupload-rails,jQuery File Upload for Rails 3.1+ Asset Pipeline,MIT +jquery-rails,4.2.2,https://github.com/rails/jquery-rails,Use jQuery with Rails 4+,MIT +jquery-ui-rails,5.0.5,https://github.com/joliss/jquery-ui-rails,jQuery UI packaged for the Rails asset pipeline,MIT +libv8,3.16.14.15,http://github.com/cowboyd/libv8,Distribution of the V8 JavaScript engine,MIT +listen,3.0.8,https://github.com/guard/listen,Listen to file modifications,MIT +loofah,2.0.3,https://github.com/flavorjones/loofah,Loofah is a general library for manipulating and transforming HTML/XML documents and fragments,MIT +mail,2.6.4,https://github.com/mikel/mail,"Mail provides a nice Ruby DSL for making, sending and reading emails.",MIT +method_source,0.8.2,http://banisterfiend.wordpress.com,retrieve the sourcecode for a method,MIT +mime-types,3.1,https://github.com/mime-types/ruby-mime-types/,The mime-types library provides a library and registry for information about MIME content type definitions,MIT +mime-types-data,3.2016.0521,https://github.com/mime-types/mime-types-data/,mime-types-data provides a registry for information about MIME media type definitions,MIT +mimemagic,0.3.0,https://github.com/minad/mimemagic,Fast mime detection by extension or content,MIT +mini_portile2,2.1.0,http://github.com/flavorjones/mini_portile,Simplistic port-like solution for developers,MIT +minitest,5.10.1,https://github.com/seattlerb/minitest,"minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking",MIT +multi_json,1.12.1,http://github.com/intridea/multi_json,A common interface to multiple JSON libraries.,MIT +multipart-parser,0.1.1,https://github.com/danabr/multipart-parser,simple parser for multipart MIME messages,MIT +nio4r,1.2.1,https://github.com/celluloid/nio4r,NIO provides a high performance selector API for monitoring IO objects,MIT +nokogiri,1.7.0.1,http://nokogiri.org,"Nokogiri (鋸) is an HTML, XML, SAX, and Reader parser",MIT +orm_adapter,0.5.0,http://github.com/ianwhite/orm_adapter,orm_adapter provides a single point of entry for using basic features of popular ruby ORMs. Its target audience is gem authors who want to support many ruby ORMs.,MIT +paperclip,4.3.2,https://github.com/thoughtbot/paperclip,File attachments as attributes for ActiveRecord,MIT +rack,2.0.1,http://rack.github.io/,a modular Ruby webserver interface,MIT +rack-test,0.6.3,http://github.com/brynary/rack-test,Simple testing API built on Rack,MIT +rails,5.0.1,http://rubyonrails.org,Full-stack web application framework.,MIT +rails-dom-testing,2.0.2,https://github.com/rails/rails-dom-testing,Dom and Selector assertions for Rails applications,MIT +rails-html-sanitizer,1.0.3,https://github.com/rails/rails-html-sanitizer,This gem is responsible to sanitize HTML fragments in Rails applications.,MIT +rails4-autocomplete,1.1.1,http://github.com/peterwillcn/rails4-autocomplete,Use jQuery's autocomplete plugin with Rails 4.,MIT +railties,5.0.1,http://rubyonrails.org,"Tools for creating, working with, and running Rails applications.",MIT +rake,12.0.0,https://github.com/ruby/rake,Rake is a Make-like program implemented in Ruby,MIT +rb-fsevent,0.9.8,http://rubygems.org/gems/rb-fsevent,Very simple & usable FSEvents API,MIT +rb-inotify,0.9.8,http://github.com/nex3/rb-inotify,"A Ruby wrapper for Linux's inotify, using FFI",MIT +ref,2.0.0,http://github.com/ruby-concurrency/ref,"Library that implements weak, soft, and strong references in Ruby.",MIT +responders,2.3.0,http://github.com/plataformatec/responders,A set of Rails responders to dry up your application,MIT +rucaptcha,2.2.0,https://github.com/huacnlee/rucaptcha,This is a Captcha gem for Rails Applications. It drawing captcha image with C code so it no dependencies.,MIT +sass,3.4.23,http://sass-lang.com/,A powerful but elegant CSS compiler that makes CSS fun again.,MIT +sass-rails,5.0.6,https://github.com/rails/sass-rails,Sass adapter for the Rails asset pipeline.,MIT +skrollr-rails,0.6.29,https://github.com/reed/skrollr-rails,Skrollr is a stand-alone parallax scrolling library for mobile and desktop. This gem integrates skrollr with the Rails asset pipeline for ease of use and version control.,MIT +spring,2.0.1,https://github.com/rails/spring,Rails application preloader,MIT +spring-watcher-listen,2.0.1,https://github.com/jonleighton/spring-watcher-listen,Makes spring watch files using the listen gem.,MIT +sprockets,3.7.1,https://github.com/rails/sprockets,Rack-based asset packaging system,MIT +sprockets-rails,3.2.0,https://github.com/rails/sprockets-rails,Sprockets Rails integration,MIT +tether-rails,1.4.0,https://github.com/jakegavin/tether-rails,tether-rails wraps the Tether JavaScript library so that it can easily be included in Rails.,MIT +therubyracer,0.12.2,http://github.com/cowboyd/therubyracer,Embed the V8 JavaScript interpreter into Ruby,MIT +thor,0.19.4,http://whatisthor.com/,Thor is a toolkit for building powerful command-line interfaces.,MIT +tilt,2.0.6,http://github.com/rtomayko/tilt/,Generic interface to multiple Ruby template engines,MIT +turbolinks,2.5.3,https://github.com/rails/turbolinks/,Turbolinks makes following links in your web application faster (use with Rails Asset Pipeline),MIT +tzinfo,1.2.2,http://tzinfo.github.io,Daylight savings aware timezone library,MIT +uglifier,3.0.4,http://github.com/lautis/uglifier,Ruby wrapper for UglifyJS JavaScript compressor,MIT +warden,1.2.6,http://github.com/hassox/warden,Rack middleware that provides authentication for rack applications,MIT +web-console,3.4.0,https://github.com/rails/web-console,A debugging tool for your Ruby on Rails applications.,MIT +websocket-driver,0.6.5,http://github.com/faye/websocket-driver-ruby,WebSocket protocol handler with pluggable I/O,MIT +websocket-extensions,0.1.2,http://github.com/faye/websocket-extensions-ruby,Generic extension manager for WebSocket connections,MIT +byebug,9.0.6,http://github.com/deivid-rodriguez/byebug,Ruby 2.0 fast debugger - base + CLI,BSD 2-Clause +io-console,0.4.5,http://www.ruby-lang.org,Console interface,BSD 2-Clause +sys-proctable,1.1.3,http://github.com/djberg96/sys-proctable,An interface for providing process table information,Apache 2.0 +thread_safe,0.3.5,https://github.com/ruby-concurrency/thread_safe,A collection of data structures and utilities to make thread-safe programming in Ruby easier,Apache 2.0 +ffi,1.9.17,http://wiki.github.com/ffi/ffi,Ruby FFI,bsd-3-clause +pg,0.19.0,https://bitbucket.org/ged/ruby-pg,Pg is the Ruby interface to the {PostgreSQL RDBMS}[http://www.postgresql.org/],bsd-3-clause +puma,3.7.0,http://puma.io,"Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications",bsd-3-clause +font-awesome-rails,4.7.0.2,https://github.com/bokmann/font-awesome-rails,an asset gemification of the font-awesome icon font library,sil open font license +jquery-datatables-rails,3.3.0,https://github.com/rweng/jquery-datatables-rails,jquery datatables for rails,unknown +json,2.1.0,http://flori.github.com/json,JSON Implementation for Ruby,Ruby +kgio,2.11.1,https://bogomips.org/kgio/,"kinder, gentler I/O for Ruby",LGPLv2.1 diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8e50d0 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "name": "genocrunch", + "private": true, + "dependencies": {} +} diff --git a/printers.xml b/printers.xml new file mode 100644 index 0000000..7e5f65a --- /dev/null +++ b/printers.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/public/.DS_Store b/public/.DS_Store new file mode 100644 index 0000000..92d716c Binary files /dev/null and b/public/.DS_Store differ diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..b612547 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
    +
    +

    The page you were looking for doesn't exist.

    +

    You may have mistyped the address or the page may have moved.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..a21f82b --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
    +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..061abc5 --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
    +
    +

    We're sorry, but something went wrong.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/app/TERMS_OF_SERVICE.txt b/public/app/TERMS_OF_SERVICE.txt new file mode 100755 index 0000000..d42818c --- /dev/null +++ b/public/app/TERMS_OF_SERVICE.txt @@ -0,0 +1,35 @@ +Genocrunch Terms of Service + +No warranties +1.This software is provided "as is" and without any warranties, whether + express or implied, including but not limited to the warranties of + merchantability, fitness for any particular purpose and noninfringement. + In addition, EPFL expressively does not warrant that: + a) The software will function, be free of bug or any defect. + b) Any defect will be corrected. + c) The software and any of its inputs or outputs will be compatible with + any third-party software. + d) Any of the software result or output will be reliable. + +2.The protection of any data, whether it was uploaded, stored, generated, + downloaded or by any mean used with this web server, is not warrantied + in any way and EPFL expressively does not warrant that: + a) The data will remain confidential or will not be accessible or + disclosed to the public or a third-party. + b) The data will not be damaged or unusable. + c) Any damaged data will be repaired. + d) The data will remain accessible for any period of time or be + accessible at all. + e) The data or any derivative result will be merchantable. + f) The data will be compatible with any third-party software. + +3.The protection of privacy, whether it involves user information or any + private, medical or sensitive information found in the data, is not + warrantied. + +Usage +1.This software is for research use only and shall not be used in medical + diagnosis. +2.The authors and the copyrights owners shall have no liability of any + kind for the use of or inability to use the software, the software + content, data, or any associated service. diff --git a/public/app/tips.json b/public/app/tips.json new file mode 100644 index 0000000..7750dcd --- /dev/null +++ b/public/app/tips.json @@ -0,0 +1,31 @@ +{ + "form":[ + { + "id":"1", + "css_string":"right:0, width:'300px',top:0", + "html":"You are currently signed out. You can perform analyses but your work will not be stored after you close your web browser. Please first Register/Sign In if you wish to store your work, recover and edit previous analyses." + }, + { + "id":"2", + "css_string":"right:window_width/2-150, width:'300px',top:0", + "html":"You can find some help by clicking on a help icon () and on the InfosDoc page." + }, + { + "id":"3", + "css_string":"left:0, width:'300px',top:0", + "html":"You can load examples from the Examples page." + } + ], + "show":[ + { + "id":"4", + "css_string":"left:window_width/2-150, width:'300px',top:0", + "html":"Data files and analysis logs can be downloaded as an archive ()." + }, + { + "id":"5", + "css_string":"right:window_width/4-150, width:'300px',top:0", + "html":"Use Edit to update your job." + } + ] +} diff --git a/public/defaults/defaults.json b/public/defaults/defaults.json new file mode 100755 index 0000000..853e0be --- /dev/null +++ b/public/defaults/defaults.json @@ -0,0 +1,10 @@ +{"sample_name":"AnalysisID", +"basic_model":"ExperimentalGroup", +"model":"ExperimentalGroup*Sex", +"is_log":"false", +"unit":"counts", +"metadata_is_log":"false", +"metadata_unit":"Conc.", +"diversity_metric":["shannon", "richness"], +"category_levels":["3", "8"], +"analysis":["abundance", "diversity", "pca", "pcoa", "heatmap"]} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/robots.txt b/public/robots.txt new file mode 100755 index 0000000..3c9c7c0 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..d19212a --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100755 index 0000000..e69de29 diff --git a/test/controllers/examples_controller_test.rb b/test/controllers/examples_controller_test.rb new file mode 100644 index 0000000..4247354 --- /dev/null +++ b/test/controllers/examples_controller_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +class ExamplesControllerTest < ActionDispatch::IntegrationTest + setup do + @example = examples(:one) + end + + test "should get index" do + get examples_url + assert_response :success + end + + test "should get new" do + get new_example_url + assert_response :success + end + + test "should create example" do + assert_difference('Example.count') do + post examples_url, params: { example: { } } + end + + assert_redirected_to example_url(Example.last) + end + + test "should show example" do + get example_url(@example) + assert_response :success + end + + test "should get edit" do + get edit_example_url(@example) + assert_response :success + end + + test "should update example" do + patch example_url(@example), params: { example: { } } + assert_redirected_to example_url(@example) + end + + test "should destroy example" do + assert_difference('Example.count', -1) do + delete example_url(@example) + end + + assert_redirected_to examples_url + end +end diff --git a/test/controllers/home_controller_test.rb b/test/controllers/home_controller_test.rb new file mode 100644 index 0000000..3e06bf0 --- /dev/null +++ b/test/controllers/home_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class HomeControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/jobs_controller_test.rb b/test/controllers/jobs_controller_test.rb new file mode 100755 index 0000000..30d0f0e --- /dev/null +++ b/test/controllers/jobs_controller_test.rb @@ -0,0 +1,49 @@ +require 'test_helper' + +class JobsControllerTest < ActionController::TestCase + setup do + @job = jobs(:one) + end + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:jobs) + end + + test "should get new" do + get :new + assert_response :success + end + + test "should create job" do + assert_difference('Job.count') do + post :create, job: { input: @job.input, map: @job.map, name: @job.name, output: @job.output, params: @job.params, status: @job.status } + end + + assert_redirected_to job_path(assigns(:job)) + end + + test "should show job" do + get :show, id: @job + assert_response :success + end + + test "should get edit" do + get :edit, id: @job + assert_response :success + end + + test "should update job" do + patch :update, id: @job, job: { input: @job.input, map: @job.map, name: @job.name, output: @job.output, params: @job.params, status: @job.status } + assert_redirected_to job_path(assigns(:job)) + end + + test "should destroy job" do + assert_difference('Job.count', -1) do + delete :destroy, id: @job + end + + assert_redirected_to jobs_path + end +end diff --git a/test/controllers/versions_controller_test.rb b/test/controllers/versions_controller_test.rb new file mode 100644 index 0000000..5110e7a --- /dev/null +++ b/test/controllers/versions_controller_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +class VersionsControllerTest < ActionDispatch::IntegrationTest + setup do + @version = versions(:one) + end + + test "should get index" do + get versions_url + assert_response :success + end + + test "should get new" do + get new_version_url + assert_response :success + end + + test "should create version" do + assert_difference('Version.count') do + post versions_url, params: { version: { } } + end + + assert_redirected_to version_url(Version.last) + end + + test "should show version" do + get version_url(@version) + assert_response :success + end + + test "should get edit" do + get edit_version_url(@version) + assert_response :success + end + + test "should update version" do + patch version_url(@version), params: { version: { } } + assert_redirected_to version_url(@version) + end + + test "should destroy version" do + assert_difference('Version.count', -1) do + delete version_url(@version) + end + + assert_redirected_to versions_url + end +end diff --git a/test/fixtures/.keep b/test/fixtures/.keep new file mode 100755 index 0000000..e69de29 diff --git a/test/fixtures/examples.yml b/test/fixtures/examples.yml new file mode 100644 index 0000000..80aed36 --- /dev/null +++ b/test/fixtures/examples.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/jobs.yml b/test/fixtures/jobs.yml new file mode 100755 index 0000000..e6f0b11 --- /dev/null +++ b/test/fixtures/jobs.yml @@ -0,0 +1,17 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html + +one: + name: MyString + input: MyString + map: MyString + params: MyString + output: MyString + status: MyString + +two: + name: MyString + input: MyString + map: MyString + params: MyString + output: MyString + status: MyString diff --git a/test/fixtures/statuses.yml b/test/fixtures/statuses.yml new file mode 100644 index 0000000..80aed36 --- /dev/null +++ b/test/fixtures/statuses.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/tests.yml b/test/fixtures/tests.yml new file mode 100644 index 0000000..0227c60 --- /dev/null +++ b/test/fixtures/tests.yml @@ -0,0 +1,7 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html + +one: + name: MyString + +two: + name: MyString diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100755 index 0000000..c63aac0 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/fixtures/versions.yml b/test/fixtures/versions.yml new file mode 100644 index 0000000..80aed36 --- /dev/null +++ b/test/fixtures/versions.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +# This model initially had no columns defined. If you add columns to the +# model remove the '{}' from the fixture names and add the columns immediately +# below each fixture, per the syntax in the comments below +# +one: {} +# column: value +# +two: {} +# column: value diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100755 index 0000000..e69de29 diff --git a/test/helpers/jobs_helper_test.rb b/test/helpers/jobs_helper_test.rb new file mode 100755 index 0000000..7c4a3fd --- /dev/null +++ b/test/helpers/jobs_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class JobsHelperTest < ActionView::TestCase +end diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100755 index 0000000..e69de29 diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100755 index 0000000..e69de29 diff --git a/test/models/.keep b/test/models/.keep new file mode 100755 index 0000000..e69de29 diff --git a/test/models/example_test.rb b/test/models/example_test.rb new file mode 100644 index 0000000..1360f2c --- /dev/null +++ b/test/models/example_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ExampleTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/job_test.rb b/test/models/job_test.rb new file mode 100755 index 0000000..5079316 --- /dev/null +++ b/test/models/job_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class JobTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/status_test.rb b/test/models/status_test.rb new file mode 100644 index 0000000..01fb2c8 --- /dev/null +++ b/test/models/status_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class StatusTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/test_test.rb b/test/models/test_test.rb new file mode 100644 index 0000000..1c99a1d --- /dev/null +++ b/test/models/test_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class TestTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100755 index 0000000..82f61e0 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/version_test.rb b/test/models/version_test.rb new file mode 100644 index 0000000..d6d1a85 --- /dev/null +++ b/test/models/version_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class VersionTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100755 index 0000000..92e39b2 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,10 @@ +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +require 'rails/test_help' + +class ActiveSupport::TestCase + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... +end diff --git a/users/.gitkeep b/users/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/assets/images/sort_asc.png b/vendor/assets/images/sort_asc.png new file mode 100644 index 0000000..47586a2 Binary files /dev/null and b/vendor/assets/images/sort_asc.png differ diff --git a/vendor/assets/images/sort_asc_disabled.png b/vendor/assets/images/sort_asc_disabled.png new file mode 100644 index 0000000..fb11dfe Binary files /dev/null and b/vendor/assets/images/sort_asc_disabled.png differ diff --git a/vendor/assets/images/sort_both.png b/vendor/assets/images/sort_both.png new file mode 100644 index 0000000..af5bc7c Binary files /dev/null and b/vendor/assets/images/sort_both.png differ diff --git a/vendor/assets/images/sort_desc.png b/vendor/assets/images/sort_desc.png new file mode 100644 index 0000000..7fac2b6 Binary files /dev/null and b/vendor/assets/images/sort_desc.png differ diff --git a/vendor/assets/images/sort_desc_disabled.png b/vendor/assets/images/sort_desc_disabled.png new file mode 100644 index 0000000..c9fdd8a Binary files /dev/null and b/vendor/assets/images/sort_desc_disabled.png differ diff --git a/vendor/assets/javascripts/.keep b/vendor/assets/javascripts/.keep new file mode 100755 index 0000000..e69de29 diff --git a/vendor/assets/javascripts/bootstrap-multiselect.js b/vendor/assets/javascripts/bootstrap-multiselect.js new file mode 100644 index 0000000..cb3eed0 --- /dev/null +++ b/vendor/assets/javascripts/bootstrap-multiselect.js @@ -0,0 +1,1717 @@ +/** + * Bootstrap Multiselect (https://github.com/davidstutz/bootstrap-multiselect) + * + * Apache License, Version 2.0: + * Copyright (c) 2012 - 2015 David Stutz + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a + * copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * BSD 3-Clause License: + * Copyright (c) 2012 - 2015 David Stutz + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * - Neither the name of David Stutz nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, + * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +!function ($) { + "use strict";// jshint ;_; + + if (typeof ko !== 'undefined' && ko.bindingHandlers && !ko.bindingHandlers.multiselect) { + ko.bindingHandlers.multiselect = { + after: ['options', 'value', 'selectedOptions', 'enable', 'disable'], + + init: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect(config); + + if (allBindings.has('options')) { + var options = allBindings.get('options'); + if (ko.isObservable(options)) { + ko.computed({ + read: function() { + options(); + setTimeout(function() { + var ms = $element.data('multiselect'); + if (ms) + ms.updateOriginalOptions();//Not sure how beneficial this is. + $element.multiselect('rebuild'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }); + } + } + + //value and selectedOptions are two-way, so these will be triggered even by our own actions. + //It needs some way to tell if they are triggered because of us or because of outside change. + //It doesn't loop but it's a waste of processing. + if (allBindings.has('value')) { + var value = allBindings.get('value'); + if (ko.isObservable(value)) { + ko.computed({ + read: function() { + value(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + //Switched from arrayChange subscription to general subscription using 'refresh'. + //Not sure performance is any better using 'select' and 'deselect'. + if (allBindings.has('selectedOptions')) { + var selectedOptions = allBindings.get('selectedOptions'); + if (ko.isObservable(selectedOptions)) { + ko.computed({ + read: function() { + selectedOptions(); + setTimeout(function() { + $element.multiselect('refresh'); + }, 1); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } + } + + var setEnabled = function (enable) { + setTimeout(function () { + if (enable) + $element.multiselect('enable'); + else + $element.multiselect('disable'); + }); + }; + + if (allBindings.has('enable')) { + var enable = allBindings.get('enable'); + if (ko.isObservable(enable)) { + ko.computed({ + read: function () { + setEnabled(enable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(enable); + } + } + + if (allBindings.has('disable')) { + var disable = allBindings.get('disable'); + if (ko.isObservable(disable)) { + ko.computed({ + read: function () { + setEnabled(!disable()); + }, + disposeWhenNodeIsRemoved: element + }).extend({ rateLimit: 100, notifyWhenChangesStop: true }); + } else { + setEnabled(!disable); + } + } + + ko.utils.domNodeDisposal.addDisposeCallback(element, function() { + $element.multiselect('destroy'); + }); + }, + + update: function(element, valueAccessor, allBindings, viewModel, bindingContext) { + var $element = $(element); + var config = ko.toJS(valueAccessor()); + + $element.multiselect('setOptions', config); + $element.multiselect('rebuild'); + } + }; + } + + function forEach(array, callback) { + for (var index = 0; index < array.length; ++index) { + callback(array[index], index); + } + } + + /** + * Constructor to create a new multiselect using the given select. + * + * @param {jQuery} select + * @param {Object} options + * @returns {Multiselect} + */ + function Multiselect(select, options) { + + this.$select = $(select); + this.options = this.mergeOptions($.extend({}, options, this.$select.data())); + + // Placeholder via data attributes + if (this.$select.attr("data-placeholder")) { + this.options.nonSelectedText = this.$select.data("placeholder"); + } + + // Initialization. + // We have to clone to create a new reference. + this.originalOptions = this.$select.clone()[0].options; + this.query = ''; + this.searchTimeout = null; + this.lastToggledInput = null; + + this.options.multiple = this.$select.attr('multiple') === "multiple"; + this.options.onChange = $.proxy(this.options.onChange, this); + this.options.onSelectAll = $.proxy(this.options.onSelectAll, this); + this.options.onDeselectAll = $.proxy(this.options.onDeselectAll, this); + this.options.onDropdownShow = $.proxy(this.options.onDropdownShow, this); + this.options.onDropdownHide = $.proxy(this.options.onDropdownHide, this); + this.options.onDropdownShown = $.proxy(this.options.onDropdownShown, this); + this.options.onDropdownHidden = $.proxy(this.options.onDropdownHidden, this); + this.options.onInitialized = $.proxy(this.options.onInitialized, this); + this.options.onFiltering = $.proxy(this.options.onFiltering, this); + + // Build select all if enabled. + this.buildContainer(); + this.buildButton(); + this.buildDropdown(); + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.wasDisabled = this.$select.prop('disabled'); + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + + this.$select.wrap('').after(this.$container); + this.options.onInitialized(this.$select, this.$container); + } + + Multiselect.prototype = { + + defaults: { + /** + * Default text function will either print 'None selected' in case no + * option is selected or a list of the selected options up to a length + * of 3 selected options. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {String} + */ + buttonText: function(options, select) { + if (this.disabledText.length > 0 + && (select.prop('disabled') || (options.length == 0 && this.disableIfEmpty))) { + + return this.disabledText; + } + else if (options.length === 0) { + return this.nonSelectedText; + } + else if (this.allSelectedText + && options.length === $('option', $(select)).length + && $('option', $(select)).length !== 1 + && this.multiple) { + + if (this.selectAllNumber) { + return this.allSelectedText + ' (' + options.length + ')'; + } + else { + return this.allSelectedText; + } + } + else if (options.length > this.numberDisplayed) { + return options.length + ' ' + this.nSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function() { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + /** + * Updates the title of the button similar to the buttonText function. + * + * @param {jQuery} options + * @param {jQuery} select + * @returns {@exp;selected@call;substr} + */ + buttonTitle: function(options, select) { + if (options.length === 0) { + return this.nonSelectedText; + } + else { + var selected = ''; + var delimiter = this.delimiterText; + + options.each(function () { + var label = ($(this).attr('label') !== undefined) ? $(this).attr('label') : $(this).text(); + selected += label + delimiter; + }); + return selected.substr(0, selected.length - this.delimiterText.length); + } + }, + checkboxName: function(option) { + return false; // no checkbox name + }, + /** + * Create a label. + * + * @param {jQuery} element + * @returns {String} + */ + optionLabel: function(element){ + return $(element).attr('label') || $(element).text(); + }, + /** + * Create a class. + * + * @param {jQuery} element + * @returns {String} + */ + optionClass: function(element) { + return $(element).attr('class') || ''; + }, + /** + * Triggered on change of the multiselect. + * + * Not triggered when selecting/deselecting options manually. + * + * @param {jQuery} option + * @param {Boolean} checked + */ + onChange : function(option, checked) { + + }, + /** + * Triggered when the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShow: function(event) { + + }, + /** + * Triggered when the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHide: function(event) { + + }, + /** + * Triggered after the dropdown is shown. + * + * @param {jQuery} event + */ + onDropdownShown: function(event) { + + }, + /** + * Triggered after the dropdown is hidden. + * + * @param {jQuery} event + */ + onDropdownHidden: function(event) { + + }, + /** + * Triggered on select all. + */ + onSelectAll: function() { + + }, + /** + * Triggered on deselect all. + */ + onDeselectAll: function() { + + }, + /** + * Triggered after initializing. + * + * @param {jQuery} $select + * @param {jQuery} $container + */ + onInitialized: function($select, $container) { + + }, + /** + * Triggered on filtering. + * + * @param {jQuery} $filter + */ + onFiltering: function($filter) { + + }, + enableHTML: false, + buttonClass: 'btn btn-default', + inheritClass: false, + buttonWidth: 'auto', + buttonContainer: '
    ', + dropRight: false, + dropUp: false, + selectedClass: 'active', + // Maximum height of the dropdown menu. + // If maximum height is exceeded a scrollbar will be displayed. + maxHeight: false, + includeSelectAllOption: false, + includeSelectAllIfMoreThan: 0, + selectAllText: ' Select all', + selectAllValue: 'multiselect-all', + selectAllName: false, + selectAllNumber: true, + selectAllJustVisible: true, + enableFiltering: false, + enableCaseInsensitiveFiltering: false, + enableFullValueFiltering: false, + enableClickableOptGroups: false, + enableCollapsibleOptGroups: false, + filterPlaceholder: 'Search', + // possible options: 'text', 'value', 'both' + filterBehavior: 'text', + includeFilterClearBtn: true, + preventInputChangeEvent: false, + nonSelectedText: 'None selected', + nSelectedText: 'selected', + allSelectedText: 'All selected', + numberDisplayed: 3, + disableIfEmpty: false, + disabledText: '', + delimiterText: ', ', + templates: { + button: '', + ul: '', + filter: '
  • ', + filterClearBtn: '', + li: '
  • ', + divider: '
  • ', + liGroup: '
  • ' + } + }, + + constructor: Multiselect, + + /** + * Builds the container of the multiselect. + */ + buildContainer: function() { + this.$container = $(this.options.buttonContainer); + this.$container.on('show.bs.dropdown', this.options.onDropdownShow); + this.$container.on('hide.bs.dropdown', this.options.onDropdownHide); + this.$container.on('shown.bs.dropdown', this.options.onDropdownShown); + this.$container.on('hidden.bs.dropdown', this.options.onDropdownHidden); + }, + + /** + * Builds the button of the multiselect. + */ + buildButton: function() { + this.$button = $(this.options.templates.button).addClass(this.options.buttonClass); + if (this.$select.attr('class') && this.options.inheritClass) { + this.$button.addClass(this.$select.attr('class')); + } + // Adopt active state. + if (this.$select.prop('disabled')) { + this.disable(); + } + else { + this.enable(); + } + + // Manually add button width if set. + if (this.options.buttonWidth && this.options.buttonWidth !== 'auto') { + this.$button.css({ + 'width': this.options.buttonWidth, + 'overflow' : 'hidden', + 'text-overflow' : 'ellipsis' + }); + this.$container.css({ + 'width': this.options.buttonWidth + }); + } + + // Keep the tab index from the select. + var tabindex = this.$select.attr('tabindex'); + if (tabindex) { + this.$button.attr('tabindex', tabindex); + } + + this.$container.prepend(this.$button); + }, + + /** + * Builds the ul representing the dropdown menu. + */ + buildDropdown: function() { + + // Build ul. + this.$ul = $(this.options.templates.ul); + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + + // Set max height of dropdown menu to activate auto scrollbar. + if (this.options.maxHeight) { + // TODO: Add a class for this option to move the css declarations. + this.$ul.css({ + 'max-height': this.options.maxHeight + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'auto' + }); + } + + if (this.options.dropUp) { + + var height = Math.min(this.options.maxHeight, $('option[data-role!="divider"]', this.$select).length*26 + $('option[data-role="divider"]', this.$select).length*19 + (this.options.includeSelectAllOption ? 26 : 0) + (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering ? 44 : 0)); + var moveCalc = height + 34; + + this.$ul.css({ + 'max-height': height + 'px', + 'overflow-y': 'auto', + 'overflow-x': 'auto', + 'margin-top': "-" + moveCalc + 'px' + }); + } + + this.$container.append(this.$ul); + }, + + /** + * Build the dropdown options and binds all necessary events. + * + * Uses createDivider and createOptionValue to create the necessary options. + */ + buildDropdownOptions: function() { + + this.$select.children().each($.proxy(function(index, element) { + + var $element = $(element); + // Support optgroups and options without a group simultaneously. + var tag = $element.prop('tagName') + .toLowerCase(); + + if ($element.prop('value') === this.options.selectAllValue) { + return; + } + + if (tag === 'optgroup') { + this.createOptgroup(element); + } + else if (tag === 'option') { + + if ($element.data('role') === 'divider') { + this.createDivider(); + } + else { + this.createOptionValue(element); + } + + } + + // Other illegal tags will be ignored. + }, this)); + + // Bind the change event on the dropdown elements. + $('li:not(.multiselect-group) input', this.$ul).on('change', $.proxy(function(event) { + var $target = $(event.target); + + var checked = $target.prop('checked') || false; + var isSelectAllOption = $target.val() === this.options.selectAllValue; + + // Apply or unapply the configured selected class. + if (this.options.selectedClass) { + if (checked) { + $target.closest('li') + .addClass(this.options.selectedClass); + } + else { + $target.closest('li') + .removeClass(this.options.selectedClass); + } + } + + // Get the corresponding option. + var value = $target.val(); + var $option = this.getOptionByValue(value); + + var $optionsNotThis = $('option', this.$select).not($option); + var $checkboxesNotThis = $('input', this.$container).not($target); + + if (isSelectAllOption) { + + if (checked) { + this.selectAll(this.options.selectAllJustVisible, true); + } + else { + this.deselectAll(this.options.selectAllJustVisible, true); + } + } + else { + if (checked) { + $option.prop('selected', true); + + if (this.options.multiple) { + // Simply select additional option. + $option.prop('selected', true); + } + else { + // Unselect all other options and corresponding checkboxes. + if (this.options.selectedClass) { + $($checkboxesNotThis).closest('li').removeClass(this.options.selectedClass); + } + + $($checkboxesNotThis).prop('checked', false); + $optionsNotThis.prop('selected', false); + + // It's a single selection, so close. + this.$button.click(); + } + + if (this.options.selectedClass === "active") { + $optionsNotThis.closest("a").css("outline", ""); + } + } + else { + // Unselect option. + $option.prop('selected', false); + } + + // To prevent select all from firing onChange: #575 + this.options.onChange($option, checked); + + // Do not update select all or optgroups on select all change! + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + } + + this.$select.change(); + this.updateButtonText(); + + if(this.options.preventInputChangeEvent) { + return false; + } + }, this)); + + $('li a', this.$ul).on('mousedown', function(e) { + if (e.shiftKey) { + // Prevent selecting text by Shift+click + return false; + } + }); + + $('li a', this.$ul).on('touchstart click', $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + + if (event.shiftKey && this.options.multiple) { + if($target.is("label")){ // Handles checkbox selection manually (see https://github.com/davidstutz/bootstrap-multiselect/issues/431) + event.preventDefault(); + $target = $target.find("input"); + $target.prop("checked", !$target.prop("checked")); + } + var checked = $target.prop('checked') || false; + + if (this.lastToggledInput !== null && this.lastToggledInput !== $target) { // Make sure we actually have a range + var from = $target.closest("li").index(); + var to = this.lastToggledInput.closest("li").index(); + + if (from > to) { // Swap the indices + var tmp = to; + to = from; + from = tmp; + } + + // Make sure we grab all elements since slice excludes the last index + ++to; + + // Change the checkboxes and underlying options + var range = this.$ul.find("li").slice(from, to).find("input"); + + range.prop('checked', checked); + + if (this.options.selectedClass) { + range.closest('li') + .toggleClass(this.options.selectedClass, checked); + } + + for (var i = 0, j = range.length; i < j; i++) { + var $checkbox = $(range[i]); + + var $option = this.getOptionByValue($checkbox.val()); + + $option.prop('selected', checked); + } + } + + // Trigger the select "change" event + $target.trigger("change"); + } + + // Remembers last clicked option + if($target.is("input") && !$target.closest("li").is(".multiselect-item")){ + this.lastToggledInput = $target; + } + + $target.blur(); + }, this)); + + // Keyboard support. + this.$container.off('keydown.multiselect').on('keydown.multiselect', $.proxy(function(event) { + if ($('input[type="text"]', this.$container).is(':focus')) { + return; + } + + if (event.keyCode === 9 && this.$container.hasClass('open')) { + this.$button.click(); + } + else { + var $items = $(this.$container).find("li:not(.divider):not(.disabled) a").filter(":visible"); + + if (!$items.length) { + return; + } + + var index = $items.index($items.filter(':focus')); + + // Navigation up. + if (event.keyCode === 38 && index > 0) { + index--; + } + // Navigate down. + else if (event.keyCode === 40 && index < $items.length - 1) { + index++; + } + else if (!~index) { + index = 0; + } + + var $current = $items.eq(index); + $current.focus(); + + if (event.keyCode === 32 || event.keyCode === 13) { + var $checkbox = $current.find('input'); + + $checkbox.prop("checked", !$checkbox.prop("checked")); + $checkbox.change(); + } + + event.stopPropagation(); + event.preventDefault(); + } + }, this)); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $("li.multiselect-group input", this.$ul).on("change", $.proxy(function(event) { + event.stopPropagation(); + + var $target = $(event.target); + var checked = $target.prop('checked') || false; + + var $li = $(event.target).closest('li'); + var $group = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden') + .not('.disabled'); + + var $inputs = $group.find("input"); + + var values = []; + var $options = []; + + if (this.options.selectedClass) { + if (checked) { + $li.addClass(this.options.selectedClass); + } + else { + $li.removeClass(this.options.selectedClass); + } + } + + $.each($inputs, $.proxy(function(index, input) { + var value = $(input).val(); + var $option = this.getOptionByValue(value); + + if (checked) { + $(input).prop('checked', true); + $(input).closest('li') + .addClass(this.options.selectedClass); + + $option.prop('selected', true); + } + else { + $(input).prop('checked', false); + $(input).closest('li') + .removeClass(this.options.selectedClass); + + $option.prop('selected', false); + } + + $options.push(this.getOptionByValue(value)); + }, this)) + + // Cannot use select or deselect here because it would call updateOptGroups again. + + this.options.onChange($options, checked); + + this.updateButtonText(); + this.updateSelectAll(); + }, this)); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $("li.multiselect-group .caret-container", this.$ul).on("click", $.proxy(function(event) { + var $li = $(event.target).closest('li'); + var $inputs = $li.nextUntil("li.multiselect-group") + .not('.multiselect-filter-hidden'); + + var visible = true; + $inputs.each(function() { + visible = visible && $(this).is(':visible'); + }); + + if (visible) { + $inputs.hide() + .addClass('multiselect-collapsible-hidden'); + } + else { + $inputs.show() + .removeClass('multiselect-collapsible-hidden'); + } + }, this)); + + $("li.multiselect-all", this.$ul).css('background', '#f3f3f3').css('border-bottom', '1px solid #eaeaea'); + $("li.multiselect-all > a > label.checkbox", this.$ul).css('padding', '3px 20px 3px 35px'); + $("li.multiselect-group > a > input", this.$ul).css('margin', '4px 0px 5px -20px'); + } + }, + + /** + * Create an option using the given select option. + * + * @param {jQuery} element + */ + createOptionValue: function(element) { + var $element = $(element); + if ($element.is(':selected')) { + $element.prop('selected', true); + } + + // Support the label attribute on options. + var label = this.options.optionLabel(element); + var classes = this.options.optionClass(element); + var value = $element.val(); + var inputType = this.options.multiple ? "checkbox" : "radio"; + + var $li = $(this.options.templates.li); + var $label = $('label', $li); + $label.addClass(inputType); + $li.addClass(classes); + + if (this.options.enableHTML) { + $label.html(" " + label); + } + else { + $label.text(" " + label); + } + + var $checkbox = $('').attr('type', inputType); + + var name = this.options.checkboxName($element); + if (name) { + $checkbox.attr('name', name); + } + + $label.prepend($checkbox); + + var selected = $element.prop('selected') || false; + $checkbox.val(value); + + if (value === this.options.selectAllValue) { + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + } + + $label.attr('title', $element.attr('title')); + + this.$ul.append($li); + + if ($element.is(':disabled')) { + $checkbox.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('a') + .attr("tabindex", "-1") + .closest('li') + .addClass('disabled'); + } + + $checkbox.prop('checked', selected); + + if (selected && this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + }, + + /** + * Creates a divider using the given select option. + * + * @param {jQuery} element + */ + createDivider: function(element) { + var $divider = $(this.options.templates.divider); + this.$ul.append($divider); + }, + + /** + * Creates an optgroup. + * + * @param {jQuery} group + */ + createOptgroup: function(group) { + var label = $(group).attr("label"); + var value = $(group).attr("value"); + var $li = $('
  • '); + + var classes = this.options.optionClass(group); + $li.addClass(classes); + + if (this.options.enableHTML) { + $('label b', $li).html(" " + label); + } + else { + $('label b', $li).text(" " + label); + } + + if (this.options.enableCollapsibleOptGroups && this.options.multiple) { + $('a', $li).append(''); + } + + if (this.options.enableClickableOptGroups && this.options.multiple) { + $('a label', $li).prepend(''); + } + + if ($(group).is(':disabled')) { + $li.addClass('disabled'); + } + + this.$ul.append($li); + + $("option", group).each($.proxy(function($, group) { + this.createOptionValue(group); + }, this)) + }, + + /** + * Build the select all. + * + * Checks if a select all has already been created. + */ + buildSelectAll: function() { + if (typeof this.options.selectAllValue === 'number') { + this.options.selectAllValue = this.options.selectAllValue.toString(); + } + + var alreadyHasSelectAll = this.hasSelectAll(); + + if (!alreadyHasSelectAll && this.options.includeSelectAllOption && this.options.multiple + && $('option', this.$select).length > this.options.includeSelectAllIfMoreThan) { + + // Check whether to add a divider after the select all. + if (this.options.includeSelectAllDivider) { + this.$ul.prepend($(this.options.templates.divider)); + } + + var $li = $(this.options.templates.li); + $('label', $li).addClass("checkbox"); + + if (this.options.enableHTML) { + $('label', $li).html(" " + this.options.selectAllText); + } + else { + $('label', $li).text(" " + this.options.selectAllText); + } + + if (this.options.selectAllName) { + $('label', $li).prepend(''); + } + else { + $('label', $li).prepend(''); + } + + var $checkbox = $('input', $li); + $checkbox.val(this.options.selectAllValue); + + $li.addClass("multiselect-item multiselect-all"); + $checkbox.parent().parent() + .addClass('multiselect-all'); + + this.$ul.prepend($li); + + $checkbox.prop('checked', false); + } + }, + + /** + * Builds the filter. + */ + buildFilter: function() { + + // Build filter if filtering OR case insensitive filtering is enabled and the number of options exceeds (or equals) enableFilterLength. + if (this.options.enableFiltering || this.options.enableCaseInsensitiveFiltering) { + var enableFilterLength = Math.max(this.options.enableFiltering, this.options.enableCaseInsensitiveFiltering); + + if (this.$select.find('option').length >= enableFilterLength) { + + this.$filter = $(this.options.templates.filter); + $('input', this.$filter).attr('placeholder', this.options.filterPlaceholder); + + // Adds optional filter clear button + if(this.options.includeFilterClearBtn) { + var clearBtn = $(this.options.templates.filterClearBtn); + clearBtn.on('click', $.proxy(function(event){ + clearTimeout(this.searchTimeout); + + this.$filter.find('.multiselect-search').val(''); + $('li', this.$ul).show().removeClass('multiselect-filter-hidden'); + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + }, this)); + this.$filter.find('.input-group').append(clearBtn); + } + + this.$ul.prepend(this.$filter); + + this.$filter.val(this.query).on('click', function(event) { + event.stopPropagation(); + }).on('input keydown', $.proxy(function(event) { + // Cancel enter key default behaviour + if (event.which === 13) { + event.preventDefault(); + } + + // This is useful to catch "keydown" events after the browser has updated the control. + clearTimeout(this.searchTimeout); + + this.searchTimeout = this.asyncFunction($.proxy(function() { + + if (this.query !== event.target.value) { + this.query = event.target.value; + + var currentGroup, currentGroupVisible; + $.each($('li', this.$ul), $.proxy(function(index, element) { + var value = $('input', element).length > 0 ? $('input', element).val() : ""; + var text = $('label', element).text(); + + var filterCandidate = ''; + if ((this.options.filterBehavior === 'text')) { + filterCandidate = text; + } + else if ((this.options.filterBehavior === 'value')) { + filterCandidate = value; + } + else if (this.options.filterBehavior === 'both') { + filterCandidate = text + '\n' + value; + } + + if (value !== this.options.selectAllValue && text) { + + // By default lets assume that element is not + // interesting for this search. + var showElement = false; + + if (this.options.enableCaseInsensitiveFiltering) { + filterCandidate = filterCandidate.toLowerCase(); + this.query = this.query.toLowerCase(); + } + + if (this.options.enableFullValueFiltering && this.options.filterBehavior !== 'both') { + var valueToMatch = filterCandidate.trim().substring(0, this.query.length); + if (this.query.indexOf(valueToMatch) > -1) { + showElement = true; + } + } + else if (filterCandidate.indexOf(this.query) > -1) { + showElement = true; + } + + // Toggle current element (group or group item) according to showElement boolean. + $(element).toggle(showElement) + .toggleClass('multiselect-filter-hidden', !showElement); + + // Differentiate groups and group items. + if ($(element).hasClass('multiselect-group')) { + // Remember group status. + currentGroup = element; + currentGroupVisible = showElement; + } + else { + // Show group name when at least one of its items is visible. + if (showElement) { + $(currentGroup).show() + .removeClass('multiselect-filter-hidden'); + } + + // Show all group items when group name satisfies filter. + if (!showElement && currentGroupVisible) { + $(element).show() + .removeClass('multiselect-filter-hidden'); + } + } + } + }, this)); + } + + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + this.options.onFiltering(event.target); + + }, this), 300, this); + }, this)); + } + } + }, + + /** + * Unbinds the whole plugin. + */ + destroy: function() { + this.$container.remove(); + this.$select.show(); + + // reset original state + this.$select.prop('disabled', this.options.wasDisabled); + + this.$select.data('multiselect', null); + }, + + /** + * Refreshs the multiselect based on the selected options of the select. + */ + refresh: function () { + var inputs = $.map($('li input', this.$ul), $); + + $('option', this.$select).each($.proxy(function (index, element) { + var $elem = $(element); + var value = $elem.val(); + var $input; + for (var i = inputs.length; 0 < i--; /**/) { + if (value !== ($input = inputs[i]).val()) + continue; // wrong li + + if ($elem.is(':selected')) { + $input.prop('checked', true); + + if (this.options.selectedClass) { + $input.closest('li') + .addClass(this.options.selectedClass); + } + } + else { + $input.prop('checked', false); + + if (this.options.selectedClass) { + $input.closest('li') + .removeClass(this.options.selectedClass); + } + } + + if ($elem.is(":disabled")) { + $input.attr('disabled', 'disabled') + .prop('disabled', true) + .closest('li') + .addClass('disabled'); + } + else { + $input.prop('disabled', false) + .closest('li') + .removeClass('disabled'); + } + break; // assumes unique values + } + }, this)); + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Select all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered if + * and only if one value is passed. + * + * @param {Array} selectValues + * @param {Boolean} triggerOnChange + */ + select: function(selectValues, triggerOnChange) { + if(!$.isArray(selectValues)) { + selectValues = [selectValues]; + } + + for (var i = 0; i < selectValues.length; i++) { + var value = selectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (!this.options.multiple) { + this.deselectAll(false); + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .addClass(this.options.selectedClass); + } + + $checkbox.prop('checked', true); + $option.prop('selected', true); + + if (triggerOnChange) { + this.options.onChange($option, true); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Clears all selected items. + */ + clearSelection: function () { + this.deselectAll(false); + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Deselects all options of the given values. + * + * If triggerOnChange is set to true, the on change event is triggered, if + * and only if one value is passed. + * + * @param {Array} deselectValues + * @param {Boolean} triggerOnChange + */ + deselect: function(deselectValues, triggerOnChange) { + if(!$.isArray(deselectValues)) { + deselectValues = [deselectValues]; + } + + for (var i = 0; i < deselectValues.length; i++) { + var value = deselectValues[i]; + + if (value === null || value === undefined) { + continue; + } + + var $option = this.getOptionByValue(value); + var $checkbox = this.getInputByValue(value); + + if($option === undefined || $checkbox === undefined) { + continue; + } + + if (this.options.selectedClass) { + $checkbox.closest('li') + .removeClass(this.options.selectedClass); + } + + $checkbox.prop('checked', false); + $option.prop('selected', false); + + if (triggerOnChange) { + this.options.onChange($option, false); + } + } + + this.updateButtonText(); + this.updateSelectAll(); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + }, + + /** + * Selects all enabled & visible options. + * + * If justVisible is true or not specified, only visible options are selected. + * + * @param {Boolean} justVisible + * @param {Boolean} triggerOnSelectAll + */ + selectAll: function (justVisible, triggerOnSelectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input:enabled' , visibleLis).prop('checked', true); + visibleLis.addClass(this.options.selectedClass); + + $('input:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + else { + $('input:enabled' , allLis).prop('checked', true); + allLis.addClass(this.options.selectedClass); + + $('input:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', true); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnSelectAll) { + this.options.onSelectAll(); + } + }, + + /** + * Deselects all options. + * + * If justVisible is true or not specified, only visible options are deselected. + * + * @param {Boolean} justVisible + */ + deselectAll: function (justVisible, triggerOnDeselectAll) { + + var justVisible = typeof justVisible === 'undefined' ? true : justVisible; + var allLis = $("li:not(.divider):not(.disabled):not(.multiselect-group)", this.$ul); + var visibleLis = $("li:not(.divider):not(.disabled):not(.multiselect-group):not(.multiselect-filter-hidden):not(.multiselect-collapisble-hidden)", this.$ul).filter(':visible'); + + if(justVisible) { + $('input[type="checkbox"]:enabled' , visibleLis).prop('checked', false); + visibleLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , visibleLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + else { + $('input[type="checkbox"]:enabled' , allLis).prop('checked', false); + allLis.removeClass(this.options.selectedClass); + + $('input[type="checkbox"]:enabled' , allLis).each($.proxy(function(index, element) { + var value = $(element).val(); + var option = this.getOptionByValue(value); + $(option).prop('selected', false); + }, this)); + } + + $('li input[value="' + this.options.selectAllValue + '"]', this.$ul).prop('checked', false); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (triggerOnDeselectAll) { + this.options.onDeselectAll(); + } + }, + + /** + * Rebuild the plugin. + * + * Rebuilds the dropdown, the filter and the select all option. + */ + rebuild: function() { + this.$ul.html(''); + + // Important to distinguish between radios and checkboxes. + this.options.multiple = this.$select.attr('multiple') === "multiple"; + + this.buildSelectAll(); + this.buildDropdownOptions(); + this.buildFilter(); + + this.updateButtonText(); + this.updateSelectAll(true); + + if (this.options.enableClickableOptGroups && this.options.multiple) { + this.updateOptGroups(); + } + + if (this.options.disableIfEmpty && $('option', this.$select).length <= 0) { + this.disable(); + } + else { + this.enable(); + } + + if (this.options.dropRight) { + this.$ul.addClass('pull-right'); + } + }, + + /** + * The provided data will be used to build the dropdown. + */ + dataprovider: function(dataprovider) { + + var groupCounter = 0; + var $select = this.$select.empty(); + + $.each(dataprovider, function (index, option) { + var $tag; + + if ($.isArray(option.children)) { // create optiongroup tag + groupCounter++; + + $tag = $('').attr({ + label: option.label || 'Group ' + groupCounter, + disabled: !!option.disabled + }); + + forEach(option.children, function(subOption) { // add children option tags + var attributes = { + value: subOption.value, + label: subOption.label || subOption.value, + title: subOption.title, + selected: !!subOption.selected, + disabled: !!subOption.disabled + }; + + //Loop through attributes object and add key-value for each attribute + for (var key in subOption.attributes) { + attributes['data-' + key] = subOption.attributes[key]; + } + //Append original attributes + new data attributes to option + $tag.append($('