From 9f18a590819a01017c15169d82763680a72848fb Mon Sep 17 00:00:00 2001 From: Aamir Khan Date: Tue, 24 Jul 2012 17:00:25 -0400 Subject: Packaging hyperkitty --- COPYING | 674 --- __init__.py | 0 apache/README.rst | 17 - apache/__init__.py | 0 apache/apache_django_wsgi.conf | 23 - apache/django.wsgi | 42 - apache/settings_production.py | 195 - apache/urls_production.py | 87 - api.py | 69 - context_processors.py | 25 - ez_setup.py | 284 + hyperkitty/__init__.py | 0 hyperkitty/apache/README.rst | 17 + hyperkitty/apache/__init__.py | 0 hyperkitty/apache/apache_django_wsgi.conf | 23 + hyperkitty/apache/django.wsgi | 42 + hyperkitty/apache/settings_production.py | 195 + hyperkitty/apache/urls_production.py | 87 + hyperkitty/api.py | 69 + hyperkitty/context_processors.py | 25 + hyperkitty/lib/__init__.py | 14 + hyperkitty/lib/mockup.py | 167 + hyperkitty/models.py | 77 + hyperkitty/robots.txt | 4 + hyperkitty/settings.py | 195 + hyperkitty/static/css/bootstrap.css | 3496 +++++++++++ hyperkitty/static/css/normalize.css | 504 ++ hyperkitty/static/css/stats.css | 137 + hyperkitty/static/css/style.css | 382 ++ hyperkitty/static/css/thread.css | 239 + hyperkitty/static/img/button_newer.png | Bin 0 -> 4400 bytes hyperkitty/static/img/button_older.png | Bin 0 -> 4261 bytes hyperkitty/static/img/discussion.png | Bin 0 -> 514 bytes hyperkitty/static/img/email_bg.png | Bin 0 -> 541 bytes hyperkitty/static/img/like.png | Bin 0 -> 915 bytes hyperkitty/static/img/likealot.png | Bin 0 -> 912 bytes hyperkitty/static/img/login/browserid.png | Bin 0 -> 1798 bytes hyperkitty/static/img/login/facebook.png | Bin 0 -> 3541 bytes hyperkitty/static/img/login/google.png | Bin 0 -> 3549 bytes hyperkitty/static/img/login/openid.png | Bin 0 -> 2950 bytes hyperkitty/static/img/login/twitter.png | Bin 0 -> 3425 bytes hyperkitty/static/img/login/yahoo.png | Bin 0 -> 3186 bytes hyperkitty/static/img/neutral.png | Bin 0 -> 895 bytes hyperkitty/static/img/newthread.png | Bin 0 -> 6161 bytes hyperkitty/static/img/notsaved.png | Bin 0 -> 671 bytes hyperkitty/static/img/participant.png | Bin 0 -> 675 bytes hyperkitty/static/img/saved.png | Bin 0 -> 700 bytes hyperkitty/static/img/show_discussion.png | Bin 0 -> 3368 bytes hyperkitty/static/img/youdislike.png | Bin 0 -> 400 bytes hyperkitty/static/img/youlike.png | Bin 0 -> 498 bytes hyperkitty/static/jquery.expander.js | 382 ++ hyperkitty/static/js/libs/jquery-1.7.1.min.js | 4 + hyperkitty/static/protovis-d3.1.js | 7725 ++++++++++++++++++++++++ hyperkitty/templates/404.html | 37 + hyperkitty/templates/500.html | 37 + hyperkitty/templates/api.html | 77 + hyperkitty/templates/base.html | 65 + hyperkitty/templates/index.html | 18 + hyperkitty/templates/login.html | 58 + hyperkitty/templates/message.html | 71 + hyperkitty/templates/messages/first_email.html | 5 + hyperkitty/templates/messages/message.html | 34 + hyperkitty/templates/month_view.html | 111 + hyperkitty/templates/recent_activities.html | 212 + hyperkitty/templates/register.html | 17 + hyperkitty/templates/search.html | 106 + hyperkitty/templates/thread.html | 99 + hyperkitty/templates/threads/add_tag_form.html | 13 + hyperkitty/templates/threads/right_col.html | 60 + hyperkitty/templates/user_profile.html | 69 + hyperkitty/templatetags/__init__.py | 0 hyperkitty/templatetags/poll_extras.py | 55 + hyperkitty/tests/__init__.py | 24 + hyperkitty/tests/test_forms.py | 20 + hyperkitty/tests/test_models.py | 20 + hyperkitty/tests/test_views.py | 72 + hyperkitty/todo | 5 + hyperkitty/urls.py | 106 + hyperkitty/utils.py | 107 + hyperkitty/views/__init__.py | 0 hyperkitty/views/accounts.py | 114 + hyperkitty/views/api.py | 27 + hyperkitty/views/forms.py | 59 + hyperkitty/views/list.py | 291 + hyperkitty/views/message.py | 82 + hyperkitty/views/pages.py | 38 + hyperkitty/views/thread.py | 113 + lib/__init__.py | 14 - lib/mockup.py | 167 - models.py | 77 - robots.txt | 4 - settings.py | 195 - setup.py | 18 + static/css/bootstrap.css | 3496 ----------- static/css/normalize.css | 504 -- static/css/stats.css | 137 - static/css/style.css | 382 -- static/css/thread.css | 239 - static/img/button_newer.png | Bin 4400 -> 0 bytes static/img/button_older.png | Bin 4261 -> 0 bytes static/img/discussion.png | Bin 514 -> 0 bytes static/img/email_bg.png | Bin 541 -> 0 bytes static/img/like.png | Bin 915 -> 0 bytes static/img/likealot.png | Bin 912 -> 0 bytes static/img/login/browserid.png | Bin 1798 -> 0 bytes static/img/login/facebook.png | Bin 3541 -> 0 bytes static/img/login/google.png | Bin 3549 -> 0 bytes static/img/login/openid.png | Bin 2950 -> 0 bytes static/img/login/twitter.png | Bin 3425 -> 0 bytes static/img/login/yahoo.png | Bin 3186 -> 0 bytes static/img/neutral.png | Bin 895 -> 0 bytes static/img/newthread.png | Bin 6161 -> 0 bytes static/img/notsaved.png | Bin 671 -> 0 bytes static/img/participant.png | Bin 675 -> 0 bytes static/img/saved.png | Bin 700 -> 0 bytes static/img/show_discussion.png | Bin 3368 -> 0 bytes static/img/youdislike.png | Bin 400 -> 0 bytes static/img/youlike.png | Bin 498 -> 0 bytes static/jquery.expander.js | 382 -- static/js/libs/jquery-1.7.1.min.js | 4 - static/protovis-d3.1.js | 7725 ------------------------ templates/404.html | 37 - templates/500.html | 37 - templates/api.html | 77 - templates/base.html | 65 - templates/index.html | 18 - templates/login.html | 58 - templates/message.html | 71 - templates/messages/first_email.html | 5 - templates/messages/message.html | 34 - templates/month_view.html | 111 - templates/recent_activities.html | 212 - templates/register.html | 17 - templates/search.html | 106 - templates/thread.html | 99 - templates/threads/add_tag_form.html | 13 - templates/threads/right_col.html | 60 - templates/user_profile.html | 69 - templatetags/__init__.py | 0 templatetags/poll_extras.py | 55 - tests/__init__.py | 24 - tests/test_forms.py | 20 - tests/test_models.py | 20 - tests/test_views.py | 72 - todo | 5 - urls.py | 106 - utils.py | 107 - views/__init__.py | 0 views/accounts.py | 114 - views/api.py | 27 - views/forms.py | 59 - views/list.py | 291 - views/message.py | 82 - views/pages.py | 38 - views/thread.py | 113 - 155 files changed, 16308 insertions(+), 16680 deletions(-) delete mode 100644 COPYING delete mode 100644 __init__.py delete mode 100644 apache/README.rst delete mode 100644 apache/__init__.py delete mode 100644 apache/apache_django_wsgi.conf delete mode 100755 apache/django.wsgi delete mode 100644 apache/settings_production.py delete mode 100644 apache/urls_production.py delete mode 100644 api.py delete mode 100644 context_processors.py create mode 100644 ez_setup.py create mode 100644 hyperkitty/__init__.py create mode 100644 hyperkitty/apache/README.rst create mode 100644 hyperkitty/apache/__init__.py create mode 100644 hyperkitty/apache/apache_django_wsgi.conf create mode 100755 hyperkitty/apache/django.wsgi create mode 100644 hyperkitty/apache/settings_production.py create mode 100644 hyperkitty/apache/urls_production.py create mode 100644 hyperkitty/api.py create mode 100644 hyperkitty/context_processors.py create mode 100644 hyperkitty/lib/__init__.py create mode 100644 hyperkitty/lib/mockup.py create mode 100644 hyperkitty/models.py create mode 100644 hyperkitty/robots.txt create mode 100644 hyperkitty/settings.py create mode 100644 hyperkitty/static/css/bootstrap.css create mode 100644 hyperkitty/static/css/normalize.css create mode 100644 hyperkitty/static/css/stats.css create mode 100644 hyperkitty/static/css/style.css create mode 100644 hyperkitty/static/css/thread.css create mode 100644 hyperkitty/static/img/button_newer.png create mode 100644 hyperkitty/static/img/button_older.png create mode 100644 hyperkitty/static/img/discussion.png create mode 100644 hyperkitty/static/img/email_bg.png create mode 100644 hyperkitty/static/img/like.png create mode 100644 hyperkitty/static/img/likealot.png create mode 100644 hyperkitty/static/img/login/browserid.png create mode 100644 hyperkitty/static/img/login/facebook.png create mode 100644 hyperkitty/static/img/login/google.png create mode 100644 hyperkitty/static/img/login/openid.png create mode 100644 hyperkitty/static/img/login/twitter.png create mode 100644 hyperkitty/static/img/login/yahoo.png create mode 100644 hyperkitty/static/img/neutral.png create mode 100644 hyperkitty/static/img/newthread.png create mode 100644 hyperkitty/static/img/notsaved.png create mode 100644 hyperkitty/static/img/participant.png create mode 100644 hyperkitty/static/img/saved.png create mode 100644 hyperkitty/static/img/show_discussion.png create mode 100644 hyperkitty/static/img/youdislike.png create mode 100644 hyperkitty/static/img/youlike.png create mode 100644 hyperkitty/static/jquery.expander.js create mode 100644 hyperkitty/static/js/libs/jquery-1.7.1.min.js create mode 100644 hyperkitty/static/protovis-d3.1.js create mode 100644 hyperkitty/templates/404.html create mode 100644 hyperkitty/templates/500.html create mode 100644 hyperkitty/templates/api.html create mode 100644 hyperkitty/templates/base.html create mode 100644 hyperkitty/templates/index.html create mode 100644 hyperkitty/templates/login.html create mode 100644 hyperkitty/templates/message.html create mode 100644 hyperkitty/templates/messages/first_email.html create mode 100644 hyperkitty/templates/messages/message.html create mode 100644 hyperkitty/templates/month_view.html create mode 100644 hyperkitty/templates/recent_activities.html create mode 100644 hyperkitty/templates/register.html create mode 100644 hyperkitty/templates/search.html create mode 100644 hyperkitty/templates/thread.html create mode 100644 hyperkitty/templates/threads/add_tag_form.html create mode 100644 hyperkitty/templates/threads/right_col.html create mode 100644 hyperkitty/templates/user_profile.html create mode 100644 hyperkitty/templatetags/__init__.py create mode 100644 hyperkitty/templatetags/poll_extras.py create mode 100644 hyperkitty/tests/__init__.py create mode 100644 hyperkitty/tests/test_forms.py create mode 100644 hyperkitty/tests/test_models.py create mode 100644 hyperkitty/tests/test_views.py create mode 100644 hyperkitty/todo create mode 100644 hyperkitty/urls.py create mode 100644 hyperkitty/utils.py create mode 100644 hyperkitty/views/__init__.py create mode 100644 hyperkitty/views/accounts.py create mode 100644 hyperkitty/views/api.py create mode 100644 hyperkitty/views/forms.py create mode 100644 hyperkitty/views/list.py create mode 100644 hyperkitty/views/message.py create mode 100644 hyperkitty/views/pages.py create mode 100644 hyperkitty/views/thread.py delete mode 100644 lib/__init__.py delete mode 100644 lib/mockup.py delete mode 100644 models.py delete mode 100644 robots.txt delete mode 100644 settings.py create mode 100644 setup.py delete mode 100644 static/css/bootstrap.css delete mode 100644 static/css/normalize.css delete mode 100644 static/css/stats.css delete mode 100644 static/css/style.css delete mode 100644 static/css/thread.css delete mode 100644 static/img/button_newer.png delete mode 100644 static/img/button_older.png delete mode 100644 static/img/discussion.png delete mode 100644 static/img/email_bg.png delete mode 100644 static/img/like.png delete mode 100644 static/img/likealot.png delete mode 100644 static/img/login/browserid.png delete mode 100644 static/img/login/facebook.png delete mode 100644 static/img/login/google.png delete mode 100644 static/img/login/openid.png delete mode 100644 static/img/login/twitter.png delete mode 100644 static/img/login/yahoo.png delete mode 100644 static/img/neutral.png delete mode 100644 static/img/newthread.png delete mode 100644 static/img/notsaved.png delete mode 100644 static/img/participant.png delete mode 100644 static/img/saved.png delete mode 100644 static/img/show_discussion.png delete mode 100644 static/img/youdislike.png delete mode 100644 static/img/youlike.png delete mode 100644 static/jquery.expander.js delete mode 100644 static/js/libs/jquery-1.7.1.min.js delete mode 100644 static/protovis-d3.1.js delete mode 100644 templates/404.html delete mode 100644 templates/500.html delete mode 100644 templates/api.html delete mode 100644 templates/base.html delete mode 100644 templates/index.html delete mode 100644 templates/login.html delete mode 100644 templates/message.html delete mode 100644 templates/messages/first_email.html delete mode 100644 templates/messages/message.html delete mode 100644 templates/month_view.html delete mode 100644 templates/recent_activities.html delete mode 100644 templates/register.html delete mode 100644 templates/search.html delete mode 100644 templates/thread.html delete mode 100644 templates/threads/add_tag_form.html delete mode 100644 templates/threads/right_col.html delete mode 100644 templates/user_profile.html delete mode 100644 templatetags/__init__.py delete mode 100644 templatetags/poll_extras.py delete mode 100644 tests/__init__.py delete mode 100644 tests/test_forms.py delete mode 100644 tests/test_models.py delete mode 100644 tests/test_views.py delete mode 100644 todo delete mode 100644 urls.py delete mode 100644 utils.py delete mode 100644 views/__init__.py delete mode 100644 views/accounts.py delete mode 100644 views/api.py delete mode 100644 views/forms.py delete mode 100644 views/list.py delete mode 100644 views/message.py delete mode 100644 views/pages.py delete mode 100644 views/thread.py diff --git a/COPYING b/COPYING deleted file mode 100644 index 94a9ed0..0000000 --- a/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 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 General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is 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. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - 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. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - 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 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. Use with the GNU Affero General Public License. - - 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 Affero 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 special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU 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 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 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 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 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - 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 GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apache/README.rst b/apache/README.rst deleted file mode 100644 index e06893a..0000000 --- a/apache/README.rst +++ /dev/null @@ -1,17 +0,0 @@ -Create logs directory ---------------------- -**mkdir -p logs** - -Create python egg directory ---------------------------- -**mkdir -p .python-egg** -chmod -R 777 .python-egg/ - - - -Edit httpd.conf ---------------- - -Add the following line in your httpd.conf (generally present at /etc/apache2/httpd.conf) - -Include "/path/to/application/apache/apache_django_wsgi.conf" diff --git a/apache/__init__.py b/apache/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/apache/apache_django_wsgi.conf b/apache/apache_django_wsgi.conf deleted file mode 100644 index 7f1ccaf..0000000 --- a/apache/apache_django_wsgi.conf +++ /dev/null @@ -1,23 +0,0 @@ - - - Alias /robots.txt /home/akhan/gsoc/robots.txt - Alias /favicon.ico /home/akhan/gsoc/favicon.ico - Alias /static /home/akhan/gsoc/static - - ErrorLog /home/akhan/gsoc/logs/error.log - CustomLog /home/akhan/gsoc/logs/access.log combined - - WSGIScriptAlias / /home/akhan/gsoc/apache/django.wsgi - WSGIDaemonProcess akhan user=akhan group=users threads=25 - - - Order deny,allow - Allow from all - - - - Order allow,deny - Allow from all - - - diff --git a/apache/django.wsgi b/apache/django.wsgi deleted file mode 100755 index cc699a4..0000000 --- a/apache/django.wsgi +++ /dev/null @@ -1,42 +0,0 @@ -import os -import sys -import site - -STAGING=True - -if STAGING: - # staging virtual environment - vepath = '/home/akhan/.virtualenvs/wackyenv/lib/python2.7/site-packages' -else: - # live virtual environment - vepath = '/home/akhan/.virtualenvs/live-server/lib/python2.7/site-packages' - -prev_sys_path = list(sys.path) - -# add the site-packages of our virtualenv as a site dir -site.addsitedir(vepath) - -# add the app's directory to the PYTHONPATH -sys.path.append('/home/akhan/gsoc') - -# reorder sys.path so new directories from the addsitedir show up first -new_sys_path = [p for p in sys.path if p not in prev_sys_path] - -for item in new_sys_path: - sys.path.remove(item) -sys.path[:0] = new_sys_path - - -#Calculate the path based on the location of the WSGI script. -apache_configuration= os.path.dirname(__file__) -project = os.path.dirname(apache_configuration) -workspace = os.path.dirname(project) -sys.path.append(workspace) - - -os.environ['DJANGO_SETTINGS_MODULE'] = 'gsoc.apache.settings_production' -# make sure this directory is writable by wsgi process -os.environ['PYTHON_EGG_CACHE'] = '/home/akhan/gsoc/.python-egg' - -from django.core.handlers.wsgi import WSGIHandler -application = WSGIHandler() diff --git a/apache/settings_production.py b/apache/settings_production.py deleted file mode 100644 index 1090b7a..0000000 --- a/apache/settings_production.py +++ /dev/null @@ -1,195 +0,0 @@ -import os - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -# Django settings for hyperkitty project. - -DEBUG = False -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('Aamir Khan', 'syst3m.w0rm+hk@gmail.com'), -) - -MANAGERS = ADMINS - -MAILMAN_API_URL=r'http://%(username)s:%(password)s@localhost:8001/3.0/' -MAILMAN_USER='mailmanapi' -MAILMAN_PASS='88ffd62d1094a6248415c59d7538793f3df5de2f04d244087952394e689e902a' - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'hk', # Or path to database file if using sqlite3. - 'USER': 'root', # Not used with sqlite3. - 'PASSWORD': 'rootroot', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -#STATIC_ROOT = '' -STATIC_ROOT = BASE_DIR + '/static_files/' - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - BASE_DIR + '/static/', -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'dtc3%x(k#mzpe32dmhtsb6!3p(izk84f7nuw1-+4x8zsxwsa^z' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - - -TEMPLATE_CONTEXT_PROCESSORS = ( - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.core.context_processors.debug", - "django.core.context_processors.i18n", - "django.core.context_processors.media", - "django.core.context_processors.static", - "django.core.context_processors.csrf", - "django.contrib.messages.context_processors.messages", - "gsoc.context_processors.app_name", -) - - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'gsoc.apache.urls_production' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - BASE_DIR + '/templates', -) - -AUTHENTICATION_BACKENDS = ( - 'social_auth.backends.google.GoogleBackend', - 'social_auth.backends.yahoo.YahooBackend', - 'social_auth.backends.browserid.BrowserIDBackend', - 'social_auth.backends.OpenIDBackend', - 'django.contrib.auth.backends.ModelBackend', -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', -# 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin', - # 'django.contrib.admindocs', - 'gsoc', - 'social_auth', - 'djangorestframework', - 'gravatar', -) - - -LOGIN_URL = '/accounts/login/' -LOGIN_REDIRECT_URL = '/' -LOGIN_ERROR_URL = '/accounts/login/' -SOCIAL_AUTH_COMPLETE_URL_NAME = 'socialauth_complete' -SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'socialauth_associate_complete' -SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' -SOCIAL_AUTH_UUID_LENGTH = 16 - -AUTH_PROFILE_MODULE = 'gsoc.UserProfile' - - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -SOCIAL_AUTH_LAST_LOGIN = 'social_auth_last_login_backend' -APP_NAME = 'Fedora Mailman App' -KITTYSTORE_URL = 'postgres://mm3:mm3@localhost/mm3' diff --git a/apache/urls_production.py b/apache/urls_production.py deleted file mode 100644 index 3bb4a65..0000000 --- a/apache/urls_production.py +++ /dev/null @@ -1,87 +0,0 @@ -from django.conf.urls.defaults import patterns, include, url -from django.views.generic.simple import direct_to_template -from gsoc.api import EmailResource, ThreadResource, SearchResource - -urlpatterns = patterns('', - # Account - url(r'^accounts/login/$', 'views.accounts.user_login', name='user_login'), - url(r'^accounts/logout/$', 'views.accounts.user_logout', name='user_logout'), - url(r'^accounts/profile/$', 'views.accounts.user_profile', name='user_profile'), - url(r'^accounts/register/$', 'views.accounts.user_registration', name='user_registration'), - - - # Index - url(r'^/$', 'views.pages.index', name='index'), - url(r'^$', 'views.pages.index', name='index'), - - # Archives - url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/(?P\d\d?)/$', - 'views.list.archives'), - url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/$', - 'views.list.archives'), - url(r'^archives/(?P.*@.*)/$', - 'views.list.archives'), - - # Threads - url(r'^thread/(?P.*@.*)/(?P.+)/$', - 'views.thread.thread_index'), - - - # Lists - url(r'^list/$', 'views.pages.index'), # Can I remove this URL? - url(r'^list/(?P.*@.*)/$', - 'views.list.list'), - - # Search Tag - url(r'^tag/(?P.*@.*)\/(?P.*)\/(?P\d+)/$', - 'views.list.search_tag'), - url(r'^tag/(?P.*@.*)\/(?P.*)/$', - 'views.list.search_tag'), - - # Search - # If page number is present in URL - url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)\/(?P\d+)/$', - 'views.list.search_keyword'), - # Show the first page as default when no page number is present in URL - url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)/$', - 'views.list.search_keyword'), - url(r'^search/(?P.*@.*)/$', - 'views.list.search'), - - - ### MESSAGE LEVEL VIEWS ### - # Vote a message - url(r'^message/(?P.*@.*)/(?P.+)/$', - 'views.message.index'), - - url(r'^vote/(?P.*@.*)/$', - 'views.message.vote'), - ### MESSAGE LEVEL VIEW ENDS ### - - - - ### THREAD LEVEL VIEWS ### - # Thread view page - url(r'^thread/(?P.*@.*)/(?P.+)/$', - 'views.thread.thread_index'), - - # Add Tag to a thread - url(r'^addtag/(?P.*@.*)\/(?P.*)/$', - 'views.thread.add_tag'), - ### THREAD LEVEL VIEW ENDS ### - - - # REST API - url(r'^api/$', 'views.api.api'), - url(r'^api/email\/(?P.*@.*)\/(?P.*)/', - EmailResource.as_view()), - url(r'^api/thread\/(?P.*@.*)\/(?P.*)/', - ThreadResource.as_view()), - url(r'^api/search\/(?P.*@.*)\/(?P.*)\/(?P.*)/', - SearchResource.as_view()), - - - # Social Auth - url(r'', include('social_auth.urls')), - -) diff --git a/api.py b/api.py deleted file mode 100644 index fe1d715..0000000 --- a/api.py +++ /dev/null @@ -1,69 +0,0 @@ -#-*- coding: utf-8 -*- - -from djangorestframework.views import View -from django.conf.urls.defaults import url -from django.conf import settings -from django.http import HttpResponseNotModified, HttpResponse -from kittystore.kittysastore import KittySAStore -import json -import re - -from gsoc.utils import log - -STORE = KittySAStore(settings.KITTYSTORE_URL) - - -class EmailResource(View): - """ Resource used to retrieve emails from the archives using the - REST API. - """ - - def get(self, request, mlist_fqdn, messageid): - list_name = mlist_fqdn.split('@')[0] - email = STORE.get_email(list_name, messageid) - if not email: - return HttpResponse(status=404) - else: - return email - - -class ThreadResource(View): - """ Resource used to retrieve threads from the archives using the - REST API. - """ - - def get(self, request, mlist_fqdn, threadid): - list_name = mlist_fqdn.split('@')[0] - thread = STORE.get_thread(list_name, threadid) - if not thread: - return HttpResponse(status=404) - else: - return thread - - -class SearchResource(View): - """ Resource used to search the archives using the REST API. - """ - - def get(self, request, mlist_fqdn, field, keyword): - list_name = mlist_fqdn.split('@')[0] - - if field not in ['Subject', 'Content', 'SubjectContent', 'From']: - return HttpResponse(status=404) - - regex = '.*%s.*' % keyword - if field == 'SubjectContent': - query_string = {'$or' : [ - {'Subject': re.compile(regex, re.IGNORECASE)}, - {'Content': re.compile(regex, re.IGNORECASE)} - ]} - else: - query_string = {field.capitalize(): - re.compile(regex, re.IGNORECASE)} - - print query_string, field, keyword - threads = STORE.search_archives(list_name, query_string) - if not threads: - return HttpResponse(status=404) - else: - return threads diff --git a/context_processors.py b/context_processors.py deleted file mode 100644 index 5c39319..0000000 --- a/context_processors.py +++ /dev/null @@ -1,25 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# - -from django.conf import settings - -def app_name(context): - return {'app_name' : settings.APP_NAME} \ No newline at end of file diff --git a/ez_setup.py b/ez_setup.py new file mode 100644 index 0000000..b74adc0 --- /dev/null +++ b/ez_setup.py @@ -0,0 +1,284 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c11" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c10-py2.3.egg': 'ce1e2ab5d3a0256456d9fc13800a7090', + 'setuptools-0.6c10-py2.4.egg': '57d6d9d6e9b80772c59a53a8433a5dd4', + 'setuptools-0.6c10-py2.5.egg': 'de46ac8b1c97c895572e5e8596aeb8c7', + 'setuptools-0.6c10-py2.6.egg': '58ea40aef06da02ce641495523a0b7f5', + 'setuptools-0.6c11-py2.3.egg': '2baeac6e13d414a9d28e7ba5b5a596de', + 'setuptools-0.6c11-py2.4.egg': 'bd639f9b0eac4c42497034dec2ec0c2b', + 'setuptools-0.6c11-py2.5.egg': '64c94f3bf7a72a13ec83e0b24f2749b2', + 'setuptools-0.6c11-py2.6.egg': 'bfa92100bd772d5a213eedd356d64086', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + except pkg_resources.DistributionNotFound: + pass + + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + + diff --git a/hyperkitty/__init__.py b/hyperkitty/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hyperkitty/apache/README.rst b/hyperkitty/apache/README.rst new file mode 100644 index 0000000..e06893a --- /dev/null +++ b/hyperkitty/apache/README.rst @@ -0,0 +1,17 @@ +Create logs directory +--------------------- +**mkdir -p logs** + +Create python egg directory +--------------------------- +**mkdir -p .python-egg** +chmod -R 777 .python-egg/ + + + +Edit httpd.conf +--------------- + +Add the following line in your httpd.conf (generally present at /etc/apache2/httpd.conf) + +Include "/path/to/application/apache/apache_django_wsgi.conf" diff --git a/hyperkitty/apache/__init__.py b/hyperkitty/apache/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hyperkitty/apache/apache_django_wsgi.conf b/hyperkitty/apache/apache_django_wsgi.conf new file mode 100644 index 0000000..7f1ccaf --- /dev/null +++ b/hyperkitty/apache/apache_django_wsgi.conf @@ -0,0 +1,23 @@ + + + Alias /robots.txt /home/akhan/gsoc/robots.txt + Alias /favicon.ico /home/akhan/gsoc/favicon.ico + Alias /static /home/akhan/gsoc/static + + ErrorLog /home/akhan/gsoc/logs/error.log + CustomLog /home/akhan/gsoc/logs/access.log combined + + WSGIScriptAlias / /home/akhan/gsoc/apache/django.wsgi + WSGIDaemonProcess akhan user=akhan group=users threads=25 + + + Order deny,allow + Allow from all + + + + Order allow,deny + Allow from all + + + diff --git a/hyperkitty/apache/django.wsgi b/hyperkitty/apache/django.wsgi new file mode 100755 index 0000000..cc699a4 --- /dev/null +++ b/hyperkitty/apache/django.wsgi @@ -0,0 +1,42 @@ +import os +import sys +import site + +STAGING=True + +if STAGING: + # staging virtual environment + vepath = '/home/akhan/.virtualenvs/wackyenv/lib/python2.7/site-packages' +else: + # live virtual environment + vepath = '/home/akhan/.virtualenvs/live-server/lib/python2.7/site-packages' + +prev_sys_path = list(sys.path) + +# add the site-packages of our virtualenv as a site dir +site.addsitedir(vepath) + +# add the app's directory to the PYTHONPATH +sys.path.append('/home/akhan/gsoc') + +# reorder sys.path so new directories from the addsitedir show up first +new_sys_path = [p for p in sys.path if p not in prev_sys_path] + +for item in new_sys_path: + sys.path.remove(item) +sys.path[:0] = new_sys_path + + +#Calculate the path based on the location of the WSGI script. +apache_configuration= os.path.dirname(__file__) +project = os.path.dirname(apache_configuration) +workspace = os.path.dirname(project) +sys.path.append(workspace) + + +os.environ['DJANGO_SETTINGS_MODULE'] = 'gsoc.apache.settings_production' +# make sure this directory is writable by wsgi process +os.environ['PYTHON_EGG_CACHE'] = '/home/akhan/gsoc/.python-egg' + +from django.core.handlers.wsgi import WSGIHandler +application = WSGIHandler() diff --git a/hyperkitty/apache/settings_production.py b/hyperkitty/apache/settings_production.py new file mode 100644 index 0000000..1090b7a --- /dev/null +++ b/hyperkitty/apache/settings_production.py @@ -0,0 +1,195 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +# Django settings for hyperkitty project. + +DEBUG = False +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('Aamir Khan', 'syst3m.w0rm+hk@gmail.com'), +) + +MANAGERS = ADMINS + +MAILMAN_API_URL=r'http://%(username)s:%(password)s@localhost:8001/3.0/' +MAILMAN_USER='mailmanapi' +MAILMAN_PASS='88ffd62d1094a6248415c59d7538793f3df5de2f04d244087952394e689e902a' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'hk', # Or path to database file if using sqlite3. + 'USER': 'root', # Not used with sqlite3. + 'PASSWORD': 'rootroot', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +#STATIC_ROOT = '' +STATIC_ROOT = BASE_DIR + '/static_files/' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/static/', +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'dtc3%x(k#mzpe32dmhtsb6!3p(izk84f7nuw1-+4x8zsxwsa^z' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.core.context_processors.csrf", + "django.contrib.messages.context_processors.messages", + "gsoc.context_processors.app_name", +) + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'gsoc.apache.urls_production' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/templates', +) + +AUTHENTICATION_BACKENDS = ( + 'social_auth.backends.google.GoogleBackend', + 'social_auth.backends.yahoo.YahooBackend', + 'social_auth.backends.browserid.BrowserIDBackend', + 'social_auth.backends.OpenIDBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', +# 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + # 'django.contrib.admindocs', + 'gsoc', + 'social_auth', + 'djangorestframework', + 'gravatar', +) + + +LOGIN_URL = '/accounts/login/' +LOGIN_REDIRECT_URL = '/' +LOGIN_ERROR_URL = '/accounts/login/' +SOCIAL_AUTH_COMPLETE_URL_NAME = 'socialauth_complete' +SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'socialauth_associate_complete' +SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' +SOCIAL_AUTH_UUID_LENGTH = 16 + +AUTH_PROFILE_MODULE = 'gsoc.UserProfile' + + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +SOCIAL_AUTH_LAST_LOGIN = 'social_auth_last_login_backend' +APP_NAME = 'Fedora Mailman App' +KITTYSTORE_URL = 'postgres://mm3:mm3@localhost/mm3' diff --git a/hyperkitty/apache/urls_production.py b/hyperkitty/apache/urls_production.py new file mode 100644 index 0000000..3bb4a65 --- /dev/null +++ b/hyperkitty/apache/urls_production.py @@ -0,0 +1,87 @@ +from django.conf.urls.defaults import patterns, include, url +from django.views.generic.simple import direct_to_template +from gsoc.api import EmailResource, ThreadResource, SearchResource + +urlpatterns = patterns('', + # Account + url(r'^accounts/login/$', 'views.accounts.user_login', name='user_login'), + url(r'^accounts/logout/$', 'views.accounts.user_logout', name='user_logout'), + url(r'^accounts/profile/$', 'views.accounts.user_profile', name='user_profile'), + url(r'^accounts/register/$', 'views.accounts.user_registration', name='user_registration'), + + + # Index + url(r'^/$', 'views.pages.index', name='index'), + url(r'^$', 'views.pages.index', name='index'), + + # Archives + url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/(?P\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P.*@.*)/$', + 'views.list.archives'), + + # Threads + url(r'^thread/(?P.*@.*)/(?P.+)/$', + 'views.thread.thread_index'), + + + # Lists + url(r'^list/$', 'views.pages.index'), # Can I remove this URL? + url(r'^list/(?P.*@.*)/$', + 'views.list.list'), + + # Search Tag + url(r'^tag/(?P.*@.*)\/(?P.*)\/(?P\d+)/$', + 'views.list.search_tag'), + url(r'^tag/(?P.*@.*)\/(?P.*)/$', + 'views.list.search_tag'), + + # Search + # If page number is present in URL + url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)\/(?P\d+)/$', + 'views.list.search_keyword'), + # Show the first page as default when no page number is present in URL + url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)/$', + 'views.list.search_keyword'), + url(r'^search/(?P.*@.*)/$', + 'views.list.search'), + + + ### MESSAGE LEVEL VIEWS ### + # Vote a message + url(r'^message/(?P.*@.*)/(?P.+)/$', + 'views.message.index'), + + url(r'^vote/(?P.*@.*)/$', + 'views.message.vote'), + ### MESSAGE LEVEL VIEW ENDS ### + + + + ### THREAD LEVEL VIEWS ### + # Thread view page + url(r'^thread/(?P.*@.*)/(?P.+)/$', + 'views.thread.thread_index'), + + # Add Tag to a thread + url(r'^addtag/(?P.*@.*)\/(?P.*)/$', + 'views.thread.add_tag'), + ### THREAD LEVEL VIEW ENDS ### + + + # REST API + url(r'^api/$', 'views.api.api'), + url(r'^api/email\/(?P.*@.*)\/(?P.*)/', + EmailResource.as_view()), + url(r'^api/thread\/(?P.*@.*)\/(?P.*)/', + ThreadResource.as_view()), + url(r'^api/search\/(?P.*@.*)\/(?P.*)\/(?P.*)/', + SearchResource.as_view()), + + + # Social Auth + url(r'', include('social_auth.urls')), + +) diff --git a/hyperkitty/api.py b/hyperkitty/api.py new file mode 100644 index 0000000..fe1d715 --- /dev/null +++ b/hyperkitty/api.py @@ -0,0 +1,69 @@ +#-*- coding: utf-8 -*- + +from djangorestframework.views import View +from django.conf.urls.defaults import url +from django.conf import settings +from django.http import HttpResponseNotModified, HttpResponse +from kittystore.kittysastore import KittySAStore +import json +import re + +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +class EmailResource(View): + """ Resource used to retrieve emails from the archives using the + REST API. + """ + + def get(self, request, mlist_fqdn, messageid): + list_name = mlist_fqdn.split('@')[0] + email = STORE.get_email(list_name, messageid) + if not email: + return HttpResponse(status=404) + else: + return email + + +class ThreadResource(View): + """ Resource used to retrieve threads from the archives using the + REST API. + """ + + def get(self, request, mlist_fqdn, threadid): + list_name = mlist_fqdn.split('@')[0] + thread = STORE.get_thread(list_name, threadid) + if not thread: + return HttpResponse(status=404) + else: + return thread + + +class SearchResource(View): + """ Resource used to search the archives using the REST API. + """ + + def get(self, request, mlist_fqdn, field, keyword): + list_name = mlist_fqdn.split('@')[0] + + if field not in ['Subject', 'Content', 'SubjectContent', 'From']: + return HttpResponse(status=404) + + regex = '.*%s.*' % keyword + if field == 'SubjectContent': + query_string = {'$or' : [ + {'Subject': re.compile(regex, re.IGNORECASE)}, + {'Content': re.compile(regex, re.IGNORECASE)} + ]} + else: + query_string = {field.capitalize(): + re.compile(regex, re.IGNORECASE)} + + print query_string, field, keyword + threads = STORE.search_archives(list_name, query_string) + if not threads: + return HttpResponse(status=404) + else: + return threads diff --git a/hyperkitty/context_processors.py b/hyperkitty/context_processors.py new file mode 100644 index 0000000..5c39319 --- /dev/null +++ b/hyperkitty/context_processors.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# + +from django.conf import settings + +def app_name(context): + return {'app_name' : settings.APP_NAME} \ No newline at end of file diff --git a/hyperkitty/lib/__init__.py b/hyperkitty/lib/__init__.py new file mode 100644 index 0000000..aa5a3d9 --- /dev/null +++ b/hyperkitty/lib/__init__.py @@ -0,0 +1,14 @@ +#-*- coding: utf-8 -*- + +from hashlib import md5 +import urllib + + +def gravatar_url(email): + '''Return a gravatar url for an email address''' + size = 64 + default = "http://fedoraproject.org/static/images/" + \ + "fedora_infinity_%ix%i.png" % (size, size) + query_string = urllib.urlencode({'s': size, 'd': default}) + identifier = md5(email).hexdigest() + return 'http://www.gravatar.com/avatar/%s?%s' % (identifier, query_string) diff --git a/hyperkitty/lib/mockup.py b/hyperkitty/lib/mockup.py new file mode 100644 index 0000000..6dfe298 --- /dev/null +++ b/hyperkitty/lib/mockup.py @@ -0,0 +1,167 @@ +#-*- coding: utf-8 -*- + + +class Email(object): + """ Email class containing the information needed to store and + display email threads. + """ + + def __init__(self): + """ Constructor. + Instanciate the default attributes of the object. + """ + self.email_id = '' + self.title = '' + self.body = '' + self.tags = [] + self.category = 'question' + self.category_tag = None + self.participants = set(['Pierre-Yves Chibon']) + self.answers = [] + self.liked = 0 + self.author = '' + self.avatar = None + +class Author(object): + """ Author class containing the information needed to get the top + author of the month! + """ + + def __init__(self): + """ Constructor. + Instanciate the default attributes of the object. + """ + self.name = None + self.kudos = 0 + self.avatar = None + + +def get_email_tag(tag): + threads = generate_random_thread() + output = [] + for email in threads: + if tag in email.tags or tag in email.category: + output.append(email) + elif email.category_tag and tag in email.category_tag: + output.append(email) + return output + + +def generate_thread_per_category(): + threads = generate_random_thread() + categories = {} + for thread in threads: + category = thread.category + if thread.category_tag: + category = thread.category_tag + if category in categories.keys(): + categories[category].append(thread) + else: + categories[category] = [thread] + return categories + +def generate_top_author(): + authors = [] + + author = Author() + author.name = 'Pierre-Yves Chibon' + author.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' + author.kudos = 3 + authors.append(author) + + author = Author() + author.name = 'Stanislav Ochotnický' + author.avatar = 'http://sochotni.fedorapeople.org/sochotni.jpg' + author.kudos = 4 + authors.append(author) + + author = Author() + author.name = 'Toshio Kuratomi' + author.avatar = 'https://secure.gravatar.com/avatar/7a9c1d88f484c9806bceca0d6d91e948' + author.kudos = 5 + authors.append(author) + + return authors + +def generate_random_thread(): + threads = [] + + ## 1 + email = Email() + email.email_id = 1 + email.title = 'Headsup! krb5 ccache defaults are changing in Rawhide' + email.age = '6 days' + email.body = '''Dear fellow developers, +with the upcoming Fedora 18 release (currently Rawhide) we are going to change the place where krb5 credential cache files are saved by default. + +The new default for credential caches will be the /run/user/username directory. +''' + email.tags.extend(['rawhide', 'krb5']) + email.participants = set(['Stephen Gallagher', 'Toshio Kuratomi', 'Kevin Fenzi', 'Seth Vidal']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 1 + email.author = 'Stephen Gallagher' + email.avatar = 'http://fedorapeople.org/~sgallagh/karrde712.png' + threads.append(email) + + ## 2 + email = Email() + email.email_id = 2 + email.title = 'Problem in packaging kicad' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['packaging', 'kicad']) + email.participants = set(['Pierre-Yves Chibon', 'Tom "spot" Callaway', 'Toshio Kuratomi', 'Kevin Fenzi']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 0 + email.author = 'Pierre-Yves Chibon' + email.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' + threads.append(email) + + ## 3 + email = Email() + email.email_id = 3 + email.title = 'Update Java Guideline' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['rawhide', 'krb5']) + email.participants = set(['Stanislav Ochotnický', 'Tom "spot" Callaway', 'Stephen Gallagher', 'Jason Tibbitts', 'Rex Dieter', 'Toshio Kuratomi']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]) + email.liked = 5 + email.category = 'todo' + email.author = 'Stanislav Ochotnický' + email.avatar = 'http://sochotni.fedorapeople.org/sochotni.jpg' + threads.append(email) + + ## 4 + email = Email() + email.email_id = 4 + email.title = 'Agenda for the next Board Meeting' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['agenda', 'board']) + email.participants = set(['Toshio Kuratomi', 'Tom "spot" Callaway', 'Robyn Bergeron', 'Max Spevack']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 20 + email.category = 'agenda' + email.author = 'Toshio Kuratomi' + email.avatar = 'https://secure.gravatar.com/avatar/7a9c1d88f484c9806bceca0d6d91e948' + threads.append(email) + + ## 5 + email = Email() + email.email_id = 5 + email.title = 'I told you so! ' + email.age = '6 days' + email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' + email.tags.extend(['systemd', 'mp3', 'pulseaudio']) + email.participants = set(['Pierre-Yves Chibon']) + email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) + email.liked = 0 + email.author = 'Pierre-Yves Chibon' + email.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' + email.category = 'shut down' + email.category_tag = 'dead' + threads.append(email) + + return threads diff --git a/hyperkitty/models.py b/hyperkitty/models.py new file mode 100644 index 0000000..588363d --- /dev/null +++ b/hyperkitty/models.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# + +from django.db import models +from django.contrib.auth.models import User +from django.conf import settings + +from kittystore.kittysastore import KittySAStore + +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +class Rating(models.Model): + # @TODO: instead of list_address, user list model from kittystore? + list_address = models.CharField(max_length=50) + + # @TODO: instead of messsageid, use message model from kittystore? + messageid = models.CharField(max_length=100) + + user = models.ForeignKey(User) + + vote = models.SmallIntegerField() + + def __unicode__(self): + """Unicode representation""" + if self.vote == 1: + return u'id = %s : %s voted up %s' % (self.id, unicode(self.user), self.messageid) + else: + return u'id = %s : %s voted down %s' % (self.id, unicode(self.user), self.messageid) + + +class UserProfile(models.Model): + # User Object + user = models.OneToOneField(User) + + karma = models.IntegerField(default=1) + + def _get_votes(self): + "Returns all the votes by a user" + # Extract all the votes by this user + try: + votes = Rating.objects.filter(user = self.user) + except Rating.DoesNotExist: + votes = {} + + for vote in votes: + list_name = vote.list_address.split('@')[0] + message = STORE.get_email(list_name, vote.messageid) + vote.message = message + + return votes + + votes = property(_get_votes) + + def __unicode__(self): + """Unicode representation""" + return u'%s' % (unicode(self.user)) diff --git a/hyperkitty/robots.txt b/hyperkitty/robots.txt new file mode 100644 index 0000000..5fe5da1 --- /dev/null +++ b/hyperkitty/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Disallow: /accounts/ +Disallow: /admin/ +Disallow: /vote/ diff --git a/hyperkitty/settings.py b/hyperkitty/settings.py new file mode 100644 index 0000000..23e3578 --- /dev/null +++ b/hyperkitty/settings.py @@ -0,0 +1,195 @@ +import os + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +# Django settings for hyperkitty project. + +DEBUG = True +TEMPLATE_DEBUG = DEBUG + +ADMINS = ( + ('Aamir Khan', 'syst3m.w0rm+hk@gmail.com'), +) + +MANAGERS = ADMINS + +MAILMAN_API_URL=r'http://%(username)s:%(password)s@localhost:8001/3.0/' +MAILMAN_USER='mailmanapi' +MAILMAN_PASS='88ffd62d1094a6248415c59d7538793f3df5de2f04d244087952394e689e902a' + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. + 'NAME': 'hk', # Or path to database file if using sqlite3. + 'USER': 'root', # Not used with sqlite3. + 'PASSWORD': 'rootroot', # Not used with sqlite3. + 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '', # Set to empty string for default. Not used with sqlite3. + } +} + +# Local time zone for this installation. Choices can be found here: +# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name +# although not all choices may be available on all operating systems. +# On Unix systems, a value of None will cause Django to use the same +# timezone as the operating system. +# If running in a Windows environment this must be set to the same as your +# system time zone. +TIME_ZONE = 'America/Chicago' + +# Language code for this installation. All choices can be found here: +# http://www.i18nguy.com/unicode/language-identifiers.html +LANGUAGE_CODE = 'en-us' + +SITE_ID = 1 + +# If you set this to False, Django will make some optimizations so as not +# to load the internationalization machinery. +USE_I18N = True + +# If you set this to False, Django will not format dates, numbers and +# calendars according to the current locale +USE_L10N = True + +# Absolute filesystem path to the directory that will hold user-uploaded files. +# Example: "/home/media/media.lawrence.com/media/" +MEDIA_ROOT = '' + +# URL that handles the media served from MEDIA_ROOT. Make sure to use a +# trailing slash. +# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" +MEDIA_URL = '' + +# Absolute path to the directory static files should be collected to. +# Don't put anything in this directory yourself; store your static files +# in apps' "static/" subdirectories and in STATICFILES_DIRS. +# Example: "/home/media/media.lawrence.com/static/" +#STATIC_ROOT = '' +STATIC_ROOT = BASE_DIR + '/static_files/' + +# URL prefix for static files. +# Example: "http://media.lawrence.com/static/" +STATIC_URL = '/static/' + +# URL prefix for admin static files -- CSS, JavaScript and images. +# Make sure to use a trailing slash. +# Examples: "http://foo.com/static/admin/", "/static/admin/". +ADMIN_MEDIA_PREFIX = '/static/admin/' + +# Additional locations of static files +STATICFILES_DIRS = ( + # Put strings here, like "/home/html/static" or "C:/www/django/static". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/static/', +) + +# List of finder classes that know how to find static files in +# various locations. +STATICFILES_FINDERS = ( + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', +# 'django.contrib.staticfiles.finders.DefaultStorageFinder', +) + +# Make this unique, and don't share it with anybody. +SECRET_KEY = 'dtc3%x(k#mzpe32dmhtsb6!3p(izk84f7nuw1-+4x8zsxwsa^z' + +# List of callables that know how to import templates from various sources. +TEMPLATE_LOADERS = ( + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', +# 'django.template.loaders.eggs.Loader', +) + + +TEMPLATE_CONTEXT_PROCESSORS = ( + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.core.context_processors.debug", + "django.core.context_processors.i18n", + "django.core.context_processors.media", + "django.core.context_processors.static", + "django.core.context_processors.csrf", + "django.contrib.messages.context_processors.messages", + "gsoc.context_processors.app_name", +) + + +MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', +) + +ROOT_URLCONF = 'urls' + +TEMPLATE_DIRS = ( + # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". + # Always use forward slashes, even on Windows. + # Don't forget to use absolute paths, not relative paths. + BASE_DIR + '/templates', +) + +AUTHENTICATION_BACKENDS = ( + 'social_auth.backends.google.GoogleBackend', + 'social_auth.backends.yahoo.YahooBackend', + 'social_auth.backends.browserid.BrowserIDBackend', + 'social_auth.backends.OpenIDBackend', + 'django.contrib.auth.backends.ModelBackend', +) + +INSTALLED_APPS = ( + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', +# 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django.contrib.admin', + # 'django.contrib.admindocs', + 'gsoc', + 'social_auth', + 'djangorestframework', + 'gravatar', +) + + +LOGIN_URL = '/accounts/login/' +LOGIN_REDIRECT_URL = '/' +LOGIN_ERROR_URL = '/accounts/login/' +SOCIAL_AUTH_COMPLETE_URL_NAME = 'socialauth_complete' +SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'socialauth_associate_complete' +SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' +SOCIAL_AUTH_UUID_LENGTH = 16 + +AUTH_PROFILE_MODULE = 'gsoc.UserProfile' + + +# A sample logging configuration. The only tangible logging +# performed by this configuration is to send an email to +# the site admins on every HTTP 500 error. +# See http://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. +LOGGING = { + 'version': 1, + 'disable_existing_loggers': False, + 'handlers': { + 'mail_admins': { + 'level': 'ERROR', + 'class': 'django.utils.log.AdminEmailHandler' + } + }, + 'loggers': { + 'django.request': { + 'handlers': ['mail_admins'], + 'level': 'ERROR', + 'propagate': True, + }, + } +} + +SOCIAL_AUTH_LAST_LOGIN = 'social_auth_last_login_backend' +APP_NAME = 'Fedora Mailman App' +KITTYSTORE_URL = 'postgres://mm3:mm3@localhost/mm3' diff --git a/hyperkitty/static/css/bootstrap.css b/hyperkitty/static/css/bootstrap.css new file mode 100644 index 0000000..c3e0c00 --- /dev/null +++ b/hyperkitty/static/css/bootstrap.css @@ -0,0 +1,3496 @@ +/*! + * Bootstrap v2.0.1 + * + * Copyright 2012 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world @twitter by @mdo and @fat. + */ +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section { + display: block; +} +audio, canvas, video { + display: inline-block; + *display: inline; + *zoom: 1; +} +audio:not([controls]) { + display: none; +} +html { + font-size: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +a:hover, a:active { + outline: 0; +} +sub, sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -0.5em; +} +sub { + bottom: -0.25em; +} +img { + max-width: 100%; + height: auto; + border: 0; + -ms-interpolation-mode: bicubic; +} +button, +input, +select, +textarea { + margin: 0; + font-size: 100%; + vertical-align: middle; +} +button, input { + *overflow: visible; + line-height: normal; +} +button::-moz-focus-inner, input::-moz-focus-inner { + padding: 0; + border: 0; +} +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} +input[type="search"] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} +textarea { + overflow: auto; + vertical-align: top; +} +.clearfix { + *zoom: 1; +} +.clearfix:before, .clearfix:after { + display: table; + content: ""; +} +.clearfix:after { + clear: both; +} +body { + margin: 0; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #333333; + background-color: #ffffff; +} +a { + color: #0088cc; + text-decoration: none; +} +a:hover { + color: #005580; + text-decoration: underline; +} +.row { + margin-left: -20px; + *zoom: 1; +} +.row:before, .row:after { + display: table; + content: ""; +} +.row:after { + clear: both; +} +[class*="span"] { + float: left; + margin-left: 20px; +} +.span1 { + width: 60px; +} +.span2 { + width: 140px; +} +.span3 { + width: 220px; +} +.span4 { + width: 300px; +} +.span5 { + width: 380px; +} +.span6 { + width: 460px; +} +.span7 { + width: 540px; +} +.span8 { + width: 620px; +} +.span9 { + width: 700px; +} +.span10 { + width: 780px; +} +.span11 { + width: 860px; +} +.span12, .container { + width: 940px; +} +.offset1 { + margin-left: 100px; +} +.offset2 { + margin-left: 180px; +} +.offset3 { + margin-left: 260px; +} +.offset4 { + margin-left: 340px; +} +.offset5 { + margin-left: 420px; +} +.offset6 { + margin-left: 500px; +} +.offset7 { + margin-left: 580px; +} +.offset8 { + margin-left: 660px; +} +.offset9 { + margin-left: 740px; +} +.offset10 { + margin-left: 820px; +} +.offset11 { + margin-left: 900px; +} +.row-fluid { + width: 100%; + *zoom: 1; +} +.row-fluid:before, .row-fluid:after { + display: table; + content: ""; +} +.row-fluid:after { + clear: both; +} +.row-fluid > [class*="span"] { + float: left; + margin-left: 2.127659574%; +} +.row-fluid > [class*="span"]:first-child { + margin-left: 0; +} +.row-fluid > .span1 { + width: 6.382978723%; +} +.row-fluid > .span2 { + width: 14.89361702%; +} +.row-fluid > .span3 { + width: 23.404255317%; +} +.row-fluid > .span4 { + width: 31.914893614%; +} +.row-fluid > .span5 { + width: 40.425531911%; +} +.row-fluid > .span6 { + width: 48.93617020799999%; +} +.row-fluid > .span7 { + width: 57.446808505%; +} +.row-fluid > .span8 { + width: 65.95744680199999%; +} +.row-fluid > .span9 { + width: 74.468085099%; +} +.row-fluid > .span10 { + width: 82.97872339599999%; +} +.row-fluid > .span11 { + width: 91.489361693%; +} +.row-fluid > .span12 { + width: 99.99999998999999%; +} +.container { + width: 940px; + margin-left: auto; + margin-right: auto; + *zoom: 1; +} +.container:before, .container:after { + display: table; + content: ""; +} +.container:after { + clear: both; +} +.container-fluid { + padding-left: 20px; + padding-right: 20px; + *zoom: 1; +} +.container-fluid:before, .container-fluid:after { + display: table; + content: ""; +} +.container-fluid:after { + clear: both; +} +p { + margin: 0 0 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + line-height: 18px; +} +p small { + font-size: 11px; + color: #999999; +} +.lead { + margin-bottom: 18px; + font-size: 20px; + font-weight: 200; + line-height: 27px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + margin: 0; + font-weight: bold; + color: #333333; + text-rendering: optimizelegibility; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small { + font-weight: normal; + color: #999999; +} +h1 { + font-size: 30px; + line-height: 36px; +} +h1 small { + font-size: 18px; +} +h2 { + font-size: 24px; + line-height: 36px; +} +h2 small { + font-size: 18px; +} +h3 { + line-height: 27px; + font-size: 18px; +} +h3 small { + font-size: 14px; +} +h4, h5, h6 { + line-height: 18px; +} +h4 { + font-size: 14px; +} +h4 small { + font-size: 12px; +} +h5 { + font-size: 12px; +} +h6 { + font-size: 11px; + color: #999999; + text-transform: uppercase; +} +.page-header { + padding-bottom: 17px; + margin: 18px 0; + border-bottom: 1px solid #eeeeee; +} +.page-header h1 { + line-height: 1; +} +ul, ol { + padding: 0; + margin: 0 0 9px 25px; +} +ul ul, +ul ol, +ol ol, +ol ul { + margin-bottom: 0; +} +ul { + list-style: disc; +} +ol { + list-style: decimal; +} +li { + line-height: 18px; +} +ul.unstyled, ol.unstyled { + margin-left: 0; + list-style: none; +} +dl { + margin-bottom: 18px; +} +dt, dd { + line-height: 18px; +} +dt { + font-weight: bold; +} +dd { + margin-left: 9px; +} +hr { + margin: 18px 0; + border: 0; + border-top: 1px solid #eeeeee; + border-bottom: 1px solid #ffffff; +} +strong { + font-weight: bold; +} +em { + font-style: italic; +} +.muted { + color: #999999; +} +abbr { + font-size: 90%; + text-transform: uppercase; + border-bottom: 1px dotted #ddd; + cursor: help; +} +blockquote { + padding: 0 0 0 15px; + margin: 0 0 18px; + border-left: 5px solid #eeeeee; +} +blockquote p { + margin-bottom: 0; + font-size: 16px; + font-weight: 300; + line-height: 22.5px; +} +blockquote small { + display: block; + line-height: 18px; + color: #999999; +} +blockquote small:before { + content: '\2014 \00A0'; +} +blockquote.pull-right { + float: right; + padding-left: 0; + padding-right: 15px; + border-left: 0; + border-right: 5px solid #eeeeee; +} +blockquote.pull-right p, blockquote.pull-right small { + text-align: right; +} +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} +address { + display: block; + margin-bottom: 18px; + line-height: 18px; + font-style: normal; +} +small { + font-size: 100%; +} +cite { + font-style: normal; +} +code, pre { + padding: 0 3px 2px; + font-family: Menlo, Monaco, "Courier New", monospace; + font-size: 12px; + color: #333333; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +code { + padding: 3px 4px; + color: #d14; + background-color: #f7f7f9; + border: 1px solid #e1e1e8; +} +pre { + display: block; + padding: 8.5px; + margin: 0 0 9px; + font-size: 12px; + line-height: 18px; + background-color: #f5f5f5; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + white-space: pre; + white-space: pre-wrap; + word-break: break-all; + word-wrap: break-word; +} +pre.prettyprint { + margin-bottom: 18px; +} +pre code { + padding: 0; + color: inherit; + background-color: transparent; + border: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +form { + margin: 0 0 18px; +} +fieldset { + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 27px; + font-size: 19.5px; + line-height: 36px; + color: #333333; + border: 0; + border-bottom: 1px solid #eee; +} +legend small { + font-size: 13.5px; + color: #999999; +} +label, +input, +button, +select, +textarea { + font-size: 13px; + font-weight: normal; + line-height: 18px; +} +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} +label { + display: block; + margin-bottom: 5px; + color: #333333; +} +input, +textarea, +select, +.uneditable-input { + display: inline-block; + width: 210px; + height: 18px; + padding: 4px; + margin-bottom: 9px; + font-size: 13px; + line-height: 18px; + color: #555555; + border: 1px solid #ccc; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.uneditable-textarea { + width: auto; + height: auto; +} +label input, label textarea, label select { + display: block; +} +input[type="image"], input[type="checkbox"], input[type="radio"] { + width: auto; + height: auto; + padding: 0; + margin: 3px 0; + *margin-top: 0; + /* IE7 */ + + line-height: normal; + cursor: pointer; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; + border: 0 \9; + /* IE9 and down */ + +} +input[type="image"] { + border: 0; +} +input[type="file"] { + width: auto; + padding: initial; + line-height: initial; + border: initial; + background-color: #ffffff; + background-color: initial; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +input[type="button"], input[type="reset"], input[type="submit"] { + width: auto; + height: auto; +} +select, input[type="file"] { + height: 28px; + /* In IE7, the height of the select element cannot be changed by height, only font-size */ + + *margin-top: 4px; + /* For IE7, add top margin to align select with labels */ + + line-height: 28px; +} +input[type="file"] { + line-height: 18px \9; +} +select { + width: 220px; + background-color: #ffffff; +} +select[multiple], select[size] { + height: auto; +} +input[type="image"] { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +textarea { + height: auto; +} +input[type="hidden"] { + display: none; +} +.radio, .checkbox { + padding-left: 18px; +} +.radio input[type="radio"], .checkbox input[type="checkbox"] { + float: left; + margin-left: -18px; +} +.controls > .radio:first-child, .controls > .checkbox:first-child { + padding-top: 5px; +} +.radio.inline, .checkbox.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; +} +.radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline { + margin-left: 10px; +} +input, textarea { + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; + -moz-transition: border linear 0.2s, box-shadow linear 0.2s; + -ms-transition: border linear 0.2s, box-shadow linear 0.2s; + -o-transition: border linear 0.2s, box-shadow linear 0.2s; + transition: border linear 0.2s, box-shadow linear 0.2s; +} +input:focus, textarea:focus { + border-color: rgba(82, 168, 236, 0.8); + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); + outline: 0; + outline: thin dotted \9; + /* IE6-9 */ + +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus, +select:focus { + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.input-mini { + width: 60px; +} +.input-small { + width: 90px; +} +.input-medium { + width: 150px; +} +.input-large { + width: 210px; +} +.input-xlarge { + width: 270px; +} +.input-xxlarge { + width: 530px; +} +input[class*="span"], +select[class*="span"], +textarea[class*="span"], +.uneditable-input { + float: none; + margin-left: 0; +} +input.span1, textarea.span1, .uneditable-input.span1 { + width: 50px; +} +input.span2, textarea.span2, .uneditable-input.span2 { + width: 130px; +} +input.span3, textarea.span3, .uneditable-input.span3 { + width: 210px; +} +input.span4, textarea.span4, .uneditable-input.span4 { + width: 290px; +} +input.span5, textarea.span5, .uneditable-input.span5 { + width: 370px; +} +input.span6, textarea.span6, .uneditable-input.span6 { + width: 450px; +} +input.span7, textarea.span7, .uneditable-input.span7 { + width: 530px; +} +input.span8, textarea.span8, .uneditable-input.span8 { + width: 610px; +} +input.span9, textarea.span9, .uneditable-input.span9 { + width: 690px; +} +input.span10, textarea.span10, .uneditable-input.span10 { + width: 770px; +} +input.span11, textarea.span11, .uneditable-input.span11 { + width: 850px; +} +input.span12, textarea.span12, .uneditable-input.span12 { + width: 930px; +} +input[disabled], +select[disabled], +textarea[disabled], +input[readonly], +select[readonly], +textarea[readonly] { + background-color: #f5f5f5; + border-color: #ddd; + cursor: not-allowed; +} +.control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline { + color: #c09853; +} +.control-group.warning input, .control-group.warning select, .control-group.warning textarea { + color: #c09853; + border-color: #c09853; +} +.control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { + border-color: #a47e3c; + -webkit-box-shadow: 0 0 6px #dbc59e; + -moz-box-shadow: 0 0 6px #dbc59e; + box-shadow: 0 0 6px #dbc59e; +} +.control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} +.control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline { + color: #b94a48; +} +.control-group.error input, .control-group.error select, .control-group.error textarea { + color: #b94a48; + border-color: #b94a48; +} +.control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus { + border-color: #953b39; + -webkit-box-shadow: 0 0 6px #d59392; + -moz-box-shadow: 0 0 6px #d59392; + box-shadow: 0 0 6px #d59392; +} +.control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} +.control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline { + color: #468847; +} +.control-group.success input, .control-group.success select, .control-group.success textarea { + color: #468847; + border-color: #468847; +} +.control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus { + border-color: #356635; + -webkit-box-shadow: 0 0 6px #7aba7b; + -moz-box-shadow: 0 0 6px #7aba7b; + box-shadow: 0 0 6px #7aba7b; +} +.control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} +input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { + color: #b94a48; + border-color: #ee5f5b; +} +input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { + border-color: #e9322d; + -webkit-box-shadow: 0 0 6px #f8b9b7; + -moz-box-shadow: 0 0 6px #f8b9b7; + box-shadow: 0 0 6px #f8b9b7; +} +.form-actions { + padding: 17px 20px 18px; + margin-top: 18px; + margin-bottom: 18px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; +} +.uneditable-input { + display: block; + background-color: #ffffff; + border-color: #eee; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); + cursor: not-allowed; +} +:-moz-placeholder { + color: #999999; +} +::-webkit-input-placeholder { + color: #999999; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 0; + color: #999999; +} +.help-inline { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-bottom: 9px; + vertical-align: middle; + padding-left: 5px; +} +.input-prepend, .input-append { + margin-bottom: 5px; + *zoom: 1; +} +.input-prepend:before, +.input-append:before, +.input-prepend:after, +.input-append:after { + display: table; + content: ""; +} +.input-prepend:after, .input-append:after { + clear: both; +} +.input-prepend input, +.input-append input, +.input-prepend .uneditable-input, +.input-append .uneditable-input { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-prepend input:focus, +.input-append input:focus, +.input-prepend .uneditable-input:focus, +.input-append .uneditable-input:focus { + position: relative; + z-index: 2; +} +.input-prepend .uneditable-input, .input-append .uneditable-input { + border-left-color: #ccc; +} +.input-prepend .add-on, .input-append .add-on { + float: left; + display: block; + width: auto; + min-width: 16px; + height: 18px; + margin-right: -1px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + color: #999999; + text-align: center; + text-shadow: 0 1px 0 #ffffff; + background-color: #f5f5f5; + border: 1px solid #ccc; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-prepend .active, .input-append .active { + background-color: #a9dba9; + border-color: #46a546; +} +.input-prepend .add-on { + *margin-top: 1px; + /* IE6-7 */ + +} +.input-append input, .input-append .uneditable-input { + float: left; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.input-append .uneditable-input { + border-left-color: #eee; + border-right-color: #ccc; +} +.input-append .add-on { + margin-right: 0; + margin-left: -1px; + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.input-append input:first-child { + *margin-left: -160px; +} +.input-append input:first-child + .add-on { + *margin-left: -21px; +} +.search-query { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; + -webkit-border-radius: 14px; + -moz-border-radius: 14px; + border-radius: 14px; +} +.form-search input, +.form-inline input, +.form-horizontal input, +.form-search textarea, +.form-inline textarea, +.form-horizontal textarea, +.form-search select, +.form-inline select, +.form-horizontal select, +.form-search .help-inline, +.form-inline .help-inline, +.form-horizontal .help-inline, +.form-search .uneditable-input, +.form-inline .uneditable-input, +.form-horizontal .uneditable-input { + display: inline-block; + margin-bottom: 0; +} +.form-search .hide, .form-inline .hide, .form-horizontal .hide { + display: none; +} +.form-search label, +.form-inline label, +.form-search .input-append, +.form-inline .input-append, +.form-search .input-prepend, +.form-inline .input-prepend { + display: inline-block; +} +.form-search .input-append .add-on, +.form-inline .input-prepend .add-on, +.form-search .input-append .add-on, +.form-inline .input-prepend .add-on { + vertical-align: middle; +} +.form-search .radio, +.form-inline .radio, +.form-search .checkbox, +.form-inline .checkbox { + margin-bottom: 0; + vertical-align: middle; +} +.control-group { + margin-bottom: 9px; +} +legend + .control-group { + margin-top: 18px; + -webkit-margin-top-collapse: separate; +} +.form-horizontal .control-group { + margin-bottom: 18px; + *zoom: 1; +} +.form-horizontal .control-group:before, .form-horizontal .control-group:after { + display: table; + content: ""; +} +.form-horizontal .control-group:after { + clear: both; +} +.form-horizontal .control-label { + float: left; + width: 140px; + padding-top: 5px; + text-align: right; +} +.form-horizontal .controls { + margin-left: 160px; +} +.form-horizontal .form-actions { + padding-left: 160px; +} +table { + max-width: 100%; + border-collapse: collapse; + border-spacing: 0; +} +.table { + width: 100%; + margin-bottom: 18px; +} +.table th, .table td { + padding: 8px; + line-height: 18px; + text-align: left; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table th { + font-weight: bold; +} +.table thead th { + vertical-align: bottom; +} +.table thead:first-child tr th, .table thead:first-child tr td { + border-top: 0; +} +.table tbody + tbody { + border-top: 2px solid #ddd; +} +.table-condensed th, .table-condensed td { + padding: 4px 5px; +} +.table-bordered { + border: 1px solid #ddd; + border-collapse: separate; + *border-collapse: collapsed; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.table-bordered th + th, +.table-bordered td + td, +.table-bordered th + td, +.table-bordered td + th { + border-left: 1px solid #ddd; +} +.table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { + border-top: 0; +} +.table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { + -webkit-border-radius: 4px 0 0 0; + -moz-border-radius: 4px 0 0 0; + border-radius: 4px 0 0 0; +} +.table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { + -webkit-border-radius: 0 4px 0 0; + -moz-border-radius: 0 4px 0 0; + border-radius: 0 4px 0 0; +} +.table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { + -webkit-border-radius: 0 0 0 4px; + -moz-border-radius: 0 0 0 4px; + border-radius: 0 0 0 4px; +} +.table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { + -webkit-border-radius: 0 0 4px 0; + -moz-border-radius: 0 0 4px 0; + border-radius: 0 0 4px 0; +} +.table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { + background-color: #f9f9f9; +} +.table tbody tr:hover td, .table tbody tr:hover th { + background-color: #f5f5f5; +} +table .span1 { + float: none; + width: 44px; + margin-left: 0; +} +table .span2 { + float: none; + width: 124px; + margin-left: 0; +} +table .span3 { + float: none; + width: 204px; + margin-left: 0; +} +table .span4 { + float: none; + width: 284px; + margin-left: 0; +} +table .span5 { + float: none; + width: 364px; + margin-left: 0; +} +table .span6 { + float: none; + width: 444px; + margin-left: 0; +} +table .span7 { + float: none; + width: 524px; + margin-left: 0; +} +table .span8 { + float: none; + width: 604px; + margin-left: 0; +} +table .span9 { + float: none; + width: 684px; + margin-left: 0; +} +table .span10 { + float: none; + width: 764px; + margin-left: 0; +} +table .span11 { + float: none; + width: 844px; + margin-left: 0; +} +table .span12 { + float: none; + width: 924px; + margin-left: 0; +} +[class^="icon-"], [class*=" icon-"] { + display: inline-block; + width: 14px; + height: 14px; + line-height: 14px; + vertical-align: text-top; + background-image: url("../img/glyphicons-halflings.png"); + background-position: 14px 14px; + background-repeat: no-repeat; + *margin-right: .3em; +} +[class^="icon-"]:last-child, [class*=" icon-"]:last-child { + *margin-left: 0; +} +.icon-white { + background-image: url("../img/glyphicons-halflings-white.png"); +} +.icon-glass { + background-position: 0 0; +} +.icon-music { + background-position: -24px 0; +} +.icon-search { + background-position: -48px 0; +} +.icon-envelope { + background-position: -72px 0; +} +.icon-heart { + background-position: -96px 0; +} +.icon-star { + background-position: -120px 0; +} +.icon-star-empty { + background-position: -144px 0; +} +.icon-user { + background-position: -168px 0; +} +.icon-film { + background-position: -192px 0; +} +.icon-th-large { + background-position: -216px 0; +} +.icon-th { + background-position: -240px 0; +} +.icon-th-list { + background-position: -264px 0; +} +.icon-ok { + background-position: -288px 0; +} +.icon-remove { + background-position: -312px 0; +} +.icon-zoom-in { + background-position: -336px 0; +} +.icon-zoom-out { + background-position: -360px 0; +} +.icon-off { + background-position: -384px 0; +} +.icon-signal { + background-position: -408px 0; +} +.icon-cog { + background-position: -432px 0; +} +.icon-trash { + background-position: -456px 0; +} +.icon-home { + background-position: 0 -24px; +} +.icon-file { + background-position: -24px -24px; +} +.icon-time { + background-position: -48px -24px; +} +.icon-road { + background-position: -72px -24px; +} +.icon-download-alt { + background-position: -96px -24px; +} +.icon-download { + background-position: -120px -24px; +} +.icon-upload { + background-position: -144px -24px; +} +.icon-inbox { + background-position: -168px -24px; +} +.icon-play-circle { + background-position: -192px -24px; +} +.icon-repeat { + background-position: -216px -24px; +} +.icon-refresh { + background-position: -240px -24px; +} +.icon-list-alt { + background-position: -264px -24px; +} +.icon-lock { + background-position: -287px -24px; +} +.icon-flag { + background-position: -312px -24px; +} +.icon-headphones { + background-position: -336px -24px; +} +.icon-volume-off { + background-position: -360px -24px; +} +.icon-volume-down { + background-position: -384px -24px; +} +.icon-volume-up { + background-position: -408px -24px; +} +.icon-qrcode { + background-position: -432px -24px; +} +.icon-barcode { + background-position: -456px -24px; +} +.icon-tag { + background-position: 0 -48px; +} +.icon-tags { + background-position: -25px -48px; +} +.icon-book { + background-position: -48px -48px; +} +.icon-bookmark { + background-position: -72px -48px; +} +.icon-print { + background-position: -96px -48px; +} +.icon-camera { + background-position: -120px -48px; +} +.icon-font { + background-position: -144px -48px; +} +.icon-bold { + background-position: -167px -48px; +} +.icon-italic { + background-position: -192px -48px; +} +.icon-text-height { + background-position: -216px -48px; +} +.icon-text-width { + background-position: -240px -48px; +} +.icon-align-left { + background-position: -264px -48px; +} +.icon-align-center { + background-position: -288px -48px; +} +.icon-align-right { + background-position: -312px -48px; +} +.icon-align-justify { + background-position: -336px -48px; +} +.icon-list { + background-position: -360px -48px; +} +.icon-indent-left { + background-position: -384px -48px; +} +.icon-indent-right { + background-position: -408px -48px; +} +.icon-facetime-video { + background-position: -432px -48px; +} +.icon-picture { + background-position: -456px -48px; +} +.icon-pencil { + background-position: 0 -72px; +} +.icon-map-marker { + background-position: -24px -72px; +} +.icon-adjust { + background-position: -48px -72px; +} +.icon-tint { + background-position: -72px -72px; +} +.icon-edit { + background-position: -96px -72px; +} +.icon-share { + background-position: -120px -72px; +} +.icon-check { + background-position: -144px -72px; +} +.icon-move { + background-position: -168px -72px; +} +.icon-step-backward { + background-position: -192px -72px; +} +.icon-fast-backward { + background-position: -216px -72px; +} +.icon-backward { + background-position: -240px -72px; +} +.icon-play { + background-position: -264px -72px; +} +.icon-pause { + background-position: -288px -72px; +} +.icon-stop { + background-position: -312px -72px; +} +.icon-forward { + background-position: -336px -72px; +} +.icon-fast-forward { + background-position: -360px -72px; +} +.icon-step-forward { + background-position: -384px -72px; +} +.icon-eject { + background-position: -408px -72px; +} +.icon-chevron-left { + background-position: -432px -72px; +} +.icon-chevron-right { + background-position: -456px -72px; +} +.icon-plus-sign { + background-position: 0 -96px; +} +.icon-minus-sign { + background-position: -24px -96px; +} +.icon-remove-sign { + background-position: -48px -96px; +} +.icon-ok-sign { + background-position: -72px -96px; +} +.icon-question-sign { + background-position: -96px -96px; +} +.icon-info-sign { + background-position: -120px -96px; +} +.icon-screenshot { + background-position: -144px -96px; +} +.icon-remove-circle { + background-position: -168px -96px; +} +.icon-ok-circle { + background-position: -192px -96px; +} +.icon-ban-circle { + background-position: -216px -96px; +} +.icon-arrow-left { + background-position: -240px -96px; +} +.icon-arrow-right { + background-position: -264px -96px; +} +.icon-arrow-up { + background-position: -289px -96px; +} +.icon-arrow-down { + background-position: -312px -96px; +} +.icon-share-alt { + background-position: -336px -96px; +} +.icon-resize-full { + background-position: -360px -96px; +} +.icon-resize-small { + background-position: -384px -96px; +} +.icon-plus { + background-position: -408px -96px; +} +.icon-minus { + background-position: -433px -96px; +} +.icon-asterisk { + background-position: -456px -96px; +} +.icon-exclamation-sign { + background-position: 0 -120px; +} +.icon-gift { + background-position: -24px -120px; +} +.icon-leaf { + background-position: -48px -120px; +} +.icon-fire { + background-position: -72px -120px; +} +.icon-eye-open { + background-position: -96px -120px; +} +.icon-eye-close { + background-position: -120px -120px; +} +.icon-warning-sign { + background-position: -144px -120px; +} +.icon-plane { + background-position: -168px -120px; +} +.icon-calendar { + background-position: -192px -120px; +} +.icon-random { + background-position: -216px -120px; +} +.icon-comment { + background-position: -240px -120px; +} +.icon-magnet { + background-position: -264px -120px; +} +.icon-chevron-up { + background-position: -288px -120px; +} +.icon-chevron-down { + background-position: -313px -119px; +} +.icon-retweet { + background-position: -336px -120px; +} +.icon-shopping-cart { + background-position: -360px -120px; +} +.icon-folder-close { + background-position: -384px -120px; +} +.icon-folder-open { + background-position: -408px -120px; +} +.icon-resize-vertical { + background-position: -432px -119px; +} +.icon-resize-horizontal { + background-position: -456px -118px; +} +.dropdown { + position: relative; +} +.dropdown-toggle { + *margin-bottom: -3px; +} +.dropdown-toggle:active, .open .dropdown-toggle { + outline: 0; +} +.caret { + display: inline-block; + width: 0; + height: 0; + text-indent: -99999px; + *text-indent: 0; + vertical-align: top; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #000000; + opacity: 0.3; + filter: alpha(opacity=30); + content: "\2193"; +} +.dropdown .caret { + margin-top: 8px; + margin-left: 2px; +} +.dropdown:hover .caret, .open.dropdown .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + float: left; + display: none; + min-width: 160px; + _width: 160px; + padding: 4px 0; + margin: 0; + list-style: none; + background-color: #ffffff; + border-color: #ccc; + border-color: rgba(0, 0, 0, 0.2); + border-style: solid; + border-width: 1px; + -webkit-border-radius: 0 0 5px 5px; + -moz-border-radius: 0 0 5px 5px; + border-radius: 0 0 5px 5px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + -webkit-background-clip: padding-box; + -moz-background-clip: padding; + background-clip: padding-box; + *border-right-width: 2px; + *border-bottom-width: 2px; +} +.dropdown-menu.bottom-up { + top: auto; + bottom: 100%; + margin-bottom: 2px; +} +.dropdown-menu .divider { + height: 1px; + margin: 5px 1px; + overflow: hidden; + background-color: #e5e5e5; + border-bottom: 1px solid #ffffff; + *width: 100%; + *margin: -5px 0 5px; +} +.dropdown-menu a { + display: block; + padding: 3px 15px; + clear: both; + font-weight: normal; + line-height: 18px; + color: #555555; + white-space: nowrap; +} +.dropdown-menu li > a:hover, .dropdown-menu .active > a, .dropdown-menu .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #0088cc; +} +.dropdown.open { + *z-index: 1000; +} +.dropdown.open .dropdown-toggle { + color: #ffffff; + background: #ccc; + background: rgba(0, 0, 0, 0.3); +} +.dropdown.open .dropdown-menu { + display: block; +} +.typeahead { + margin-top: 2px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #eee; + border: 1px solid rgba(0, 0, 0, 0.05); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} +.fade { + -webkit-transition: opacity 0.15s linear; + -moz-transition: opacity 0.15s linear; + -ms-transition: opacity 0.15s linear; + -o-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; + opacity: 0; +} +.fade.in { + opacity: 1; +} +.collapse { + -webkit-transition: height 0.35s ease; + -moz-transition: height 0.35s ease; + -ms-transition: height 0.35s ease; + -o-transition: height 0.35s ease; + transition: height 0.35s ease; + position: relative; + overflow: hidden; + height: 0; +} +.collapse.in { + height: auto; +} +.close { + float: right; + font-size: 20px; + font-weight: bold; + line-height: 18px; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} +.close:hover { + color: #000000; + text-decoration: none; + opacity: 0.4; + filter: alpha(opacity=40); + cursor: pointer; +} +.btn { + display: inline-block; + padding: 4px 10px 4px; + margin-bottom: 0; + font-size: 13px; + line-height: 18px; + color: #333333; + text-align: center; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + vertical-align: middle; + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(top, #ffffff, #e6e6e6); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + border: 1px solid #ccc; + border-bottom-color: #bbb; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + cursor: pointer; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + *margin-left: .3em; +} +.btn:hover, +.btn:active, +.btn.active, +.btn.disabled, +.btn[disabled] { + background-color: #e6e6e6; +} +.btn:active, .btn.active { + background-color: #cccccc \9; +} +.btn:first-child { + *margin-left: 0; +} +.btn:hover { + color: #333333; + text-decoration: none; + background-color: #e6e6e6; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -ms-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; +} +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn.active, .btn:active { + background-image: none; + -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + background-color: #e6e6e6; + background-color: #d9d9d9 \9; + outline: 0; +} +.btn.disabled, .btn[disabled] { + cursor: default; + background-image: none; + background-color: #e6e6e6; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; +} +.btn-large { + padding: 9px 14px; + font-size: 15px; + line-height: normal; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-large [class^="icon-"] { + margin-top: 1px; +} +.btn-small { + padding: 5px 9px; + font-size: 11px; + line-height: 16px; +} +.btn-small [class^="icon-"] { + margin-top: -1px; +} +.btn-mini { + padding: 2px 6px; + font-size: 11px; + line-height: 14px; +} +.btn-primary, +.btn-primary:hover, +.btn-warning, +.btn-warning:hover, +.btn-danger, +.btn-danger:hover, +.btn-success, +.btn-success:hover, +.btn-info, +.btn-info:hover, +.btn-inverse, +.btn-inverse:hover { + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + color: #ffffff; +} +.btn-primary.active, +.btn-warning.active, +.btn-danger.active, +.btn-success.active, +.btn-info.active, +.btn-dark.active { + color: rgba(255, 255, 255, 0.75); +} +.btn-primary { + background-color: #006dcc; + background-image: -moz-linear-gradient(top, #0088cc, #0044cc); + background-image: -ms-linear-gradient(top, #0088cc, #0044cc); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); + background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); + background-image: -o-linear-gradient(top, #0088cc, #0044cc); + background-image: linear-gradient(top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-primary:hover, +.btn-primary:active, +.btn-primary.active, +.btn-primary.disabled, +.btn-primary[disabled] { + background-color: #0044cc; +} +.btn-primary:active, .btn-primary.active { + background-color: #003399 \9; +} +.btn-warning { + background-color: #faa732; + background-image: -moz-linear-gradient(top, #fbb450, #f89406); + background-image: -ms-linear-gradient(top, #fbb450, #f89406); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); + background-image: -webkit-linear-gradient(top, #fbb450, #f89406); + background-image: -o-linear-gradient(top, #fbb450, #f89406); + background-image: linear-gradient(top, #fbb450, #f89406); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); + border-color: #f89406 #f89406 #ad6704; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-warning:hover, +.btn-warning:active, +.btn-warning.active, +.btn-warning.disabled, +.btn-warning[disabled] { + background-color: #f89406; +} +.btn-warning:active, .btn-warning.active { + background-color: #c67605 \9; +} +.btn-danger { + background-color: #da4f49; + background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); + background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); + background-image: linear-gradient(top, #ee5f5b, #bd362f); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); + border-color: #bd362f #bd362f #802420; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-danger:hover, +.btn-danger:active, +.btn-danger.active, +.btn-danger.disabled, +.btn-danger[disabled] { + background-color: #bd362f; +} +.btn-danger:active, .btn-danger.active { + background-color: #942a25 \9; +} +.btn-success { + background-color: #5bb75b; + background-image: -moz-linear-gradient(top, #62c462, #51a351); + background-image: -ms-linear-gradient(top, #62c462, #51a351); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); + background-image: -webkit-linear-gradient(top, #62c462, #51a351); + background-image: -o-linear-gradient(top, #62c462, #51a351); + background-image: linear-gradient(top, #62c462, #51a351); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); + border-color: #51a351 #51a351 #387038; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-success:hover, +.btn-success:active, +.btn-success.active, +.btn-success.disabled, +.btn-success[disabled] { + background-color: #51a351; +} +.btn-success:active, .btn-success.active { + background-color: #408140 \9; +} +.btn-info { + background-color: #49afcd; + background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); + background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); + background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); + background-image: linear-gradient(top, #5bc0de, #2f96b4); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); + border-color: #2f96b4 #2f96b4 #1f6377; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-info:hover, +.btn-info:active, +.btn-info.active, +.btn-info.disabled, +.btn-info[disabled] { + background-color: #2f96b4; +} +.btn-info:active, .btn-info.active { + background-color: #24748c \9; +} +.btn-inverse { + background-color: #393939; + background-image: -moz-linear-gradient(top, #454545, #262626); + background-image: -ms-linear-gradient(top, #454545, #262626); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626)); + background-image: -webkit-linear-gradient(top, #454545, #262626); + background-image: -o-linear-gradient(top, #454545, #262626); + background-image: linear-gradient(top, #454545, #262626); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0); + border-color: #262626 #262626 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.btn-inverse:hover, +.btn-inverse:active, +.btn-inverse.active, +.btn-inverse.disabled, +.btn-inverse[disabled] { + background-color: #262626; +} +.btn-inverse:active, .btn-inverse.active { + background-color: #0c0c0c \9; +} +button.btn, input[type="submit"].btn { + *padding-top: 2px; + *padding-bottom: 2px; +} +button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { + padding: 0; + border: 0; +} +button.btn.large, input[type="submit"].btn.large { + *padding-top: 7px; + *padding-bottom: 7px; +} +button.btn.small, input[type="submit"].btn.small { + *padding-top: 3px; + *padding-bottom: 3px; +} +.btn-group { + position: relative; + *zoom: 1; + *margin-left: .3em; +} +.btn-group:before, .btn-group:after { + display: table; + content: ""; +} +.btn-group:after { + clear: both; +} +.btn-group:first-child { + *margin-left: 0; +} +.btn-group + .btn-group { + margin-left: 5px; +} +.btn-toolbar { + margin-top: 9px; + margin-bottom: 9px; +} +.btn-toolbar .btn-group { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; +} +.btn-group .btn { + position: relative; + float: left; + margin-left: -1px; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.btn-group .btn:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; + -webkit-border-bottom-left-radius: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} +.btn-group .btn:last-child, .btn-group .dropdown-toggle { + -webkit-border-top-right-radius: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; + -webkit-border-bottom-right-radius: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} +.btn-group .btn.large:first-child { + margin-left: 0; + -webkit-border-top-left-radius: 6px; + -moz-border-radius-topleft: 6px; + border-top-left-radius: 6px; + -webkit-border-bottom-left-radius: 6px; + -moz-border-radius-bottomleft: 6px; + border-bottom-left-radius: 6px; +} +.btn-group .btn.large:last-child, .btn-group .large.dropdown-toggle { + -webkit-border-top-right-radius: 6px; + -moz-border-radius-topright: 6px; + border-top-right-radius: 6px; + -webkit-border-bottom-right-radius: 6px; + -moz-border-radius-bottomright: 6px; + border-bottom-right-radius: 6px; +} +.btn-group .btn:hover, +.btn-group .btn:focus, +.btn-group .btn:active, +.btn-group .btn.active { + z-index: 2; +} +.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group .dropdown-toggle { + padding-left: 8px; + padding-right: 8px; + -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + *padding-top: 5px; + *padding-bottom: 5px; +} +.btn-group.open { + *z-index: 1000; +} +.btn-group.open .dropdown-menu { + display: block; + margin-top: 1px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.btn-group.open .dropdown-toggle { + background-image: none; + -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.btn .caret { + margin-top: 7px; + margin-left: 0; +} +.btn:hover .caret, .open.btn-group .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.btn-primary .caret, +.btn-danger .caret, +.btn-info .caret, +.btn-success .caret, +.btn-inverse .caret { + border-top-color: #ffffff; + opacity: 0.75; + filter: alpha(opacity=75); +} +.btn-small .caret { + margin-top: 4px; +} +.alert { + padding: 8px 35px 8px 14px; + margin-bottom: 18px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + background-color: #fcf8e3; + border: 1px solid #fbeed5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.alert, .alert-heading { + color: #c09853; +} +.alert .close { + position: relative; + top: -2px; + right: -21px; + line-height: 18px; +} +.alert-success { + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success, .alert-success .alert-heading { + color: #468847; +} +.alert-danger, .alert-error { + background-color: #f2dede; + border-color: #eed3d7; +} +.alert-danger, +.alert-error, +.alert-danger .alert-heading, +.alert-error .alert-heading { + color: #b94a48; +} +.alert-info { + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info, .alert-info .alert-heading { + color: #3a87ad; +} +.alert-block { + padding-top: 14px; + padding-bottom: 14px; +} +.alert-block > p, .alert-block > ul { + margin-bottom: 0; +} +.alert-block p + p { + margin-top: 5px; +} +.nav { + margin-left: 0; + margin-bottom: 18px; + list-style: none; +} +.nav > li > a { + display: block; +} +.nav > li > a:hover { + text-decoration: none; + background-color: #eeeeee; +} +.nav .nav-header { + display: block; + padding: 3px 15px; + font-size: 11px; + font-weight: bold; + line-height: 18px; + color: #999999; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-transform: uppercase; +} +.nav li + .nav-header { + margin-top: 9px; +} +.nav-list { + padding-left: 14px; + padding-right: 14px; + margin-bottom: 0; +} +.nav-list > li > a, .nav-list .nav-header { + margin-left: -15px; + margin-right: -15px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); +} +.nav-list > li > a { + padding: 3px 15px; +} +.nav-list .active > a, .nav-list .active > a:hover { + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); + background-color: #0088cc; +} +.nav-list [class^="icon-"] { + margin-right: 2px; +} +.nav-tabs, .nav-pills { + *zoom: 1; +} +.nav-tabs:before, +.nav-pills:before, +.nav-tabs:after, +.nav-pills:after { + display: table; + content: ""; +} +.nav-tabs:after, .nav-pills:after { + clear: both; +} +.nav-tabs > li, .nav-pills > li { + float: left; +} +.nav-tabs > li > a, .nav-pills > li > a { + padding-right: 12px; + padding-left: 12px; + margin-right: 2px; + line-height: 14px; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + margin-bottom: -1px; +} +.nav-tabs > li > a { + padding-top: 9px; + padding-bottom: 9px; + border: 1px solid transparent; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} +.nav-tabs > .active > a, .nav-tabs > .active > a:hover { + color: #555555; + background-color: #ffffff; + border: 1px solid #ddd; + border-bottom-color: transparent; + cursor: default; +} +.nav-pills > li > a { + padding-top: 8px; + padding-bottom: 8px; + margin-top: 2px; + margin-bottom: 2px; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} +.nav-pills .active > a, .nav-pills .active > a:hover { + color: #ffffff; + background-color: #0088cc; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li > a { + margin-right: 0; +} +.nav-tabs.nav-stacked { + border-bottom: 0; +} +.nav-tabs.nav-stacked > li > a { + border: 1px solid #ddd; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.nav-tabs.nav-stacked > li:first-child > a { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.nav-tabs.nav-stacked > li:last-child > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.nav-tabs.nav-stacked > li > a:hover { + border-color: #ddd; + z-index: 2; +} +.nav-pills.nav-stacked > li > a { + margin-bottom: 3px; +} +.nav-pills.nav-stacked > li:last-child > a { + margin-bottom: 1px; +} +.nav-tabs .dropdown-menu, .nav-pills .dropdown-menu { + margin-top: 1px; + border-width: 1px; +} +.nav-pills .dropdown-menu { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.nav-tabs .dropdown-toggle .caret, .nav-pills .dropdown-toggle .caret { + border-top-color: #0088cc; + margin-top: 6px; +} +.nav-tabs .dropdown-toggle:hover .caret, .nav-pills .dropdown-toggle:hover .caret { + border-top-color: #005580; +} +.nav-tabs .active .dropdown-toggle .caret, .nav-pills .active .dropdown-toggle .caret { + border-top-color: #333333; +} +.nav > .dropdown.active > a:hover { + color: #000000; + cursor: pointer; +} +.nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, .nav > .open.active > a:hover { + color: #ffffff; + background-color: #999999; + border-color: #999999; +} +.nav .open .caret, .nav .open.active .caret, .nav .open a:hover .caret { + border-top-color: #ffffff; + opacity: 1; + filter: alpha(opacity=100); +} +.tabs-stacked .open > a:hover { + border-color: #999999; +} +.tabbable { + *zoom: 1; +} +.tabbable:before, .tabbable:after { + display: table; + content: ""; +} +.tabbable:after { + clear: both; +} +.tab-content { + overflow: hidden; +} +.tabs-below .nav-tabs, .tabs-right .nav-tabs, .tabs-left .nav-tabs { + border-bottom: 0; +} +.tab-content > .tab-pane, .pill-content > .pill-pane { + display: none; +} +.tab-content > .active, .pill-content > .active { + display: block; +} +.tabs-below .nav-tabs { + border-top: 1px solid #ddd; +} +.tabs-below .nav-tabs > li { + margin-top: -1px; + margin-bottom: 0; +} +.tabs-below .nav-tabs > li > a { + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.tabs-below .nav-tabs > li > a:hover { + border-bottom-color: transparent; + border-top-color: #ddd; +} +.tabs-below .nav-tabs .active > a, .tabs-below .nav-tabs .active > a:hover { + border-color: transparent #ddd #ddd #ddd; +} +.tabs-left .nav-tabs > li, .tabs-right .nav-tabs > li { + float: none; +} +.tabs-left .nav-tabs > li > a, .tabs-right .nav-tabs > li > a { + min-width: 74px; + margin-right: 0; + margin-bottom: 3px; +} +.tabs-left .nav-tabs { + float: left; + margin-right: 19px; + border-right: 1px solid #ddd; +} +.tabs-left .nav-tabs > li > a { + margin-right: -1px; + -webkit-border-radius: 4px 0 0 4px; + -moz-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} +.tabs-left .nav-tabs > li > a:hover { + border-color: #eeeeee #dddddd #eeeeee #eeeeee; +} +.tabs-left .nav-tabs .active > a, .tabs-left .nav-tabs .active > a:hover { + border-color: #ddd transparent #ddd #ddd; + *border-right-color: #ffffff; +} +.tabs-right .nav-tabs { + float: right; + margin-left: 19px; + border-left: 1px solid #ddd; +} +.tabs-right .nav-tabs > li > a { + margin-left: -1px; + -webkit-border-radius: 0 4px 4px 0; + -moz-border-radius: 0 4px 4px 0; + border-radius: 0 4px 4px 0; +} +.tabs-right .nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #eeeeee #dddddd; +} +.tabs-right .nav-tabs .active > a, .tabs-right .nav-tabs .active > a:hover { + border-color: #ddd #ddd #ddd transparent; + *border-left-color: #ffffff; +} +.navbar { + overflow: visible; + margin-bottom: 18px; +} +.navbar-inner { + padding-left: 20px; + padding-right: 20px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); +} +.btn-navbar { + display: none; + float: right; + padding: 7px 10px; + margin-left: 5px; + margin-right: 5px; + background-color: #2c2c2c; + background-image: -moz-linear-gradient(top, #333333, #222222); + background-image: -ms-linear-gradient(top, #333333, #222222); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); + background-image: -webkit-linear-gradient(top, #333333, #222222); + background-image: -o-linear-gradient(top, #333333, #222222); + background-image: linear-gradient(top, #333333, #222222); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); + border-color: #222222 #222222 #000000; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); +} +.btn-navbar:hover, +.btn-navbar:active, +.btn-navbar.active, +.btn-navbar.disabled, +.btn-navbar[disabled] { + background-color: #222222; +} +.btn-navbar:active, .btn-navbar.active { + background-color: #080808 \9; +} +.btn-navbar .icon-bar { + display: block; + width: 18px; + height: 2px; + background-color: #f5f5f5; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; + -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); +} +.btn-navbar .icon-bar + .icon-bar { + margin-top: 3px; +} +.nav-collapse.collapse { + height: auto; +} +.navbar .brand:hover { + text-decoration: none; +} +.navbar .brand { + float: left; + display: block; + padding: 8px 20px 12px; + margin-left: -20px; + font-size: 20px; + font-weight: 200; + line-height: 1; + color: #ffffff; +} +.navbar .navbar-text { + margin-bottom: 0; + line-height: 40px; + color: #999999; +} +.navbar .navbar-text a:hover { + color: #ffffff; + background-color: transparent; +} +.navbar .btn, .navbar .btn-group { + margin-top: 5px; +} +.navbar .btn-group .btn { + margin-top: 0; +} +.navbar-form { + margin-bottom: 0; + *zoom: 1; +} +.navbar-form:before, .navbar-form:after { + display: table; + content: ""; +} +.navbar-form:after { + clear: both; +} +.navbar-form input, .navbar-form select { + display: inline-block; + margin-top: 5px; + margin-bottom: 0; +} +.navbar-form .radio, .navbar-form .checkbox { + margin-top: 5px; +} +.navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { + margin-top: 3px; +} +.navbar-form .input-append, .navbar-form .input-prepend { + margin-top: 6px; + white-space: nowrap; +} +.navbar-form .input-append input, .navbar-form .input-prepend input { + margin-top: 0; +} +.navbar-search { + position: relative; + float: left; + margin-top: 6px; + margin-bottom: 0; +} +.navbar-search .search-query { + padding: 4px 9px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 13px; + font-weight: normal; + line-height: 1; + color: #ffffff; + color: rgba(255, 255, 255, 0.75); + background: #666; + background: rgba(255, 255, 255, 0.3); + border: 1px solid #111; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); + -webkit-transition: none; + -moz-transition: none; + -ms-transition: none; + -o-transition: none; + transition: none; +} +.navbar-search .search-query :-moz-placeholder { + color: #eeeeee; +} +.navbar-search .search-query::-webkit-input-placeholder { + color: #eeeeee; +} +.navbar-search .search-query:hover { + color: #ffffff; + background-color: #999999; + background-color: rgba(255, 255, 255, 0.5); +} +.navbar-search .search-query:focus, .navbar-search .search-query.focused { + padding: 5px 10px; + color: #333333; + text-shadow: 0 1px 0 #ffffff; + background-color: #ffffff; + border: 0; + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); + outline: 0; +} +.navbar-fixed-top { + position: fixed; + top: 0; + right: 0; + left: 0; + z-index: 1030; +} +.navbar-fixed-top .navbar-inner { + padding-left: 0; + padding-right: 0; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} +.navbar .nav { + position: relative; + left: 0; + display: block; + float: left; + margin: 0 10px 0 0; +} +.navbar .nav.pull-right { + float: right; +} +.navbar .nav > li { + display: block; + float: left; +} +.navbar .nav > li > a { + float: none; + padding: 10px 10px 11px; + line-height: 19px; + color: #999999; + text-decoration: none; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.navbar .nav > li > a:hover { + background-color: transparent; + color: #ffffff; + text-decoration: none; +} +.navbar .nav .active > a, .navbar .nav .active > a:hover { + color: #ffffff; + text-decoration: none; + background-color: #222222; +} +.navbar .divider-vertical { + height: 40px; + width: 1px; + margin: 0 9px; + overflow: hidden; + background-color: #222222; + border-right: 1px solid #333333; +} +.navbar .nav.pull-right { + margin-left: 10px; + margin-right: 0; +} +.navbar .dropdown-menu { + margin-top: 1px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.navbar .dropdown-menu:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 9px; +} +.navbar .dropdown-menu:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 10px; +} +.navbar .nav .dropdown-toggle .caret, .navbar .nav .open.dropdown .caret { + border-top-color: #ffffff; +} +.navbar .nav .active .caret { + opacity: 1; + filter: alpha(opacity=100); +} +.navbar .nav .open > .dropdown-toggle, .navbar .nav .active > .dropdown-toggle, .navbar .nav .open.active > .dropdown-toggle { + background-color: transparent; +} +.navbar .nav .active > .dropdown-toggle:hover { + color: #ffffff; +} +.navbar .nav.pull-right .dropdown-menu { + left: auto; + right: 0; +} +.navbar .nav.pull-right .dropdown-menu:before { + left: auto; + right: 12px; +} +.navbar .nav.pull-right .dropdown-menu:after { + left: auto; + right: 13px; +} +.breadcrumb { + padding: 7px 14px; + margin: 0 0 18px; + background-color: #fbfbfb; + background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); + background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); + background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); + background-image: linear-gradient(top, #ffffff, #f5f5f5); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); + border: 1px solid #ddd; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; +} +.breadcrumb li { + display: inline-block; + text-shadow: 0 1px 0 #ffffff; +} +.breadcrumb .divider { + padding: 0 5px; + color: #999999; +} +.breadcrumb .active a { + color: #333333; +} +.pagination { + height: 36px; + margin: 18px 0; +} +.pagination ul { + display: inline-block; + *display: inline; + /* IE7 inline-block hack */ + + *zoom: 1; + margin-left: 0; + margin-bottom: 0; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} +.pagination li { + display: inline; +} +.pagination a { + float: left; + padding: 0 14px; + line-height: 34px; + text-decoration: none; + border: 1px solid #ddd; + border-left-width: 0; +} +.pagination a:hover, .pagination .active a { + background-color: #f5f5f5; +} +.pagination .active a { + color: #999999; + cursor: default; +} +.pagination .disabled a, .pagination .disabled a:hover { + color: #999999; + background-color: transparent; + cursor: default; +} +.pagination li:first-child a { + border-left-width: 1px; + -webkit-border-radius: 3px 0 0 3px; + -moz-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} +.pagination li:last-child a { + -webkit-border-radius: 0 3px 3px 0; + -moz-border-radius: 0 3px 3px 0; + border-radius: 0 3px 3px 0; +} +.pagination-centered { + text-align: center; +} +.pagination-right { + text-align: right; +} +.pager { + margin-left: 0; + margin-bottom: 18px; + list-style: none; + text-align: center; + *zoom: 1; +} +.pager:before, .pager:after { + display: table; + content: ""; +} +.pager:after { + clear: both; +} +.pager li { + display: inline; +} +.pager a { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + -webkit-border-radius: 15px; + -moz-border-radius: 15px; + border-radius: 15px; +} +.pager a:hover { + text-decoration: none; + background-color: #f5f5f5; +} +.pager .next a { + float: right; +} +.pager .previous a { + float: left; +} +.modal-open .dropdown-menu { + z-index: 2050; +} +.modal-open .dropdown.open { + *z-index: 2050; +} +.modal-open .popover { + z-index: 2060; +} +.modal-open .tooltip { + z-index: 2070; +} +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + background-color: #000000; +} +.modal-backdrop.fade { + opacity: 0; +} +.modal-backdrop, .modal-backdrop.fade.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.modal { + position: fixed; + top: 50%; + left: 50%; + z-index: 1050; + max-height: 500px; + overflow: auto; + width: 560px; + margin: -250px 0 0 -280px; + background-color: #ffffff; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, 0.3); + *border: 1px solid #999; + /* IE6-7 */ + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.modal.fade { + -webkit-transition: opacity .3s linear, top .3s ease-out; + -moz-transition: opacity .3s linear, top .3s ease-out; + -ms-transition: opacity .3s linear, top .3s ease-out; + -o-transition: opacity .3s linear, top .3s ease-out; + transition: opacity .3s linear, top .3s ease-out; + top: -25%; +} +.modal.fade.in { + top: 50%; +} +.modal-header { + padding: 9px 15px; + border-bottom: 1px solid #eee; +} +.modal-header .close { + margin-top: 2px; +} +.modal-body { + padding: 15px; +} +.modal-body .modal-form { + margin-bottom: 0; +} +.modal-footer { + padding: 14px 15px 15px; + margin-bottom: 0; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + -webkit-border-radius: 0 0 6px 6px; + -moz-border-radius: 0 0 6px 6px; + border-radius: 0 0 6px 6px; + -webkit-box-shadow: inset 0 1px 0 #ffffff; + -moz-box-shadow: inset 0 1px 0 #ffffff; + box-shadow: inset 0 1px 0 #ffffff; + *zoom: 1; +} +.modal-footer:before, .modal-footer:after { + display: table; + content: ""; +} +.modal-footer:after { + clear: both; +} +.modal-footer .btn { + float: right; + margin-left: 5px; + margin-bottom: 0; +} +.tooltip { + position: absolute; + z-index: 1020; + display: block; + visibility: visible; + padding: 5px; + font-size: 11px; + opacity: 0; + filter: alpha(opacity=0); +} +.tooltip.in { + opacity: 0.8; + filter: alpha(opacity=80); +} +.tooltip.top { + margin-top: -2px; +} +.tooltip.right { + margin-left: 2px; +} +.tooltip.bottom { + margin-top: 2px; +} +.tooltip.left { + margin-left: -2px; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + padding: 5px; +} +.popover.top { + margin-top: -5px; +} +.popover.right { + margin-left: 5px; +} +.popover.bottom { + margin-top: 5px; +} +.popover.left { + margin-left: -5px; +} +.popover.top .arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #000000; +} +.popover.right .arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-right: 5px solid #000000; +} +.popover.bottom .arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-bottom: 5px solid #000000; +} +.popover.left .arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + border-left: 5px solid #000000; +} +.popover .arrow { + position: absolute; + width: 0; + height: 0; +} +.popover-inner { + padding: 3px; + width: 280px; + overflow: hidden; + background: #000000; + background: rgba(0, 0, 0, 0.8); + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; + -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); + box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); +} +.popover-title { + padding: 9px 15px; + line-height: 1; + background-color: #f5f5f5; + border-bottom: 1px solid #eee; + -webkit-border-radius: 3px 3px 0 0; + -moz-border-radius: 3px 3px 0 0; + border-radius: 3px 3px 0 0; +} +.popover-content { + padding: 14px; + background-color: #ffffff; + -webkit-border-radius: 0 0 3px 3px; + -moz-border-radius: 0 0 3px 3px; + border-radius: 0 0 3px 3px; + -webkit-background-clip: padding-box; + -moz-background-clip: padding-box; + background-clip: padding-box; +} +.popover-content p, .popover-content ul, .popover-content ol { + margin-bottom: 0; +} +.thumbnails { + margin-left: -20px; + list-style: none; + *zoom: 1; +} +.thumbnails:before, .thumbnails:after { + display: table; + content: ""; +} +.thumbnails:after { + clear: both; +} +.thumbnails > li { + float: left; + margin: 0 0 18px 20px; +} +.thumbnail { + display: block; + padding: 4px; + line-height: 1; + border: 1px solid #ddd; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); +} +a.thumbnail:hover { + border-color: #0088cc; + -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); + box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); +} +.thumbnail > img { + display: block; + max-width: 100%; + margin-left: auto; + margin-right: auto; +} +.thumbnail .caption { + padding: 9px; +} +.label { + padding: 2px 4px 3px; + font-size: 11.049999999999999px; + font-weight: bold; + color: #ffffff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #999999; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.label:hover { + color: #ffffff; + text-decoration: none; +} +.label-important { + background-color: #b94a48; +} +.label-important:hover { + background-color: #953b39; +} +.label-warning { + background-color: #f89406; +} +.label-warning:hover { + background-color: #c67605; +} +.label-success { + background-color: #468847; +} +.label-success:hover { + background-color: #356635; +} +.label-info { + background-color: #3a87ad; +} +.label-info:hover { + background-color: #2d6987; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@-moz-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} +.progress { + overflow: hidden; + height: 18px; + margin-bottom: 18px; + background-color: #f7f7f7; + background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); + background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); + background-image: linear-gradient(top, #f5f5f5, #f9f9f9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.progress .bar { + width: 0%; + height: 18px; + color: #ffffff; + font-size: 12px; + text-align: center; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); + background-color: #0e90d2; + background-image: -moz-linear-gradient(top, #149bdf, #0480be); + background-image: -ms-linear-gradient(top, #149bdf, #0480be); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); + background-image: -webkit-linear-gradient(top, #149bdf, #0480be); + background-image: -o-linear-gradient(top, #149bdf, #0480be); + background-image: linear-gradient(top, #149bdf, #0480be); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: width 0.6s ease; + -moz-transition: width 0.6s ease; + -ms-transition: width 0.6s ease; + -o-transition: width 0.6s ease; + transition: width 0.6s ease; +} +.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-danger .bar { + background-color: #dd514c; + background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); + background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); + background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); + background-image: linear-gradient(top, #ee5f5b, #c43c35); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); +} +.progress-danger.progress-striped .bar { + background-color: #ee5f5b; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-success .bar { + background-color: #5eb95e; + background-image: -moz-linear-gradient(top, #62c462, #57a957); + background-image: -ms-linear-gradient(top, #62c462, #57a957); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); + background-image: -webkit-linear-gradient(top, #62c462, #57a957); + background-image: -o-linear-gradient(top, #62c462, #57a957); + background-image: linear-gradient(top, #62c462, #57a957); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); +} +.progress-success.progress-striped .bar { + background-color: #62c462; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.progress-info .bar { + background-color: #4bb1cf; + background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); + background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); + background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); + background-image: -o-linear-gradient(top, #5bc0de, #339bb9); + background-image: linear-gradient(top, #5bc0de, #339bb9); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); +} +.progress-info.progress-striped .bar { + background-color: #5bc0de; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} +.accordion { + margin-bottom: 18px; +} +.accordion-group { + margin-bottom: 2px; + border: 1px solid #e5e5e5; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.accordion-heading { + border-bottom: 0; +} +.accordion-heading .accordion-toggle { + display: block; + padding: 8px 15px; +} +.accordion-inner { + padding: 9px 15px; + border-top: 1px solid #e5e5e5; +} +.carousel { + position: relative; + margin-bottom: 18px; + line-height: 1; +} +.carousel-inner { + overflow: hidden; + width: 100%; + position: relative; +} +.carousel .item { + display: none; + position: relative; + -webkit-transition: 0.6s ease-in-out left; + -moz-transition: 0.6s ease-in-out left; + -ms-transition: 0.6s ease-in-out left; + -o-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} +.carousel .item > img { + display: block; + line-height: 1; +} +.carousel .active, .carousel .next, .carousel .prev { + display: block; +} +.carousel .active { + left: 0; +} +.carousel .next, .carousel .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel .next { + left: 100%; +} +.carousel .prev { + left: -100%; +} +.carousel .next.left, .carousel .prev.right { + left: 0; +} +.carousel .active.left { + left: -100%; +} +.carousel .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 40%; + left: 15px; + width: 40px; + height: 40px; + margin-top: -20px; + font-size: 60px; + font-weight: 100; + line-height: 30px; + color: #ffffff; + text-align: center; + background: #222222; + border: 3px solid #ffffff; + -webkit-border-radius: 23px; + -moz-border-radius: 23px; + border-radius: 23px; + opacity: 0.5; + filter: alpha(opacity=50); +} +.carousel-control.right { + left: auto; + right: 15px; +} +.carousel-control:hover { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} +.carousel-caption { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding: 10px 15px 5px; + background: #333333; + background: rgba(0, 0, 0, 0.75); +} +.carousel-caption h4, .carousel-caption p { + color: #ffffff; +} +.hero-unit { + padding: 60px; + margin-bottom: 30px; + background-color: #f5f5f5; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} +.hero-unit h1 { + margin-bottom: 0; + font-size: 60px; + line-height: 1; + letter-spacing: -1px; +} +.hero-unit p { + font-size: 18px; + font-weight: 200; + line-height: 27px; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.hide { + display: none; +} +.show { + display: block; +} +.invisible { + visibility: hidden; +} diff --git a/hyperkitty/static/css/normalize.css b/hyperkitty/static/css/normalize.css new file mode 100644 index 0000000..f056d58 --- /dev/null +++ b/hyperkitty/static/css/normalize.css @@ -0,0 +1,504 @@ +/*! normalize.css 2012-03-06T10:21 UTC - http://github.com/necolas/normalize.css */ + +/* ============================================================================= + HTML5 display definitions + ========================================================================== */ + +/* + * Corrects block display not defined in IE6/7/8/9 & FF3 + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects inline-block display not defined in IE6/7/8/9 & FF3 + */ + +audio, +canvas, +video { + display: inline-block; + *display: inline; + *zoom: 1; +} + +/* + * Prevents modern browsers from displaying 'audio' without controls + * Remove excess height in iOS5 devices + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/* + * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 + * Known issue: no IE6 support + */ + +[hidden] { + display: none; +} + + +/* ============================================================================= + Base + ========================================================================== */ + +/* + * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units + * http://clagnut.com/blog/348/#c790 + * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom + * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ + */ + +html { + font-size: 100%; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -ms-text-size-adjust: 100%; /* 2 */ +} + +/* + * Addresses font-family inconsistency between 'textarea' and other form elements. + */ + +html, +button, +input, +select, +textarea { + font-family: sans-serif; +} + +/* + * Addresses margins handled incorrectly in IE6/7 + */ + +body { + margin: 0; +} + + +/* ============================================================================= + Links + ========================================================================== */ + +/* + * Addresses outline displayed oddly in Chrome + */ + +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers + * people.opera.com/patrickl/experiments/keyboard/test + */ + +a:hover, +a:active { + outline: 0; +} + + +/* ============================================================================= + Typography + ========================================================================== */ + +/* + * Addresses font sizes and margins set differently in IE6/7 + * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +h2 { + font-size: 1.5em; + margin: 0.83em 0; +} + +h3 { + font-size: 1.17em; + margin: 1em 0; +} + +h4 { + font-size: 1em; + margin: 1.33em 0; +} + +h5 { + font-size: 0.83em; + margin: 1.67em 0; +} + +h6 { + font-size: 0.75em; + margin: 2.33em 0; +} + +/* + * Addresses styling not present in IE7/8/9, S5, Chrome + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to 'bolder' in FF3+, S4/5, Chrome +*/ + +b, +strong { + font-weight: bold; +} + +blockquote { + margin: 1em 40px; +} + +/* + * Addresses styling not present in S5, Chrome + */ + +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE6/7/8/9 + */ + +mark { + background: #ff0; + color: #000; +} + +/* + * Addresses margins set differently in IE6/7 + */ + +p, +pre { + margin: 1em 0; +} + +/* + * Corrects font family set oddly in IE6, S4/5, Chrome + * en.wikipedia.org/wiki/User:Davidgothberg/Test59 + */ + +pre, +code, +kbd, +samp { + font-family: monospace, serif; + _font-family: 'courier new', monospace; + font-size: 1em; +} + +/* + * Improves readability of pre-formatted text in all browsers + */ + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* + * 1. Addresses CSS quotes not supported in IE6/7 + * 2. Addresses quote property not supported in S4 + */ + +/* 1 */ + +q { + quotes: none; +} + +/* 2 */ + +q:before, +q:after { + content: ''; + content: none; +} + +small { + font-size: 75%; +} + +/* + * Prevents sub and sup affecting line-height in all browsers + * gist.github.com/413930 + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + + +/* ============================================================================= + Lists + ========================================================================== */ + +/* + * Addresses margins set differently in IE6/7 + */ + +dl, +menu, +ol, +ul { + margin: 1em 0; +} + +dd { + margin: 0 0 0 40px; +} + +/* + * Addresses paddings set differently in IE6/7 + */ + +menu, +ol, +ul { + padding: 0 0 0 40px; +} + +/* + * Corrects list images handled incorrectly in IE7 + */ + +nav ul, +nav ol { + list-style: none; + list-style-image: none; +} + + +/* ============================================================================= + Embedded content + ========================================================================== */ + +/* + * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 + * 2. Improves image quality when scaled in IE7 + * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ + */ + +img { + border: 0; /* 1 */ + -ms-interpolation-mode: bicubic; /* 2 */ +} + +/* + * Corrects overflow displayed oddly in IE9 + */ + +svg:not(:root) { + overflow: hidden; +} + + +/* ============================================================================= + Figures + ========================================================================== */ + +/* + * Addresses margin not present in IE6/7/8/9, S5, O11 + */ + +figure { + margin: 0; +} + + +/* ============================================================================= + Forms + ========================================================================== */ + +/* + * Corrects margin displayed oddly in IE6/7 + */ + +form { + margin: 0; +} + +/* + * Define consistent border, margin, and padding + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE6/7/8/9 + * 2. Corrects text not wrapping in FF3 + * 3. Corrects alignment displayed oddly in IE6/7 + */ + +legend { + border: 0; /* 1 */ + padding: 0; + white-space: normal; /* 2 */ + *margin-left: -7px; /* 3 */ +} + +/* + * 1. Corrects font size not being inherited in all browsers + * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome + * 3. Improves appearance and consistency in all browsers + */ + +button, +input, +select, +textarea { + font-size: 100%; /* 1 */ + margin: 0; /* 2 */ + vertical-align: baseline; /* 3 */ + *vertical-align: middle; /* 3 */ +} + +/* + * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet + */ + +button, +input { + line-height: normal; /* 1 */ +} + +/* + * 1. Improves usability and consistency of cursor style between image-type 'input' and others + * 2. Corrects inability to style clickable 'input' types in iOS + * 3. Removes inner spacing in IE7 without affecting normal text inputs + * Known issue: inner spacing remains in IE6 + */ + +button, +input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; /* 1 */ + -webkit-appearance: button; /* 2 */ + *overflow: visible; /* 3 */ +} + +/* + * Re-set default cursor for disabled elements + */ + +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to content-box in IE8/9 + * 2. Removes excess padding in IE8/9 + * 3. Removes excess padding in IE7 + Known issue: excess padding remains in IE6 + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + *height: 13px; /* 3 */ + *width: 13px; /* 3 */ +} + +/* + * 1. Addresses appearance set to searchfield in S5, Chrome + * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in S5, Chrome on OS X + */ + +input[type="search"]::-webkit-search-decoration, +input[type="search"]::-webkit-search-cancel-button { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in FF3+ + * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE6/7/8/9 + * 2. Improves readability and alignment in all browsers + */ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + + +/* ============================================================================= + Tables + ========================================================================== */ + +/* + * Remove most spacing between table cells + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} diff --git a/hyperkitty/static/css/stats.css b/hyperkitty/static/css/stats.css new file mode 100644 index 0000000..ec1bf4d --- /dev/null +++ b/hyperkitty/static/css/stats.css @@ -0,0 +1,137 @@ +h2 { + margin-top: 0px; + padding-top: 10px; +} +/* Add icons to some text */ +.neutral { + background: url("../img/neutral.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.like { + background: url("../img/like.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.likealot { + background: url("../img/likealot.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +/* The content section of the page */ +.content { + width: 1024px; + margin: auto; +} + +#graph { + vertical-align: middle; +} + +#fig { + position: relative; + margin: auto; + width: 540px; + height: 330px; +} + +#top_discussion { + width: 45%; + margin-right: 22px; + margin-left: 10px; +} + +#discussion_by_topic { + width: 45%; + margin-top: 20px; + margin-right: 22px; + margin-left: 10px; +} + +#most_active { + float: right; + width: 45%; +} + +#discussion_marker { + float: right; + width: 45%; + margin-top: 20px; +} + +.thread { + white-space: nowrap; +} + +.thread * { + white-space: normal; +} + +.thread_id { + font-weight: bold; + font-size: 125%; + color: rgb(102, 102, 102); + vertical-align: top; + padding-right: 10px; +} + +.thread_title{ + padding-right:20px; + color: rgb(102, 102, 102); + display: inline-block; +} + +.thread_stats ul li { + margin-right:10px; +} + +.category { + font-variant: small-caps; + font-weight: bold; + color: white; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + vertical-align: top; + margin-bottom: 10px; + padding-top: 0px; + padding-left: 10px; +} + +.category_entry { + list-style-type: circle; + margin-top: 0px; + padding-bottom: 10px; + padding-left: 25px; +} + +.category_entry li { + padding-bottom: 10px; +} + +.maker { + color: rgb(102, 102, 102); + padding-right: 10px; + padding-bottom: 20px; +} + +.maker_id, .marker_name{ + font-weight: bold; + font-size: 115%; + vertical-align: top; + padding-right: 20px; +} + +.gravatar { + padding-right: 20px; +} + +.score{ + font-weight: bold; +} diff --git a/hyperkitty/static/css/style.css b/hyperkitty/static/css/style.css new file mode 100644 index 0000000..30535a2 --- /dev/null +++ b/hyperkitty/static/css/style.css @@ -0,0 +1,382 @@ +body { + position: relative; + padding-top: 90px; + background-color: white; +} + +.Sb { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + background-color: white; + clear: both; + font-size: 13px; + line-height: 1.4; + margin: 20px 0 20px 68px; + outline: none; + position: relative; + width: 497px; + word-wrap: break-word; +} + +.ZX { + color: #999; + height: 40px; + margin: 0 2px; + position: relative; + bottom: -3px; + background-color: #F8F8F8; + border: 1px solid #CCC; + +} + +.socialLogin { + list-style: none; + margin: 0px; +} + +.socialLogin li { + float: left; + padding: 5px; +} + +.right { + text-align: right; +} + +.inline-block { + display: inline-block; +} + +.inline li, .inline-block li { + display: inline-block; + list-style-type: none; +} + +/* Add icons to some text */ +.participant { + background: url("../img/participant.png") no-repeat scroll left top; + padding-left: 20px; +} + +.discussion { + background: url("../img/discussion.png") no-repeat scroll left top; + padding-left: 20px; +} + +.saved { + background: url("../img/saved.png") no-repeat scroll left top; + padding-left: 20px; +} + +.notsaved { + background: url("../img/notsaved.png") no-repeat scroll left top; + padding-left: 20px; +} + +.gravatar { + vertical-align: top; + width:40px; + font-size: 70%; +} + +.gravatar img { + width: 40px; +} + +.neutral { + background: url("../img/neutral.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.like { + background: url("../img/like.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.likealot { + background: url("../img/likealot.png") no-repeat scroll left; + padding-left: 20px; + padding-right: 20px; + font-weight: bold; +} + +.youlike { + background: url("../img/youlike.png") no-repeat scroll left; + padding-left: 15px; + padding-right: 5px; +} + +.youdislike { + background: url("../img/youdislike.png") no-repeat scroll left; + padding-left: 15px; + padding-right: 5px; +} + +.showdiscussion { + background-image: linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -o-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -moz-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -webkit-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -ms-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.11, rgb(204,204,204)), + color-stop(1, rgb(255,255,255)) + ); + padding: 3px 7px 3px 7px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + border-style: solid; + border-width: 1px; + border-color: rgb(179, 179, 179); +} + +.showdiscussion a { + color: rgb(77, 77, 77); +} + + +/* Top of the page -- header */ +.header { + background-color: rgb(236, 236, 236); + min-height : 100px; +} + +#white { + color: rgb(255, 255, 255); + background-color: rgb(255, 255, 255); + margin-bottom: 0px; +} + +#headline { + min-height: 50px; +} + +.list_nav { + float: left; + list-style: none; + margin: 0; + padding: 5px 0 0 0; +} + +.list_nav li { + float: left; + margin-left: 20px; +} + +.user_nav { + float: right; + list-style: none; + margin: 0; + padding: 5px 0 0 0; +} + +.user_nav a { + float: none; + padding: 10px 10px 11px; + line-height: 19px; + color: #999; + text-decoration: none; + text-shadow: 0 -1px 0 + rgba(0, 0, 0, 0.25); +} + +.user_nav li { + float: left; + margin-left: 20px; +} + +#thread_content { + width: 70%; +} + +.email_date .date { + font-weight: bold; +} + +#top_right { + position: absolute; + right: 20px; + bottom: 0; + color: rgb(102, 102, 102); +} + +#top_right li { + margin-left:10px; +} + +#list_name { + font-weight: bold; +} + +#page_date { + font-size: 150%; +} + + +#searchbox { + text-align:right; + padding-right: 20px; +} + +#searchbox input { + width: 250px +} + +#searchbox input::-webkit-input-placeholder { + font-style: italic; +} + +#searchbox input:-moz-placeholder { + font-style: italic; +} + +#recent_activities{ + width: 88%; + margin-top: 20px; + margin-right: 10px; + float: right; +} + +#archives{ + width: 9%; + margin-left: 10px; + margin-top: 20px; + float: left; +/* + margin-right: 2px; +*/ +} + +#archives ul { + padding: 0; + margin: 0; +} + +#archives li { + list-style-type: none; +} + +/* Thread list */ + +.thread_title { + font-weight: bold; + font-size: 125%; +} + +.thread_date { + font-style: italic; + font-size: 70%; + color: rgb(128, 0, 0); +} + +.thread_content { + margin-top:10px; +} + +.thread_info { + text-align:right; + padding-right: 50px; +} + +.tags { + text-align:left; + margin: 0px 0px 0px 200px; + padding: 0px; +} + +/* Part containing the body of the mail which can be shown/hidden */ +.expander { + width: 665px; + background-image: linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -o-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -moz-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -webkit-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + background-image: -ms-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); + + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.11, rgb(236,236,236)), + color-stop(1, rgb(255,255,255)) + ); + border-style: solid; + border-width: 1px; + border-color: rgb(236,236,236); + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + padding-left: 20px; + margin-left: 21px; + display: inline-block; + vertical-align: top; + white-space: pre; +} + +.expander a { + float: right; + padding: 20px 10px 0px 0px; +} + +/* Thread types */ +.type { + font-variant: small-caps; + font-weight: bold; + color: white; + padding: 3px; + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + vertical-align: top; + width: 110px; + text-align:center; +} + +.type a { + color: white; +} + +.type_question { + background-color: rgb(179, 128, 255); +} + +.type_agenda { + background-color: rgb(42, 127, 255); +} + +.type_todo { + background-color: rgb(200, 171, 55); +} + +.type_dead { + background-color: rgb(0, 0, 0); +} + +.type_announcement { + background-color: rgb(170, 212, 0); +} + +.type_policy { + background-color: rgb(200, 55, 171); +} + +.type_test { + background-color: rgb(200, 171, 55); +} + +.invisible { + visibility: hidden; +} + +.removed { + display: none; +}[ diff --git a/hyperkitty/static/css/thread.css b/hyperkitty/static/css/thread.css new file mode 100644 index 0000000..4815ace --- /dev/null +++ b/hyperkitty/static/css/thread.css @@ -0,0 +1,239 @@ +#thread_nav{ + margin:auto; + width:100%; + text-align:center; +} + +#thread_nav * { + vertical-align: middle; +} + +#thread_nav .thread_title{ + margin:auto; + width: 50%; +} + +#thread_nav br { + margin-top: 10px; +} + +#thread_nav .thread_info { + margin-top:10px; + margin-bottom:10px; + font-size: 70%; + font-weight: normal; + text-align: center; +} + +#thread_nav .thread_info li { + margin-left:3em; +} + +#olderhread, #newewthread { + font-size: 70%; + color: rgb(167, 169, 172); +} + +#olderhread { + float: right; + margin-top: 2em; + margin-right: 20px; +} + +/* Define the two columns */ +#thread_content { + width: 70%; + margin-right: 22px; +} + +#thread_overview_info { + float: right; + width: 22%; +} + +/* Thread general information column */ +#days_old { + margin-left:1em; +} + +.days_text { + font-size: 70%; +} + +.days_num { + font-size: 200%; +} + +#add_to_fav a{ + color: rgb(167, 169, 172); +} + +#grey { + color: rgb(167, 169, 172); + background-color: rgb(167, 169, 172); + margin: 0px; + border: 0 none; + height: 1px; +} + +#tags { + color: rgb(167, 169, 172); + margin-top: 20px; +} + +#tag_title { + color: rgb(77, 77, 77); + text-transform: uppercase; +} + +#tags ul { + padding: 10px 0px 10px 0px; + margin: 0; +} + +#add_tag_field { + width:70%; +} + +#participants { + margin-top: 20px; + color: rgb(167, 169, 172); +} + +#participants_title { + color: rgb(77, 77, 77); + text-transform: uppercase; +} + +#participants ul { + padding: 10px 0px 10px 0px; + margin: 0; +} + +#participants li { + list-style-type: none; +} + +#participants img { + width: 20px; +} + +/* Main section with the whole thread */ + +/* First email of the thread. */ + +.first_email { +} + +.email_header { + position:relative; + margin-top: 20px; + margin-bottom: 20px; +} + +.email_header img { + width: 40px; +} + +.email_author .name{ + color: rgb(55, 113, 200); + font-weight: bold; +} + +.email_author .rank{ + color: rgb(167, 169, 172); + font-size: 80%; + font-weight: bold; +} + +.email_date { + position: absolute; + right: 20px; + bottom: 0px; +} + +.email_date .date { + font-weight: bold; +} + +.email_date .time { + color: rgb(167, 169, 172); +} + +.email_info { + padding: 0px; +} + +.add_comment { + float: right; +} + +/* The email thread */ +.even { + background-color: rgb(246, 246, 246); + border-top: 1px solid rgb(179, 179, 179); + padding-left: 20px; + margin: 20px 0px 20px 0px; +} + +.odd { + background-color: rgb(238, 238, 238); + border-top: 1px solid rgb(179, 179, 179); + padding-left: 20px; + margin: 20px 0px 20px 0px; +} + +.email { +} + +.email .email_header { + margin-top: 10px; + margin-bottom: 10px; +} + +.email .email_author { + font-size: 90%; +} + +.email .email_date, .email .email_date .date { + font-size: 90%; +} + +.email_body{ + -webkit-border-radius: 5px 5px 5px 5px; + -moz-border-radius: 5px 5px 5px 5px; + border-radius: 5px 5px 5px 5px; + border-style: solid; + border-width: 1px; + border-color: rgb(179, 179, 179); + padding: 5px; + min-height: 40px; + background-color: rgb(255, 255, 255); + white-space: pre; + display: inline-block; +} + +#first_email_body { + white-space: pre; + display: inline-block; +} + + +.email_body a { + float: right; + padding: 3px 10px 0px 0px; +} + +.email_info { + padding: 0px; + margin-top: 5px; +} + +.thread_email { + padding-left: 20px; + margin-left: 21px; + display: inline-block; + vertical-align: top; + white-space: pre; +} + diff --git a/hyperkitty/static/img/button_newer.png b/hyperkitty/static/img/button_newer.png new file mode 100644 index 0000000..14cfaa6 Binary files /dev/null and b/hyperkitty/static/img/button_newer.png differ diff --git a/hyperkitty/static/img/button_older.png b/hyperkitty/static/img/button_older.png new file mode 100644 index 0000000..6c3c950 Binary files /dev/null and b/hyperkitty/static/img/button_older.png differ diff --git a/hyperkitty/static/img/discussion.png b/hyperkitty/static/img/discussion.png new file mode 100644 index 0000000..26e60d9 Binary files /dev/null and b/hyperkitty/static/img/discussion.png differ diff --git a/hyperkitty/static/img/email_bg.png b/hyperkitty/static/img/email_bg.png new file mode 100644 index 0000000..f3ae7b7 Binary files /dev/null and b/hyperkitty/static/img/email_bg.png differ diff --git a/hyperkitty/static/img/like.png b/hyperkitty/static/img/like.png new file mode 100644 index 0000000..7406cdd Binary files /dev/null and b/hyperkitty/static/img/like.png differ diff --git a/hyperkitty/static/img/likealot.png b/hyperkitty/static/img/likealot.png new file mode 100644 index 0000000..5ce4b88 Binary files /dev/null and b/hyperkitty/static/img/likealot.png differ diff --git a/hyperkitty/static/img/login/browserid.png b/hyperkitty/static/img/login/browserid.png new file mode 100644 index 0000000..919a5c7 Binary files /dev/null and b/hyperkitty/static/img/login/browserid.png differ diff --git a/hyperkitty/static/img/login/facebook.png b/hyperkitty/static/img/login/facebook.png new file mode 100644 index 0000000..551100d Binary files /dev/null and b/hyperkitty/static/img/login/facebook.png differ diff --git a/hyperkitty/static/img/login/google.png b/hyperkitty/static/img/login/google.png new file mode 100644 index 0000000..b840860 Binary files /dev/null and b/hyperkitty/static/img/login/google.png differ diff --git a/hyperkitty/static/img/login/openid.png b/hyperkitty/static/img/login/openid.png new file mode 100644 index 0000000..bc81687 Binary files /dev/null and b/hyperkitty/static/img/login/openid.png differ diff --git a/hyperkitty/static/img/login/twitter.png b/hyperkitty/static/img/login/twitter.png new file mode 100644 index 0000000..14960b8 Binary files /dev/null and b/hyperkitty/static/img/login/twitter.png differ diff --git a/hyperkitty/static/img/login/yahoo.png b/hyperkitty/static/img/login/yahoo.png new file mode 100644 index 0000000..e9deaf2 Binary files /dev/null and b/hyperkitty/static/img/login/yahoo.png differ diff --git a/hyperkitty/static/img/neutral.png b/hyperkitty/static/img/neutral.png new file mode 100644 index 0000000..392f8c7 Binary files /dev/null and b/hyperkitty/static/img/neutral.png differ diff --git a/hyperkitty/static/img/newthread.png b/hyperkitty/static/img/newthread.png new file mode 100644 index 0000000..e61b871 Binary files /dev/null and b/hyperkitty/static/img/newthread.png differ diff --git a/hyperkitty/static/img/notsaved.png b/hyperkitty/static/img/notsaved.png new file mode 100644 index 0000000..a427a91 Binary files /dev/null and b/hyperkitty/static/img/notsaved.png differ diff --git a/hyperkitty/static/img/participant.png b/hyperkitty/static/img/participant.png new file mode 100644 index 0000000..f2d700b Binary files /dev/null and b/hyperkitty/static/img/participant.png differ diff --git a/hyperkitty/static/img/saved.png b/hyperkitty/static/img/saved.png new file mode 100644 index 0000000..b240cd5 Binary files /dev/null and b/hyperkitty/static/img/saved.png differ diff --git a/hyperkitty/static/img/show_discussion.png b/hyperkitty/static/img/show_discussion.png new file mode 100644 index 0000000..f7f42f1 Binary files /dev/null and b/hyperkitty/static/img/show_discussion.png differ diff --git a/hyperkitty/static/img/youdislike.png b/hyperkitty/static/img/youdislike.png new file mode 100644 index 0000000..0c6387b Binary files /dev/null and b/hyperkitty/static/img/youdislike.png differ diff --git a/hyperkitty/static/img/youlike.png b/hyperkitty/static/img/youlike.png new file mode 100644 index 0000000..affe451 Binary files /dev/null and b/hyperkitty/static/img/youlike.png differ diff --git a/hyperkitty/static/jquery.expander.js b/hyperkitty/static/jquery.expander.js new file mode 100644 index 0000000..9eabab4 --- /dev/null +++ b/hyperkitty/static/jquery.expander.js @@ -0,0 +1,382 @@ +/*! + * jQuery Expander Plugin v1.4 + * + * Date: Sun Dec 11 15:08:42 2011 EST + * Requires: jQuery v1.3+ + * + * Copyright 2011, Karl Swedberg + * Dual licensed under the MIT and GPL licenses (just like jQuery): + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + * + * + * +*/ + +(function($) { + $.expander = { + version: '1.4', + defaults: { + // the number of characters at which the contents will be sliced into two parts. + slicePoint: 100, + + // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false) + preserveWords: true, + + // a threshold of sorts for whether to initially hide/collapse part of the element's contents. + // If after slicing the contents in two there are fewer words in the second part than + // the value set by widow, we won't bother hiding/collapsing anything. + widow: 4, + + // text displayed in a link instead of the hidden part of the element. + // clicking this will expand/show the hidden/collapsed text + expandText: 'read more', + expandPrefix: '… ', + + expandAfterSummary: false, + + // class names for summary element and detail element + summaryClass: 'summary', + detailClass: 'details', + + // class names for around "read-more" link and "read-less" link + moreClass: 'read-more', + lessClass: 'read-less', + + // number of milliseconds after text has been expanded at which to collapse the text again. + // when 0, no auto-collapsing + collapseTimer: 0, + + // effects for expanding and collapsing + expandEffect: 'fadeIn', + expandSpeed: 250, + collapseEffect: 'fadeOut', + collapseSpeed: 200, + + // allow the user to re-collapse the expanded text. + userCollapse: true, + + // text to use for the link to re-collapse the text + userCollapseText: 'read less', + userCollapsePrefix: ' ', + + + // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called + + onSlice: null, // function() {} + beforeExpand: null, // function() {}, + afterExpand: null, // function() {}, + onCollapse: null // function(byUser) {} + } + }; + + $.fn.expander = function(options) { + var meth = 'init'; + + if (typeof options == 'string') { + meth = options; + options = {}; + } + + var opts = $.extend({}, $.expander.defaults, options), + rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i, + rAmpWordEnd = /(&(?:[^;]+;)?|\w+)$/, + rOpenCloseTag = /<\/?(\w+)[^>]*>/g, + rOpenTag = /<(\w+)[^>]*>/g, + rCloseTag = /<\/(\w+)>/g, + rLastCloseTag = /(<\/[^>]+>)\s*$/, + rTagPlus = /^<[^>]+>.?/, + delayedCollapse; + + var methods = { + init: function() { + this.each(function() { + var i, l, tmp, summTagLess, summOpens, summCloses, lastCloseTag, detailText, + $thisDetails, $readMore, + openTagsForDetails = [], + closeTagsForsummaryText = [], + defined = {}, + thisEl = this, + $this = $(this), + $summEl = $([]), + o = $.meta ? $.extend({}, opts, $this.data()) : opts, + hasDetails = !!$this.find('.' + o.detailClass).length, + hasBlocks = !!$this.find('*').filter(function() { + var display = $(this).css('display'); + return (/^block|table|list/).test(display); + }).length, + el = hasBlocks ? 'div' : 'span', + detailSelector = el + '.' + o.detailClass, + moreSelector = 'span.' + o.moreClass, + expandSpeed = o.expandSpeed || 0, + allHtml = $.trim( $this.html() ), + allText = $.trim( $this.text() ), + summaryText = allHtml.slice(0, o.slicePoint); + + // bail out if we've already set up the expander on this element + if ( $.data(this, 'expander') ) { + return; + } + $.data(this, 'expander', true); + + // determine which callback functions are defined + $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) { + defined[val] = $.isFunction(o[val]); + }); + + // back up if we're in the middle of a tag or word + summaryText = backup(summaryText); + + // summary text sans tags length + summTagless = summaryText.replace(rOpenCloseTag, '').length; + + // add more characters to the summary, one for each character in the tags + while (summTagless < o.slicePoint) { + newChar = allHtml.charAt(summaryText.length); + if (newChar == '<') { + newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0]; + } + summaryText += newChar; + summTagless++; + } + + summaryText = backup(summaryText, o.preserveWords); + + // separate open tags from close tags and clean up the lists + summOpens = summaryText.match(rOpenTag) || []; + summCloses = summaryText.match(rCloseTag) || []; + + // filter out self-closing tags + tmp = []; + $.each(summOpens, function(index, val) { + if ( !rSelfClose.test(val) ) { + tmp.push(val); + } + }); + summOpens = tmp; + + // strip close tags to just the tag name + l = summCloses.length; + for (i = 0; i < l; i++) { + summCloses[i] = summCloses[i].replace(rCloseTag, '$1'); + } + + // tags that start in summary and end in detail need: + // a). close tag at end of summary + // b). open tag at beginning of detail + $.each(summOpens, function(index, val) { + var thisTagName = val.replace(rOpenTag, '$1'); + var closePosition = $.inArray(thisTagName, summCloses); + if (closePosition === -1) { + openTagsForDetails.push(val); + closeTagsForsummaryText.push(''); + + } else { + summCloses.splice(closePosition, 1); + } + }); + + // reverse the order of the close tags for the summary so they line up right + closeTagsForsummaryText.reverse(); + + // create necessary summary and detail elements if they don't already exist + if ( !hasDetails ) { + + // end script if there is no detail text or if detail has fewer words than widow option + detailText = allHtml.slice(summaryText.length); + + if ( detailText === '' || detailText.split(/\s+/).length < o.widow ) { + return; + } + + // otherwise, continue... + lastCloseTag = closeTagsForsummaryText.pop() || ''; + summaryText += closeTagsForsummaryText.join(''); + detailText = openTagsForDetails.join('') + detailText; + + } else { + // assume that even if there are details, we still need readMore/readLess/summary elements + // (we already bailed out earlier when readMore el was found) + // but we need to create els differently + + // remove the detail from the rest of the content + detailText = $this.find(detailSelector).remove().html(); + + // The summary is what's left + summaryText = $this.html(); + + // allHtml is the summary and detail combined (this is needed when content has block-level elements) + allHtml = summaryText + detailText; + + lastCloseTag = ''; + } + o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o); + + if (hasBlocks) { + detailText = allHtml; + } + summaryText += lastCloseTag; + + // onSlice callback + o.summary = summaryText; + o.details = detailText; + o.lastCloseTag = lastCloseTag; + + if (defined.onSlice) { + // user can choose to return a modified options object + // one last chance for user to change the options. sneaky, huh? + // but could be tricky so use at your own risk. + tmp = o.onSlice.call(thisEl, o); + + // so, if the returned value from the onSlice function is an object with a details property, we'll use that! + o = tmp && tmp.details ? tmp : o; + } + + // build the html with summary and detail and use it to replace old contents + var html = buildHTML(o, hasBlocks); + + $this.html( html ); + + // set up details and summary for expanding/collapsing + $thisDetails = $this.find(detailSelector); + $readMore = $this.find(moreSelector); + $thisDetails.hide(); + $readMore.find('a').unbind('click.expander').bind('click.expander', expand); + + $summEl = $this.find('div.' + o.summaryClass); + + if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) { + $this + .find(detailSelector) + .append('' + o.userCollapsePrefix + '' + o.userCollapseText + ''); + } + + $this + .find('span.' + o.lessClass + ' a') + .unbind('click.expander') + .bind('click.expander', function(event) { + event.preventDefault(); + clearTimeout(delayedCollapse); + var $detailsCollapsed = $(this).closest(detailSelector); + reCollapse(o, $detailsCollapsed); + if (defined.onCollapse) { + o.onCollapse.call(thisEl, true); + } + }); + + function expand(event) { + event.preventDefault(); + $readMore.hide(); + $summEl.hide(); + if (defined.beforeExpand) { + o.beforeExpand.call(thisEl); + } + + $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() { + $thisDetails.css({zoom: ''}); + if (defined.afterExpand) {o.afterExpand.call(thisEl);} + delayCollapse(o, $thisDetails, thisEl); + }); + } + + }); // this.each + }, + destroy: function() { + if ( !this.data('expander') ) { + return; + } + this.removeData('expander'); + this.each(function() { + var $this = $(this), + o = $.meta ? $.extend({}, opts, $this.data()) : opts, + details = $this.find('.' + o.detailClass).contents(); + + $this.find('.' + o.moreClass).remove(); + $this.find('.' + o.summaryClass).remove(); + $this.find('.' + o.detailClass).after(details).remove(); + $this.find('.' + o.lessClass).remove(); + + }); + } + }; + + // run the methods (almost always "init") + if ( methods[meth] ) { + methods[ meth ].call(this); + } + + // utility functions + function buildHTML(o, blocks) { + var el = 'span', + summary = o.summary; + if ( blocks ) { + el = 'div'; + // if summary ends with a close tag, tuck the moreLabel inside it + if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) { + summary = summary.replace(rLastCloseTag, o.moreLabel + '$1'); + } else { + // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary + // fixes #19 + summary += o.moreLabel; + } + + // and wrap it in a div + summary = '
' + summary + '
'; + } else { + summary += o.moreLabel; + } + + return [ + summary, + '<', + el + ' class="' + o.detailClass + '"', + '>', + o.details, + '' + ].join(''); + } + + function buildMoreLabel(o) { + var ret = '' + o.expandPrefix; + ret += '' + o.expandText + ''; + return ret; + } + + function backup(txt, preserveWords) { + if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) { + txt = txt.slice( 0, txt.lastIndexOf('<') ); + } + if (preserveWords) { + txt = txt.replace(rAmpWordEnd,''); + } + return txt; + } + + function reCollapse(o, el) { + el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function() { + var prevMore = el.prev('span.' + o.moreClass).show(); + if (!prevMore.length) { + el.parent().children('div.' + o.summaryClass).show() + .find('span.' + o.moreClass).show(); + } + }); + } + + function delayCollapse(option, $collapseEl, thisEl) { + if (option.collapseTimer) { + delayedCollapse = setTimeout(function() { + reCollapse(option, $collapseEl); + if ( $.isFunction(option.onCollapse) ) { + option.onCollapse.call(thisEl, false); + } + }, option.collapseTimer); + } + } + + return this; + }; + + // plugin defaults + $.fn.expander.defaults = $.expander.defaults; +})(jQuery); diff --git a/hyperkitty/static/js/libs/jquery-1.7.1.min.js b/hyperkitty/static/js/libs/jquery-1.7.1.min.js new file mode 100644 index 0000000..198b3ff --- /dev/null +++ b/hyperkitty/static/js/libs/jquery-1.7.1.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.1 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; +f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() +{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/hyperkitty/static/protovis-d3.1.js b/hyperkitty/static/protovis-d3.1.js new file mode 100644 index 0000000..af56eac --- /dev/null +++ b/hyperkitty/static/protovis-d3.1.js @@ -0,0 +1,7725 @@ +/** + * @class The built-in Array class. + * @name Array + */ + +if (!Array.prototype.map) { + /** + * Creates a new array with the results of calling a provided function on + * every element in this array. Implemented in Javascript 1.6. + * + * @see map + * documentation. + * @param {function} f function that produces an element of the new Array from + * an element of the current one. + * @param [o] object to use as this when executing f. + */ + Array.prototype.map = function(f, o) { + var n = this.length; + var result = new Array(n); + for (var i = 0; i < n; i++) { + if (i in this) { + result[i] = f.call(o, this[i], i, this); + } + } + return result; + }; +} + +if (!Array.prototype.filter) { + /** + * Creates a new array with all elements that pass the test implemented by the + * provided function. Implemented in Javascript 1.6. + * + * @see filter + * documentation. + * @param {function} f function to test each element of the array. + * @param [o] object to use as this when executing f. + */ + Array.prototype.filter = function(f, o) { + var n = this.length; + var result = new Array(); + for (var i = 0; i < n; i++) { + if (i in this) { + var v = this[i]; + if (f.call(o, v, i, this)) result.push(v); + } + } + return result; + }; +} + +if (!Array.prototype.forEach) { + /** + * Executes a provided function once per array element. Implemented in + * Javascript 1.6. + * + * @see forEach + * documentation. + * @param {function} f function to execute for each element. + * @param [o] object to use as this when executing f. + */ + Array.prototype.forEach = function(f, o) { + var n = this.length >>> 0; + for (var i = 0; i < n; i++) { + if (i in this) f.call(o, this[i], i, this); + } + }; +} + +if (!Array.prototype.reduce) { + /** + * Apply a function against an accumulator and each value of the array (from + * left-to-right) as to reduce it to a single value. Implemented in Javascript + * 1.8. + * + * @see reduce + * documentation. + * @param {function} f function to execute on each value in the array. + * @param [v] object to use as the first argument to the first call of + * t. + */ + Array.prototype.reduce = function(f, v) { + var len = this.length; + if (!len && (arguments.length == 1)) { + throw new Error("reduce: empty array, no initial value"); + } + + var i = 0; + if (arguments.length < 2) { + while (true) { + if (i in this) { + v = this[i++]; + break; + } + if (++i >= len) { + throw new Error("reduce: no values, no initial value"); + } + } + } + + for (; i < len; i++) { + if (i in this) { + v = f(v, this[i], i, this); + } + } + return v; + }; +} +/** + * @class The built-in Date class. + * @name Date + */ + +Date.__parse__ = Date.parse; + +/** + * Parses a date from a string, optionally using the specified formatting. If + * only a single argument is specified (i.e., format is not specified), + * this method invokes the native implementation to guarantee + * backwards-compatibility. + * + *

The format string is in the same format expected by the strptime + * function in C. The following conversion specifications are supported:

    + * + *
  • %b - abbreviated month names.
  • + *
  • %B - full month names.
  • + *
  • %h - same as %b.
  • + *
  • %d - day of month [1,31].
  • + *
  • %e - same as %d.
  • + *
  • %H - hour (24-hour clock) [0,23].
  • + *
  • %m - month number [1,12].
  • + *
  • %M - minute [0,59].
  • + *
  • %S - second [0,61].
  • + *
  • %y - year with century [0,99].
  • + *
  • %Y - year including century.
  • + *
  • %% - %.
  • + * + *
The following conversion specifications are unsupported (for now):
    + * + *
  • %a - day of week, either abbreviated or full name.
  • + *
  • %A - same as %a.
  • + *
  • %c - locale's appropriate date and time.
  • + *
  • %C - century number.
  • + *
  • %D - same as %m/%d/%y.
  • + *
  • %I - hour (12-hour clock) [1,12].
  • + *
  • %j - day number [1,366].
  • + *
  • %n - any white space.
  • + *
  • %p - locale's equivalent of a.m. or p.m.
  • + *
  • %r - same as %I:%M:%S %p.
  • + *
  • %R - same as %H:%M.
  • + *
  • %t - same as %n.
  • + *
  • %T - same as %H:%M:%S.
  • + *
  • %U - week number [0,53].
  • + *
  • %w - weekday [0,6].
  • + *
  • %W - week number [0,53].
  • + *
  • %x - locale's equivalent to %m/%d/%y.
  • + *
  • %X - locale's equivalent to %I:%M:%S %p.
  • + * + *
+ * + * @see strptime + * documentation. + * @param {string} s the string to parse as a date. + * @param {string} [format] an optional format string. + * @returns {Date} the parsed date. + */ +Date.parse = function(s, format) { + if (arguments.length == 1) { + return Date.__parse__(s); + } + + var year = 1970, month = 0, date = 1, hour = 0, minute = 0, second = 0; + var fields = [function() {}]; + format = format.replace(/[\\\^\$\*\+\?\[\]\(\)\.\{\}]/g, "\\$&"); + format = format.replace(/%[a-zA-Z0-9]/g, function(s) { + switch (s) { + // TODO %a: day of week, either abbreviated or full name + // TODO %A: same as %a + case '%b': { + fields.push(function(x) { month = { + Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, + Sep: 8, Oct: 9, Nov: 10, Dec: 11 + }[x]; }); + return "([A-Za-z]+)"; + } + case '%h': + case '%B': { + fields.push(function(x) { month = { + January: 0, February: 1, March: 2, April: 3, May: 4, June: 5, + July: 6, August: 7, September: 8, October: 9, November: 10, + December: 11 + }[x]; }); + return "([A-Za-z]+)"; + } + // TODO %c: locale's appropriate date and time + // TODO %C: century number[0,99] + case '%e': + case '%d': { + fields.push(function(x) { date = x; }); + return "([0-9]+)"; + } + // TODO %D: same as %m/%d/%y + case '%H': { + fields.push(function(x) { hour = x; }); + return "([0-9]+)"; + } + // TODO %I: hour (12-hour clock) [1,12] + // TODO %j: day number [1,366] + case '%m': { + fields.push(function(x) { month = x - 1; }); + return "([0-9]+)"; + } + case '%M': { + fields.push(function(x) { minute = x; }); + return "([0-9]+)"; + } + // TODO %n: any white space + // TODO %p: locale's equivalent of a.m. or p.m. + // TODO %r: %I:%M:%S %p + // TODO %R: %H:%M + case '%S': { + fields.push(function(x) { second = x; }); + return "([0-9]+)"; + } + // TODO %t: any white space + // TODO %T: %H:%M:%S + // TODO %U: week number [00,53] + // TODO %w: weekday [0,6] + // TODO %W: week number [00, 53] + // TODO %x: locale date (%m/%d/%y) + // TODO %X: locale time (%I:%M:%S %p) + case '%y': { + fields.push(function(x) { + x = Number(x); + year = x + (((0 <= x) && (x < 69)) ? 2000 + : (((x >= 69) && (x < 100) ? 1900 : 0))); + }); + return "([0-9]+)"; + } + case '%Y': { + fields.push(function(x) { year = x; }); + return "([0-9]+)"; + } + case '%%': { + fields.push(function() {}); + return "%"; + } + } + return s; + }); + + var match = s.match(format); + if (match) match.forEach(function(m, i) { fields[i](m); }); + return new Date(year, month, date, hour, minute, second); +}; + +if (Date.prototype.toLocaleFormat) { + Date.prototype.format = Date.prototype.toLocaleFormat; +} else { + +/** + * Converts a date to a string using the specified formatting. If the + * Date object already supports the toLocaleFormat method, as + * in Firefox, this is simply an alias to the built-in method. + * + *

The format string is in the same format expected by the strftime + * function in C. The following conversion specifications are supported:

    + * + *
  • %a - abbreviated weekday name.
  • + *
  • %A - full weekday name.
  • + *
  • %b - abbreviated month names.
  • + *
  • %B - full month names.
  • + *
  • %c - locale's appropriate date and time.
  • + *
  • %C - century number.
  • + *
  • %d - day of month [01,31] (zero padded).
  • + *
  • %D - same as %m/%d/%y.
  • + *
  • %e - day of month [ 1,31] (space padded).
  • + *
  • %h - same as %b.
  • + *
  • %H - hour (24-hour clock) [00,23] (zero padded).
  • + *
  • %I - hour (12-hour clock) [01,12] (zero padded).
  • + *
  • %m - month number [01,12] (zero padded).
  • + *
  • %M - minute [0,59] (zero padded).
  • + *
  • %n - newline character.
  • + *
  • %p - locale's equivalent of a.m. or p.m.
  • + *
  • %r - same as %I:%M:%S %p.
  • + *
  • %R - same as %H:%M.
  • + *
  • %S - second [00,61] (zero padded).
  • + *
  • %t - tab character.
  • + *
  • %T - same as %H:%M:%S.
  • + *
  • %x - same as %m/%d/%y.
  • + *
  • %X - same as %I:%M:%S %p.
  • + *
  • %y - year with century [00,99] (zero padded).
  • + *
  • %Y - year including century.
  • + *
  • %% - %.
  • + * + *
The following conversion specifications are unsupported (for now):
    + * + *
  • %j - day number [1,366].
  • + *
  • %u - weekday number [1,7].
  • + *
  • %U - week number [00,53].
  • + *
  • %V - week number [01,53].
  • + *
  • %w - weekday number [0,6].
  • + *
  • %W - week number [00,53].
  • + *
  • %Z - timezone name or abbreviation.
  • + * + *
+ * + * @see Date.toLocaleFormat + * documentation. + * @see strftime + * documentation. + * @param {string} format a format string. + * @returns {string} the formatted date. + */ +Date.prototype.format = function(format) { + function pad(n, p) { return (n < 10) ? (p || "0") + n : n; } + var d = this; + return format.replace(/%[a-zA-Z0-9]/g, function(s) { + switch (s) { + case '%a': return [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + ][d.getDay()]; + case '%A': return [ + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", + "Saturday" + ][d.getDay()]; + case '%h': + case '%b': return [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", + "Oct", "Nov", "Dec" + ][d.getMonth()]; + case '%B': return [ + "January", "February", "March", "April", "May", "June", "July", + "August", "September", "October", "November", "December" + ][d.getMonth()]; + case '%c': return d.toLocaleString(); + case '%C': return pad(Math.floor(d.getFullYear() / 100) % 100); + case '%d': return pad(d.getDate()); + case '%x': + case '%D': return pad(d.getMonth() + 1) + + "/" + pad(d.getDate()) + + "/" + pad(d.getFullYear() % 100); + case '%e': return pad(d.getDate(), " "); + case '%H': return pad(d.getHours()); + case '%I': { + var h = d.getHours() % 12; + return h ? pad(h) : 12; + } + // TODO %j: day of year as a decimal number [001,366] + case '%m': return pad(d.getMonth() + 1); + case '%M': return pad(d.getMinutes()); + case '%n': return "\n"; + case '%p': return d.getHours() < 12 ? "AM" : "PM"; + case '%T': + case '%X': + case '%r': { + var h = d.getHours() % 12; + return (h ? pad(h) : 12) + + ":" + pad(d.getMinutes()) + + ":" + pad(d.getSeconds()) + + " " + (d.getHours() < 12 ? "AM" : "PM"); + } + case '%R': return pad(d.getHours()) + ":" + pad(d.getMinutes()); + case '%S': return pad(d.getSeconds()); + case '%t': return "\t"; + case '%u': { + var w = d.getDay(); + return w ? w : 1; + } + // TODO %U: week number (sunday first day) [00,53] + // TODO %V: week number (monday first day) [01,53] ... with weirdness + case '%w': return d.getDay(); + // TODO %W: week number (monday first day) [00,53] ... with weirdness + case '%y': return pad(d.getFullYear() % 100); + case '%Y': return d.getFullYear(); + // TODO %Z: timezone name or abbreviation + case '%%': return "%"; + } + return s; + }); + }; +} +var pv = function() {/** + * The top-level Protovis namespace. All public methods and fields should be + * registered on this object. Note that core Protovis source is surrounded by an + * anonymous function, so any other declared globals will not be visible outside + * of core methods. This also allows multiple versions of Protovis to coexist, + * since each version will see their own pv namespace. + * + * @namespace The top-level Protovis namespace, pv. + */ +var pv = {}; + +/** + * @private Returns a prototype object suitable for extending the given class + * f. Rather than constructing a new instance of f to serve as + * the prototype (which unnecessarily runs the constructor on the created + * prototype object, potentially polluting it), an anonymous function is + * generated internally that shares the same prototype: + * + *
function g() {}
+ * g.prototype = f.prototype;
+ * return new g();
+ * + * For more details, see Douglas Crockford's essay on prototypal inheritance. + * + * @param {function} f a constructor. + * @returns a suitable prototype object. + * @see Douglas Crockford's essay on prototypal + * inheritance. + */ +pv.extend = function(f) { + function g() {} + g.prototype = f.prototype || f; + return new g(); +}; + +try { + eval("pv.parse = function(x) x;"); // native support +} catch (e) { + +/** + * @private Parses a Protovis specification, which may use JavaScript 1.8 + * function expresses, replacing those function expressions with proper + * functions such that the code can be run by a JavaScript 1.6 interpreter. This + * hack only supports function expressions (using clumsy regular expressions, no + * less), and not other JavaScript 1.8 features such as let expressions. + * + * @param {string} s a Protovis specification (i.e., a string of JavaScript 1.8 + * source code). + * @returns {string} a conformant JavaScript 1.6 source code. + */ + pv.parse = function(js) { // hacky regex support + var re = new RegExp("function(\\s+\\w+)?\\([^)]*\\)\\s*", "mg"), m, d, i = 0, s = ""; + while (m = re.exec(js)) { + var j = m.index + m[0].length; + if (js.charAt(j--) != '{') { + s += js.substring(i, j) + "{return "; + i = j; + for (var p = 0; p >= 0 && j < js.length; j++) { + var c = js.charAt(j); + switch (c) { + case '"': case '\'': { + while (++j < js.length && (d = js.charAt(j)) != c) { + if (d == '\\') j++; + } + break; + } + case '[': case '(': p++; break; + case ']': case ')': p--; break; + case ';': + case ',': if (p == 0) p--; break; + } + } + s += pv.parse(js.substring(i, --j)) + ";}"; + i = j; + } + re.lastIndex = j; + } + s += js.substring(i); + return s; + }; +} + +/** + * Returns the passed-in argument, x; the identity function. This method + * is provided for convenience since it is used as the default behavior for a + * number of property functions. + * + * @param x a value. + * @returns the value x. + */ +pv.identity = function(x) { return x; }; + +/** + * Returns this.index. This method is provided for convenience for use + * with scales. For example, to color bars by their index, say: + * + *
.fillStyle(pv.Colors.category10().by(pv.index))
+ * + * This method is equivalent to function() this.index, but more + * succinct. Note that the index property is also supported for + * accessor functions with {@link pv.max}, {@link pv.min} and other array + * utility methods. + * + * @see pv.Scale + * @see pv.Mark#index + */ +pv.index = function() { return this.index; }; + +/** + * Returns this.childIndex. This method is provided for convenience for + * use with scales. For example, to color bars by their child index, say: + * + *
.fillStyle(pv.Colors.category10().by(pv.child))
+ * + * This method is equivalent to function() this.childIndex, but more + * succinct. + * + * @see pv.Scale + * @see pv.Mark#childIndex + */ +pv.child = function() { return this.childIndex; }; + +/** + * Returns this.parent.index. This method is provided for convenience + * for use with scales. This method is provided for convenience for use with + * scales. For example, to color bars by their parent index, say: + * + *
.fillStyle(pv.Colors.category10().by(pv.parent))
+ * + * Tthis method is equivalent to function() this.parent.index, but more + * succinct. + * + * @see pv.Scale + * @see pv.Mark#index + */ +pv.parent = function() { return this.parent.index; }; + +/** + * Returns an array of numbers, starting at start, incrementing by + * step, until stop is reached. The stop value is exclusive. If + * only a single argument is specified, this value is interpeted as the + * stop value, with the start value as zero. If only two arguments + * are specified, the step value is implied to be one. + * + *

The method is modeled after the built-in range method from + * Python. See the Python documentation for more details. + * + * @see Python range + * @param {number} [start] the start value. + * @param {number} stop the stop value. + * @param {number} [step] the step value. + * @returns {number[]} an array of numbers. + */ +pv.range = function(start, stop, step) { + if (arguments.length == 1) { + stop = start; + start = 0; + } + if (step == undefined) step = 1; + else if (!step) throw new Error("step must be non-zero"); + var array = [], i = 0, j; + if (step < 0) { + while ((j = start + step * i++) > stop) { + array.push(j); + } + } else { + while ((j = start + step * i++) < stop) { + array.push(j); + } + } + return array; +}; + +/** + * Returns a random number in the range [min, max) that is a + * multiple of step. More specifically, the returned number is of the + * form min + n * step, where n is a nonnegative + * integer. If step is not specified, it defaults to 1, returning a + * random integer if min is also an integer. + * + * @param min {number} minimum value. + * @param [max] {number} maximum value. + * @param [step] {numbeR} step value. + */ +pv.random = function(min, max, step) { + if (arguments.length == 1) { + max = min; + min = 0; + } + if (step == undefined) { + step = 1; + } + return step + ? (Math.floor(Math.random() * (max - min) / step) * step + min) + : (Math.random() * (max - min) + min); +}; + +/** + * Concatenates the specified array with itself n times. For example, + * pv.repeat([1, 2]) returns [1, 2, 1, 2]. + * + * @param {array} a an array. + * @param {number} [n] the number of times to repeat; defaults to two. + * @returns {array} an array that repeats the specified array. + */ +pv.repeat = function(array, n) { + if (arguments.length == 1) n = 2; + return pv.blend(pv.range(n).map(function() { return array; })); +}; + +/** + * Given two arrays a and b, returns an array of all possible + * pairs of elements [ai, bj]. The outer loop is on array + * a, while the inner loop is on b, such that the order of + * returned elements is [a0, b0], [a0, + * b1], ... [a0, bm], [a1, + * b0], [a1, b1], ... [a1, + * bm], ... [an, bm]. If either array is empty, + * an empty array is returned. + * + * @param {array} a an array. + * @param {array} b an array. + * @returns {array} an array of pairs of elements in a and b. + */ +pv.cross = function(a, b) { + var array = []; + for (var i = 0, n = a.length, m = b.length; i < n; i++) { + for (var j = 0, x = a[i]; j < m; j++) { + array.push([x, b[j]]); + } + } + return array; +}; + +/** + * Given the specified array of arrays, concatenates the arrays into a single + * array. If the individual arrays are explicitly known, an alternative to blend + * is to use JavaScript's concat method directly. These two equivalent + * expressions:

    + * + *
  • pv.blend([[1, 2, 3], ["a", "b", "c"]]) + *
  • [1, 2, 3].concat(["a", "b", "c"]) + * + *
return [1, 2, 3, "a", "b", "c"]. + * + * @param {array[]} arrays an array of arrays. + * @returns {array} an array containing all the elements of each array in + * arrays. + */ +pv.blend = function(arrays) { + return Array.prototype.concat.apply([], arrays); +}; + +/** + * Given the specified array of arrays, transposes each element + * arrayij with arrayji. If the array has dimensions + * n×m, it will have dimensions m×n + * after this method returns. This method transposes the elements of the array + * in place, mutating the array, and returning a reference to the array. + * + * @param {array[]} arrays an array of arrays. + * @returns {array[]} the passed-in array, after transposing the elements. + */ +pv.transpose = function(arrays) { + var n = arrays.length, m = pv.max(arrays, function(d) { return d.length; }); + + if (m > n) { + arrays.length = m; + for (var i = n; i < m; i++) { + arrays[i] = new Array(n); + } + for (var i = 0; i < n; i++) { + for (var j = i + 1; j < m; j++) { + var t = arrays[i][j]; + arrays[i][j] = arrays[j][i]; + arrays[j][i] = t; + } + } + } else { + for (var i = 0; i < m; i++) { + arrays[i].length = n; + } + for (var i = 0; i < n; i++) { + for (var j = 0; j < i; j++) { + var t = arrays[i][j]; + arrays[i][j] = arrays[j][i]; + arrays[j][i] = t; + } + } + } + + arrays.length = m; + for (var i = 0; i < m; i++) { + arrays[i].length = n; + } + + return arrays; +}; + +/** + * Returns all of the property names (keys) of the specified object (a map). The + * order of the returned array is not defined. + * + * @param map an object. + * @returns {string[]} an array of strings corresponding to the keys. + * @see #entries + */ +pv.keys = function(map) { + var array = []; + for (var key in map) { + array.push(key); + } + return array; +}; + +/** + * Returns all of the entries (key-value pairs) of the specified object (a + * map). The order of the returned array is not defined. Each key-value pair is + * represented as an object with key and value attributes, + * e.g., {key: "foo", value: 42}. + * + * @param map an object. + * @returns {array} an array of key-value pairs corresponding to the keys. + */ +pv.entries = function(map) { + var array = []; + for (var key in map) { + array.push({ key: key, value: map[key] }); + } + return array; +}; + +/** + * Returns all of the values (attribute values) of the specified object (a + * map). The order of the returned array is not defined. + * + * @param map an object. + * @returns {array} an array of objects corresponding to the values. + * @see #entries + */ +pv.values = function(map) { + var array = []; + for (var key in map) { + array.push(map[key]); + } + return array; +}; + +/** + * @private A private variant of Array.prototype.map that supports the index + * property. + */ +function map(array, f) { + var o = {}; + return f + ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) + : array.slice(); +}; + +/** + * Returns a normalized copy of the specified array, such that the sum of the + * returned elements sum to one. If the specified array is not an array of + * numbers, an optional accessor function f can be specified to map the + * elements to numbers. For example, if array is an array of objects, + * and each object has a numeric property "foo", the expression + * + *
pv.normalize(array, function(d) d.foo)
+ * + * returns a normalized array on the "foo" property. If an accessor function is + * not specified, the identity function is used. Accessor functions can refer to + * this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number[]} an array of numbers that sums to one. + */ +pv.normalize = function(array, f) { + var norm = map(array, f), sum = pv.sum(norm); + for (var i = 0; i < norm.length; i++) norm[i] /= sum; + return norm; +}; + +/** + * Returns the sum of the specified array. If the specified array is not an + * array of numbers, an optional accessor function f can be specified + * to map the elements to numbers. See {@link #normalize} for an example. + * Accessor functions can refer to this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the sum of the specified array. + */ +pv.sum = function(array, f) { + var o = {}; + return array.reduce(f + ? function(p, d, i) { o.index = i; return p + f.call(o, d); } + : function(p, d) { return p + d; }, 0); +}; + +/** + * Returns the maximum value of the specified array. If the specified array is + * not an array of numbers, an optional accessor function f can be + * specified to map the elements to numbers. See {@link #normalize} for an + * example. Accessor functions can refer to this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the maximum value of the specified array. + */ +pv.max = function(array, f) { + if (f == pv.index) return array.length - 1; + return Math.max.apply(null, f ? map(array, f) : array); +}; + +/** + * Returns the index of the maximum value of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * f can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the index of the maximum value of the specified array. + */ +pv.max.index = function(array, f) { + if (f == pv.index) return array.length - 1; + if (!f) f = pv.identity; + var maxi = -1, maxx = -Infinity, o = {}; + for (var i = 0; i < array.length; i++) { + o.index = i; + var x = f.call(o, array[i]); + if (x > maxx) { + maxx = x; + maxi = i; + } + } + return maxi; +} + +/** + * Returns the minimum value of the specified array of numbers. If the specified + * array is not an array of numbers, an optional accessor function f + * can be specified to map the elements to numbers. See {@link #normalize} for + * an example. Accessor functions can refer to this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the minimum value of the specified array. + */ +pv.min = function(array, f) { + if (f == pv.index) return 0; + return Math.min.apply(null, f ? map(array, f) : array); +}; + +/** + * Returns the index of the minimum value of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * f can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the index of the minimum value of the specified array. + */ +pv.min.index = function(array, f) { + if (f == pv.index) return 0; + if (!f) f = pv.identity; + var mini = -1, minx = Infinity, o = {}; + for (var i = 0; i < array.length; i++) { + o.index = i; + var x = f.call(o, array[i]); + if (x < minx) { + minx = x; + mini = i; + } + } + return mini; +} + +/** + * Returns the arithmetic mean, or average, of the specified array. If the + * specified array is not an array of numbers, an optional accessor function + * f can be specified to map the elements to numbers. See + * {@link #normalize} for an example. Accessor functions can refer to + * this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the mean of the specified array. + */ +pv.mean = function(array, f) { + return pv.sum(array, f) / array.length; +}; + +/** + * Returns the median of the specified array. If the specified array is not an + * array of numbers, an optional accessor function f can be specified + * to map the elements to numbers. See {@link #normalize} for an example. + * Accessor functions can refer to this.index. + * + * @param {array} array an array of objects, or numbers. + * @param {function} [f] an optional accessor function. + * @returns {number} the median of the specified array. + */ +pv.median = function(array, f) { + if (f == pv.index) return (array.length - 1) / 2; + array = map(array, f).sort(pv.naturalOrder); + if (array.length % 2) return array[Math.floor(array.length / 2)]; + var i = array.length / 2; + return (array[i - 1] + array[i]) / 2; +}; + +/** + * Returns a map constructed from the specified keys, using the + * function f to compute the value for each key. The single argument to + * the value function is the key. The callback is invoked only for indexes of + * the array which have assigned values; it is not invoked for indexes which + * have been deleted or which have never been assigned values. + * + *

For example, this expression creates a map from strings to string length: + * + *

pv.dict(["one", "three", "seventeen"], function(s) s.length)
+ * + * The returned value is {one: 3, three: 5, seventeen: 9}. Accessor + * functions can refer to this.index. + * + * @param {array} keys an array. + * @param {function} f a value function. + * @returns a map from keys to values. + */ +pv.dict = function(keys, f) { + var m = {}, o = {}; + for (var i = 0; i < keys.length; i++) { + if (i in keys) { + var k = keys[i]; + o.index = i; + m[k] = f.call(o, k); + } + } + return m; +}; + +/** + * Returns a permutation of the specified array, using the specified array of + * indexes. The returned array contains the corresponding element in + * array for each index in indexes, in order. For example, + * + *
pv.permute(["a", "b", "c"], [1, 2, 0])
+ * + * returns ["b", "c", "a"]. It is acceptable for the array of indexes + * to be a different length from the array of elements, and for indexes to be + * duplicated or omitted. The optional accessor function f can be used + * to perform a simultaneous mapping of the array elements. Accessor functions + * can refer to this.index. + * + * @param {array} array an array. + * @param {number[]} indexes an array of indexes into array. + * @param {function} [f] an optional accessor function. + * @returns {array} an array of elements from array; a permutation. + */ +pv.permute = function(array, indexes, f) { + if (!f) f = pv.identity; + var p = new Array(indexes.length), o = {}; + indexes.forEach(function(j, i) { o.index = j; p[i] = f.call(o, array[j]); }); + return p; +}; + +/** + * Returns a map from key to index for the specified keys array. For + * example, + * + *
pv.numerate(["a", "b", "c"])
+ * + * returns {a: 0, b: 1, c: 2}. Note that since JavaScript maps only + * support string keys, keys must contain strings, or other values that + * naturally map to distinct string values. Alternatively, an optional accessor + * function f can be specified to compute the string key for the given + * element. Accessor functions can refer to this.index. + * + * @param {array} keys an array, usually of string keys. + * @param {function} [f] an optional key function. + * @returns a map from key to index. + */ +pv.numerate = function(keys, f) { + if (!f) f = pv.identity; + var map = {}, o = {}; + keys.forEach(function(x, i) { o.index = i; map[f.call(o, x)] = i; }); + return map; +}; + +/** + * The comparator function for natural order. This can be used in conjunction with + * the built-in array sort method to sort elements by their natural + * order, ascending. Note that if no comparator function is specified to the + * built-in sort method, the default order is lexicographic, not + * natural! + * + * @see Array.sort. + * @param a an element to compare. + * @param b an element to compare. + * @returns {number} negative if a < b; positive if a > b; otherwise 0. + */ +pv.naturalOrder = function(a, b) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + +/** + * The comparator function for reverse natural order. This can be used in + * conjunction with the built-in array sort method to sort elements by + * their natural order, descending. Note that if no comparator function is + * specified to the built-in sort method, the default order is + * lexicographic, not natural! + * + * @see #naturalOrder + * @param a an element to compare. + * @param b an element to compare. + * @returns {number} negative if a < b; positive if a > b; otherwise 0. + */ +pv.reverseOrder = function(b, a) { + return (a < b) ? -1 : ((a > b) ? 1 : 0); +}; + +/** + * @private Computes the value of the specified CSS property p on the + * specified element e. + * + * @param {string} p the name of the CSS property. + * @param e the element on which to compute the CSS property. + */ +pv.css = function(e, p) { + return window.getComputedStyle + ? window.getComputedStyle(e, null).getPropertyValue(p) + : e.currentStyle[p]; +}; + +/** + * Namespace constants for SVG, XMLNS, and XLINK. + * + * @namespace Namespace constants for SVG, XMLNS, and XLINK. + */ +pv.ns = { + /** + * The SVG namespace, "http://www.w3.org/2000/svg". + * + * @type string + * @constant + */ + svg: "http://www.w3.org/2000/svg", + + /** + * The XMLNS namespace, "http://www.w3.org/2000/xmlns". + * + * @type string + * @constant + */ + xmlns: "http://www.w3.org/2000/xmlns", + + /** + * The XLINK namespace, "http://www.w3.org/1999/xlink". + * + * @type string + * @constant + */ + xlink: "http://www.w3.org/1999/xlink" +}; + +/** + * Protovis major and minor version numbers. + * + * @namespace Protovis major and minor version numbers. + */ +pv.version = { + /** + * The major version number. + * + * @type number + * @constant + */ + major: 3, + + /** + * The minor version number. + * + * @type number + * @constant + */ + minor: 1 +}; + +/** + * @private Reports the specified error to the JavaScript console. Mozilla only + * allows logging to the console for privileged code; if the console is + * unavailable, the alert dialog box is used instead. + * + * @param e the exception that triggered the error. + */ +pv.error = function(e) { + (typeof console == "undefined") ? alert(e) : console.error(e); +}; + +/** + * @private Registers the specified listener for events of the specified type on + * the specified target. For standards-compliant browsers, this method uses + * addEventListener; for Internet Explorer, attachEvent. + * + * @param target a DOM element. + * @param {string} type the type of event, such as "click". + * @param {function} the listener callback function. + */ +pv.listen = function(target, type, listener) { + return target.addEventListener + ? target.addEventListener(type, listener, false) + : target.attachEvent("on" + type, listener); +}; + +/** + * Returns the logarithm with a given base value. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the logarithm value. + */ +pv.log = function(x, b) { + return Math.log(x) / Math.log(b); +}; + +/** + * Computes a zero-symmetric logarithm. Computes the logarithm of the absolute + * value of the input, and determines the sign of the output according to the + * sign of the input value. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the symmetric log value. + */ +pv.logSymmetric = function(x, b) { + return (x == 0) ? 0 : ((x < 0) ? -pv.log(-x, b) : pv.log(x, b)); +}; + +/** + * Computes a zero-symmetric logarithm, with adjustment to values between zero + * and the logarithm base. This adjustment introduces distortion for values less + * than the base number, but enables simultaneous plotting of log-transformed + * data involving both positive and negative numbers. + * + * @param {number} x the number for which to compute the logarithm. + * @param {number} b the base of the logarithm. + * @returns {number} the adjusted, symmetric log value. + */ +pv.logAdjusted = function(x, b) { + var negative = x < 0; + if (x < b) x += (b - x) / b; + return negative ? -pv.log(x, b) : pv.log(x, b); +}; + +/** + * Rounds an input value down according to its logarithm. The method takes the + * floor of the logarithm of the value and then uses the resulting value as an + * exponent for the base value. + * + * @param {number} x the number for which to compute the logarithm floor. + * @param {number} b the base of the logarithm. + * @return {number} the rounded-by-logarithm value. + */ +pv.logFloor = function(x, b) { + return (x > 0) + ? Math.pow(b, Math.floor(pv.log(x, b))) + : -Math.pow(b, -Math.floor(-pv.log(-x, b))); +}; + +/** + * Rounds an input value up according to its logarithm. The method takes the + * ceiling of the logarithm of the value and then uses the resulting value as an + * exponent for the base value. + * + * @param {number} x the number for which to compute the logarithm ceiling. + * @param {number} b the base of the logarithm. + * @return {number} the rounded-by-logarithm value. + */ +pv.logCeil = function(x, b) { + return (x > 0) + ? Math.pow(b, Math.ceil(pv.log(x, b))) + : -Math.pow(b, -Math.ceil(-pv.log(-x, b))); +}; + +/** + * Searches the specified array of numbers for the specified value using the + * binary search algorithm. The array must be sorted (as by the sort + * method) prior to making this call. If it is not sorted, the results are + * undefined. If the array contains multiple elements with the specified value, + * there is no guarantee which one will be found. + * + *

The insertion point is defined as the point at which the value + * would be inserted into the array: the index of the first element greater than + * the value, or array.length, if all elements in the array are less + * than the specified value. Note that this guarantees that the return value + * will be nonnegative if and only if the value is found. + * + * @param {number[]} array the array to be searched. + * @param {number} value the value to be searched for. + * @returns the index of the search value, if it is contained in the array; + * otherwise, (-(insertion point) - 1). + * @param {function} [f] an optional key function. + */ +pv.search = function(array, value, f) { + if (!f) f = pv.identity; + var low = 0, high = array.length - 1; + while (low <= high) { + var mid = (low + high) >> 1, midValue = f(array[mid]); + if (midValue < value) low = mid + 1; + else if (midValue > value) high = mid - 1; + else return mid; + } + return -low - 1; +}; + +pv.search.index = function(array, value, f) { + var i = pv.search(array, value, f); + return (i < 0) ? (-i - 1) : i; +}; +/** + * Returns a {@link pv.Tree} operator for the specified array. This is a + * convenience factory method, equivalent to new pv.Tree(array). + * + * @see pv.Tree + * @param {array} array an array from which to construct a tree. + * @returns {pv.Tree} a tree operator for the specified array. + */ +pv.tree = function(array) { + return new pv.Tree(array); +}; + +/** + * Constructs a tree operator for the specified array. This constructor should + * not be invoked directly; use {@link pv.tree} instead. + * + * @class Represents a tree operator for the specified array. The tree operator + * allows a hierarchical map to be constructed from an array; it is similar to + * the {@link pv.Nest} operator, except the hierarchy is derived dynamically + * from the array elements. + * + *

For example, given an array of size information for ActionScript classes: + * + *

{ name: "flare.flex.FlareVis", size: 4116 },
+ * { name: "flare.physics.DragForce", size: 1082 },
+ * { name: "flare.physics.GravityForce", size: 1336 }, ...
+ * + * To facilitate visualization, it may be useful to nest the elements by their + * package hierarchy: + * + *
var tree = pv.tree(classes)
+ *     .keys(function(d) d.name.split("."))
+ *     .map();
+ * + * The resulting tree is: + * + *
{ flare: {
+ *     flex: {
+ *       FlareVis: {
+ *         name: "flare.flex.FlareVis",
+ *         size: 4116 } },
+ *     physics: {
+ *       DragForce: {
+ *         name: "flare.physics.DragForce",
+ *         size: 1082 },
+ *       GravityForce: {
+ *         name: "flare.physics.GravityForce",
+ *         size: 1336 } },
+ *     ... } }
+ * + * By specifying a value function, + * + *
var tree = pv.tree(classes)
+ *     .keys(function(d) d.name.split("."))
+ *     .value(function(d) d.size)
+ *     .map();
+ * + * we can further eliminate redundant data: + * + *
{ flare: {
+ *     flex: {
+ *       FlareVis: 4116 },
+ *     physics: {
+ *       DragForce: 1082,
+ *       GravityForce: 1336 },
+ *   ... } }
+ * + * For visualizations with large data sets, performance improvements may be seen + * by storing the data in a tree format, and then flattening it into an array at + * runtime with {@link pv.Flatten}. + * + * @param {array} array an array from which to construct a tree. + */ +pv.Tree = function(array) { + this.array = array; +}; + +/** + * Assigns a keys function to this operator; required. The keys function + * returns an array of strings for each element in the associated + * array; these keys determine how the elements are nested in the tree. The + * returned keys should be unique for each element in the array; otherwise, the + * behavior of this operator is undefined. + * + * @param {function} k the keys function. + * @returns {pv.Tree} this. + */ +pv.Tree.prototype.keys = function(k) { + this.k = k; + return this; +}; + +/** + * Assigns a value function to this operator; optional. The value + * function specifies an optional transformation of the element in the array + * before it is inserted into the map. If no value function is specified, it is + * equivalent to using the identity function. + * + * @param {function} k the value function. + * @returns {pv.Tree} this. + */ +pv.Tree.prototype.value = function(v) { + this.v = v; + return this; +}; + +/** + * Returns a hierarchical map of values. The hierarchy is determined by the keys + * function; the values in the map are determined by the value function. + * + * @returns a hierarchical map of values. + */ +pv.Tree.prototype.map = function() { + var map = {}, o = {}; + for (var i = 0; i < this.array.length; i++) { + o.index = i; + var value = this.array[i], keys = this.k.call(o, value), node = map; + for (var j = 0; j < keys.length - 1; j++) { + node = node[keys[j]] || (node[keys[j]] = {}); + } + node[keys[j]] = this.v ? this.v.call(o, value) : value; + } + return map; +}; +/** + * Returns a {@link pv.Nest} operator for the specified array. This is a + * convenience factory method, equivalent to new pv.Nest(array). + * + * @see pv.Nest + * @param {array} array an array of elements to nest. + * @returns {pv.Nest} a nest operator for the specified array. + */ +pv.nest = function(array) { + return new pv.Nest(array); +}; + +/** + * Constructs a nest operator for the specified array. This constructor should + * not be invoked directly; use {@link pv.nest} instead. + * + * @class Represents a {@link Nest} operator for the specified array. Nesting + * allows elements in an array to be grouped into a hierarchical tree + * structure. The levels in the tree are specified by key functions. The + * leaf nodes of the tree can be sorted by value, while the internal nodes can + * be sorted by key. Finally, the tree can be returned either has a + * multidimensional array via {@link #entries}, or as a hierarchical map via + * {@link #map}. The {@link #rollup} routine similarly returns a map, collapsing + * the elements in each leaf node using a summary function. + * + *

For example, consider the following tabular data structure of Barley + * yields, from various sites in Minnesota during 1931-2: + * + *

{ yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" },
+ * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" },
+ * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, ...
+ * + * To facilitate visualization, it may be useful to nest the elements first by + * year, and then by variety, as follows: + * + *
var nest = pv.nest(yields)
+ *     .key(function(d) d.year)
+ *     .key(function(d) d.variety)
+ *     .entries();
+ * + * This returns a nested array. Each element of the outer array is a key-values + * pair, listing the values for each distinct key: + * + *
{ key: 1931, values: [
+ *   { key: "Manchuria", values: [
+ *       { yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" },
+ *       { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" },
+ *       { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" },
+ *       ...
+ *     ] },
+ *   { key: "Glabron", values: [
+ *       { yield: 43.07, variety: "Glabron", year: 1931, site: "University Farm" },
+ *       { yield: 55.20, variety: "Glabron", year: 1931, site: "Waseca" },
+ *       ...
+ *     ] },
+ *   ] },
+ * { key: 1932, values: ... }
+ * + * Further details, including sorting and rollup, is provided below on the + * corresponding methods. + * + * @param {array} array an array of elements to nest. + */ +pv.Nest = function(array) { + this.array = array; + this.keys = []; +}; + +/** + * Nests using the specified key function. Multiple keys may be added to the + * nest; the array elements will be nested in the order keys are specified. + * + * @param {function} key a key function; must return a string or suitable map + * key. + * @return {pv.Nest} this. + */ +pv.Nest.prototype.key = function(key) { + this.keys.push(key); + return this; +}; + +/** + * Sorts the previously-added keys. The natural sort order is used by default + * (see {@link pv.naturalOrder}); if an alternative order is desired, + * order should be a comparator function. If this method is not called + * (i.e., keys are unsorted), keys will appear in the order they appear + * in the underlying elements array. For example, + * + *
pv.nest(yields)
+ *     .key(function(d) d.year)
+ *     .key(function(d) d.variety)
+ *     .sortKeys()
+ *     .entries()
+ * + * groups yield data by year, then variety, and sorts the variety groups + * lexicographically (since the variety attribute is a string). + * + *

Key sort order is only used in conjunction with {@link #entries}, which + * returns an array of key-values pairs. If the nest is used to construct a + * {@link #map} instead, keys are unsorted. + * + * @param {function} [order] an optional comparator function. + * @returns {pv.Nest} this. + */ +pv.Nest.prototype.sortKeys = function(order) { + this.keys[this.keys.length - 1].order = order || pv.naturalOrder; + return this; +}; + +/** + * Sorts the leaf values. The natural sort order is used by default (see + * {@link pv.naturalOrder}); if an alternative order is desired, order + * should be a comparator function. If this method is not called (i.e., values + * are unsorted), values will appear in the order they appear in the + * underlying elements array. For example, + * + *

pv.nest(yields)
+ *     .key(function(d) d.year)
+ *     .key(function(d) d.variety)
+ *     .sortValues(function(a, b) a.yield - b.yield)
+ *     .entries()
+ * + * groups yield data by year, then variety, and sorts the values for each + * variety group by yield. + * + *

Value sort order, unlike keys, applies to both {@link #entries} and + * {@link #map}. It has no effect on {@link #rollup}. + * + * @param {function} [order] an optional comparator function. + * @return {pv.Nest} this. + */ +pv.Nest.prototype.sortValues = function(order) { + this.order = order || pv.naturalOrder; + return this; +}; + +/** + * Returns a hierarchical map of values. Each key adds one level to the + * hierarchy. With only a single key, the returned map will have a key for each + * distinct value of the key function; the correspond value with be an array of + * elements with that key value. If a second key is added, this will be a nested + * map. For example: + * + *

pv.nest(yields)
+ *     .key(function(d) d.variety)
+ *     .key(function(d) d.site)
+ *     .map()
+ * + * returns a map m such that m[variety][site] is an array, a subset of + * yields, with each element having the given variety and site. + * + * @returns a hierarchical map of values. + */ +pv.Nest.prototype.map = function() { + var map = {}, values = []; + + /* Build the map. */ + for (var i, j = 0; j < this.array.length; j++) { + var x = this.array[j]; + var m = map; + for (i = 0; i < this.keys.length - 1; i++) { + var k = this.keys[i](x); + if (!m[k]) m[k] = {}; + m = m[k]; + } + k = this.keys[i](x); + if (!m[k]) { + var a = []; + values.push(a); + m[k] = a; + } + m[k].push(x); + } + + /* Sort each leaf array. */ + if (this.order) { + for (var i = 0; i < values.length; i++) { + values[i].sort(this.order); + } + } + + return map; +}; + +/** + * Returns a hierarchical nested array. This method is similar to + * {@link pv.entries}, but works recursively on the entire hierarchy. Rather + * than returning a map like {@link #map}, this method returns a nested + * array. Each element of the array has a key and values + * field. For leaf nodes, the values array will be a subset of the + * underlying elements array; for non-leaf nodes, the values array will + * contain more key-values pairs. + * + *

For an example usage, see the {@link Nest} constructor. + * + * @returns a hierarchical nested array. + */ +pv.Nest.prototype.entries = function() { + + /** Recursively extracts the entries for the given map. */ + function entries(map) { + var array = []; + for (var k in map) { + var v = map[k]; + array.push({ key: k, values: (v instanceof Array) ? v : entries(v) }); + }; + return array; + } + + /** Recursively sorts the values for the given key-values array. */ + function sort(array, i) { + var o = this.keys[i].order; + if (o) array.sort(function(a, b) { return o(a.key, b.key); }); + if (++i < this.keys.length) { + for (var j = 0; j < array.length; j++) { + sort.call(this, array[j].values, i); + } + } + return array; + } + + return sort.call(this, entries(this.map()), 0); +}; + +/** + * Returns a rollup map. The behavior of this method is the same as + * {@link #map}, except that the leaf values are replaced with the return value + * of the specified rollup function f. For example, + * + *

pv.nest(yields)
+ *      .key(function(d) d.site)
+ *      .rollup(function(v) pv.median(v, function(d) d.yield))
+ * + * first groups yield data by site, and then returns a map from site to median + * yield for the given site. + * + * @see #map + * @param {function} f a rollup function. + * @returns a hierarchical map, with the leaf values computed by f. + */ +pv.Nest.prototype.rollup = function(f) { + + /** Recursively descends to the leaf nodes (arrays) and does rollup. */ + function rollup(map) { + for (var key in map) { + var value = map[key]; + if (value instanceof Array) { + map[key] = f(value); + } else { + rollup(value); + } + } + return map; + } + + return rollup(this.map()); +}; +/** + * Returns a {@link pv.Flatten} operator for the specified map. This is a + * convenience factory method, equivalent to new pv.Flatten(map). + * + * @see pv.Flatten + * @param map a map to flatten. + * @returns {pv.Flatten} a flatten operator for the specified map. + */ +pv.flatten = function(map) { + return new pv.Flatten(map); +}; + +/** + * Constructs a flatten operator for the specified map. This constructor should + * not be invoked directly; use {@link pv.flatten} instead. + * + * @class Represents a flatten operator for the specified array. Flattening + * allows hierarchical maps to be flattened into an array. The levels in the + * input tree are specified by key functions. + * + *

For example, consider the following hierarchical data structure of Barley + * yields, from various sites in Minnesota during 1931-2: + * + *

{ 1931: {
+ *     Manchuria: {
+ *       "University Farm": 27.00,
+ *       "Waseca": 48.87,
+ *       "Morris": 27.43,
+ *       ... },
+ *     Glabron: {
+ *       "University Farm": 43.07,
+ *       "Waseca": 55.20,
+ *       ... } },
+ *   1932: {
+ *     ... } }
+ * + * To facilitate visualization, it may be useful to flatten the tree into a + * tabular array: + * + *
var array = pv.flatten(yields)
+ *     .key("year")
+ *     .key("variety")
+ *     .key("site")
+ *     .key("yield")
+ *     .array();
+ * + * This returns an array of object elements. Each element in the array has + * attributes corresponding to this flatten operator's keys: + * + *
{ site: "University Farm", variety: "Manchuria", year: 1931, yield: 27 },
+ * { site: "Waseca", variety: "Manchuria", year: 1931, yield: 48.87 },
+ * { site: "Morris", variety: "Manchuria", year: 1931, yield: 27.43 },
+ * { site: "University Farm", variety: "Glabron", year: 1931, yield: 43.07 },
+ * { site: "Waseca", variety: "Glabron", year: 1931, yield: 55.2 }, ...
+ * + *

The flatten operator is roughly the inverse of the {@link pv.Nest} and + * {@link pv.Tree} operators. + * + * @param map a map to flatten. + */ +pv.Flatten = function(map) { + this.map = map; + this.keys = []; +}; + +/** + * Flattens using the specified key function. Multiple keys may be added to the + * flatten; the tiers of the underlying tree must correspond to the specified + * keys, in order. The order of the returned array is undefined; however, you + * can easily sort it. + * + * @param {string} key the key name. + * @param {function} [f] an optional value map function. + * @return {pv.Nest} this. + */ +pv.Flatten.prototype.key = function(key, f) { + this.keys.push({name: key, value: f}); + return this; +}; + +/** + * Returns the flattened array. Each entry in the array is an object; each + * object has attributes corresponding to this flatten operator's keys. + * + * @returns an array of elements from the flattened map. + */ +pv.Flatten.prototype.array = function() { + var entries = [], stack = [], keys = this.keys; + + /* Recursively visits the specified value. */ + function visit(value, i) { + if (i < keys.length - 1) { + for (var key in value) { + stack.push(key); + visit(value[key], i + 1); + stack.pop(); + } + } else { + entries.push(stack.concat(value)); + } + } + + visit(this.map, 0); + return entries.map(function(stack) { + var m = {}; + for (var i = 0; i < keys.length; i++) { + var k = keys[i], v = stack[i]; + m[k.name] = k.value ? k.value.call(null, v) : v; + } + return m; + }); +}; +/** + * Returns a {@link pv.Vector} for the specified x and y + * coordinate. This is a convenience factory method, equivalent to new + * pv.Vector(x, y). + * + * @see pv.Vector + * @param {number} x the x coordinate. + * @param {number} y the y coordinate. + * @returns {pv.Vector} a vector for the specified coordinates. + */ +pv.vector = function(x, y) { + return new pv.Vector(x, y); +}; + +/** + * Constructs a {@link pv.Vector} for the specified x and y + * coordinate. This constructor should not be invoked directly; use + * {@link pv.vector} instead. + * + * @class Represents a two-dimensional vector; a 2-tuple ⟨x, + * y⟩. + * + * @param {number} x the x coordinate. + * @param {number} y the y coordinate. + */ +pv.Vector = function(x, y) { + this.x = x; + this.y = y; +}; + +/** + * Returns a vector perpendicular to this vector: ⟨-y, x⟩. + * + * @returns {pv.Vector} a perpendicular vector. + */ +pv.Vector.prototype.perp = function() { + return new pv.Vector(-this.y, this.x); +}; + +/** + * Returns a normalized copy of this vector: a vector with the same direction, + * but unit length. If this vector has zero length this method returns a copy of + * this vector. + * + * @returns {pv.Vector} a unit vector. + */ +pv.Vector.prototype.norm = function() { + var l = this.length(); + return this.times(l ? (1 / l) : 1); +}; + +/** + * Returns the magnitude of this vector, defined as sqrt(x * x + y * y). + * + * @returns {number} a length. + */ +pv.Vector.prototype.length = function() { + return Math.sqrt(this.x * this.x + this.y * this.y); +}; + +/** + * Returns a scaled copy of this vector: ⟨x * k, y * k⟩. + * To perform the equivalent divide operation, use 1 / k. + * + * @param {number} k the scale factor. + * @returns {pv.Vector} a scaled vector. + */ +pv.Vector.prototype.times = function(k) { + return new pv.Vector(this.x * k, this.y * k); +}; + +/** + * Returns this vector plus the vector v: ⟨x + v.x, y + + * v.y⟩. If only one argument is specified, it is interpreted as the + * vector v. + * + * @param {number} x the x coordinate to add. + * @param {number} y the y coordinate to add. + * @returns {pv.Vector} a new vector. + */ +pv.Vector.prototype.plus = function(x, y) { + return (arguments.length == 1) + ? new pv.Vector(this.x + x.x, this.y + x.y) + : new pv.Vector(this.x + x, this.y + y); +}; + +/** + * Returns this vector minus the vector v: ⟨x - v.x, y - + * v.y⟩. If only one argument is specified, it is interpreted as the + * vector v. + * + * @param {number} x the x coordinate to subtract. + * @param {number} y the y coordinate to subtract. + * @returns {pv.Vector} a new vector. + */ +pv.Vector.prototype.minus = function(x, y) { + return (arguments.length == 1) + ? new pv.Vector(this.x - x.x, this.y - x.y) + : new pv.Vector(this.x - x, this.y - y); +}; + +/** + * Returns the dot product of this vector and the vector v: x * v.x + + * y * v.y. If only one argument is specified, it is interpreted as the + * vector v. + * + * @param {number} x the x coordinate to dot. + * @param {number} y the y coordinate to dot. + * @returns {number} a dot product. + */ +pv.Vector.prototype.dot = function(x, y) { + return (arguments.length == 1) + ? this.x * x.x + this.y * x.y + : this.x * x + this.y * y; +}; +// TODO code-sharing between scales + +/** + * @ignore + * @class + */ +pv.Scale = function() {}; + +/** + * @private Returns a function that interpolators from the start value to the + * end value, given a parameter t in [0, 1]. + * + * @param start the start value. + * @param end the end value. + */ +pv.Scale.interpolator = function(start, end) { + if (typeof start == "number") { + return function(t) { + return t * (end - start) + start; + }; + } + + /* For now, assume color. */ + start = pv.color(start).rgb(); + end = pv.color(end).rgb(); + return function(t) { + var a = start.a * (1 - t) + end.a * t; + if (a < 1e-5) a = 0; // avoid scientific notation + return (start.a == 0) ? pv.rgb(end.r, end.g, end.b, a) + : ((end.a == 0) ? pv.rgb(start.r, start.g, start.b, a) + : pv.rgb( + Math.round(start.r * (1 - t) + end.r * t), + Math.round(start.g * (1 - t) + end.g * t), + Math.round(start.b * (1 - t) + end.b * t), a)); + }; +}; +/** + * Returns a linear scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents a linear scale. Most commonly, a linear scale + * represents a 1-dimensional linear transformation from a numeric domain of + * input data [d0, d1] to a numeric range of + * pixels [r0, r1]. The equation for such a + * scale is: + * + *

f(x) = (x - d0) / (d1 - d0) * + * (r1 - r0) + r0
+ * + * For example, a linear scale from the domain [0, 100] to range [0, 640]: + * + *
f(x) = (x - 0) / (100 - 0) * (640 - 0) + 0
+ * f(x) = x / 100 * 640
+ * f(x) = x * 6.4
+ *
+ * + * Thus, saying + * + *
.height(function(d) d * 6.4)
+ * + * is identical to + * + *
.height(pv.Scale.linear(0, 100).range(0, 640))
+ * + * As you can see, scales do not always make code smaller, but they should make + * code more explicit and easier to maintain. In addition to readability, scales + * offer several useful features: + * + *

1. The range can be expressed in colors, rather than pixels. Changing the + * example above to + * + *

.fillStyle(pv.Scale.linear(0, 100).range("red", "green"))
+ * + * will cause it to fill the marks "red" on an input value of 0, "green" on an + * input value of 100, and some color in-between for intermediate values. + * + *

2. The domain and range can be subdivided for a "poly-linear" + * transformation. For example, you may want a diverging color scale that is + * increasingly red for negative values, and increasingly green for positive + * values: + * + *

.fillStyle(pv.Scale.linear(-1, 0, 1).range("red", "white", "green"))
+ * + * The domain can be specified as a series of n monotonically-increasing + * values; the range must also be specified as n values, resulting in + * n - 1 contiguous linear scales. + * + *

3. Linear scales can be inverted for interaction. The {@link #invert} + * method takes a value in the output range, and returns the corresponding value + * in the input domain. This is frequently used to convert the mouse location + * (see {@link pv.Mark#mouse}) to a value in the input domain. Note that + * inversion is only supported for numeric ranges, and not colors. + * + *

4. A scale can be queried for reasonable "tick" values. The {@link #ticks} + * method provides a convenient way to get a series of evenly-spaced rounded + * values in the input domain. Frequently these are used in conjunction with + * {@link pv.Rule} to display tick marks or grid lines. + * + *

5. A scale can be "niced" to extend the domain to suitable rounded + * numbers. If the minimum and maximum of the domain are messy because they are + * derived from data, you can use {@link #nice} to round these values down and + * up to even numbers. + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.linear} a linear scale. + */ +pv.Scale.linear = function() { + var d = [0, 1], r = [0, 1], i = [pv.identity], precision = 0; + + /** @private */ + function scale(x) { + var j = pv.search(d, x); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return i[j]((x - d[j]) / (d[j + 1] - d[j])); + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + *

1. domain(min, ..., max) + * + *

Specifying the domain as a series of numbers is the most explicit and + * recommended approach. Most commonly, two numbers are specified: the minimum + * and maximum value. However, for a diverging scale, or other subdivided + * poly-linear scales, multiple values can be specified. Values can be derived + * from data using {@link pv.min} and {@link pv.max}. For example: + * + *

.domain(0, pv.max(array))
+ * + * An alternative method for deriving minimum and maximum values from data + * follows. + * + *

2. domain(array, minf, maxf) + * + *

When both the minimum and maximum value are derived from data, the + * arguments to the domain method can be specified as the array of + * data, followed by zero, one or two accessor functions. For example, if the + * array of data is just an array of numbers: + * + *

.domain(array)
+ * + * On the other hand, if the array elements are objects representing stock + * values per day, and the domain should consider the stock's daily low and + * daily high: + * + *
.domain(array, function(d) d.low, function(d) d.high)
+ * + * The first method of setting the domain is preferred because it is more + * explicit; setting the domain using this second method should be used only + * if brevity is required. + * + *

3. domain() + * + *

Invoking the domain method with no arguments returns the + * current domain as an array of numbers. + * + * @function + * @name pv.Scale.linear.prototype.domain + * @param {number...} domain... domain values. + * @returns {pv.Scale.linear} this, or the current domain. + */ + scale.domain = function(array, min, max) { + if (arguments.length) { + if (array instanceof Array) { + if (arguments.length < 2) min = pv.identity; + if (arguments.length < 3) max = min; + d = [pv.min(array, min), pv.max(array, max)]; + } else { + d = Array.prototype.slice.call(arguments); + } + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + *

1. range(min, ..., max) + * + *

The range may be specified as a series of numbers or colors. Most + * commonly, two numbers are specified: the minimum and maximum pixel values. + * For a color scale, values may be specified as {@link pv.Color}s or + * equivalent strings. For a diverging scale, or other subdivided poly-linear + * scales, multiple values can be specified. For example: + * + *

.range("red", "white", "green")
+ * + *

Currently, only numbers and colors are supported as range values. The + * number of range values must exactly match the number of domain values, or + * the behavior of the scale is undefined. + * + *

2. range() + * + *

Invoking the range method with no arguments returns the current + * range as an array of numbers or colors. + * + * @function + * @name pv.Scale.linear.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.linear} this, or the current range. + */ + scale.range = function() { + if (arguments.length) { + r = Array.prototype.slice.call(arguments); + i = []; + for (var j = 0; j < r.length - 1; j++) { + i.push(pv.Scale.interpolator(r[j], r[j + 1])); + } + return this; + } + return r; + }; + + /** + * Inverts the specified value in the output range, returning the + * corresponding value in the input domain. This is frequently used to convert + * the mouse location (see {@link pv.Mark#mouse}) to a value in the input + * domain. Inversion is only supported for numeric ranges, and not colors. + * + *

Note that this method does not do any rounding or bounds checking. If + * the input domain is discrete (e.g., an array index), the returned value + * should be rounded. If the specified y value is outside the range, + * the returned value may be equivalently outside the input domain. + * + * @function + * @name pv.Scale.linear.prototype.invert + * @param {number} y a value in the output range (a pixel location). + * @returns {number} a value in the input domain. + */ + scale.invert = function(y) { + var j = pv.search(r, y); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return (y - r[j]) / (r[j + 1] - r[j]) * (d[j + 1] - d[j]) + d[j]; + }; + + /** + * Returns an array of evenly-spaced, suitably-rounded values in the input + * domain. This method attempts to return between 5 and 10 tick values. These + * values are frequently used in conjunction with {@link pv.Rule} to display + * tick marks or grid lines. + * + * @function + * @name pv.Scale.linear.prototype.ticks + * @returns {number[]} an array input domain values to use as ticks. + */ + scale.ticks = function() { + var min = d[0], + max = d[d.length - 1], + span = max - min, + step = pv.logCeil(span / 10, 10); + if (span / step < 2) step /= 5; + else if (span / step < 5) step /= 2; + var start = Math.ceil(min / step) * step, + end = Math.floor(max / step) * step; + precision = Math.max(0, -Math.floor(pv.log(step, 10) + .01)); + return pv.range(start, end + step, step); + }; + + /** + * Formats the specified tick value using the appropriate precision, based on + * the step interval between tick marks. + * + * @function + * @name pv.Scale.linear.prototype.tickFormat + * @param {number} t a tick value. + * @return {string} a formatted tick value. + */ + scale.tickFormat = function(t) { + return t.toFixed(precision); + }; + + /** + * "Nices" this scale, extending the bounds of the input domain to + * evenly-rounded values. Nicing is useful if the domain is computed + * dynamically from data, and may be irregular. For example, given a domain of + * [0.20147987687960267, 0.996679553296417], a call to nice() might + * extend the domain to [0.2, 1]. + * + *

This method must be invoked each time after setting the domain. + * + * @function + * @name pv.Scale.linear.prototype.nice + * @returns {pv.Scale.linear} this. + */ + scale.nice = function() { + var min = d[0], + max = d[d.length - 1], + step = Math.pow(10, Math.round(Math.log(max - min) / Math.log(10)) - 1); + d = [Math.floor(min / step) * step, Math.ceil(max / step) * step]; + return this; + }; + + /** + * Returns a view of this scale by the specified accessor function f. + * Given a scale y, y.by(function(d) d.foo) is equivalent to + * function(d) y(d.foo). + * + *

This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements + * that have a score attribute with the domain [0, 1], the height + * property could be specified as: + * + *

.height(pv.Scale.linear().range(0, 480).by(function(d) d.score))
+ * + * This is equivalent to: + * + *
.height(function(d) d.score * 480)
+ * + * This method should be used judiciously; it is typically more clear to + * invoke the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.linear.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.linear} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a log scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents a log scale. Most commonly, a log scale represents + * a 1-dimensional log transformation from a numeric domain of input data + * [d0, d1] to a numeric range of pixels + * [r0, r1]. The equation for such a scale + * is: + * + *
f(x) = (log(x) - log(d0)) / (log(d1) - + * log(d0)) * (r1 - r0) + + * r0
+ * + * where log(x) represents the zero-symmetric logarthim of x using + * the scale's associated base (default: 10, see {@link pv.logSymmetric}). For + * example, a log scale from the domain [1, 100] to range [0, 640]: + * + *
f(x) = (log(x) - log(1)) / (log(100) - log(1)) * (640 - 0) + 0
+ * f(x) = log(x) / 2 * 640
+ * f(x) = log(x) * 320
+ *
+ * + * Thus, saying + * + *
.height(function(d) Math.log(d) * 138.974)
+ * + * is equivalent to + * + *
.height(pv.Scale.log(1, 100).range(0, 640))
+ * + * As you can see, scales do not always make code smaller, but they should make + * code more explicit and easier to maintain. In addition to readability, scales + * offer several useful features: + * + *

1. The range can be expressed in colors, rather than pixels. Changing the + * example above to + * + *

.fillStyle(pv.Scale.log(1, 100).range("red", "green"))
+ * + * will cause it to fill the marks "red" on an input value of 1, "green" on an + * input value of 100, and some color in-between for intermediate values. + * + *

2. The domain and range can be subdivided for a "poly-log" + * transformation. For example, you may want a diverging color scale that is + * increasingly red for small values, and increasingly green for large values: + * + *

.fillStyle(pv.Scale.log(1, 10, 100).range("red", "white", "green"))
+ * + * The domain can be specified as a series of n monotonically-increasing + * values; the range must also be specified as n values, resulting in + * n - 1 contiguous log scales. + * + *

3. Log scales can be inverted for interaction. The {@link #invert} method + * takes a value in the output range, and returns the corresponding value in the + * input domain. This is frequently used to convert the mouse location (see + * {@link pv.Mark#mouse}) to a value in the input domain. Note that inversion is + * only supported for numeric ranges, and not colors. + * + *

4. A scale can be queried for reasonable "tick" values. The {@link #ticks} + * method provides a convenient way to get a series of evenly-spaced rounded + * values in the input domain. Frequently these are used in conjunction with + * {@link pv.Rule} to display tick marks or grid lines. + * + *

5. A scale can be "niced" to extend the domain to suitable rounded + * numbers. If the minimum and maximum of the domain are messy because they are + * derived from data, you can use {@link #nice} to round these values down and + * up to even numbers. + * + * @param {number...} domain... domain values. + * @returns {pv.Scale.log} a log scale. + */ +pv.Scale.log = function() { + var d = [1, 10], l = [0, 1], b = 10, r = [0, 1], i = [pv.identity]; + + /** @private */ + function scale(x) { + var j = pv.search(d, x); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + return i[j]((log(x) - l[j]) / (l[j + 1] - l[j])); + } + + /** @private */ + function log(x) { + return pv.logSymmetric(x, b); + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + *

1. domain(min, ..., max) + * + *

Specifying the domain as a series of numbers is the most explicit and + * recommended approach. Most commonly, two numbers are specified: the minimum + * and maximum value. However, for a diverging scale, or other subdivided + * poly-log scales, multiple values can be specified. Values can be derived + * from data using {@link pv.min} and {@link pv.max}. For example: + * + *

.domain(1, pv.max(array))
+ * + * An alternative method for deriving minimum and maximum values from data + * follows. + * + *

2. domain(array, minf, maxf) + * + *

When both the minimum and maximum value are derived from data, the + * arguments to the domain method can be specified as the array of + * data, followed by zero, one or two accessor functions. For example, if the + * array of data is just an array of numbers: + * + *

.domain(array)
+ * + * On the other hand, if the array elements are objects representing stock + * values per day, and the domain should consider the stock's daily low and + * daily high: + * + *
.domain(array, function(d) d.low, function(d) d.high)
+ * + * The first method of setting the domain is preferred because it is more + * explicit; setting the domain using this second method should be used only + * if brevity is required. + * + *

3. domain() + * + *

Invoking the domain method with no arguments returns the + * current domain as an array of numbers. + * + * @function + * @name pv.Scale.log.prototype.domain + * @param {number...} domain... domain values. + * @returns {pv.Scale.log} this, or the current domain. + */ + scale.domain = function(array, min, max) { + if (arguments.length) { + if (array instanceof Array) { + if (arguments.length < 2) min = pv.identity; + if (arguments.length < 3) max = min; + d = [pv.min(array, min), pv.max(array, max)]; + } else { + d = Array.prototype.slice.call(arguments); + } + l = d.map(log); + return this; + } + return d; + }; + + /** + * @function + * @name pv.Scale.log.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.log} this. + */ + scale.range = function() { + if (arguments.length) { + r = Array.prototype.slice.call(arguments); + i = []; + for (var j = 0; j < r.length - 1; j++) { + i.push(pv.Scale.interpolator(r[j], r[j + 1])); + } + return this; + } + return r; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + *

1. range(min, ..., max) + * + *

The range may be specified as a series of numbers or colors. Most + * commonly, two numbers are specified: the minimum and maximum pixel values. + * For a color scale, values may be specified as {@link pv.Color}s or + * equivalent strings. For a diverging scale, or other subdivided poly-log + * scales, multiple values can be specified. For example: + * + *

.range("red", "white", "green")
+ * + *

Currently, only numbers and colors are supported as range values. The + * number of range values must exactly match the number of domain values, or + * the behavior of the scale is undefined. + * + *

2. range() + * + *

Invoking the range method with no arguments returns the current + * range as an array of numbers or colors. + * + * @function + * @name pv.Scale.log.prototype.invert + * @param {...} range... range values. + * @returns {pv.Scale.log} this, or the current range. + */ + scale.invert = function(y) { + var j = pv.search(r, y); + if (j < 0) j = -j - 2; + j = Math.max(0, Math.min(i.length - 1, j)); + var t = l[j] + (y - r[j]) / (r[j + 1] - r[j]) * (l[j + 1] - l[j]); + return (d[j] < 0) ? -Math.pow(b, -t) : Math.pow(b, t); + }; + + /** + * Returns an array of evenly-spaced, suitably-rounded values in the input + * domain. These values are frequently used in conjunction with {@link + * pv.Rule} to display tick marks or grid lines. + * + * @function + * @name pv.Scale.log.prototype.ticks + * @returns {number[]} an array input domain values to use as ticks. + */ + scale.ticks = function() { + // TODO: support multiple domains + var start = Math.floor(l[0]), + end = Math.ceil(l[1]), + ticks = []; + for (var i = start; i < end; i++) { + var x = Math.pow(b, i); + if (d[0] < 0) x = -x; + for (var j = 1; j < b; j++) { + ticks.push(x * j); + } + } + ticks.push(Math.pow(b, end)); + if (ticks[0] < d[0]) ticks.shift(); + if (ticks[ticks.length - 1] > d[1]) ticks.pop(); + return ticks; + }; + + /** + * Formats the specified tick value using the appropriate precision, assuming + * base 10. + * + * @function + * @name pv.Scale.log.prototype.tickFormat + * @param {number} t a tick value. + * @return {string} a formatted tick value. + */ + scale.tickFormat = function(t) { + return t.toPrecision(1); + }; + + /** + * "Nices" this scale, extending the bounds of the input domain to + * evenly-rounded values. This method uses {@link pv.logFloor} and {@link + * pv.logCeil}. Nicing is useful if the domain is computed dynamically from + * data, and may be irregular. For example, given a domain of + * [0.20147987687960267, 0.996679553296417], a call to nice() might + * extend the domain to [0.1, 1]. + * + *

This method must be invoked each time after setting the domain (and + * base). + * + * @function + * @name pv.Scale.log.prototype.nice + * @returns {pv.Scale.log} this. + */ + scale.nice = function() { + // TODO: support multiple domains + d = [pv.logFloor(d[0], b), pv.logCeil(d[1], b)]; + l = d.map(log); + return this; + }; + + /** + * Sets or gets the logarithm base. Defaults to 10. + * + * @function + * @name pv.Scale.log.prototype.base + * @param {number} [v] the new base. + * @returns {pv.Scale.log} this, or the current base. + */ + scale.base = function(v) { + if (arguments.length) { + b = v; + l = d.map(log); + return this; + } + return b; + }; + + /** + * Returns a view of this scale by the specified accessor function f. + * Given a scale y, y.by(function(d) d.foo) is equivalent to + * function(d) y(d.foo). + * + *

This method is provided for convenience, such that scales can be + * succinctly defined inline. For example, given an array of data elements + * that have a score attribute with the domain [0, 1], the height + * property could be specified as: + * + *

.height(pv.Scale.log().range(0, 480).by(function(d) d.score))
+ * + * This is equivalent to: + * + *
.height(function(d) d.score * 480)
+ * + * This method should be used judiciously; it is typically more clear to + * invoke the scale directly, passing in the value to be scaled. + * + * @function + * @name pv.Scale.log.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.log} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns an ordinal scale for the specified domain. The arguments to this + * constructor are optional, and equivalent to calling {@link #domain}. + * + * @class Represents an ordinal scale. An ordinal scale represents a + * pairwise mapping from n discrete values in the input domain to + * n discrete values in the output range. For example, an ordinal scale + * might map a domain of species ["setosa", "versicolor", "virginica"] to colors + * ["red", "green", "blue"]. Thus, saying + * + *
.fillStyle(function(d) {
+ *     switch (d.species) {
+ *       case "setosa": return "red";
+ *       case "versicolor": return "green";
+ *       case "virginica": return "blue";
+ *     }
+ *   })
+ * + * is equivalent to + * + *
.fillStyle(pv.Scale.ordinal("setosa", "versicolor", "virginica")
+ *     .range("red", "green", "blue")
+ *     .by(function(d) d.species))
+ * + * If the mapping from species to color does not need to be specified + * explicitly, the domain can be omitted. In this case it will be inferred + * lazily from the data: + * + *
.fillStyle(pv.colors("red", "green", "blue")
+ *     .by(function(d) d.species))
+ * + * When the domain is inferred, the first time the scale is invoked, the first + * element from the range will be returned. Subsequent calls with unique values + * will return subsequent elements from the range. If the inferred domain grows + * larger than the range, range values will be reused. However, it is strongly + * recommended that the domain and the range contain the same number of + * elements. + * + *

A range can be discretized from a continuous interval (e.g., for pixel + * positioning) by using {@link #split}, {@link #splitFlush} or + * {@link #splitBanded} after the domain has been set. For example, if + * states is an array of the fifty U.S. state names, the state name can + * be encoded in the left position: + * + *

.left(pv.Scale.ordinal(states)
+ *     .split(0, 640)
+ *     .by(function(d) d.state))
+ * + *

N.B.: ordinal scales are not invertible (at least not yet), since the + * domain and range and discontinuous. A workaround is to use a linear scale. + * + * @param {...} domain... domain values. + * @returns {pv.Scale.ordinal} an ordinal scale. + * @see pv.colors + */ +pv.Scale.ordinal = function() { + var d = [], i = {}, r = [], band = 0; + + /** @private */ + function scale(x) { + if (!(x in i)) i[x] = d.push(x) - 1; + return r[i[x] % r.length]; + } + + /** + * Sets or gets the input domain. This method can be invoked several ways: + * + *

1. domain(values...) + * + *

Specifying the domain as a series of values is the most explicit and + * recommended approach. However, if the domain values are derived from data, + * you may find the second method more appropriate. + * + *

2. domain(array, f) + * + *

Rather than enumerating the domain values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the domain values from the + * array. + * + *

3. domain() + * + *

Invoking the domain method with no arguments returns the + * current domain as an array. + * + * @function + * @name pv.Scale.ordinal.prototype.domain + * @param {...} domain... domain values. + * @returns {pv.Scale.ordinal} this, or the current domain. + */ + scale.domain = function(array, f) { + if (arguments.length) { + array = (array instanceof Array) + ? ((arguments.length > 1) ? map(array, f) : array) + : Array.prototype.slice.call(arguments); + + /* Filter the specified ordinals to their unique values. */ + d = []; + var seen = {}; + for (var j = 0; j < array.length; j++) { + var o = array[j]; + if (!(o in seen)) { + seen[o] = true; + d.push(o); + } + } + + i = pv.numerate(d); + return this; + } + return d; + }; + + /** + * Sets or gets the output range. This method can be invoked several ways: + * + *

1. range(values...) + * + *

Specifying the range as a series of values is the most explicit and + * recommended approach. However, if the range values are derived from data, + * you may find the second method more appropriate. + * + *

2. range(array, f) + * + *

Rather than enumerating the range values as explicit arguments to this + * method, you can specify a single argument of an array. In addition, you can + * specify an optional accessor function to extract the range values from the + * array. + * + *

3. range() + * + *

Invoking the range method with no arguments returns the + * current range as an array. + * + * @function + * @name pv.Scale.ordinal.prototype.range + * @param {...} range... range values. + * @returns {pv.Scale.ordinal} this, or the current range. + */ + scale.range = function(array, f) { + if (arguments.length) { + r = (array instanceof Array) + ? ((arguments.length > 1) ? map(array, f) : array) + : Array.prototype.slice.call(arguments); + if (typeof r[0] == "string") r = r.map(pv.color); + return this; + } + return r; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [min, max] is subdivided into n equispaced points, + * where n is the number of (unique) values in the domain. The first + * and last point are offset from the edge of the range by half the distance + * between points. + * + *

This method must be called after the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.split + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @returns {pv.Scale.ordinal} this. + * @see #splitFlush + * @see #splitBanded + */ + scale.split = function(min, max) { + var step = (max - min) / this.domain().length; + r = pv.range(min + step / 2, max, step); + return this; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [min, max] is subdivided into n equispaced points, + * where n is the number of (unique) values in the domain. The first + * and last point are exactly on the edge of the range. + * + *

This method must be called after the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.splitFlush + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @returns {pv.Scale.ordinal} this. + * @see #split + */ + scale.splitFlush = function(min, max) { + var n = this.domain().length, step = (max - min) / (n - 1); + r = (n == 1) ? [(min + max) / 2] + : pv.range(min, max + step / 2, step); + return this; + }; + + /** + * Sets the range from the given continuous interval. The interval + * [min, max] is subdivided into n equispaced bands, + * where n is the number of (unique) values in the domain. The first + * and last band are offset from the edge of the range by the distance between + * bands. + * + *

The band width argument, band, is typically in the range [0, 1] + * and defaults to 1. This fraction corresponds to the amount of space in the + * range to allocate to the bands, as opposed to padding. A value of 0.5 means + * that the band width will be equal to the padding width. The computed + * absolute band width can be retrieved from the range as + * scale.range().band. + * + *

If the band width argument is negative, this method will allocate bands + * of a fixed width -band, rather than a relative fraction of + * the available space. + * + *

Tip: to inset the bands by a fixed amount p, specify a minimum + * value of min + p (or simply p, if min is + * 0). Then set the mark width to scale.range().band - p. + * + *

This method must be called after the domain is set. + * + * @function + * @name pv.Scale.ordinal.prototype.splitBanded + * @param {number} min minimum value of the output range. + * @param {number} max maximum value of the output range. + * @param {number} [band] the fractional band width in [0, 1]; defaults to 1. + * @returns {pv.Scale.ordinal} this. + * @see #split + */ + scale.splitBanded = function(min, max, band) { + if (arguments.length < 3) band = 1; + if (band < 0) { + var n = this.domain().length, + total = -band * n, + remaining = max - min - total, + padding = remaining / (n + 1); + r = pv.range(min + padding, max, padding - band); + r.band = -band; + } else { + var step = (max - min) / (this.domain().length + (1 - band)); + r = pv.range(min + step * (1 - band), max, step); + r.band = step * band; + } + return this; + }; + + /** + * Returns a view of this scale by the specified accessor function f. + * Given a scale y, y.by(function(d) d.foo) is equivalent to + * function(d) y(d.foo). This method should be used judiciously; it + * is typically more clear to invoke the scale directly, passing in the value + * to be scaled. + * + * @function + * @name pv.Scale.ordinal.prototype.by + * @param {function} f an accessor function. + * @returns {pv.Scale.ordinal} a view of this scale by the specified accessor + * function. + */ + scale.by = function(f) { + function by() { return scale(f.apply(this, arguments)); } + for (var method in scale) by[method] = scale[method]; + return by; + }; + + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns the {@link pv.Color} for the specified color format string. Colors + * may have an associated opacity, or alpha channel. Color formats are specified + * by CSS Color Modular Level 3, using either in RGB or HSL color space. For + * example:

    + * + *
  • #f00 // #rgb + *
  • #ff0000 // #rrggbb + *
  • rgb(255, 0, 0) + *
  • rgb(100%, 0%, 0%) + *
  • hsl(0, 100%, 50%) + *
  • rgba(0, 0, 255, 0.5) + *
  • hsla(120, 100%, 50%, 1) + * + *
The SVG 1.0 color keywords names are also supported, such as "aliceblue" + * and "yellowgreen". The "transparent" keyword is supported for a + * fully-transparent color. + * + *

If the format argument is already an instance of Color, + * the argument is returned with no further processing. + * + * @param {string} format the color specification string, such as "#f00". + * @returns {pv.Color} the corresponding Color. + * @see SVG color + * keywords + * @see CSS3 color module + */ +pv.color = function(format) { + if (!format || (format == "transparent")) { + return pv.rgb(0, 0, 0, 0); + } + if (format instanceof pv.Color) { + return format; + } + + /* Handle hsl, rgb. */ + var m1 = /([a-z]+)\((.*)\)/i.exec(format); + if (m1) { + var m2 = m1[2].split(","), a = 1; + switch (m1[1]) { + case "hsla": + case "rgba": { + a = parseFloat(m2[3]); + break; + } + } + switch (m1[1]) { + case "hsla": + case "hsl": { + var h = parseFloat(m2[0]), // degrees + s = parseFloat(m2[1]) / 100, // percentage + l = parseFloat(m2[2]) / 100; // percentage + return (new pv.Color.Hsl(h, s, l, a)).rgb(); + } + case "rgba": + case "rgb": { + function parse(c) { // either integer or percentage + var f = parseFloat(c); + return (c[c.length - 1] == '%') ? Math.round(f * 2.55) : f; + } + var r = parse(m2[0]), g = parse(m2[1]), b = parse(m2[2]); + return pv.rgb(r, g, b, a); + } + } + } + + /* Named colors. */ + format = pv.Color.names[format] || format; + + /* Hexadecimal colors: #rgb and #rrggbb. */ + if (format.charAt(0) == "#") { + var r, g, b; + if (format.length == 4) { + r = format.charAt(1); r += r; + g = format.charAt(2); g += g; + b = format.charAt(3); b += b; + } else if (format.length == 7) { + r = format.substring(1, 3); + g = format.substring(3, 5); + b = format.substring(5, 7); + } + return pv.rgb(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1); + } + + /* Otherwise, assume named colors. TODO allow lazy conversion to RGB. */ + return new pv.Color(format, 1); +}; + +/** + * Constructs a color with the specified color format string and opacity. This + * constructor should not be invoked directly; use {@link pv.color} instead. + * + * @class Represents an abstract (possibly translucent) color. The color is + * divided into two parts: the color attribute, an opaque color format + * string, and the opacity attribute, a float in [0, 1]. The color + * space is dependent on the implementing class; all colors support the + * {@link #rgb} method to convert to RGB color space for interpolation. + * + *

See also the Color guide. + * + * @param {string} color an opaque color format string, such as "#f00". + * @param {number} opacity the opacity, in [0,1]. + * @see pv.color + */ +pv.Color = function(color, opacity) { + /** + * An opaque color format string, such as "#f00". + * + * @type string + * @see SVG color + * keywords + * @see CSS3 color module + */ + this.color = color; + + /** + * The opacity, a float in [0, 1]. + * + * @type number + */ + this.opacity = opacity; +}; + +/** + * Returns a new color that is a brighter version of this color. The behavior of + * this method may vary slightly depending on the underlying color space. + * Although brighter and darker are inverse operations, the results of a series + * of invocations of these two methods might be inconsistent because of rounding + * errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #darker + * @returns {pv.Color} a brighter color. + */ +pv.Color.prototype.brighter = function(k) { + return this.rgb().brighter(k); +}; + +/** + * Returns a new color that is a brighter version of this color. The behavior of + * this method may vary slightly depending on the underlying color space. + * Although brighter and darker are inverse operations, the results of a series + * of invocations of these two methods might be inconsistent because of rounding + * errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #brighter + * @returns {pv.Color} a darker color. + */ +pv.Color.prototype.darker = function(k) { + return this.rgb().darker(k); +}; + +/** + * Constructs a new RGB color with the specified channel values. + * + * @param {number} r the red channel, an integer in [0,255]. + * @param {number} g the green channel, an integer in [0,255]. + * @param {number} b the blue channel, an integer in [0,255]. + * @param {number} [a] the alpha channel, a float in [0,1]. + * @returns pv.Color.Rgb + */ +pv.rgb = function(r, g, b, a) { + return new pv.Color.Rgb(r, g, b, (arguments.length == 4) ? a : 1); +}; + +/** + * Constructs a new RGB color with the specified channel values. + * + * @class Represents a color in RGB space. + * + * @param {number} r the red channel, an integer in [0,255]. + * @param {number} g the green channel, an integer in [0,255]. + * @param {number} b the blue channel, an integer in [0,255]. + * @param {number} a the alpha channel, a float in [0,1]. + * @extends pv.Color + */ +pv.Color.Rgb = function(r, g, b, a) { + pv.Color.call(this, a ? ("rgb(" + r + "," + g + "," + b + ")") : "none", a); + + /** + * The red channel, an integer in [0, 255]. + * + * @type number + */ + this.r = r; + + /** + * The green channel, an integer in [0, 255]. + * + * @type number + */ + this.g = g; + + /** + * The blue channel, an integer in [0, 255]. + * + * @type number + */ + this.b = b; + + /** + * The alpha channel, a float in [0, 1]. + * + * @type number + */ + this.a = a; +}; +pv.Color.Rgb.prototype = pv.extend(pv.Color); + +/** + * Constructs a new RGB color with the same green, blue and alpha channels as + * this color, with the specified red channel. + * + * @param {number} r the red channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.red = function(r) { + return pv.rgb(r, this.g, this.b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, blue and alpha channels as this + * color, with the specified green channel. + * + * @param {number} g the green channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.green = function(g) { + return pv.rgb(this.r, g, this.b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, green and alpha channels as + * this color, with the specified blue channel. + * + * @param {number} b the blue channel, an integer in [0,255]. + */ +pv.Color.Rgb.prototype.blue = function(b) { + return pv.rgb(this.r, this.g, b, this.a); +}; + +/** + * Constructs a new RGB color with the same red, green and blue channels as this + * color, with the specified alpha channel. + * + * @param {number} a the alpha channel, a float in [0,1]. + */ +pv.Color.Rgb.prototype.alpha = function(a) { + return pv.rgb(this.r, this.g, this.b, a); +}; + +/** + * Returns the RGB color equivalent to this color. This method is abstract and + * must be implemented by subclasses. + * + * @returns {pv.Color.Rgb} an RGB color. + * @function + * @name pv.Color.prototype.rgb + */ + +/** + * Returns this. + * + * @returns {pv.Color.Rgb} this. + */ +pv.Color.Rgb.prototype.rgb = function() { return this; }; + +/** + * Returns a new color that is a brighter version of this color. This method + * applies an arbitrary scale factor to each of the three RGB components of this + * color to create a brighter version of this color. Although brighter and + * darker are inverse operations, the results of a series of invocations of + * these two methods might be inconsistent because of rounding errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #darker + * @returns {pv.Color.Rgb} a brighter color. + */ +pv.Color.Rgb.prototype.brighter = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + var r = this.r, g = this.g, b = this.b, i = 30; + if (!r && !g && !b) return pv.rgb(i, i, i, this.a); + if (r && (r < i)) r = i; + if (g && (g < i)) g = i; + if (b && (b < i)) b = i; + return pv.rgb( + Math.min(255, Math.floor(r / k)), + Math.min(255, Math.floor(g / k)), + Math.min(255, Math.floor(b / k)), + this.a); +}; + +/** + * Returns a new color that is a darker version of this color. This method + * applies an arbitrary scale factor to each of the three RGB components of this + * color to create a darker version of this color. Although brighter and darker + * are inverse operations, the results of a series of invocations of these two + * methods might be inconsistent because of rounding errors. + * + * @param [k] {number} an optional scale factor; defaults to 1. + * @see #brighter + * @returns {pv.Color.Rgb} a darker color. + */ +pv.Color.Rgb.prototype.darker = function(k) { + k = Math.pow(0.7, arguments.length ? k : 1); + return pv.rgb( + Math.max(0, Math.floor(k * this.r)), + Math.max(0, Math.floor(k * this.g)), + Math.max(0, Math.floor(k * this.b)), + this.a); +}; + +/** + * Constructs a new HSL color with the specified values. + * + * @param {number} h the hue, an integer in [0, 360]. + * @param {number} s the saturation, a float in [0, 1]. + * @param {number} l the lightness, a float in [0, 1]. + * @param {number} [a] the opacity, a float in [0, 1]. + * @returns pv.Color.Hsl + */ +pv.hsl = function(h, s, l, a) { + return new pv.Color.Hsl(h, s, l, (arguments.length == 4) ? a : 1); +}; + +/** + * Constructs a new HSL color with the specified values. + * + * @class Represents a color in HSL space. + * + * @param {number} h the hue, an integer in [0, 360]. + * @param {number} s the saturation, a float in [0, 1]. + * @param {number} l the lightness, a float in [0, 1]. + * @param {number} a the opacity, a float in [0, 1]. + * @extends pv.Color + */ +pv.Color.Hsl = function(h, s, l, a) { + pv.Color.call(this, "hsl(" + h + "," + (s * 100) + "%," + (l * 100) + "%)", a); + + /** + * The hue, an integer in [0, 360]. + * + * @type number + */ + this.h = h; + + /** + * The saturation, a float in [0, 1]. + * + * @type number + */ + this.s = s; + + /** + * The lightness, a float in [0, 1]. + * + * @type number + */ + this.l = l; + + /** + * The opacity, a float in [0, 1]. + * + * @type number + */ + this.a = a; +}; +pv.Color.Hsl.prototype = pv.extend(pv.Color); + +/** + * Constructs a new HSL color with the same saturation, lightness and alpha as + * this color, and the specified hue. + * + * @param {number} h the hue, an integer in [0, 360]. + */ +pv.Color.Hsl.prototype.hue = function(h) { + return pv.hsl(h, this.s, this.l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, lightness and alpha as this + * color, and the specified saturation. + * + * @param {number} s the saturation, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.saturation = function(s) { + return pv.hsl(this.h, s, this.l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, saturation and alpha as this + * color, and the specified lightness. + * + * @param {number} l the lightness, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.lightness = function(l) { + return pv.hsl(this.h, this.s, l, this.a); +}; + +/** + * Constructs a new HSL color with the same hue, saturation and lightness as + * this color, and the specified alpha. + * + * @param {number} a the opacity, a float in [0, 1]. + */ +pv.Color.Hsl.prototype.alpha = function(a) { + return pv.hsl(this.h, this.s, this.l, a); +}; + +/** + * Returns the RGB color equivalent to this HSL color. + * + * @returns {pv.Color.Rgb} an RGB color. + */ +pv.Color.Hsl.prototype.rgb = function() { + var h = this.h, s = this.s, l = this.l; + + /* Some simple corrections for h, s and l. */ + h = h % 360; if (h < 0) h += 360; + s = Math.max(0, Math.min(s, 1)); + l = Math.max(0, Math.min(l, 1)); + + /* From FvD 13.37, CSS Color Module Level 3 */ + var m2 = (l <= .5) ? (l * (1 + s)) : (l + s - l * s); + var m1 = 2 * l - m2; + function v(h) { + if (h > 360) h -= 360; + else if (h < 0) h += 360; + if (h < 60) return m1 + (m2 - m1) * h / 60; + if (h < 180) return m2; + if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; + return m1; + } + function vv(h) { + return Math.round(v(h) * 255); + } + + return pv.rgb(vv(h + 120), vv(h), vv(h - 120), this.a); +}; + +/** + * @private SVG color keywords, per CSS Color Module Level 3. + * + * @see SVG color + * keywords + */ +pv.Color.names = { + aliceblue: "#f0f8ff", + antiquewhite: "#faebd7", + aqua: "#00ffff", + aquamarine: "#7fffd4", + azure: "#f0ffff", + beige: "#f5f5dc", + bisque: "#ffe4c4", + black: "#000000", + blanchedalmond: "#ffebcd", + blue: "#0000ff", + blueviolet: "#8a2be2", + brown: "#a52a2a", + burlywood: "#deb887", + cadetblue: "#5f9ea0", + chartreuse: "#7fff00", + chocolate: "#d2691e", + coral: "#ff7f50", + cornflowerblue: "#6495ed", + cornsilk: "#fff8dc", + crimson: "#dc143c", + cyan: "#00ffff", + darkblue: "#00008b", + darkcyan: "#008b8b", + darkgoldenrod: "#b8860b", + darkgray: "#a9a9a9", + darkgreen: "#006400", + darkgrey: "#a9a9a9", + darkkhaki: "#bdb76b", + darkmagenta: "#8b008b", + darkolivegreen: "#556b2f", + darkorange: "#ff8c00", + darkorchid: "#9932cc", + darkred: "#8b0000", + darksalmon: "#e9967a", + darkseagreen: "#8fbc8f", + darkslateblue: "#483d8b", + darkslategray: "#2f4f4f", + darkslategrey: "#2f4f4f", + darkturquoise: "#00ced1", + darkviolet: "#9400d3", + deeppink: "#ff1493", + deepskyblue: "#00bfff", + dimgray: "#696969", + dimgrey: "#696969", + dodgerblue: "#1e90ff", + firebrick: "#b22222", + floralwhite: "#fffaf0", + forestgreen: "#228b22", + fuchsia: "#ff00ff", + gainsboro: "#dcdcdc", + ghostwhite: "#f8f8ff", + gold: "#ffd700", + goldenrod: "#daa520", + gray: "#808080", + green: "#008000", + greenyellow: "#adff2f", + grey: "#808080", + honeydew: "#f0fff0", + hotpink: "#ff69b4", + indianred: "#cd5c5c", + indigo: "#4b0082", + ivory: "#fffff0", + khaki: "#f0e68c", + lavender: "#e6e6fa", + lavenderblush: "#fff0f5", + lawngreen: "#7cfc00", + lemonchiffon: "#fffacd", + lightblue: "#add8e6", + lightcoral: "#f08080", + lightcyan: "#e0ffff", + lightgoldenrodyellow: "#fafad2", + lightgray: "#d3d3d3", + lightgreen: "#90ee90", + lightgrey: "#d3d3d3", + lightpink: "#ffb6c1", + lightsalmon: "#ffa07a", + lightseagreen: "#20b2aa", + lightskyblue: "#87cefa", + lightslategray: "#778899", + lightslategrey: "#778899", + lightsteelblue: "#b0c4de", + lightyellow: "#ffffe0", + lime: "#00ff00", + limegreen: "#32cd32", + linen: "#faf0e6", + magenta: "#ff00ff", + maroon: "#800000", + mediumaquamarine: "#66cdaa", + mediumblue: "#0000cd", + mediumorchid: "#ba55d3", + mediumpurple: "#9370db", + mediumseagreen: "#3cb371", + mediumslateblue: "#7b68ee", + mediumspringgreen: "#00fa9a", + mediumturquoise: "#48d1cc", + mediumvioletred: "#c71585", + midnightblue: "#191970", + mintcream: "#f5fffa", + mistyrose: "#ffe4e1", + moccasin: "#ffe4b5", + navajowhite: "#ffdead", + navy: "#000080", + oldlace: "#fdf5e6", + olive: "#808000", + olivedrab: "#6b8e23", + orange: "#ffa500", + orangered: "#ff4500", + orchid: "#da70d6", + palegoldenrod: "#eee8aa", + palegreen: "#98fb98", + paleturquoise: "#afeeee", + palevioletred: "#db7093", + papayawhip: "#ffefd5", + peachpuff: "#ffdab9", + peru: "#cd853f", + pink: "#ffc0cb", + plum: "#dda0dd", + powderblue: "#b0e0e6", + purple: "#800080", + red: "#ff0000", + rosybrown: "#bc8f8f", + royalblue: "#4169e1", + saddlebrown: "#8b4513", + salmon: "#fa8072", + sandybrown: "#f4a460", + seagreen: "#2e8b57", + seashell: "#fff5ee", + sienna: "#a0522d", + silver: "#c0c0c0", + skyblue: "#87ceeb", + slateblue: "#6a5acd", + slategray: "#708090", + slategrey: "#708090", + snow: "#fffafa", + springgreen: "#00ff7f", + steelblue: "#4682b4", + tan: "#d2b48c", + teal: "#008080", + thistle: "#d8bfd8", + tomato: "#ff6347", + turquoise: "#40e0d0", + violet: "#ee82ee", + wheat: "#f5deb3", + white: "#ffffff", + whitesmoke: "#f5f5f5", + yellow: "#ffff00", + yellowgreen: "#9acd32" +}; +/** + * Returns a new categorical color encoding using the specified colors. The + * arguments to this method are an array of colors; see {@link pv.color}. For + * example, to create a categorical color encoding using the species + * attribute: + * + *

pv.colors("red", "green", "blue").by(function(d) d.species)
+ * + * The result of this expression can be used as a fill- or stroke-style + * property. This assumes that the data's species attribute is a + * string. + * + * @param {string} colors... categorical colors. + * @see pv.Scale.ordinal + * @returns {pv.Scale.ordinal} an ordinal color scale. + */ +pv.colors = function() { + var scale = pv.Scale.ordinal(); + scale.range.apply(scale, arguments); + return scale; +}; + +/** + * A collection of standard color palettes for categorical encoding. + * + * @namespace A collection of standard color palettes for categorical encoding. + */ +pv.Colors = {}; + +/** + * Returns a new 10-color scheme. The arguments to this constructor are + * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The + * following colors are used: + * + *
#1f77b4
+ *
#ff7f0e
+ *
#2ca02c
+ *
#d62728
+ *
#9467bd
+ *
#8c564b
+ *
#e377c2
+ *
#7f7f7f
+ *
#bcbd22
+ *
#17becf
+ * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color + */ +pv.Colors.category10 = function() { + var scale = pv.colors( + "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", + "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"); + scale.domain.apply(scale, arguments); + return scale; +}; + +/** + * Returns a new 20-color scheme. The arguments to this constructor are + * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The + * following colors are used: + * + *
#1f77b4
+ *
#aec7e8
+ *
#ff7f0e
+ *
#ffbb78
+ *
#2ca02c
+ *
#98df8a
+ *
#d62728
+ *
#ff9896
+ *
#9467bd
+ *
#c5b0d5
+ *
#8c564b
+ *
#c49c94
+ *
#e377c2
+ *
#f7b6d2
+ *
#7f7f7f
+ *
#c7c7c7
+ *
#bcbd22
+ *
#dbdb8d
+ *
#17becf
+ *
#9edae5
+ * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color +*/ +pv.Colors.category20 = function() { + var scale = pv.colors( + "#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", + "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", + "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", + "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"); + scale.domain.apply(scale, arguments); + return scale; +}; + +/** + * Returns a new alternative 19-color scheme. The arguments to this constructor + * are optional, and equivalent to calling + * {@link pv.Scale.OrdinalScale#domain}. The following colors are used: + * + *
#9c9ede
+ *
#7375b5
+ *
#4a5584
+ *
#cedb9c
+ *
#b5cf6b
+ *
#8ca252
+ *
#637939
+ *
#e7cb94
+ *
#e7ba52
+ *
#bd9e39
+ *
#8c6d31
+ *
#e7969c
+ *
#d6616b
+ *
#ad494a
+ *
#843c39
+ *
#de9ed6
+ *
#ce6dbd
+ *
#a55194
+ *
#7b4173
+ * + * @param {number...} domain... domain values. + * @returns {pv.Scale.ordinal} a new ordinal color scale. + * @see pv.color + */ +pv.Colors.category19 = function() { + var scale = pv.colors( + "#9c9ede", "#7375b5", "#4a5584", "#cedb9c", "#b5cf6b", + "#8ca252", "#637939", "#e7cb94", "#e7ba52", "#bd9e39", + "#8c6d31", "#e7969c", "#d6616b", "#ad494a", "#843c39", + "#de9ed6", "#ce6dbd", "#a55194", "#7b4173"); + scale.domain.apply(scale, arguments); + return scale; +}; +/** + * Returns a linear color ramp from the specified start color to the + * specified end color. The color arguments may be specified either as + * strings or as {@link pv.Color}s. + * + * @param {string} start the start color; may be a pv.Color. + * @param {string} end the end color; may be a pv.Color. + * @returns {Function} a color ramp from start to end. + * @see pv.Scale.linear + */ +pv.ramp = function(start, end) { + var scale = pv.Scale.linear(); + scale.range.apply(scale, arguments); + return scale; +}; +// TODO don't populate default attributes? + +/** + * @private + * @namespace + */ +pv.Scene = pv.SvgScene = {}; + +/** + * Updates the display for the specified array of scene nodes. + * + * @param scenes {array} an array of scene nodes. + */ +pv.SvgScene.updateAll = function(scenes) { + if (!scenes.length) return; + if ((scenes[0].reverse) + && (scenes.type != "line") + && (scenes.type != "area")) { + var reversed = pv.extend(scenes); + for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { + reversed[i] = scenes[j]; + } + scenes = reversed; + } + this.removeSiblings(this[scenes.type](scenes)); +}; + +/** + * Creates a new SVG element of the specified type. + * + * @param type {string} an SVG element type, such as "rect". + * @return a new SVG element. + */ +pv.SvgScene.create = function(type) { + return document.createElementNS(pv.ns.svg, type); +}; + +/** + * Expects the element e to be the specified type. If the element does + * not exist, a new one is created. If the element does exist but is the wrong + * type, it is replaced with the specified element. + * + * @param type {string} an SVG element type, such as "rect". + * @return a new SVG element. + */ +pv.SvgScene.expect = function(type, e) { + if (!e) return this.create(type); + if (e.tagName == "a") e = e.firstChild; + if (e.tagName == type) return e; + var n = this.create(type); + e.parentNode.replaceChild(n, e); + return n; +}; + +/** TODO */ +pv.SvgScene.append = function(e, scenes, index) { + e.$scene = {scenes:scenes, index:index}; + e = this.title(e, scenes[index]); + if (!e.parentNode) scenes.$g.appendChild(e); + return e.nextSibling; +}; + +/** + * Applies a title tooltip to the specified element e, using the + * title property of the specified scene node s. Note that + * this implementation does not create an SVG title element as a child + * of e; although this is the recommended standard, it is only + * supported in Opera. Instead, an anchor element is created around the element + * e, and the xlink:title attribute is set accordingly. + * + * @param e an SVG element. + * @param s a scene node. + */ +pv.SvgScene.title = function(e, s) { + var a = e.parentNode, t = String(s.title); + if (a && (a.tagName != "a")) a = null; + if (t) { + if (!a) { + a = this.create("a"); + if (e.parentNode) e.parentNode.replaceChild(a, e); + a.appendChild(e); + } + a.setAttributeNS(pv.ns.xlink, "title", t); + return a; + } + if (a) a.parentNode.replaceChild(e, a); + return e; +}; + +/** TODO */ +pv.SvgScene.dispatch = function(e) { + var t = e.target.$scene; + if (t) { + t.scenes.mark.dispatch(e.type, t.scenes, t.index); + e.preventDefault(); + } +}; + +/** TODO */ +pv.SvgScene.removeSiblings = function(e) { + while (e) { + var n = e.nextSibling; + e.parentNode.removeChild(e); + e = n; + } +}; +// TODO strokeStyle for areaSegment? + +pv.SvgScene.area = function(scenes) { + var e = scenes.$g.firstChild; + if (!scenes.length) return e; + var s = scenes[0]; + + /* segmented */ + if (s.segmented) return this.areaSegment(scenes); + + /* visible */ + if (!s.visible) return e; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) return e; + + /* points */ + var p1 = "", p2 = ""; + for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { + var si = scenes[i], sj = scenes[j]; + p1 += si.left + "," + si.top + " "; + p2 += (sj.left + sj.width) + "," + (sj.top + sj.height) + " "; + + /* interpolate (assume linear by default) */ + if (i < scenes.length - 1) { + var sk = scenes[i + 1], sl = scenes[j - 1]; + switch (s.interpolate) { + case "step-before": { + p1 += si.left + "," + sk.top + " "; + p2 += (sl.left + sl.width) + "," + (sj.top + sj.height) + " "; + break; + } + case "step-after": { + p1 += sk.left + "," + si.top + " "; + p2 += (sj.left + sj.width) + "," + (sl.top + sl.height) + " "; + break; + } + } + } + } + + e = this.expect("polygon", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("points", p1 + p2); + var fill = pv.color(s.fillStyle); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + var stroke = pv.color(s.strokeStyle); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + return this.append(e, scenes, 0); +}; + +pv.SvgScene.areaSegment = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0, n = scenes.length - 1; i < n; i++) { + var s1 = scenes[i], s2 = scenes[i + 1]; + + /* visible */ + if (!s1.visible || !s2.visible) continue; + var fill = pv.color(s1.fillStyle), stroke = pv.color(s1.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var p = s1.left + "," + s1.top + " " + + s2.left + "," + s2.top + " " + + (s2.left + s2.width) + "," + (s2.top + s2.height) + " " + + (s1.left + s1.width) + "," + (s1.top + s1.height); + + e = this.expect("polygon", e); + e.setAttribute("cursor", s1.cursor); + e.setAttribute("points", p); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s1.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.bar = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + e = this.expect("rect", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", Math.max(1E-10, s.width)); + e.setAttribute("height", Math.max(1E-10, s.height)); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.dot = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var radius = Math.sqrt(s.size), fillPath = "", strokePath = ""; + switch (s.shape) { + case "cross": { + fillPath = "M" + -radius + "," + -radius + + "L" + radius + "," + radius + + "M" + radius + "," + -radius + + "L" + -radius + "," + radius; + break; + } + case "triangle": { + var h = radius, w = radius * 2 / Math.sqrt(3); + fillPath = "M0," + h + + "L" + w +"," + -h + + " " + -w + "," + -h + + "Z"; + break; + } + case "diamond": { + radius *= Math.sqrt(2); + fillPath = "M0," + -radius + + "L" + radius + ",0" + + " 0," + radius + + " " + -radius + ",0" + + "Z"; + break; + } + case "square": { + fillPath = "M" + -radius + "," + -radius + + "L" + radius + "," + -radius + + " " + radius + "," + radius + + " " + -radius + "," + radius + + "Z"; + break; + } + case "tick": { + fillPath = "M0,0L0," + -s.size; + break; + } + default: { + function circle(r) { + return "M0," + r + + "A" + r + "," + r + " 0 1,1 0," + (-r) + + "A" + r + "," + r + " 0 1,1 0," + r + + "Z"; + } + if (s.lineWidth / 2 > radius) strokePath = circle(s.lineWidth); + fillPath = circle(radius); + break; + } + } + + /* transform */ + var transform = "translate(" + s.left + "," + s.top + ")" + + (s.angle ? " rotate(" + 180 * s.angle / Math.PI + ")" : ""); + + /* The normal fill path. */ + e = this.expect("path", e); + e.setAttribute("d", fillPath); + e.setAttribute("transform", transform); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("cursor", s.cursor); + if (strokePath) { + e.setAttribute("stroke", "none"); + } else { + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + } + e = this.append(e, scenes, i); + + /* The special-case stroke path. */ + if (strokePath) { + e = this.expect("path", e); + e.setAttribute("d", strokePath); + e.setAttribute("transform", transform); + e.setAttribute("fill", stroke.color); + e.setAttribute("fill-opacity", stroke.opacity); + e.setAttribute("cursor", s.cursor); + e = this.append(e, scenes, i); + } + } + return e; +}; +pv.SvgScene.image = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + + /* fill */ + e = this.fill(e, scenes, i); + + /* image */ + e = this.expect("image", e); + e.setAttribute("preserveAspectRatio", "none"); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", s.width); + e.setAttribute("height", s.height); + e.setAttribute("cursor", s.cursor); + e.setAttributeNS(pv.ns.xlink, "href", s.url); + e = this.append(e, scenes, i); + + /* stroke */ + e = this.stroke(e, scenes, i); + } + return e; +}; +pv.SvgScene.label = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.textStyle); + if (!fill.opacity) continue; + + /* text-baseline, text-align */ + var x = 0, y = 0, dy = 0, anchor = "start"; + switch (s.textBaseline) { + case "middle": dy = ".35em"; break; + case "top": dy = ".71em"; y = s.textMargin; break; + case "bottom": y = "-" + s.textMargin; break; + } + switch (s.textAlign) { + case "right": anchor = "end"; x = "-" + s.textMargin; break; + case "center": anchor = "middle"; break; + case "left": x = s.textMargin; break; + } + + e = this.expect("text", e); + e.setAttribute("pointer-events", "none"); + e.setAttribute("x", x); + e.setAttribute("y", y); + e.setAttribute("dy", dy); + e.setAttribute("text-anchor", anchor); + e.setAttribute("transform", + "translate(" + s.left + "," + s.top + ")" + + (s.textAngle ? " rotate(" + 180 * s.textAngle / Math.PI + ")" : "")); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.style.font = s.font; + e.style.textShadow = s.textShadow; + if (e.firstChild) e.firstChild.nodeValue = s.text; + else e.appendChild(document.createTextNode(s.text)); + e = this.append(e, scenes, i); + } + return e; +}; +// TODO fillStyle for lineSegment? +// TODO lineOffset for flow maps? + +pv.SvgScene.line = function(scenes) { + var e = scenes.$g.firstChild; + if (scenes.length < 2) return e; + var s = scenes[0]; + + /* segmented */ + if (s.segmented) return this.lineSegment(scenes); + + /* visible */ + if (!s.visible) return e; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) return e; + + /* points */ + var p = ""; + for (var i = 0; i < scenes.length; i++) { + var si = scenes[i]; + p += si.left + "," + si.top + " "; + + /* interpolate (assume linear by default) */ + if (i < scenes.length - 1) { + var sj = scenes[i + 1]; + switch (s.interpolate) { + case "step-before": { + p += si.left + "," + sj.top + " "; + break; + } + case "step-after": { + p += sj.left + "," + si.top + " "; + break; + } + } + } + } + + + e = this.expect("polyline", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("points", p); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + return this.append(e, scenes, 0); +}; + +pv.SvgScene.lineSegment = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0, n = scenes.length - 1; i < n; i++) { + var s1 = scenes[i], s2 = scenes[i + 1]; + + /* visible */ + if (!s1.visible || !s2.visible) continue; + var stroke = pv.color(s1.strokeStyle); + if (!stroke.opacity) continue; + + /* Line-line intersection, per Akenine-Moller 16.16.1. */ + function intersect(o1, d1, o2, d2) { + return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp()))); + } + + /* + * P1-P2 is the current line segment. V is a vector that is perpendicular to + * the line segment, and has length lineWidth / 2. ABCD forms the initial + * bounding box of the line segment (i.e., the line segment if we were to do + * no joins). + */ + var p1 = pv.vector(s1.left, s1.top), + p2 = pv.vector(s2.left, s2.top), + p = p2.minus(p1), + v = p.perp().norm(), + w = v.times(s1.lineWidth / 2), + a = p1.plus(w), + b = p2.plus(w), + c = p2.minus(w), + d = p1.minus(w); + + /* + * Start join. P0 is the previous line segment's start point. We define the + * cutting plane as the average of the vector perpendicular to P0-P1, and + * the vector perpendicular to P1-P2. This insures that the cross-section of + * the line on the cutting plane is equal if the line-width is unchanged. + * Note that we don't implement miter limits, so these can get wild. + */ + if (i > 0) { + var s0 = scenes[i - 1]; + if (s0.visible) { + var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v); + d = intersect(p1, v1, d, p); + a = intersect(p1, v1, a, p); + } + } + + /* Similarly, for end join. */ + if (i < (n - 1)) { + var s3 = scenes[i + 2]; + if (s3.visible) { + var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v); + c = intersect(p2, v2, c, p); + b = intersect(p2, v2, b, p); + } + } + + /* points */ + var p = a.x + "," + a.y + " " + + b.x + "," + b.y + " " + + c.x + "," + c.y + " " + + d.x + "," + d.y; + + e = this.expect("polygon", e); + e.setAttribute("cursor", s1.cursor); + e.setAttribute("points", p); + e.setAttribute("fill", stroke.color); + e.setAttribute("fill-opacity", stroke.opacity); + e = this.append(e, scenes, i); + } + return e; +}; +var guid = 0; + +pv.SvgScene.panel = function(scenes) { + var g = scenes.$g, e = g && g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + + /* svg */ + if (!scenes.parent) { + s.canvas.style.display = "inline-block"; + g = s.canvas.firstChild; + if (!g) { + g = s.canvas.appendChild(this.create("svg")); + g.onclick + = g.onmousedown + = g.onmouseup + = g.onmousemove + = g.onmouseout + = g.onmouseover + = pv.SvgScene.dispatch; + } + scenes.$g = g; + g.setAttribute("width", s.width + s.left + s.right); + g.setAttribute("height", s.height + s.top + s.bottom); + if (typeof e == "undefined") e = g.firstChild; + } + + /* clip (nest children) */ + if (s.overflow == "hidden") { + var c = this.expect("g", e), id = (guid++).toString(36); + c.setAttribute("clip-path", "url(#" + id + ")"); + if (!c.parentNode) g.appendChild(c); + scenes.$g = g = c; + e = c.firstChild; + + e = this.expect("clipPath", e); + e.setAttribute("id", id); + var r = e.firstChild || e.appendChild(this.create("rect")); + r.setAttribute("x", s.left); + r.setAttribute("y", s.top); + r.setAttribute("width", s.width); + r.setAttribute("height", s.height); + if (!e.parentNode) g.appendChild(e); + e = e.nextSibling; + } + + /* fill */ + e = this.fill(e, scenes, i); + + /* children */ + for (var j = 0; j < s.children.length; j++) { + s.children[j].$g = e = this.expect("g", e); + e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); + this.updateAll(s.children[j]); + if (!e.parentNode) g.appendChild(e); + e = e.nextSibling; + } + + /* stroke */ + e = this.stroke(e, scenes, i); + + /* clip (restore group) */ + if (s.overflow == "hidden") { + scenes.$g = g = c.parentNode; + e = c.nextSibling; + } + } + return e; +}; + +pv.SvgScene.fill = function(e, scenes, i) { + var s = scenes[i], fill = pv.color(s.fillStyle); + if (fill.opacity) { + e = this.expect("rect", e); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", s.width); + e.setAttribute("height", s.height); + e.setAttribute("cursor", s.cursor); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e = this.append(e, scenes, i); + } + return e; +}; + +pv.SvgScene.stroke = function(e, scenes, i) { + var s = scenes[i], stroke = pv.color(s.strokeStyle); + if (stroke.opacity) { + e = this.expect("rect", e); + e.setAttribute("x", s.left); + e.setAttribute("y", s.top); + e.setAttribute("width", Math.max(1E-10, s.width)); + e.setAttribute("height", Math.max(1E-10, s.height)); + e.setAttribute("cursor", s.cursor); + e.setAttribute("fill", "none"); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.rule = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var stroke = pv.color(s.strokeStyle); + if (!stroke.opacity) continue; + + e = this.expect("line", e); + e.setAttribute("cursor", s.cursor); + e.setAttribute("x1", s.left); + e.setAttribute("y1", s.top); + e.setAttribute("x2", s.left + s.width); + e.setAttribute("y2", s.top + s.height); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +pv.SvgScene.wedge = function(scenes) { + var e = scenes.$g.firstChild; + for (var i = 0; i < scenes.length; i++) { + var s = scenes[i]; + + /* visible */ + if (!s.visible) continue; + var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); + if (!fill.opacity && !stroke.opacity) continue; + + /* points */ + var r1 = s.innerRadius, r2 = s.outerRadius, a = Math.abs(s.angle), p; + if (a >= 2 * Math.PI) { + if (r1) { + p = "M0," + r2 + + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + + "M0," + r1 + + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) + + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 + + "Z"; + } else { + p = "M0," + r2 + + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) + + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 + + "Z"; + } + } else { + var sa = Math.min(s.startAngle, s.endAngle), + ea = Math.max(s.startAngle, s.endAngle), + c1 = Math.cos(sa), c2 = Math.cos(ea), + s1 = Math.sin(sa), s2 = Math.sin(ea); + if (r1) { + p = "M" + r2 * c1 + "," + r2 * s1 + + "A" + r2 + "," + r2 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",1 " + + r2 * c2 + "," + r2 * s2 + + "L" + r1 * c2 + "," + r1 * s2 + + "A" + r1 + "," + r1 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",0 " + + r1 * c1 + "," + r1 * s1 + "Z"; + } else { + p = "M" + r2 * c1 + "," + r2 * s1 + + "A" + r2 + "," + r2 + " 0 " + + ((a < Math.PI) ? "0" : "1") + ",1 " + + r2 * c2 + "," + r2 * s2 + "L0,0Z"; + } + } + + e = this.expect("path", e); + e.setAttribute("fill-rule", "evenodd"); + e.setAttribute("cursor", s.cursor); + e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); + e.setAttribute("d", p); + e.setAttribute("fill", fill.color); + e.setAttribute("fill-opacity", fill.opacity); + e.setAttribute("stroke", stroke.color); + e.setAttribute("stroke-opacity", stroke.opacity); + e.setAttribute("stroke-width", s.lineWidth); + e = this.append(e, scenes, i); + } + return e; +}; +/** + * Constructs a new mark with default properties. Marks, with the exception of + * the root panel, are not typically constructed directly; instead, they are + * added to a panel or an existing mark via {@link pv.Mark#add}. + * + * @class Represents a data-driven graphical mark. The Mark class is + * the base class for all graphical marks in Protovis; it does not provide any + * specific rendering functionality, but together with {@link Panel} establishes + * the core framework. + * + *

Concrete mark types include familiar visual elements such as bars, lines + * and labels. Although a bar mark may be used to construct a bar chart, marks + * know nothing about charts; it is only through their specification and + * composition that charts are produced. These building blocks permit many + * combinatorial possibilities. + * + *

Marks are associated with data: a mark is generated once per + * associated datum, mapping the datum to visual properties such as + * position and color. Thus, a single mark specification represents a set of + * visual elements that share the same data and visual encoding. The type of + * mark defines the names of properties and their meaning. A property may be + * static, ignoring the associated datum and returning a constant; or, it may be + * dynamic, derived from the associated datum or index. Such dynamic encodings + * can be specified succinctly using anonymous functions. Special properties + * called event handlers can be registered to add interactivity. + * + *

Protovis uses inheritance to simplify the specification of related + * marks: a new mark can be derived from an existing mark, inheriting its + * properties. The new mark can then override properties to specify new + * behavior, potentially in terms of the old behavior. In this way, the old mark + * serves as the prototype for the new mark. Most mark types share the + * same basic properties for consistency and to facilitate inheritance. + * + *

The prioritization of redundant properties is as follows:

    + * + *
  1. If the width property is not specified (i.e., null), its value + * is the width of the parent panel, minus this mark's left and right margins; + * the left and right margins are zero if not specified. + * + *
  2. Otherwise, if the right margin is not specified, its value is + * the width of the parent panel, minus this mark's width and left margin; the + * left margin is zero if not specified. + * + *
  3. Otherwise, if the left property is not specified, its value is + * the width of the parent panel, minus this mark's width and the right margin. + * + *
This prioritization is then duplicated for the height, + * bottom and top properties, respectively. + * + *

While most properties are variable, some mark types, such as lines + * and areas, generate a single visual element rather than a distinct visual + * element per datum. With these marks, some properties may be fixed. + * Fixed properties can vary per mark, but not per datum! These + * properties are evaluated solely for the first (0-index) datum, and typically + * are specified as a constant. However, it is valid to use a function if the + * property varies between panels or is dynamically generated. + * + *

See also the Protovis guide. + */ +pv.Mark = function() { + /* + * TYPE 0 constant defs + * TYPE 1 function defs + * TYPE 2 constant properties + * TYPE 3 function properties + * in order of evaluation! + */ + this.$properties = []; +}; + +/** @private TOOD */ +pv.Mark.prototype.properties = {}; + +/** + * @private Defines and registers a property method for the property with the + * given name. This method should be called on a mark class prototype to define + * each exposed property. (Note this refers to the JavaScript + * prototype, not the Protovis mark prototype, which is the {@link + * #proto} field.) + * + *

The created property method supports several modes of invocation:

    + * + *
  1. If invoked with a Function argument, this function is evaluated + * for each associated datum. The return value of the function is used as the + * computed property value. The context of the function (this) is this + * mark. The arguments to the function are the associated data of this mark and + * any enclosing panels. For example, a linear encoding of numerical data to + * height is specified as + * + *
    m.height(function(d) d * 100);
    + * + * The expression d * 100 will be evaluated for the height property of + * each mark instance. The return value of the property method (e.g., + * m.height) is this mark (m)).

    + * + *

  2. If invoked with a non-function argument, the property is treated as a + * constant. The return value of the property method (e.g., m.height) + * is this mark.

    + * + *

  3. If invoked with no arguments, the computed property value for the current + * mark instance in the scene graph is returned. This facilitates property + * chaining, where one mark's properties are defined in terms of another's. + * For example, to offset a mark's location from its prototype, you might say + * + *
    m.top(function() this.proto.top() + 10);
    + * + * Note that the index of the mark being evaluated (in the above example, + * this.proto) is inherited from the Mark class and set by + * this mark. So, if the fifth element's top property is being evaluated, the + * fifth instance of this.proto will similarly be queried for the value + * of its top property. If the mark being evaluated has a different number of + * instances, or its data is unrelated, the behavior of this method is + * undefined. In these cases it may be better to index the scene + * explicitly to specify the exact instance. + * + *

Property names should follow standard JavaScript method naming + * conventions, using lowerCamel-style capitalization. + * + *

In addition to creating the property method, every property is registered + * in the {@link #properties} map on the prototype. Although this is an + * instance field, it is considered immutable and shared by all instances of a + * given mark type. The properties map can be queried to see if a mark + * type defines a particular property, such as width or height. + * + * @param {string} name the property name. + */ +pv.Mark.prototype.property = function(name) { + if (!this.hasOwnProperty("properties")) { + this.properties = pv.extend(this.properties); + } + this.properties[name] = true; + + /* + * Define the setter-getter globally, since the default behavior should be the + * same for all properties, and since the Protovis inheritance chain is + * independent of the JavaScript inheritance chain. For example, anchors + * define a "name" property that is evaluated on derived marks, even though + * those marks don't normally have a name. + */ + pv.Mark.prototype[name] = function(v) { + if (arguments.length) { + this.$properties.push({ + name: name, + type: (typeof v == "function") ? 3 : 2, + value: v + }); + return this; + } + return this.scene[this.index][name]; + }; + + return this; +}; + +/* Define all global properties. */ +pv.Mark.prototype + .property("data") + .property("visible") + .property("left") + .property("right") + .property("top") + .property("bottom") + .property("cursor") + .property("title") + .property("reverse"); + +/** + * The mark type; a lower camelCase name. The type name controls rendering + * behavior, and unless the rendering engine is extended, must be one of the + * built-in concrete mark types: area, bar, dot, image, label, line, panel, + * rule, or wedge. + * + * @type string + * @name pv.Mark.prototype.type + */ + +/** + * The mark prototype, possibly undefined, from which to inherit property + * functions. The mark prototype is not necessarily of the same type as this + * mark. Any properties defined on this mark will override properties inherited + * either from the prototype or from the type-specific defaults. + * + * @type pv.Mark + * @name pv.Mark.prototype.proto + */ + +/** + * The enclosing parent panel. The parent panel is generally undefined only for + * the root panel; however, it is possible to create "offscreen" marks that are + * used only for inheritance purposes. + * + * @type pv.Panel + * @name pv.Mark.prototype.parent + */ + +/** + * The child index. -1 if the enclosing parent panel is null; otherwise, the + * zero-based index of this mark into the parent panel's children array. + * + * @type number + */ +pv.Mark.prototype.childIndex = -1; + +/** + * The mark index. The value of this field depends on which instance (i.e., + * which element of the data array) is currently being evaluated. During the + * build phase, the index is incremented over each datum; when handling events, + * the index is set to the instance that triggered the event. + * + * @type number + */ +pv.Mark.prototype.index = -1; + +/** + * The scene graph. The scene graph is an array of objects; each object (or + * "node") corresponds to an instance of this mark and an element in the data + * array. The scene graph can be traversed to lookup previously-evaluated + * properties. + * + *

For instance, consider a stacked area chart. The bottom property of the + * area can be defined using the cousin instance, which is the current + * area instance in the previous instantiation of the parent panel. In this + * sample code, + * + *

new pv.Panel()
+ *     .width(150).height(150)
+ *   .add(pv.Panel)
+ *     .data([[1, 1.2, 1.7, 1.5, 1.7],
+ *            [.5, 1, .8, 1.1, 1.3],
+ *            [.2, .5, .8, .9, 1]])
+ *   .add(pv.Area)
+ *     .data(function(d) d)
+ *     .bottom(function() {
+ *         var c = this.cousin();
+ *         return c ? (c.bottom + c.height) : 0;
+ *       })
+ *     .height(function(d) d * 40)
+ *     .left(function() this.index * 35)
+ *   .root.render();
+ * + * the bottom property is computed based on the upper edge of the corresponding + * datum in the previous series. The area's parent panel is instantiated once + * per series, so the cousin refers to the previous (below) area mark. (Note + * that the position of the upper edge is not the same as the top property, + * which refers to the top margin: the distance from the top edge of the panel + * to the top edge of the mark.) + * + * @see #first + * @see #last + * @see #sibling + * @see #cousin + * @name pv.Mark.prototype.scene + */ + +/** + * The root parent panel. This may be undefined for "offscreen" marks that are + * created for inheritance purposes only. + * + * @type pv.Panel + * @name pv.Mark.prototype.root + */ + +/** + * The data property; an array of objects. The size of the array determines the + * number of marks that will be instantiated; each element in the array will be + * passed to property functions to compute the property values. Typically, the + * data property is specified as a constant array, such as + * + *
m.data([1, 2, 3, 4, 5]);
+ * + * However, it is perfectly acceptable to define the data property as a + * function. This function might compute the data dynamically, allowing + * different data to be used per enclosing panel. For instance, in the stacked + * area graph example (see {@link #scene}), the data function on the area mark + * dereferences each series. + * + * @type array + * @name pv.Mark.prototype.data + */ + +/** + * The visible property; a boolean determining whether or not the mark instance + * is visible. If a mark instance is not visible, its other properties will not + * be evaluated. Similarly, for panels no child marks will be rendered. + * + * @type boolean + * @name pv.Mark.prototype.visible + */ + +/** + * The left margin; the distance, in pixels, between the left edge of the + * enclosing panel and the left edge of this mark. Note that in some cases this + * property may be redundant with the right property, or with the conjunction of + * right and width. + * + * @type number + * @name pv.Mark.prototype.left + */ + +/** + * The right margin; the distance, in pixels, between the right edge of the + * enclosing panel and the right edge of this mark. Note that in some cases this + * property may be redundant with the left property, or with the conjunction of + * left and width. + * + * @type number + * @name pv.Mark.prototype.right + */ + +/** + * The top margin; the distance, in pixels, between the top edge of the + * enclosing panel and the top edge of this mark. Note that in some cases this + * property may be redundant with the bottom property, or with the conjunction + * of bottom and height. + * + * @type number + * @name pv.Mark.prototype.top + */ + +/** + * The bottom margin; the distance, in pixels, between the bottom edge of the + * enclosing panel and the bottom edge of this mark. Note that in some cases + * this property may be redundant with the top property, or with the conjunction + * of top and height. + * + * @type number + * @name pv.Mark.prototype.bottom + */ + +/** + * The cursor property; corresponds to the CSS cursor property. This is + * typically used in conjunction with event handlers to indicate interactivity. + * + * @type string + * @name pv.Mark.prototype.cursor + * @see CSS2 cursor + */ + +/** + * The title property; corresponds to the HTML/SVG title property, allowing the + * general of simple plain text tooltips. + * + * @type string + * @name pv.Mark.prototype.title + */ + +/** + * The reverse property; a boolean determining whether marks are ordered from + * front-to-back or back-to-front. SVG does not support explicit z-ordering; + * shapes are rendered in the order they appear. Thus, by default, marks are + * rendered in data order. Setting the reverse property to false reverses the + * order in which they are rendered; however, the properties are still evaluated + * (i.e., built) in forward order. + * + * @type boolean + * @name pv.Mark.prototype.reverse + */ + +/** + * Default properties for all mark types. By default, the data array is the + * parent data as a single-element array; if the data property is not specified, + * this causes each mark to be instantiated as a singleton with the parents + * datum. The visible property is true by default, and the reverse property is + * false. + * + * @type pv.Mark + */ +pv.Mark.prototype.defaults = new pv.Mark() + .data(function(d) { return [d]; }) + .visible(true) + .reverse(false) + .cursor("") + .title(""); + +/* Private categorical colors for default fill & stroke styles. */ +var defaultFillStyle = pv.Colors.category20().by(pv.parent), + defaultStrokeStyle = pv.Colors.category10().by(pv.parent); + +/** + * Sets the prototype of this mark to the specified mark. Any properties not + * defined on this mark may be inherited from the specified prototype mark, or + * its prototype, and so on. The prototype mark need not be the same type of + * mark as this mark. (Note that for inheritance to be useful, properties with + * the same name on different mark types should have equivalent meaning.) + * + * @param {pv.Mark} proto the new prototype. + * @return {pv.Mark} this mark. + * @see #add + */ +pv.Mark.prototype.extend = function(proto) { + this.proto = proto; + return this; +}; + +/** + * Adds a new mark of the specified type to the enclosing parent panel, whilst + * simultaneously setting the prototype of the new mark to be this mark. + * + * @param {function} type the type of mark to add; a constructor, such as + * pv.Bar. + * @return {pv.Mark} the new mark. + * @see #extend + */ +pv.Mark.prototype.add = function(type) { + return this.parent.add(type).extend(this); +}; + +/** + * Defines a local variable on this mark. Local variables are initialized once + * per mark (i.e., per parent panel instance), and can be used to store local + * state for the mark. Here are a few reasons you might want to use + * def: + * + *

1. To store local state. For example, say you were visualizing employment + * statistics, and your root panel had an array of occupations. In a child + * panel, you might want to initialize a local scale, and reference it from a + * property function: + * + *

.def("y", function(d) pv.Scale.linear(0, pv.max(d.values)).range(0, h))
+ * .height(function(d) this.y()(d))
+ * + * In this example, this.y() returns the defined local scale. We then + * invoke the scale function, passing in the datum, to compute the height. Note + * that defs are similar to fixed properties: they are only evaluated once per + * parent panel, and this.y() returns a function, rather than + * automatically evaluating this function as a property. + * + *

2. To store temporary state for interaction. Say you have an array of + * bars, and you want to color the bar differently if the mouse is over it. Use + * def to define a local variable, and event handlers to override this + * variable interactively: + * + *

.def("i", -1)
+ * .event("mouseover", function() this.i(this.index))
+ * .event("mouseout", function() this.i(-1))
+ * .fillStyle(function() this.i() == this.index ? "red" : "blue")
+ * + * Notice that this.i() can be used both to set the value of i + * (when an argument is specified), and to get the value of i (when no + * arguments are specified). In this way, it's like other property methods. + * + *

3. To specify fixed properties efficiently. Sometimes, the value of a + * property may be locally a constant, but dependent on parent panel data which + * is variable. In this scenario, you can use def to define a property; + * it will only get computed once per mark, rather than once per datum. + * + * @param {string} name the name of the local variable. + * @param {function} [value] an optional initializer; may be a constant or a + * function. + */ +pv.Mark.prototype.def = function(name, value) { + this.$properties.push({ + name: name, + type: (typeof value == "function") ? 1 : 0, + value: value + }); + return this; +}; + +/** + * Returns an anchor with the specified name. While anchor names are typically + * constants, the anchor name is a true property, which means you can specify a + * function to compute the anchor name dynamically. See the + * {@link pv.Anchor#name} property for details. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} the new anchor. + */ +pv.Mark.prototype.anchor = function(name) { + var anchor = new pv.Anchor().extend(this).name(name); + anchor.parent = this.parent; + return anchor; +}; + +/** + * Returns the anchor target of this mark, if it is derived from an anchor; + * otherwise returns null. For example, if a label is derived from a bar anchor, + * + *

bar.anchor("top").add(pv.Label);
+ * + * then property functions on the label can refer to the bar via the + * anchorTarget method. This method is also useful for mark types + * defining properties on custom anchors. + * + * @returns {pv.Mark} the anchor target of this mark; possibly null. + */ +pv.Mark.prototype.anchorTarget = function() { + var target = this; + while (!(target instanceof pv.Anchor)) { + target = target.proto; + if (!target) return null; + } + return target.proto; +}; + +/** + * Returns the first instance of this mark in the scene graph. This method can + * only be called when the mark is bound to the scene graph (for example, from + * an event handler, or within a property function). + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.first = function() { + return this.scene[0]; +}; + +/** + * Returns the last instance of this mark in the scene graph. This method can + * only be called when the mark is bound to the scene graph (for example, from + * an event handler, or within a property function). In addition, note that mark + * instances are built sequentially, so the last instance of this mark may not + * yet be constructed. + * + * @returns a node in the scene graph. + */ +pv.Mark.prototype.last = function() { + return this.scene[this.scene.length - 1]; +}; + +/** + * Returns the previous instance of this mark in the scene graph, or null if + * this is the first instance. + * + * @returns a node in the scene graph, or null. + */ +pv.Mark.prototype.sibling = function() { + return (this.index == 0) ? null : this.scene[this.index - 1]; +}; + +/** + * Returns the current instance in the scene graph of this mark, in the previous + * instance of the enclosing parent panel. May return null if this instance + * could not be found. See the {@link pv.Layout.stack} function for an example + * property function using cousin. + * + * @see pv.Layout.stack + * @returns a node in the scene graph, or null. + */ +pv.Mark.prototype.cousin = function() { + var p = this.parent, s = p && p.sibling(); + return (s && s.children) ? s.children[this.childIndex][this.index] : null; +}; + +/** + * Renders this mark, including recursively rendering all child marks if this is + * a panel. + */ +pv.Mark.prototype.render = function() { + /* + * Rendering consists of three phases: bind, build and update. The update + * phase is decoupled to allow different rendering engines. + * + * In the bind phase, inherited property definitions are cached so they do not + * need to be queried during build. In the build phase, properties are + * evaluated, and the scene graph is generated. In the update phase, the scene + * is rendered by creating and updating elements and attributes in the SVG + * image. No properties are evaluated during the update phase; instead the + * values computed previously in the build phase are simply translated into + * SVG. + */ + this.bind(); + this.build(); + pv.Scene.updateAll(this.scene); +}; + +/** @private Computes the root data stack for the specified mark. */ +function argv(mark) { + var stack = []; + while (mark) { + stack.push(mark.scene[mark.index].data); + mark = mark.parent; + } + return stack; +} + +/** @private TODO */ +pv.Mark.prototype.bind = function() { + var seen = {}, types = [[], [], [], []], data, visible; + + /** TODO */ + function bind(mark) { + do { + var properties = mark.$properties; + for (var i = properties.length - 1; i >= 0 ; i--) { + var p = properties[i]; + if (!(p.name in seen)) { + seen[p.name] = 1; + switch (p.name) { + case "data": data = p; break; + case "visible": visible = p; break; + default: types[p.type].push(p); break; + } + } + } + } while (mark = mark.proto); + } + + /** TODO */ + function def(name) { + return function(v) { + var defs = this.scene.defs; + if (arguments.length) { + if (v == undefined) { + delete defs.locked[name]; + } else { + defs.locked[name] = true; + } + defs.values[name] = v; + return this; + } else { + return defs.values[name]; + } + }; + } + + /* Scan the proto chain for all defined properties. */ + bind(this); + bind(this.defaults); + types[1].reverse(); + types[3].reverse(); + + /* Any undefined properties are null. */ + var mark = this; + do for (var name in mark.properties) { + if (!(name in seen)) { + seen[name] = 1; + types[2].push({name: name, type: 2, value: null}); + } + } while (mark = mark.proto); + + /* Define setter-getter for inherited defs. */ + var defs = types[0].concat(types[1]); + for (var i = 0; i < defs.length; i++) { + var d = defs[i]; + this[d.name] = def(d.name); + } + + /* Setup binds to evaluate constants before functions. */ + this.binds = { + data: data, + visible: visible, + defs: defs, + properties: pv.blend(types) + }; +}; + +/** + * @private Evaluates properties and computes implied properties. Properties are + * stored in the {@link #scene} array for each instance of this mark. + * + *

As marks are built recursively, the {@link #index} property is updated to + * match the current index into the data array for each mark. Note that the + * index property is only set for the mark currently being built and its + * enclosing parent panels. The index property for other marks is unset, but is + * inherited from the global Mark class prototype. This allows mark + * properties to refer to properties on other marks in the same panel + * conveniently; however, in general it is better to reference mark instances + * specifically through the scene graph rather than depending on the magical + * behavior of {@link #index}. + * + *

The root scene array has a special property, data, which stores + * the current data stack. The first element in this stack is the current datum, + * followed by the datum of the enclosing parent panel, and so on. The data + * stack should not be accessed directly; instead, property functions are passed + * the current data stack as arguments. + * + *

The evaluation of the data and visible properties is + * special. The data property is evaluated first; unlike the other + * properties, the data stack is from the parent panel, rather than the current + * mark, since the data is not defined until the data property is evaluated. + * The visisble property is subsequently evaluated for each instance; + * only if true will the {@link #buildInstance} method be called, evaluating + * other properties and recursively building the scene graph. + * + *

If this mark is being re-built, any old instances of this mark that no + * longer exist (because the new data array contains fewer elements) will be + * cleared using {@link #clearInstance}. + * + * @param parent the instance of the parent panel from the scene graph. + */ +pv.Mark.prototype.build = function() { + var scene = this.scene; + if (!scene) { + scene = this.scene = []; + scene.mark = this; + scene.type = this.type; + scene.childIndex = this.childIndex; + if (this.parent) { + scene.parent = this.parent.scene; + scene.parentIndex = this.parent.index; + } + } + + /* Set the data stack. */ + var stack = this.root.scene.data; + if (!stack) this.root.scene.data = stack = argv(this.parent); + + /* Evaluate defs. */ + if (this.binds.defs.length) { + var defs = scene.defs; + if (!defs) scene.defs = defs = {values: {}, locked: {}}; + for (var i = 0; i < this.binds.defs.length; i++) { + var d = this.binds.defs[i]; + if (!(d.name in defs.locked)) { + var v = d.value; + if (d.type == 1) { + property = d.name; + v = v.apply(this, stack); + } + defs.values[d.name] = v; + } + } + } + + /* Evaluate special data property. */ + var data = this.binds.data; + switch (data.type) { + case 0: case 1: data = defs.values.data; break; + case 2: data = data.value; break; + case 3: { + property = "data"; + data = data.value.apply(this, stack); + break; + } + } + + /* Create, update and delete scene nodes. */ + stack.unshift(null); + scene.length = data.length; + for (var i = 0; i < data.length; i++) { + pv.Mark.prototype.index = this.index = i; + var s = scene[i]; + if (!s) scene[i] = s = {}; + s.data = stack[0] = data[i]; + + /* Evaluate special visible property. */ + var visible = this.binds.visible; + switch (visible.type) { + case 0: case 1: visible = defs.values.visible; break; + case 2: visible = visible.value; break; + case 3: { + property = "visible"; + visible = visible.value.apply(this, stack); + break; + } + } + + if (s.visible = visible) this.buildInstance(s); + } + stack.shift(); + delete this.index; + pv.Mark.prototype.index = -1; + if (!this.parent) scene.data = null; + + return this; +}; + +/** + * @private Evaluates the specified array of properties for the specified + * instance s in the scene graph. + * + * @param s a node in the scene graph; the instance of the mark to build. + * @param properties an array of properties. + */ +pv.Mark.prototype.buildProperties = function(s, properties) { + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i], v = p.value; + switch (p.type) { + case 0: case 1: v = this.scene.defs.values[p.name]; break; + case 3: { + property = p.name; + v = v.apply(this, this.root.scene.data); + break; + } + } + s[p.name] = v; + } +}; + +/** + * @private Evaluates all of the properties for this mark for the specified + * instance s in the scene graph. The set of properties to evaluate is + * retrieved from the {@link #properties} array for this mark type (see {@link + * #type}). After these properties are evaluated, any implied properties + * may be computed by the mark and set on the scene graph; see + * {@link #buildImplied}. + * + *

For panels, this method recursively builds the scene graph for all child + * marks as well. In general, this method should not need to be overridden by + * concrete mark types. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Mark.prototype.buildInstance = function(s) { + this.buildProperties(s, this.binds.properties); + this.buildImplied(s); +}; + +/** + * @private Computes the implied properties for this mark for the specified + * instance s in the scene graph. Implied properties are those with + * dependencies on multiple other properties; for example, the width property + * may be implied if the left and right properties are set. This method can be + * overridden by concrete mark types to define new implied properties, if + * necessary. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Mark.prototype.buildImplied = function(s) { + var l = s.left; + var r = s.right; + var t = s.top; + var b = s.bottom; + + /* Assume width and height are zero if not supported by this mark type. */ + var p = this.properties; + var w = p.width ? s.width : 0; + var h = p.height ? s.height : 0; + + /* Compute implied width, right and left. */ + var width = this.parent ? this.parent.width() : (w + l + r); + if (w == null) { + w = width - (r = r || 0) - (l = l || 0); + } else if (r == null) { + r = width - w - (l = l || 0); + } else if (l == null) { + l = width - w - (r = r || 0); + } + + /* Compute implied height, bottom and top. */ + var height = this.parent ? this.parent.height() : (h + t + b); + if (h == null) { + h = height - (t = t || 0) - (b = b || 0); + } else if (b == null) { + b = height - h - (t = t || 0); + } else if (t == null) { + t = height - h - (b = b || 0); + } + + s.left = l; + s.right = r; + s.top = t; + s.bottom = b; + + /* Only set width and height if they are supported by this mark type. */ + if (p.width) s.width = w; + if (p.height) s.height = h; +}; + +/** + * @private The name of the property being evaluated, for so-called "smart" + * functions that change behavior depending on which property is being + * evaluated. This functionality is somewhat magical, so for now, this feature + * is not exposed outside the library. + * + * @type string + */ +var property; + +/** @private The current mouse location. */ +var pageX = 0, pageY = 0; +pv.listen(window, "mousemove", function(e) { pageX = e.pageX; pageY = e.pageY; }); + +/** + * Returns the current location of the mouse (cursor) relative to this mark's + * parent. The x coordinate corresponds to the left margin, while the + * y coordinate corresponds to the top margin. + * + * @returns {pv.Vector} the mouse location. + */ +pv.Mark.prototype.mouse = function() { + var x = 0, y = 0, mark = (this instanceof pv.Panel) ? this : this.parent; + do { + x += mark.left(); + y += mark.top(); + } while (mark = mark.parent); + var node = this.root.canvas(); + do { + x += node.offsetLeft; + y += node.offsetTop; + } while (node = node.offsetParent); + return pv.vector(pageX - x, pageY - y); +}; + +/** + * Registers an event handler for the specified event type with this mark. When + * an event of the specified type is triggered, the specified handler will be + * invoked. The handler is invoked in a similar method to property functions: + * the context is this mark instance, and the arguments are the full + * data stack. Event handlers can use property methods to manipulate the display + * properties of the mark: + * + *

m.event("click", function() this.fillStyle("red"));
+ * + * Alternatively, the external data can be manipulated and the visualization + * redrawn: + * + *
m.event("click", function(d) {
+ *     data = all.filter(function(k) k.name == d);
+ *     vis.render();
+ *   });
+ * + * The return value of the event handler determines which mark gets re-rendered. + * Use defs ({@link #def}) to set temporary state from event handlers. + * + *

The complete set of event types is defined by SVG; see the reference + * below. The set of supported event types is:

    + * + *
  • click + *
  • mousedown + *
  • mouseup + *
  • mouseover + *
  • mousemove + *
  • mouseout + * + *
Since Protovis does not specify any concept of focus, it does not + * support key events; these should be handled outside the visualization using + * standard JavaScript. In the future, support for interaction may be extended + * to support additional event types, particularly those most relevant to + * interactive visualization, such as selection. + * + *

TODO In the current implementation, event handlers are not inherited from + * prototype marks. They must be defined explicitly on each interactive mark. In + * addition, only one event handler for a given event type can be defined; when + * specifying multiple event handlers for the same type, only the last one will + * be used. + * + * @see SVG events + * @param {string} type the event type. + * @param {function} handler the event handler. + * @returns {pv.Mark} this. + */ +pv.Mark.prototype.event = function(type, handler) { + if (!this.$handlers) this.$handlers = {}; + this.$handlers[type] = handler; + return this; +}; + +/** @private TODO */ +pv.Mark.prototype.dispatch = function(type, scenes, index) { + var l = this.$handlers && this.$handlers[type]; + if (!l) { + if (this.parent) { + this.parent.dispatch(type, scenes.parent, scenes.parentIndex); + } + return; + } + try { + + /* Setup the scene stack. */ + var mark = this; + do { + mark.index = index; + mark.scene = scenes; + index = scenes.parentIndex; + scenes = scenes.parent; + } while (mark = mark.parent); + + /* Execute the event listener. */ + try { + mark = l.apply(this, this.root.scene.data = argv(this)); + } finally { + this.root.scene.data = null; + } + + /* Update the display. TODO dirtying. */ + if (mark instanceof pv.Mark) mark.render(); + + } finally { + + /* Restore the scene stack. */ + var mark = this; + do { + if (mark.parent) delete mark.scene; + delete mark.index; + } while (mark = mark.parent); + } +}; +/** + * Constructs a new mark anchor with default properties. + * + * @class Represents an anchor on a given mark. An anchor is itself a mark, but + * without a visual representation. It serves only to provide useful default + * properties that can be inherited by other marks. Each type of mark can define + * any number of named anchors for convenience. If the concrete mark type does + * not define an anchor implementation specifically, one will be inherited from + * the mark's parent class. + * + *

For example, the bar mark provides anchors for its four sides: left, + * right, top and bottom. Adding a label to the top anchor of a bar, + * + *

bar.anchor("top").add(pv.Label);
+ * + * will render a text label on the top edge of the bar; the top anchor defines + * the appropriate position properties (top and left), as well as text-rendering + * properties for convenience (textAlign and textBaseline). + * + * @extends pv.Mark + */ +pv.Anchor = function() { + pv.Mark.call(this); +}; + +pv.Anchor.prototype = pv.extend(pv.Mark) + .property("name"); + +/** + * The anchor name. The set of supported anchor names is dependent on the + * concrete mark type; see the mark type for details. For example, bars support + * left, right, top and bottom anchors. + * + *

While anchor names are typically constants, the anchor name is a true + * property, which means you can specify a function to compute the anchor name + * dynamically. For instance, if you wanted to alternate top and bottom anchors, + * saying + * + *

m.anchor(function() (this.index % 2) ? "top" : "bottom").add(pv.Dot);
+ * + * would have the desired effect. + * + * @type string + * @name pv.Anchor.prototype.name + */ +/** + * Constructs a new area mark with default properties. Areas are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents an area mark: the solid area between two series of + * connected line segments. Unsurprisingly, areas are used most frequently for + * area charts. + * + *

Just as a line represents a polyline, the Area mark type + * represents a polygon. However, an area is not an arbitrary polygon; + * vertices are paired either horizontally or vertically into parallel + * spans, and each span corresponds to an associated datum. Either the + * width or the height must be specified, but not both; this determines whether + * the area is horizontally-oriented or vertically-oriented. Like lines, areas + * can be stroked and filled with arbitrary colors. + * + *

See also the Area guide. + * + * @extends pv.Mark + */ +pv.Area = function() { + pv.Mark.call(this); +}; + +pv.Area.prototype = pv.extend(pv.Mark) + .property("width") + .property("height") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle") + .property("segmented") + .property("interpolate"); + +pv.Area.prototype.type = "area"; + +/** + * The width of a given span, in pixels; used for horizontal spans. If the width + * is specified, the height property should be 0 (the default). Either the top + * or bottom property should be used to space the spans vertically, typically as + * a multiple of the index. + * + * @type number + * @name pv.Area.prototype.width + */ + +/** + * The height of a given span, in pixels; used for vertical spans. If the height + * is specified, the width property should be 0 (the default). Either the left + * or right property should be used to space the spans horizontally, typically + * as a multiple of the index. + * + * @type number + * @name pv.Area.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * strokeStyle to stroke the perimeter of the area. Unlike the + * {@link Line} mark type, the entire perimeter is stroked, rather than just one + * edge. The default value of this property is 1.5, but since the default stroke + * style is null, area marks are not stroked by default. + * + *

This property is fixed for non-segmented areas. See + * {@link pv.Mark}. + * + * @type number + * @name pv.Area.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with lineWidth to + * stroke the perimeter of the area. Unlike the {@link Line} mark type, the + * entire perimeter is stroked, rather than just one edge. The default value of + * this property is null, meaning areas are not stroked by default. + * + *

This property is fixed for non-segmented areas. See + * {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.strokeStyle + * @see pv.color + */ + +/** + * The area fill style; if non-null, the interior of the polygon forming the + * area is filled with the specified color. The default value of this property + * is a categorical color. + * + *

This property is fixed for non-segmented areas. See + * {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.fillStyle + * @see pv.color + */ + +/** + * Whether the area is segmented; whether variations in fill style, stroke + * style, and the other properties are treated as fixed. Rendering segmented + * areas is noticeably slower than non-segmented areas. + * + *

This property is fixed. See {@link pv.Mark}. + * + * @type boolean + * @name pv.Area.prototype.segmented + */ + +/** + * How to interpolate between values. Linear interpolation ("linear") is the + * default, producing a straight line between points. For piecewise constant + * functions (i.e., step functions), either "step-before" or "step-after" can be + * specified. + * + *

Note: this property is currently supported only on non-segmented areas. + * + *

This property is fixed. See {@link pv.Mark}. + * + * @type string + * @name pv.Area.prototype.interpolate + */ + +/** + * Default properties for areas. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Area + */ +pv.Area.prototype.defaults = new pv.Area() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .fillStyle(defaultFillStyle) + .interpolate("linear"); + +/** + * Constructs a new area anchor with default properties. Areas support five + * different anchors:

    + * + *
  • top + *
  • left + *
  • center + *
  • bottom + *
  • right + * + *
In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear inside the area polygon. + * + *

To facilitate stacking of areas, the anchors are defined in terms of their + * opposite edge. For example, the top anchor defines the bottom property, such + * that the area grows upwards; the bottom anchor instead defines the top + * property, such that the area grows downwards. Of course, in general it is + * more robust to use panels and the cousin accessor to define stacked area + * marks; see {@link pv.Mark#scene} for an example. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Area.prototype.anchor = function(name) { + var area = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return area.left() + area.width() / 2; + case "right": return area.left() + area.width(); + } + return null; + }) + .right(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return area.right() + area.width() / 2; + case "left": return area.right() + area.width(); + } + return null; + }) + .top(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return area.top() + area.height() / 2; + case "bottom": return area.top() + area.height(); + } + return null; + }) + .bottom(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return area.bottom() + area.height() / 2; + case "top": return area.bottom() + area.height(); + } + return null; + }) + .textAlign(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return "center"; + case "right": return "right"; + } + return "left"; + }) + .textBaseline(function() { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "top"; + } + return "bottom"; + }); +}; + +/** + * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such + * that the width and height are set to zero if null. + * + * @param s a node in the scene graph; the instance of the mark to build. + */ +pv.Area.prototype.buildImplied = function(s) { + if (s.height == null) s.height = 0; + if (s.width == null) s.width = 0; + pv.Mark.prototype.buildImplied.call(this, s); +}; + +/** @private */ +var pv_Area_specials = {left:1, top:1, right:1, bottom:1, width:1, height:1, name:1}; + +/** @private */ +pv.Area.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + var binds = this.binds, + properties = binds.properties, + specials = binds.specials = []; + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i]; + if (p.name in pv_Area_specials) specials.push(p); + } +}; + +/** @private */ +pv.Area.prototype.buildInstance = function(s) { + if (this.index && !this.scene[0].segmented) { + this.buildProperties(s, this.binds.specials); + this.buildImplied(s); + } else { + pv.Mark.prototype.buildInstance.call(this, s); + } +}; +/** + * Constructs a new bar mark with default properties. Bars are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a bar: an axis-aligned rectangle that can be stroked and + * filled. Bars are used for many chart types, including bar charts, histograms + * and Gantt charts. Bars can also be used as decorations, for example to draw a + * frame border around a panel; in fact, a panel is a special type (a subclass) + * of bar. + * + *

Bars can be positioned in several ways. Most commonly, one of the four + * corners is fixed using two margins, and then the width and height properties + * determine the extent of the bar relative to this fixed location. For example, + * using the bottom and left properties fixes the bottom-left corner; the width + * then extends to the right, while the height extends to the top. As an + * alternative to the four corners, a bar can be positioned exclusively using + * margins; this is convenient as an inset from the containing panel, for + * example. See {@link pv.Mark} for details on the prioritization of redundant + * positioning properties. + * + *

See also the Bar guide. + * + * @extends pv.Mark + */ +pv.Bar = function() { + pv.Mark.call(this); +}; + +pv.Bar.prototype = pv.extend(pv.Mark) + .property("width") + .property("height") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle"); + +pv.Bar.prototype.type = "bar"; + +/** + * The width of the bar, in pixels. If the left position is specified, the bar + * extends rightward from the left edge; if the right position is specified, the + * bar extends leftward from the right edge. + * + * @type number + * @name pv.Bar.prototype.width + */ + +/** + * The height of the bar, in pixels. If the bottom position is specified, the + * bar extends upward from the bottom edge; if the top position is specified, + * the bar extends downward from the top edge. + * + * @type number + * @name pv.Bar.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * strokeStyle to stroke the bar's border. + * + * @type number + * @name pv.Bar.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with lineWidth to + * stroke the bar's border. The default value of this property is null, meaning + * bars are not stroked by default. + * + * @type string + * @name pv.Bar.prototype.strokeStyle + * @see pv.color + */ + +/** + * The bar fill style; if non-null, the interior of the bar is filled with the + * specified color. The default value of this property is a categorical color. + * + * @type string + * @name pv.Bar.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for bars. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Bar + */ +pv.Bar.prototype.defaults = new pv.Bar() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .fillStyle(defaultFillStyle); + +/** + * Constructs a new bar anchor with default properties. Bars support five + * different anchors:

    + * + *
  • top + *
  • left + *
  • center + *
  • bottom + *
  • right + * + *
In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text + * is rendered to appear inside the bar. + * + *

To facilitate stacking of bars, the anchors are defined in terms of their + * opposite edge. For example, the top anchor defines the bottom property, such + * that the bar grows upwards; the bottom anchor instead defines the top + * property, such that the bar grows downwards. Of course, in general it is more + * robust to use panels and the cousin accessor to define stacked bars; see + * {@link pv.Mark#scene} for an example. + * + *

Bar anchors also "smartly" specify position properties based on whether + * the derived mark type supports the width and height properties. If the + * derived mark type does not support these properties (e.g., dots), the + * position will be centered on the corresponding edge. Otherwise (e.g., bars), + * the position will be in the opposite side. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Bar.prototype.anchor = function(name) { + var bar = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return bar.left() + (this.properties.width ? 0 : (bar.width() / 2)); + case "right": return bar.left() + bar.width(); + } + return null; + }) + .right(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return bar.right() + (this.properties.width ? 0 : (bar.width() / 2)); + case "left": return bar.right() + bar.width(); + } + return null; + }) + .top(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return bar.top() + (this.properties.height ? 0 : (bar.height() / 2)); + case "bottom": return bar.top() + bar.height(); + } + return null; + }) + .bottom(function() { + switch (this.name()) { + case "left": + case "right": + case "center": return bar.bottom() + (this.properties.height ? 0 : (bar.height() / 2)); + case "top": return bar.bottom() + bar.height(); + } + return null; + }) + .textAlign(function() { + switch (this.name()) { + case "bottom": + case "top": + case "center": return "center"; + case "right": return "right"; + } + return "left"; + }) + .textBaseline(function() { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "top"; + } + return "bottom"; + }); +}; +/** + * Constructs a new dot mark with default properties. Dots are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a dot; a dot is simply a sized glyph centered at a given + * point that can also be stroked and filled. The size property is + * proportional to the area of the rendered glyph to encourage meaningful visual + * encodings. Dots can visually encode up to eight dimensions of data, though + * this may be unwise due to integrality. See {@link pv.Mark} for details on the + * prioritization of redundant positioning properties. + * + *

See also the Dot guide. + * + * @extends pv.Mark + */ +pv.Dot = function() { + pv.Mark.call(this); +}; + +pv.Dot.prototype = pv.extend(pv.Mark) + .property("size") + .property("shape") + .property("angle") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle"); + +pv.Dot.prototype.type = "dot"; + +/** + * The size of the dot, in square pixels. Square pixels are used such that the + * area of the dot is linearly proportional to the value of the size property, + * facilitating representative encodings. + * + * @see #radius + * @type number + * @name pv.Dot.prototype.size + */ + +/** + * The shape name. Several shapes are supported:

    + * + *
  • cross + *
  • triangle + *
  • diamond + *
  • square + *
  • tick + *
  • circle + * + *
These shapes can be further changed using the {@link #angle} property; + * for instance, a cross can be turned into a plus by rotating. Similarly, the + * tick, which is vertical by default, can be rotated horizontally. Note that + * some shapes (cross and tick) do not have interior areas, and thus do not + * support fill style meaningfully. + * + *

Note: it may be more natural to use the {@link pv.Rule} mark for + * horizontal and vertical ticks. The tick shape is only necessary if angled + * ticks are needed. + * + * @type string + * @name pv.Dot.prototype.shape + */ + +/** + * The rotation angle, in radians. Used to rotate shapes, such as to turn a + * cross into a plus. + * + * @type number + * @name pv.Dot.prototype.angle + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * strokeStyle to stroke the dot's shape. + * + * @type number + * @name pv.Dot.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with lineWidth to + * stroke the dot's shape. The default value of this property is a categorical + * color. + * + * @type string + * @name pv.Dot.prototype.strokeStyle + * @see pv.color + */ + +/** + * The fill style; if non-null, the interior of the dot is filled with the + * specified color. The default value of this property is null, meaning dots are + * not filled by default. + * + * @type string + * @name pv.Dot.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for dots. By default, there is no fill and the stroke + * style is a categorical color. The default shape is "circle" with size 20. + * + * @type pv.Dot + */ +pv.Dot.prototype.defaults = new pv.Dot() + .extend(pv.Mark.prototype.defaults) + .size(20) + .shape("circle") + .lineWidth(1.5) + .strokeStyle(defaultStrokeStyle); + +/** + * Constructs a new dot anchor with default properties. Dots support five + * different anchors:

    + * + *
  • top + *
  • left + *
  • center + *
  • bottom + *
  • right + * + *
In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the dot. Note that this behavior is different from + * other mark anchors, which default to rendering text inside the mark. + * + *

For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Dot.prototype.anchor = function(name) { + var dot = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function(d) { + switch (this.name()) { + case "bottom": + case "top": + case "center": return dot.left(); + case "right": return dot.left() + dot.radius(); + } + return null; + }) + .right(function(d) { + switch (this.name()) { + case "bottom": + case "top": + case "center": return dot.right(); + case "left": return dot.right() + dot.radius(); + } + return null; + }) + .top(function(d) { + switch (this.name()) { + case "left": + case "right": + case "center": return dot.top(); + case "bottom": return dot.top() + dot.radius(); + } + return null; + }) + .bottom(function(d) { + switch (this.name()) { + case "left": + case "right": + case "center": return dot.bottom(); + case "top": return dot.bottom() + dot.radius(); + } + return null; + }) + .textAlign(function(d) { + switch (this.name()) { + case "left": return "right"; + case "bottom": + case "top": + case "center": return "center"; + } + return "left"; + }) + .textBaseline(function(d) { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "bottom": return "top"; + } + return "bottom"; + }); +}; + +/** + * Returns the radius of the dot, which is defined to be the square root of the + * {@link #size} property. + * + * @returns {number} the radius. + */ +pv.Dot.prototype.radius = function() { + return Math.sqrt(this.size()); +}; +/** + * Constructs a new label mark with default properties. Labels are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a text label, allowing textual annotation of other marks or + * arbitrary text within the visualization. The character data must be plain + * text (unicode), though the text can be styled using the {@link #font} + * property. If rich text is needed, external HTML elements can be overlaid on + * the canvas by hand. + * + *

Labels are positioned using the box model, similarly to {@link Dot}. Thus, + * a label has no width or height, but merely a text anchor location. The text + * is positioned relative to this anchor location based on the + * {@link #textAlign}, {@link #textBaseline} and {@link #textMargin} properties. + * Furthermore, the text may be rotated using {@link #textAngle}. + * + *

Labels ignore events, so as to not interfere with event handlers on + * underlying marks, such as bars. In the future, we may support event handlers + * on labels. + * + *

See also the Label guide. + * + * @extends pv.Mark + */ +pv.Label = function() { + pv.Mark.call(this); +}; + +pv.Label.prototype = pv.extend(pv.Mark) + .property("text") + .property("font") + .property("textAngle") + .property("textStyle") + .property("textAlign") + .property("textBaseline") + .property("textMargin") + .property("textShadow"); + +pv.Label.prototype.type = "label"; + +/** + * The character data to render; a string. The default value of the text + * property is the identity function, meaning the label's associated datum will + * be rendered using its toString. + * + * @type string + * @name pv.Label.prototype.text + */ + +/** + * The font format, per the CSS Level 2 specification. The default font is "10px + * sans-serif", for consistency with the HTML 5 canvas element specification. + * Note that since text is not wrapped, any line-height property will be + * ignored. The other font-style, font-variant, font-weight, font-size and + * font-family properties are supported. + * + * @see CSS2 fonts + * @type string + * @name pv.Label.prototype.font + */ + +/** + * The rotation angle, in radians. Text is rotated clockwise relative to the + * anchor location. For example, with the default left alignment, an angle of + * Math.PI / 2 causes text to proceed downwards. The default angle is zero. + * + * @type number + * @name pv.Label.prototype.textAngle + */ + +/** + * The text color. The name "textStyle" is used for consistency with "fillStyle" + * and "strokeStyle", although it might be better to rename this property (and + * perhaps use the same name as "strokeStyle"). The default color is black. + * + * @type string + * @name pv.Label.prototype.textStyle + * @see pv.color + */ + +/** + * The horizontal text alignment. One of:

    + * + *
  • left + *
  • center + *
  • right + * + *
The default horizontal alignment is left. + * + * @type string + * @name pv.Label.prototype.textAlign + */ + +/** + * The vertical text alignment. One of:
    + * + *
  • top + *
  • middle + *
  • bottom + * + *
The default vertical alignment is bottom. + * + * @type string + * @name pv.Label.prototype.textBaseline + */ + +/** + * The text margin; may be specified in pixels, or in font-dependent units (such + * as ".1ex"). The margin can be used to pad text away from its anchor location, + * in a direction dependent on the horizontal and vertical alignment + * properties. For example, if the text is left- and middle-aligned, the margin + * shifts the text to the right. The default margin is 3 pixels. + * + * @type number + * @name pv.Label.prototype.textMargin + */ + +/** + * A list of shadow effects to be applied to text, per the CSS Text Level 3 + * text-shadow property. An example specification is "0.1em 0.1em 0.1em + * rgba(0,0,0,.5)"; the first length is the horizontal offset, the second the + * vertical offset, and the third the blur radius. + * + * @see CSS3 text + * @type string + * @name pv.Label.prototype.textShadow + */ + +/** + * Default properties for labels. See the individual properties for the default + * values. + * + * @type pv.Label + */ +pv.Label.prototype.defaults = new pv.Label() + .extend(pv.Mark.prototype.defaults) + .text(pv.identity) + .font("10px sans-serif") + .textAngle(0) + .textStyle("black") + .textAlign("left") + .textBaseline("bottom") + .textMargin(3); +/** + * Constructs a new line mark with default properties. Lines are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a series of connected line segments, or polyline, + * that can be stroked with a configurable color and thickness. Each + * articulation point in the line corresponds to a datum; for n points, + * n-1 connected line segments are drawn. The point is positioned using + * the box model. Arbitrary paths are also possible, allowing radar plots and + * other custom visualizations. + * + *

Like areas, lines can be stroked and filled with arbitrary colors. In most + * cases, lines are only stroked, but the fill style can be used to construct + * arbitrary polygons. + * + *

See also the Line guide. + * + * @extends pv.Mark + */ +pv.Line = function() { + pv.Mark.call(this); +}; + +pv.Line.prototype = pv.extend(pv.Mark) + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle") + .property("segmented") + .property("interpolate"); + +pv.Line.prototype.type = "line"; + +/** + * The width of stroked lines, in pixels; used in conjunction with + * strokeStyle to stroke the line. + * + * @type number + * @name pv.Line.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with lineWidth to + * stroke the line. The default value of this property is a categorical color. + * + * @type string + * @name pv.Line.prototype.strokeStyle + * @see pv.color + */ + +/** + * The line fill style; if non-null, the interior of the line is closed and + * filled with the specified color. The default value of this property is a + * null, meaning that lines are not filled by default. + * + * @type string + * @name pv.Line.prototype.fillStyle + * @see pv.color + */ + +/** + * Whether the line is segmented; whether variations in stroke style, line width + * and the other properties are treated as fixed. Rendering segmented lines is + * noticeably slower than non-segmented lines. + * + *

This property is fixed. See {@link pv.Mark}. + * + * @type boolean + * @name pv.Line.prototype.segmented + */ + +/** + * How to interpolate between values. Linear interpolation ("linear") is the + * default, producing a straight line between points. For piecewise constant + * functions (i.e., step functions), either "step-before" or "step-after" can be + * specified. + * + *

Note: this property is currently supported only on non-segmented lines. + * + *

This property is fixed. See {@link pv.Mark}. + * + * @type string + * @name pv.Line.prototype.interpolate + */ + +/** + * Default properties for lines. By default, there is no fill and the stroke + * style is a categorical color. The default interpolation is linear. + * + * @type pv.Line + */ +pv.Line.prototype.defaults = new pv.Line() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1.5) + .strokeStyle(defaultStrokeStyle) + .interpolate("linear"); + +/** @private */ +var pv_Line_specials = {left:1, top:1, right:1, bottom:1, name:1}; + +/** @private */ +pv.Line.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + var binds = this.binds, + properties = binds.properties, + specials = binds.specials = []; + for (var i = 0, n = properties.length; i < n; i++) { + var p = properties[i]; + if (p.name in pv_Line_specials) specials.push(p); + } +}; + +/** @private */ +pv.Line.prototype.buildInstance = function(s) { + if (this.index && !this.scene[0].segmented) { + this.buildProperties(s, this.binds.specials); + this.buildImplied(s); + } else { + pv.Mark.prototype.buildInstance.call(this, s); + } +}; +/** + * Constructs a new rule with default properties. Rules are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a horizontal or vertical rule. Rules are frequently used + * for axes and grid lines. For example, specifying only the bottom property + * draws horizontal rules, while specifying only the left draws vertical + * rules. Rules can also be used as thin bars. The visual style is controlled in + * the same manner as lines. + * + *

Rules are positioned exclusively the standard box model properties. The + * following combinations of properties are supported: + * + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
PropertiesOrientation
leftvertical
rightvertical
left, bottom, topvertical
right, bottom, topvertical
tophorizontal
bottomhorizontal
top, left, righthorizontal
bottom, left, righthorizontal
left, top, heightvertical
left, bottom, heightvertical
right, top, heightvertical
right, bottom, heightvertical
left, top, widthhorizontal
left, bottom, widthhorizontal
right, top, widthhorizontal
right, bottom, widthhorizontal
+ * + *

Small rules can be used as tick marks; alternatively, a {@link Dot} with + * the "tick" shape can be used. + * + *

See also the Rule guide. + * + * @see pv.Line + * @extends pv.Mark + */ +pv.Rule = function() { + pv.Mark.call(this); +}; + +pv.Rule.prototype = pv.extend(pv.Mark) + .property("width") + .property("height") + .property("lineWidth") + .property("strokeStyle"); + +pv.Rule.prototype.type = "rule"; + +/** + * The width of the rule, in pixels. If the left position is specified, the rule + * extends rightward from the left edge; if the right position is specified, the + * rule extends leftward from the right edge. + * + * @type number + * @name pv.Rule.prototype.width + */ + +/** + * The height of the rule, in pixels. If the bottom position is specified, the + * rule extends upward from the bottom edge; if the top position is specified, + * the rule extends downward from the top edge. + * + * @type number + * @name pv.Rule.prototype.height + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * strokeStyle to stroke the rule. The default value is 1 pixel. + * + * @type number + * @name pv.Rule.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with lineWidth to + * stroke the rule. The default value of this property is black. + * + * @type string + * @name pv.Rule.prototype.strokeStyle + * @see pv.color + */ + +/** + * Default properties for rules. By default, a single-pixel black line is + * stroked. + * + * @type pv.Rule + */ +pv.Rule.prototype.defaults = new pv.Rule() + .extend(pv.Mark.prototype.defaults) + .lineWidth(1) + .strokeStyle("black"); + +/** + * Constructs a new rule anchor with default properties. Rules support five + * different anchors:

    + * + *
  • top + *
  • left + *
  • center + *
  • bottom + *
  • right + * + *
In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline). Text is + * rendered to appear outside the rule. Note that this behavior is different + * from other mark anchors, which default to rendering text inside the + * mark. + * + *

For consistency with the other mark types, the anchor positions are + * defined in terms of their opposite edge. For example, the top anchor defines + * the bottom property, such that a bar added to the top anchor grows upward. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Rule.prototype.anchor = function(name) { + return pv.Bar.prototype.anchor.call(this, name) + .textAlign(function(d) { + switch (this.name()) { + case "left": return "right"; + case "bottom": + case "top": + case "center": return "center"; + case "right": return "left"; + } + }) + .textBaseline(function(d) { + switch (this.name()) { + case "right": + case "left": + case "center": return "middle"; + case "top": return "bottom"; + case "bottom": return "top"; + } + }); +}; + +/** + * @private Overrides the default behavior of {@link pv.Mark.buildImplied} to + * determine the orientation (vertical or horizontal) of the rule. + * + * @param s a node in the scene graph; the instance of the rule to build. + */ +pv.Rule.prototype.buildImplied = function(s) { + var l = s.left, r = s.right, t = s.top, b = s.bottom; + + /* Determine horizontal or vertical orientation. */ + if ((s.width != null) + || ((l == null) && (r == null)) + || ((r != null) && (l != null))) { + s.height = 0; + } else { + s.width = 0; + } + + pv.Mark.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new, empty panel with default properties. Panels, with the + * exception of the root panel, are not typically constructed directly; instead, + * they are added to an existing panel or mark via {@link pv.Mark#add}. + * + * @class Represents a container mark. Panels allow repeated or nested + * structures, commonly used in small multiple displays where a small + * visualization is tiled to facilitate comparison across one or more + * dimensions. Other types of visualizations may benefit from repeated and + * possibly overlapping structure as well, such as stacked area charts. Panels + * can also offset the position of marks to provide padding from surrounding + * content. + * + *

All Protovis displays have at least one panel; this is the root panel to + * which marks are rendered. The box model properties (four margins, width and + * height) are used to offset the positions of contained marks. The data + * property determines the panel count: a panel is generated once per associated + * datum. When nested panels are used, property functions can declare additional + * arguments to access the data associated with enclosing panels. + * + *

Panels can be rendered inline, facilitating the creation of sparklines. + * This allows designers to reuse browser layout features, such as text flow and + * tables; designers can also overlay HTML elements such as rich text and + * images. + * + *

All panels have a children array (possibly empty) containing the + * child marks in the order they were added. Panels also have a root + * field which points to the root (outermost) panel; the root panel's root field + * points to itself. + * + *

See also the Protovis guide. + * + * @extends pv.Bar + */ +pv.Panel = function() { + pv.Bar.call(this); + + /** + * The child marks; zero or more {@link pv.Mark}s in the order they were + * added. + * + * @see #add + * @type pv.Mark[] + */ + this.children = []; + this.root = this; + + /** + * The internal $dom field is set by the Protovis loader; see lang/init.js. It + * refers to the script element that contains the Protovis specification, so + * that the panel knows where in the DOM to insert the generated SVG element. + * + * @private + */ + this.$dom = pv.Panel.$dom; +}; + +pv.Panel.prototype = pv.extend(pv.Bar) + .property("canvas") + .property("overflow"); + +pv.Panel.prototype.type = "panel"; + +/** + * The canvas element; either the string ID of the canvas element in the current + * document, or a reference to the canvas element itself. If null, a canvas + * element will be created and inserted into the document at the location of the + * script element containing the current Protovis specification. This property + * only applies to root panels and is ignored on nested panels. + * + *

Note: the "canvas" element here refers to a div (or other suitable + * HTML container element), not a canvas element. The name of + * this property is a historical anachronism from the first implementation that + * used HTML 5 canvas, rather than SVG. + * + * @type string + * @name pv.Panel.prototype.canvas + */ + +/** + * Default properties for panels. By default, the margins are zero, the fill + * style is transparent. + * + * @type pv.Panel + */ +pv.Panel.prototype.defaults = new pv.Panel() + .extend(pv.Bar.prototype.defaults) + .fillStyle(null) + .overflow("visible"); + +/** + * Returns an anchor with the specified name. This method is overridden since + * the behavior of Panel anchors is slightly different from normal anchors: + * adding to an anchor adds to the anchor target's, rather than the anchor + * target's parent. To avoid double margins, we override the anchor's proto so + * that the margins are zero. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} the new anchor. + */ +pv.Panel.prototype.anchor = function(name) { + + /* A "view" of this panel whose margins appear to be zero. */ + function z() { return 0; } + z.prototype = this; + z.prototype.left = z.prototype.right = z.prototype.top = z.prototype.bottom = z; + + var anchor = pv.Bar.prototype.anchor.call(new z(), name) + .data(function(d) { return [d]; }); + anchor.parent = this; + return anchor; +}; + +/** + * Adds a new mark of the specified type to this panel. Unlike the normal + * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark + * to inherit from the panel. Since the contained marks are offset by the panel + * margins already, inheriting properties is generally undesirable; of course, + * it is always possible to change this behavior by calling {@link Mark#extend} + * explicitly. + * + * @param {function} type the type of the new mark to add. + * @returns {pv.Mark} the new mark. + */ +pv.Panel.prototype.add = function(type) { + var child = new type(); + child.parent = this; + child.root = this.root; + child.childIndex = this.children.length; + this.children.push(child); + return child; +}; + +/** @private TODO */ +pv.Panel.prototype.bind = function() { + pv.Mark.prototype.bind.call(this); + for (var i = 0; i < this.children.length; i++) { + this.children[i].bind(); + } +}; + +/** + * @private Evaluates all of the properties for this panel for the specified + * instance s in the scene graph, including recursively building the + * scene graph for child marks. + * + * @param s a node in the scene graph; the instance of the panel to build. + * @see Mark#scene + */ +pv.Panel.prototype.buildInstance = function(s) { + pv.Bar.prototype.buildInstance.call(this, s); + if (!s.children) s.children = []; + + /* + * Build each child, passing in the parent (this panel) scene graph node. The + * child mark's scene is initialized from the corresponding entry in the + * existing scene graph, such that properties from the previous build can be + * reused; this is largely to facilitate the recycling of SVG elements. + */ + for (var i = 0; i < this.children.length; i++) { + this.children[i].scene = s.children[i]; // possibly undefined + this.children[i].build(); + } + + /* + * Once the child marks have been built, the new scene graph nodes are removed + * from the child marks and placed into the scene graph. The nodes cannot + * remain on the child nodes because this panel (or a parent panel) may be + * instantiated multiple times! + */ + for (var i = 0; i < this.children.length; i++) { + s.children[i] = this.children[i].scene; + delete this.children[i].scene; + } + + /* Delete any expired child scenes, should child marks have been removed. */ + s.children.length = this.children.length; +}; + +/** + * @private Computes the implied properties for this panel for the specified + * instance s in the scene graph. Panels have two implied + * properties:

    + * + *
  • The canvas property references the DOM element, typically a DIV, + * that contains the SVG element that is used to display the visualization. This + * property may be specified as a string, referring to the unique ID of the + * element in the DOM. The string is converted to a reference to the DOM + * element. The width and height of the SVG element is inferred from this DOM + * element. If no canvas property is specified, a new SVG element is created and + * inserted into the document, using the panel dimensions; see + * {@link #createCanvas}. + * + *
  • The children array, while not a property per se, contains the + * scene graph for each child mark. This array is initialized to be empty, and + * is populated above in {@link #buildInstance}. + * + *
The current implementation creates the SVG element, if necessary, during + * the build phase; in the future, it may be preferrable to move this to the + * update phase, although then the canvas property would be undefined. In + * addition, DOM inspection is necessary to define the implied width and height + * properties that may be inferred from the DOM. + * + * @param s a node in the scene graph; the instance of the panel to build. + */ +pv.Panel.prototype.buildImplied = function(s) { + if (!this.parent) { + var c = s.canvas; + if (c) { + if (typeof c == "string") c = document.getElementById(c); + + /* Clear the container if it's not associated with this panel. */ + if (c.$panel != this) { + c.$panel = this; + c.innerHTML = ""; + } + + /* If width and height weren't specified, inspect the container. */ + var w, h; + if (s.width == null) { + w = parseFloat(pv.css(c, "width")); + s.width = w - s.left - s.right; + } + if (s.height == null) { + h = parseFloat(pv.css(c, "height")); + s.height = h - s.top - s.bottom; + } + } else if (s.$canvas) { + + /* + * If the canvas property is null, and we previously created a canvas for + * this scene node, reuse the previous canvas rather than creating a new + * one. + */ + c = s.$canvas; + } else { + + /** + * Returns the last element in the current document's body. The canvas + * element is appended to this last element if another DOM element has not + * already been specified via the $dom field. + */ + function lastElement() { + var node = document.body; + while (node.lastChild && node.lastChild.tagName) { + node = node.lastChild; + } + return (node == document.body) ? node : node.parentNode; + } + + /* Insert a new container into the DOM. */ + c = s.$canvas = document.createElement("span"); + this.$dom // script element for text/javascript+protovis + ? this.$dom.parentNode.insertBefore(c, this.$dom) + : lastElement().appendChild(c); + } + s.canvas = c; + } + pv.Bar.prototype.buildImplied.call(this, s); +}; +/** + * Constructs a new dot mark with default properties. Images are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents an image. Images share the same layout and style properties as + * bars, in conjunction with an external image such as PNG or JPEG. The image is + * specified via the {@link #url} property. The fill, if specified, appears + * beneath the image, while the optional stroke appears above the image. + * + *

TODO Restore support for dynamic images (such as heatmaps). These were + * supported in the canvas implementation using the pixel buffer API; although + * SVG does not support pixel manipulation, it is possible to embed a canvas + * element in SVG using foreign objects. + * + *

TODO Allow different modes of image placement: "scale" -- scale and + * preserve aspect ratio, "tile" -- repeat the image, "center" -- center the + * image, "fill" -- scale without preserving aspect ratio. + * + *

See {@link pv.Bar} for details on positioning properties. + * + * @extends pv.Bar + */ +pv.Image = function() { + pv.Bar.call(this); +}; + +pv.Image.prototype = pv.extend(pv.Bar) + .property("url"); + +pv.Image.prototype.type = "image"; + +/** + * The URL of the image to display. The set of supported image types is + * browser-dependent; PNG and JPEG are recommended. + * + * @type string + * @name pv.Image.prototype.url + */ + +/** + * Default properties for images. By default, there is no stroke or fill style. + * + * @type pv.Image + */ +pv.Image.prototype.defaults = new pv.Image() + .extend(pv.Bar.prototype.defaults) + .fillStyle(null); +/** + * Constructs a new wedge with default properties. Wedges are not typically + * constructed directly, but by adding to a panel or an existing mark via + * {@link pv.Mark#add}. + * + * @class Represents a wedge, or pie slice. Specified in terms of start and end + * angle, inner and outer radius, wedges can be used to construct donut charts + * and polar bar charts as well. If the {@link #angle} property is used, the end + * angle is implied by adding this value to start angle. By default, the start + * angle is the previously-generated wedge's end angle. This design allows + * explicit control over the wedge placement if desired, while offering + * convenient defaults for the construction of radial graphs. + * + *

The center point of the circle is positioned using the standard box model. + * The wedge can be stroked and filled, similar to {link Bar}. + * + *

See also the Wedge guide. + * + * @extends pv.Mark + */ +pv.Wedge = function() { + pv.Mark.call(this); +}; + +pv.Wedge.prototype = pv.extend(pv.Mark) + .property("startAngle") + .property("endAngle") + .property("angle") + .property("innerRadius") + .property("outerRadius") + .property("lineWidth") + .property("strokeStyle") + .property("fillStyle"); + +pv.Wedge.prototype.type = "wedge"; + +/** + * The start angle of the wedge, in radians. The start angle is measured + * clockwise from the 3 o'clock position. The default value of this property is + * the end angle of the previous instance (the {@link Mark#sibling}), or -PI / 2 + * for the first wedge; for pie and donut charts, typically only the + * {@link #angle} property needs to be specified. + * + * @type number + * @name pv.Wedge.prototype.startAngle + */ + +/** + * The end angle of the wedge, in radians. If not specified, the end angle is + * implied as the start angle plus the {@link #angle}. + * + * @type number + * @name pv.Wedge.prototype.endAngle + */ + +/** + * The angular span of the wedge, in radians. This property is used if end angle + * is not specified. + * + * @type number + * @name pv.Wedge.prototype.angle + */ + +/** + * The inner radius of the wedge, in pixels. The default value of this property + * is zero; a positive value will produce a donut slice rather than a pie slice. + * The inner radius can vary per-wedge. + * + * @type number + * @name pv.Wedge.prototype.innerRadius + */ + +/** + * The outer radius of the wedge, in pixels. This property is required. For + * pies, only this radius is required; for donuts, the inner radius must be + * specified as well. The outer radius can vary per-wedge. + * + * @type number + * @name pv.Wedge.prototype.outerRadius + */ + +/** + * The width of stroked lines, in pixels; used in conjunction with + * strokeStyle to stroke the wedge's border. + * + * @type number + * @name pv.Wedge.prototype.lineWidth + */ + +/** + * The style of stroked lines; used in conjunction with lineWidth to + * stroke the wedge's border. The default value of this property is null, + * meaning wedges are not stroked by default. + * + * @type string + * @name pv.Wedge.prototype.strokeStyle + * @see pv.color + */ + +/** + * The wedge fill style; if non-null, the interior of the wedge is filled with + * the specified color. The default value of this property is a categorical + * color. + * + * @type string + * @name pv.Wedge.prototype.fillStyle + * @see pv.color + */ + +/** + * Default properties for wedges. By default, there is no stroke and the fill + * style is a categorical color. + * + * @type pv.Wedge + */ +pv.Wedge.prototype.defaults = new pv.Wedge() + .extend(pv.Mark.prototype.defaults) + .startAngle(function() { + var s = this.sibling(); + return s ? s.endAngle : -Math.PI / 2; + }) + .innerRadius(0) + .lineWidth(1.5) + .strokeStyle(null) + .fillStyle(defaultFillStyle.by(pv.index)); + +/** + * Returns the mid-radius of the wedge, which is defined as half-way between the + * inner and outer radii. + * + * @see #innerRadius + * @see #outerRadius + * @returns {number} the mid-radius, in pixels. + */ +pv.Wedge.prototype.midRadius = function() { + return (this.innerRadius() + this.outerRadius()) / 2; +}; + +/** + * Returns the mid-angle of the wedge, which is defined as half-way between the + * start and end angles. + * + * @see #startAngle + * @see #endAngle + * @returns {number} the mid-angle, in radians. + */ +pv.Wedge.prototype.midAngle = function() { + return (this.startAngle() + this.endAngle()) / 2; +}; + +/** + * Constructs a new wedge anchor with default properties. Wedges support five + * different anchors:

    + * + *
  • outer + *
  • inner + *
  • center + *
  • start + *
  • end + * + *
In addition to positioning properties (left, right, top bottom), the + * anchors support text rendering properties (text-align, text-baseline, + * textAngle). Text is rendered to appear inside the wedge. + * + * @param {string} name the anchor name; either a string or a property function. + * @returns {pv.Anchor} + */ +pv.Wedge.prototype.anchor = function(name) { + var w = this; + return pv.Mark.prototype.anchor.call(this, name) + .left(function() { + switch (this.name()) { + case "outer": return w.left() + w.outerRadius() * Math.cos(w.midAngle()); + case "inner": return w.left() + w.innerRadius() * Math.cos(w.midAngle()); + case "start": return w.left() + w.midRadius() * Math.cos(w.startAngle()); + case "center": return w.left() + w.midRadius() * Math.cos(w.midAngle()); + case "end": return w.left() + w.midRadius() * Math.cos(w.endAngle()); + } + }) + .right(function() { + switch (this.name()) { + case "outer": return w.right() + w.outerRadius() * Math.cos(w.midAngle()); + case "inner": return w.right() + w.innerRadius() * Math.cos(w.midAngle()); + case "start": return w.right() + w.midRadius() * Math.cos(w.startAngle()); + case "center": return w.right() + w.midRadius() * Math.cos(w.midAngle()); + case "end": return w.right() + w.midRadius() * Math.cos(w.endAngle()); + } + }) + .top(function() { + switch (this.name()) { + case "outer": return w.top() + w.outerRadius() * Math.sin(w.midAngle()); + case "inner": return w.top() + w.innerRadius() * Math.sin(w.midAngle()); + case "start": return w.top() + w.midRadius() * Math.sin(w.startAngle()); + case "center": return w.top() + w.midRadius() * Math.sin(w.midAngle()); + case "end": return w.top() + w.midRadius() * Math.sin(w.endAngle()); + } + }) + .bottom(function() { + switch (this.name()) { + case "outer": return w.bottom() + w.outerRadius() * Math.sin(w.midAngle()); + case "inner": return w.bottom() + w.innerRadius() * Math.sin(w.midAngle()); + case "start": return w.bottom() + w.midRadius() * Math.sin(w.startAngle()); + case "center": return w.bottom() + w.midRadius() * Math.sin(w.midAngle()); + case "end": return w.bottom() + w.midRadius() * Math.sin(w.endAngle()); + } + }) + .textAlign(function() { + switch (this.name()) { + case "outer": return pv.Wedge.upright(w.midAngle()) ? "right" : "left"; + case "inner": return pv.Wedge.upright(w.midAngle()) ? "left" : "right"; + } + return "center"; + }) + .textBaseline(function() { + switch (this.name()) { + case "start": return pv.Wedge.upright(w.startAngle()) ? "top" : "bottom"; + case "end": return pv.Wedge.upright(w.endAngle()) ? "bottom" : "top"; + } + return "middle"; + }) + .textAngle(function() { + var a = 0; + switch (this.name()) { + case "center": + case "inner": + case "outer": a = w.midAngle(); break; + case "start": a = w.startAngle(); break; + case "end": a = w.endAngle(); break; + } + return pv.Wedge.upright(a) ? a : (a + Math.PI); + }); +}; + +/** + * Returns true if the specified angle is considered "upright", as in, text + * rendered at that angle would appear upright. If the angle is not upright, + * text is rotated 180 degrees to be upright, and the text alignment properties + * are correspondingly changed. + * + * @param {number} angle an angle, in radius. + * @returns {boolean} true if the specified angle is upright. + */ +pv.Wedge.upright = function(angle) { + angle = angle % (2 * Math.PI); + angle = (angle < 0) ? (2 * Math.PI + angle) : angle; + return (angle < Math.PI / 2) || (angle > 3 * Math.PI / 2); +}; + +/** + * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such + * that the end angle is computed from the start angle and angle (angular span) + * if not specified. + * + * @param s a node in the scene graph; the instance of the wedge to build. + */ +pv.Wedge.prototype.buildImplied = function(s) { + pv.Mark.prototype.buildImplied.call(this, s); + + /* + * TODO If the angle or endAngle is updated by an event handler, the implied + * properties won't recompute correctly, so this will lead to potentially + * buggy redraw. How to re-evaluate implied properties on update? + */ + if (s.endAngle == null) s.endAngle = s.startAngle + s.angle; + if (s.angle == null) s.angle = s.endAngle - s.startAngle; +}; +/** + * @ignore + * @namespace + */ +pv.Layout = {}; +/** + * Returns a new grid layout. + * + * @class A grid layout with regularly-sized rows and columns. The number of rows + * and columns are determined from the array, which should be in row-major + * order. For example, the 2×3 array: + * + *
1 2 3
+ * 4 5 6
+ * + * should be represented as: + * + *
[[1, 2, 3], [4, 5, 6]]
+ * + * If your data is in column-major order, you can use {@link pv.transpose} to + * transpose it. + * + *

This layout defines left, top, width, height and data properties. The data + * property will be the associated element in the array. For example, if the + * array is a two-dimensional array of values in the range [0,1], a simple + * heatmap can be generated as: + * + *

.add(pv.Bar)
+ *   .extend(pv.Layout.grid(array))
+ *   .fillStyle(pv.ramp("white", "black"))
+ * + * By default, the grid fills the full width and height of the parent panel. + * + * @param {array[]} arrays an array of arrays. + * @returns {pv.Layout.grid} a grid layout. + */ +pv.Layout.grid = function(arrays) { + var rows = arrays.length, cols = arrays[0].length; + + /** @private */ + function w() { return this.parent.width() / cols; } + + /** @private */ + function h() { return this.parent.height() / rows; } + + /* A dummy mark, like an anchor, which the caller extends. */ + return new pv.Mark() + .data(pv.blend(arrays)) + .left(function() { return w.call(this) * (this.index % cols); }) + .top(function() { return h.call(this) * Math.floor(this.index / cols); }) + .width(w) + .height(h); +}; +/** + * Returns a new stack layout. + * + * @class A layout for stacking marks vertically or horizontally, using the + * cousin instance. This layout is designed to be used for one of the + * four positional properties in the box model, and changes behavior depending + * on the property being evaluated:
    + * + *
  • bottom: cousin.bottom + cousin.height + *
  • top: cousin.top + cousin.height + *
  • left: cousin.left + cousin.width + *
  • right: cousin.right + cousin.width + * + *
If no cousin instance is available (for example, for first instance), + * the specified offset is used. If no offset is specified, zero is used. For + * example, + * + *
new pv.Panel()
+ *     .width(150).height(150)
+ *   .add(pv.Panel)
+ *     .data([[1, 1.2, 1.7, 1.5, 1.7],
+ *            [.5, 1, .8, 1.1, 1.3],
+ *            [.2, .5, .8, .9, 1]])
+ *   .add(pv.Area)
+ *     .data(function(d) d)
+ *     .bottom(pv.Layout.stack())
+ *     .height(function(d) d * 40)
+ *     .left(function() this.index * 35)
+ *   .root.render();
+ * + * specifies a vertically-stacked area chart. + * + * @returns {pv.Layout.stack} a stack property function. + * @see pv.Mark#cousin + */ +pv.Layout.stack = function() { + /** @private */ + var offset = function() { return 0; }; + + /** @private */ + function layout() { + + /* Find the previous visible parent instance. */ + var i = this.parent.index, p, c; + while ((i-- > 0) && !c) { + p = this.parent.scene[i]; + if (p.visible) c = p.children[this.childIndex][this.index]; + } + + if (c) { + switch (property) { + case "bottom": return c.bottom + c.height; + case "top": return c.top + c.height; + case "left": return c.left + c.width; + case "right": return c.right + c.width; + } + } + + return offset.apply(this, arguments); + } + + /** + * Sets the offset for this stack layout. The offset can either be specified + * as a function or as a constant. If a function, the function is invoked in + * the same context as a normal property function: this refers to the + * mark, and the arguments are the full data stack. By default the offset is + * zero. + * + * @function + * @name pv.Layout.stack.prototype.offset + * @param {function} f offset function, or constant value. + * @returns {pv.Layout.stack} this. + */ + layout.offset = function(f) { + offset = (f instanceof Function) ? f : function() { return f; }; + return this; + }; + + return layout; +}; +// TODO share code with Treemap +// TODO vertical / horizontal orientation? + +/** + * Returns a new icicle tree layout. + * + * @class A tree layout in the form of an icicle. The first row corresponds to the root + * of the tree; subsequent rows correspond to each tier. Rows are subdivided + * into cells based on the size of nodes, per {@link #size}. Within a row, cells + * are sorted by size. + * + *

This tree layout is intended to be extended (see {@link pv.Mark#extend}) + * by a {@link pv.Bar}. The data property returns an array of nodes for use by + * other property functions. The following node attributes are supported: + * + *

    + *
  • left - the cell left position. + *
  • top - the cell top position. + *
  • width - the cell width. + *
  • height - the cell height. + *
  • depth - the node depth (tier; the root is 0). + *
  • keys - an array of string keys for the node. + *
  • size - the aggregate node size. + *
  • children - child nodes, if any. + *
  • data - the associated tree element, for leaf nodes. + *
+ * + * To produce a default icicle layout, say: + * + *
.add(pv.Bar)
+ *   .extend(pv.Layout.icicle(tree))
+ * + * To customize the tree to highlight leaf nodes bigger than 10,000 (1E4), you + * might say: + * + *
.add(pv.Bar)
+ *   .extend(pv.Layout.icicle(tree))
+ *   .fillStyle(function(n) n.data > 1e4 ? "#ff0" : "#fff")
+ * + * The format of the tree argument is any hierarchical object whose + * leaf nodes are numbers corresponding to their size. For an example, and + * information on how to convert tabular data into such a tree, see + * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function + * can be specified to override how the tree is interpreted. This size function + * can also be used to transform the data. + * + *

By default, the icicle fills the full width and height of the parent + * panel. An optional root key can be specified using {@link #root} for + * convenience. + * + * @param tree a tree (an object) who leaf attributes have sizes. + * @returns {pv.Layout.icicle} a tree layout. + */ +pv.Layout.icicle = function(tree) { + var keys = [], sizeof = Number; + + /** @private */ + function accumulate(map) { + var node = {size: 0, children: [], keys: keys.slice()}; + for (var key in map) { + var child = map[key], size = sizeof(child); + keys.push(key); + if (isNaN(size)) { + child = accumulate(child); + } else { + child = {size: size, data: child, keys: keys.slice()}; + } + node.children.push(child); + node.size += child.size; + keys.pop(); + } + node.children.sort(function(a, b) { return b.size - a.size; }); + return node; + } + + /** @private */ + function scale(node, k) { + node.size *= k; + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + scale(node.children[i], k); + } + } + } + + /** @private */ + function depth(node, i) { + i = i ? (i + 1) : 1; + return node.children + ? pv.max(node.children, function(n) { return depth(n, i); }) + : i; + } + + /** @private */ + function layout(node) { + if (node.children) { + icify(node); + for (var i = 0; i < node.children.length; i++) { + layout(node.children[i]); + } + } + } + + /** @private */ + function icify(node) { + var left = node.left; + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i], width = (child.size / node.size) * node.width; + child.left = left; + child.top = node.top + node.height; + child.width = width; + child.height = node.height; + child.depth = node.depth + 1; + left += width; + if (child.children) { + icify(child); + } + } + } + + /** @private */ + function flatten(node, array) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + flatten(node.children[i], array); + } + } + array.push(node) + return array; + } + + /** @private */ + function data() { + var root = accumulate(tree); + root.top = 0; + root.left = 0; + root.width = this.parent.width(); + root.height = this.parent.height() / depth(root); + root.depth = 0; + layout(root); + return flatten(root, []).reverse(); + } + + /* A dummy mark, like an anchor, which the caller extends. */ + var mark = new pv.Mark() + .data(data) + .left(function(n) { return n.left; }) + .top(function(n) { return n.top; }) + .width(function(n) { return n.width; }) + .height(function(n) { return n.height; }); + + /** + * Specifies the root key; optional. The root key is prepended to the + * keys attribute for all generated nodes. This method is provided + * for convenience and does not affect layout. + * + * @param {string} v the root key. + * @function + * @name pv.Layout.icicle.prototype.root + * @returns {pv.Layout.icicle} this. + */ + mark.root = function(v) { + keys = [v]; + return this; + }; + + /** + * Specifies the sizing function. By default, the sizing function is + * Number. The sizing function is invoked for each node in the tree + * (passed to the constructor): the sizing function must return + * undefined or NaN for internal nodes, and a number for + * leaf nodes. The aggregate sizes of internal nodes will be automatically + * computed by the layout. + * + *

For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a bytes attribute, you can + * specify a size function as: + * + *

.size(function(d) d.bytes)
+ * + * This function will return undefined for internal nodes (since + * these do not have a bytes attribute), and a number for leaf nodes. + * + *

Note that the built-in Math.sqrt and Math.log methods + * can also be used as sizing functions. These function similarly to + * Number, except perform a root and log scale, respectively. + * + * @param {function} f the new sizing function. + * @function + * @name pv.Layout.icicle.prototype.size + * @returns {pv.Layout.icicle} this. + */ + mark.size = function(f) { + sizeof = f; + return this; + }; + + return mark; +}; +// TODO share code with Treemap +// TODO inspect parent panel dimensions to set inner and outer radii + +/** + * Returns a new sunburst tree layout. + * + * @class A tree layout in the form of a sunburst. The + * center circle corresponds to the root of the tree; subsequent rings + * correspond to each tier. Rings are subdivided into wedges based on the size + * of nodes, per {@link #size}. Within a ring, wedges are sorted by size. + * + *

The tree layout is intended to be extended (see {@link pv.Mark#extend} by + * a {@link pv.Wedge}. The data property returns an array of nodes for use by + * other property functions. The following node attributes are supported: + * + *

    + *
  • left - the wedge left position. + *
  • top - the wedge top position. + *
  • innerRadius - the wedge inner radius. + *
  • outerRadius - the wedge outer radius. + *
  • startAngle - the wedge start angle. + *
  • endAngle - the wedge end angle. + *
  • angle - the wedge angle. + *
  • depth - the node depth (tier; the root is 0). + *
  • keys - an array of string keys for the node. + *
  • size - the aggregate node size. + *
  • children - child nodes, if any. + *
  • data - the associated tree element, for leaf nodes. + *
+ * + *

To produce a default sunburst layout, say: + * + *

.add(pv.Wedge)
+ *   .extend(pv.Layout.sunburst(tree))
+ * + * To only show nodes at a depth of two or greater, you might say: + * + *
.add(pv.Wedge)
+ *   .extend(pv.Layout.sunburst(tree))
+ *   .visible(function(n) n.depth > 1)
+ * + * The format of the tree argument is a hierarchical object whose leaf + * nodes are numbers corresponding to their size. For an example, and + * information on how to convert tabular data into such a tree, see + * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function + * can be specified to override how the tree is interpreted. This size function + * can also be used to transform the data. + * + *

By default, the sunburst fills the full width and height of the parent + * panel. An optional root key can be specified using {@link #root} for + * convenience. + * + * @param tree a tree (an object) who leaf attributes have sizes. + * @returns {pv.Layout.sunburst} a tree layout. + */ +pv.Layout.sunburst = function(tree) { + var keys = [], sizeof = Number, w, h, r; + + /** @private */ + function accumulate(map) { + var node = {size: 0, children: [], keys: keys.slice()}; + for (var key in map) { + var child = map[key], size = sizeof(child); + keys.push(key); + if (isNaN(size)) { + child = accumulate(child); + } else { + child = {size: size, data: child, keys: keys.slice()}; + } + node.children.push(child); + node.size += child.size; + keys.pop(); + } + node.children.sort(function(a, b) { return b.size - a.size; }); + return node; + } + + /** @private */ + function scale(node, k) { + node.size *= k; + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + scale(node.children[i], k); + } + } + } + + /** @private */ + function depth(node, i) { + i = i ? (i + 1) : 1; + return node.children + ? pv.max(node.children, function(n) { return depth(n, i); }) + : i; + } + + /** @private */ + function layout(node) { + if (node.children) { + wedgify(node); + for (var i = 0; i < node.children.length; i++) { + layout(node.children[i]); + } + } + } + + /** @private */ + function wedgify(node) { + var startAngle = node.startAngle; + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i], angle = (child.size / node.size) * node.angle; + child.startAngle = startAngle; + child.angle = angle; + child.endAngle = startAngle + angle; + child.depth = node.depth + 1; + child.left = w / 2; + child.top = h / 2; + child.innerRadius = Math.max(0, child.depth - .5) * r; + child.outerRadius = (child.depth + .5) * r; + startAngle += angle; + if (child.children) { + wedgify(child); + } + } + } + + /** @private */ + function flatten(node, array) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + flatten(node.children[i], array); + } + } + array.push(node) + return array; + } + + /** @private */ + function data() { + var root = accumulate(tree); + w = this.parent.width(); + h = this.parent.height(); + r = Math.min(w, h) / 2 / (depth(root) - .5); + root.left = w / 2; + root.top = h / 2; + root.startAngle = 0; + root.angle = 2 * Math.PI; + root.endAngle = 2 * Math.PI; + root.innerRadius = 0; + root.outerRadius = r; + root.depth = 0; + layout(root); + return flatten(root, []).reverse(); + } + + /* A dummy mark, like an anchor, which the caller extends. */ + var mark = new pv.Mark() + .data(data) + .left(function(n) { return n.left; }) + .top(function(n) { return n.top; }) + .startAngle(function(n) { return n.startAngle; }) + .angle(function(n) { return n.angle; }) + .innerRadius(function(n) { return n.innerRadius; }) + .outerRadius(function(n) { return n.outerRadius; }); + + /** + * Specifies the root key; optional. The root key is prepended to the + * keys attribute for all generated nodes. This method is provided + * for convenience and does not affect layout. + * + * @param {string} v the root key. + * @function + * @name pv.Layout.sunburst.prototype.root + * @returns {pv.Layout.sunburst} this. + */ + mark.root = function(v) { + keys = [v]; + return this; + }; + + /** + * Specifies the sizing function. By default, the sizing function is + * Number. The sizing function is invoked for each node in the tree + * (passed to the constructor): the sizing function must return + * undefined or NaN for internal nodes, and a number for + * leaf nodes. The aggregate sizes of internal nodes will be automatically + * computed by the layout. + * + *

For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a bytes attribute, you can + * specify a size function as: + * + *

.size(function(d) d.bytes)
+ * + * This function will return undefined for internal nodes (since + * these do not have a bytes attribute), and a number for leaf nodes. + * + *

Note that the built-in Math.sqrt and Math.log methods + * can be used as sizing functions. These function similarly to + * Number, except perform a root and log scale, respectively. + * + * @param {function} f the new sizing function. + * @function + * @name pv.Layout.sunburst.prototype.size + * @returns {pv.Layout.sunburst} this. + */ + mark.size = function(f) { + sizeof = f; + return this; + }; + + return mark; +}; +// TODO add `by` function for determining size (and children?) + +/** + * Returns a new treemap tree layout. + * + * @class A tree layout in the form of an treemap. Treemaps + * are a form of space-filling layout that represents nodes as boxes, with child + * nodes placed within parent boxes. The size of each box is proportional to the + * size of the node in the tree. + * + *

This particular algorithm is taken from Bruls, D.M., C. Huizing, and + * J.J. van Wijk, "Squarified + * Treemaps" in Data Visualization 2000, Proceedings of the Joint + * Eurographics and IEEE TCVG Sumposium on Visualization, 2000, + * pp. 33-42. + * + *

This tree layout is intended to be extended (see {@link pv.Mark#extend}) + * by a {@link pv.Bar}. The data property returns an array of nodes for use by + * other property functions. The following node attributes are supported: + * + *

    + *
  • left - the cell left position. + *
  • top - the cell top position. + *
  • width - the cell width. + *
  • height - the cell height. + *
  • depth - the node depth (tier; the root is 0). + *
  • keys - an array of string keys for the node. + *
  • size - the aggregate node size. + *
  • children - child nodes, if any. + *
  • data - the associated tree element, for leaf nodes. + *
+ * + * To produce a default treemap layout, say: + * + *
.add(pv.Bar)
+ *   .extend(pv.Layout.treemap(tree))
+ * + * To display internal nodes, and color by depth, say: + * + *
.add(pv.Bar)
+ *   .extend(pv.Layout.treemap(tree).inset(10))
+ *   .fillStyle(pv.Colors.category19().by(function(n) n.depth))
+ * + * The format of the tree argument is a hierarchical object whose leaf + * nodes are numbers corresponding to their size. For an example, and + * information on how to convert tabular data into such a tree, see + * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function + * can be specified to override how the tree is interpreted. This size function + * can also be used to transform the data. + * + *

By default, the treemap fills the full width and height of the parent + * panel, and only leaf nodes are rendered. If an {@link #inset} is specified, + * internal nodes will be rendered, each inset from their parent by the + * specified margins. Rounding can be enabled using {@link #round}. Finally, an + * optional root key can be specified using {@link #root} for convenience. + * + * @param tree a tree (an object) who leaf attributes have sizes. + * @returns {pv.Layout.treemap} a tree layout. + */ +pv.Layout.treemap = function(tree) { + var keys = [], round, inset, sizeof = Number; + + /** @private */ + function rnd(i) { + return round ? Math.round(i) : i; + } + + /** @private */ + function accumulate(map) { + var node = {size: 0, children: [], keys: keys.slice()}; + for (var key in map) { + var child = map[key], size = sizeof(child); + keys.push(key); + if (isNaN(size)) { + child = accumulate(child); + } else { + child = {size: size, data: child, keys: keys.slice()}; + } + node.children.push(child); + node.size += child.size; + keys.pop(); + } + node.children.sort(function(a, b) { return a.size - b.size; }); + return node; + } + + /** @private */ + function scale(node, k) { + node.size *= k; + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + scale(node.children[i], k); + } + } + } + + /** @private */ + function ratio(row, l) { + var rmax = -Infinity, rmin = Infinity, s = 0; + for (var i = 0; i < row.length; i++) { + var r = row[i].size; + if (r < rmin) rmin = r; + if (r > rmax) rmax = r; + s += r; + } + s = s * s; + l = l * l; + return Math.max(l * rmax / s, s / (l * rmin)); + } + + /** @private */ + function squarify(node) { + var row = [], mink = Infinity; + var x = node.left + (inset ? inset.left : 0), + y = node.top + (inset ? inset.top : 0), + w = node.width - (inset ? inset.left + inset.right : 0), + h = node.height - (inset ? inset.top + inset.bottom : 0), + l = Math.min(w, h); + + scale(node, w * h / node.size); + + function position(row) { + var s = pv.sum(row, function(node) { return node.size; }), + hh = (l == 0) ? 0 : rnd(s / l); + + for (var i = 0, d = 0; i < row.length; i++) { + var n = row[i], nw = rnd(n.size / hh); + if (w == l) { + n.left = x + d; + n.top = y; + n.width = nw; + n.height = hh; + } else { + n.left = x; + n.top = y + d; + n.width = hh; + n.height = nw; + } + d += nw; + } + + if (w == l) { + if (n) n.width += w - d; // correct rounding error + y += hh; + h -= hh; + } else { + if (n) n.height += h - d; // correct rounding error + x += hh; + w -= hh; + } + l = Math.min(w, h); + } + + var children = node.children.slice(); // copy + while (children.length > 0) { + var child = children[children.length - 1]; + if (child.size <= 0) { + children.pop(); + continue; + } + row.push(child); + + var k = ratio(row, l); + if (k <= mink) { + children.pop(); + mink = k; + } else { + row.pop(); + position(row); + row.length = 0; + mink = Infinity; + } + } + + if (row.length > 0) { + position(row); + } + + /* correct rounding error */ + if (w == l) { + for (var i = 0; i < row.length; i++) { + row[i].width += w; + } + } else { + for (var i = 0; i < row.length; i++) { + row[i].height += h; + } + } + } + + /** @private */ + function layout(node) { + if (node.children) { + squarify(node); + for (var i = 0; i < node.children.length; i++) { + var child = node.children[i]; + child.depth = node.depth + 1; + layout(child); + } + } + } + + /** @private */ + function flatten(node, array) { + if (node.children) { + for (var i = 0; i < node.children.length; i++) { + flatten(node.children[i], array); + } + } + if (inset || !node.children) { + array.push(node) + } + return array; + } + + /** @private */ + function data() { + var root = accumulate(tree); + root.left = 0; + root.top = 0; + root.width = this.parent.width(); + root.height = this.parent.height(); + root.depth = 0; + layout(root); + return flatten(root, []).reverse(); + } + + /* A dummy mark, like an anchor, which the caller extends. */ + var mark = new pv.Mark() + .data(data) + .left(function(n) { return n.left; }) + .top(function(n) { return n.top; }) + .width(function(n) { return n.width; }) + .height(function(n) { return n.height; }); + + /** + * Enables or disables rounding. When rounding is enabled, the left, top, + * width and height properties will be rounded to integer pixel values. The + * rounding algorithm uses error accumulation to ensure an exact fit. + * + * @param {boolean} v whether rounding should be enabled. + * @function + * @name pv.Layout.treemap.prototype.round + * @returns {pv.Layout.treemap} this. + */ + mark.round = function(v) { + round = v; + return this; + }; + + /** + * Specifies the margins to inset child nodes from their parents; as a side + * effect, this also enables the display of internal nodes, which are hidden + * by default. If only a single argument is specified, this value is used to + * inset all four sides. + * + * @param {number} top the top margin. + * @param {number} [right] the right margin. + * @param {number} [bottom] the bottom margin. + * @param {number} [left] the left margin. + * @function + * @name pv.Layout.treemap.prototype.inset + * @returns {pv.Layout.treemap} this. + */ + mark.inset = function(top, right, bottom, left) { + if (arguments.length == 1) right = bottom = left = top; + inset = {top:top, right:right, bottom:bottom, left:left}; + return this; + }; + + /** + * Specifies the root key; optional. The root key is prepended to the + * keys attribute for all generated nodes. This method is provided + * for convenience and does not affect layout. + * + * @param {string} v the root key. + * @function + * @name pv.Layout.treemap.prototype.root + * @returns {pv.Layout.treemap} this. + */ + mark.root = function(v) { + keys = [v]; + return this; + }; + + /** + * Specifies the sizing function. By default, the sizing function is + * Number. The sizing function is invoked for each node in the tree + * (passed to the constructor): the sizing function must return + * undefined or NaN for internal nodes, and a number for + * leaf nodes. The aggregate sizes of internal nodes will be automatically + * computed by the layout. + * + *

For example, if the tree data structure represents a file system, with + * files as leaf nodes, and each file has a bytes attribute, you can + * specify a size function as: + * + *

.size(function(d) d.bytes)
+ * + * This function will return undefined for internal nodes (since + * these do not have a bytes attribute), and a number for leaf nodes. + * + *

Note that the built-in Math.sqrt and Math.log methods + * can be used as sizing functions. These function similarly to + * Number, except perform a root and log scale, respectively. + * + * @param {function} f the new sizing function. + * @function + * @name pv.Layout.treemap.prototype.size + * @returns {pv.Layout.treemap} this. + */ + mark.size = function(f) { + sizeof = f; + return this; + }; + + return mark; +}; + return pv;}();/* + * Parses the Protovis specifications on load, allowing the use of JavaScript + * 1.8 function expressions on browsers that only support JavaScript 1.6. + * + * @see pv.parse + */ +pv.listen(window, "load", function() { + var scripts = document.getElementsByTagName("script"); + for (var i = 0; i < scripts.length; i++) { + var s = scripts[i]; + if (s.type == "text/javascript+protovis") { + try { + pv.Panel.$dom = s; + window.eval(pv.parse(s.textContent || s.innerHTML)); // IE + } catch (e) { + pv.error(e); + } + delete pv.Panel.$dom; + } + } + }); diff --git a/hyperkitty/templates/404.html b/hyperkitty/templates/404.html new file mode 100644 index 0000000..dbc98cb --- /dev/null +++ b/hyperkitty/templates/404.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} + + +

+
+ 404 +

+ Oh! Snap +

+ This is not the page you were looking for right! + Fix This +
+
+ +{% endblock %} \ No newline at end of file diff --git a/hyperkitty/templates/500.html b/hyperkitty/templates/500.html new file mode 100644 index 0000000..6c156d7 --- /dev/null +++ b/hyperkitty/templates/500.html @@ -0,0 +1,37 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} + + +
+
+ 500 +

+ Oh! Snap +

+ This is not the page you were looking for right! + Fix This +
+
+ +{% endblock %} \ No newline at end of file diff --git a/hyperkitty/templates/api.html b/hyperkitty/templates/api.html new file mode 100644 index 0000000..85f3cf0 --- /dev/null +++ b/hyperkitty/templates/api.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block additional_stylesheets %} + +{% endblock %} + +{% block content %} +

REST API

+

+ HyperKitty comes with a small REST API allowing you to programatically retrieve + emails and information. +

+ +
+

Formats

+

+ This REST API can return the information into several formats. + The default format is html to allow human readibility.
+ To change the format, just add + ?format=<FORMAT> to the url +

+

The list of available formats is:

+ +
+ +
+

Emails /api/email/<list name>/<Message-ID>

+

+Using the address /api/email/<list name>/<Message-ID> you will be able to +retrieve the information known about a specific email on the specified mailing-list. +

+

For example: + /api/email/devel@fp.o/<1312985457.28933.34.camel@ankur.pc>/ + +

+
+
+

Threads /api/thread/<list name>/<ThreadID>

+

+

+

+Using the address /api/thread/<list name>/<Message-ID> you will be able to +retrieve the all the email for a specific thread on the specified mailing-list. +

+

For example: + /api/email/devel@fp.o/1/ + +

+
+
+

Search /api/search/<list name>/<field>/<keyword>

+

+

+

+Using the address /api/search/<list name>/<field>/<keyword> you will be able to +search for all emails of the specified mailing-list containing the provided keyword in the given field. +

+

The list of available field is:

+
    +
  • From
  • +
  • Subject
  • +
  • Content
  • +
  • SubjectContent
  • +
+

For example: + /api/search/devel@fp.o/From/pingoured + +

+
+{% endblock %} diff --git a/hyperkitty/templates/base.html b/hyperkitty/templates/base.html new file mode 100644 index 0000000..0ae6d4c --- /dev/null +++ b/hyperkitty/templates/base.html @@ -0,0 +1,65 @@ + + + + + + {% block title %}{{ app_name|title }}{% endblock %} + + + + + + {% block additional_stylesheets %} {% endblock %} + + {% load i18n %} + + + + + + + +
+ {% block content %} {% endblock %} +
+ {% block footer %} {% endblock %} + + + + {% block additionaljs %} {% endblock %} + diff --git a/hyperkitty/templates/index.html b/hyperkitty/templates/index.html new file mode 100644 index 0000000..87ba148 --- /dev/null +++ b/hyperkitty/templates/index.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load i18n %} +{% block content %} +

{% trans 'Lists' %}

+ + + + {% for mlist in lists %} + + + + + + {% endfor %} + +
{{ mlist }} Overview Archives
+ +{% endblock %} diff --git a/hyperkitty/templates/login.html b/hyperkitty/templates/login.html new file mode 100644 index 0000000..a137c97 --- /dev/null +++ b/hyperkitty/templates/login.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + +

Login with username and password

+ + + + +
    +
  • Google
  • + +
  • Yahoo
  • + +
  • +
    + {% csrf_token %} + + Login using BrowserID +
    +
  • + +
+ +{% endblock %} + +{% block additionaljs %} + + + + + +{% endblock additionaljs %} diff --git a/hyperkitty/templates/message.html b/hyperkitty/templates/message.html new file mode 100644 index 0000000..592e85d --- /dev/null +++ b/hyperkitty/templates/message.html @@ -0,0 +1,71 @@ +{% extends "base.html" %} +{% load gravatar %} + +{% block additional_stylesheets %} + +{% endblock %} + +{% block content %} + +
+ {% include 'messages/first_email.html' with first_mail=message %} +
+ +{% endblock %} + +{% block additionaljs %} + + +{% endblock %} diff --git a/hyperkitty/templates/messages/first_email.html b/hyperkitty/templates/messages/first_email.html new file mode 100644 index 0000000..36c8468 --- /dev/null +++ b/hyperkitty/templates/messages/first_email.html @@ -0,0 +1,5 @@ +{% load gravatar %} + +
+ {% include 'messages/message.html' with email=first_mail unfolded='True' %} +
diff --git a/hyperkitty/templates/messages/message.html b/hyperkitty/templates/messages/message.html new file mode 100644 index 0000000..feffcb9 --- /dev/null +++ b/hyperkitty/templates/messages/message.html @@ -0,0 +1,34 @@ +{% load gravatar %} + + +{% if unfolded %} +
+{% else %} + + + diff --git a/hyperkitty/templates/month_view.html b/hyperkitty/templates/month_view.html new file mode 100644 index 0000000..5ba2445 --- /dev/null +++ b/hyperkitty/templates/month_view.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} +{% load poll_extras %} +{% load gravatar %} + +{% block content %} + +
+ {% for email in threads %} + +
+
+ {{email.subject}} + {{email.date}} +
+
+ {% if email.category_tag %} + + {% else %} + + {% endif %} +
+ {% if email.email %} + {% gravatar_img_for_email email.email 40 %} +
+ {% endif %} + {{email.sender}} +
+
+ {{email.content}} +
+
+
+
    +
  • + Tags: +
  • + {% for tag in email.tags %} +
  • + {{tag}} +
  • + {% endfor %} +
+
    +
  • + {{email.participants|length}} participants +
  • +
  • + {{email.answers}} comments +
  • +
+ +
+
+ + {% empty %} + Sorry no emails could be found for your search. + {% endfor %} +
+
+ {% for key, value in archives_length|sort %} +

{{ key }}

+
+ +
+ {% endfor %} +
+ +{% endblock %} + +{% block additionaljs %} + + +{% endblock %} + diff --git a/hyperkitty/templates/recent_activities.html b/hyperkitty/templates/recent_activities.html new file mode 100644 index 0000000..d6f4516 --- /dev/null +++ b/hyperkitty/templates/recent_activities.html @@ -0,0 +1,212 @@ +{% extends "base.html" %} +{% load poll_extras %} +{% load gravatar %} + +{% block additional_stylesheets %} + +{% endblock %} + +{% block content %} + +
+
+
+ +
+
+
+

Recently active discussions

+ {% for email in most_active_threads %} + +
+ #{{forloop.counter}} + {{email.subject}} +
+ +
+
+ + {% endfor %} +
+ +
+

Top discussions the last 30 days

+ {% for email in top_threads %} + +
+ #{{forloop.counter}} + {{email.subject}} +
+ +
+
+ + {% endfor %} +
+ +
+

Prominent discussion maker

+ {% for author in top_author %} + +
+
+ #{{forloop.counter}} +
+
+ {% if author.email %} + {% gravatar_img_for_email author.email 40 %} +
+ {% endif %} +
+
+ {{author.name}} +
+ +{{author.kudos}} kudos +
+
+ + {% endfor %} + +

Tag cloud

+
+ +
+

Discussion by topic the last 30 days

+ {% for category, thread in threads_per_category.items %} +
+

{{category}}

+
    + {% for email in thread %} +
  • + {{email.title}} +
  • + {% endfor %} +
+
+ {% endfor %} +
+
+
+ {% for key, value in archives_length|sort %} +

{{ key }}

+
+ +
+ {% endfor %} +
+{% endblock %} + +{% block additionaljs %} + +{% endblock %} + diff --git a/hyperkitty/templates/register.html b/hyperkitty/templates/register.html new file mode 100644 index 0000000..1e96907 --- /dev/null +++ b/hyperkitty/templates/register.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + + + +{% endblock %} diff --git a/hyperkitty/templates/search.html b/hyperkitty/templates/search.html new file mode 100644 index 0000000..e0e4f8c --- /dev/null +++ b/hyperkitty/templates/search.html @@ -0,0 +1,106 @@ +{% extends "base.html" %} +{% load poll_extras %} +{% load gravatar %} + +{% block content %} + +{% if threads.object_list %} + +{% endif %} + +{% for email in threads.object_list %} + +
+
+ {{email.subject}} + {{email.date}} +
+
+ {% if email.category_tag %} + + {% else %} + + {% endif %} +
+ {% if email.email %} + {% gravatar_img_for_email email.email 40 %} +
+ {% endif %} + {{email.sender}} +
+
+ {{email.content}} +
+
+
+
    +
  • + Tags: +
  • + {% for tag in email.tags %} +
  • + {{tag}} +
  • + {% endfor %} +
+
    +
  • + {{email.participants|length}} participants +
  • +
  • + {{email.answers}} comments +
  • +
+ +
+
+ +{% empty %} +Sorry no emails could be found for your search. +{% endfor %} + +{% if threads.object_list %} + +{% endif %} + +{% endblock %} + +{% block additionaljs %} + + +{% endblock %} + diff --git a/hyperkitty/templates/thread.html b/hyperkitty/templates/thread.html new file mode 100644 index 0000000..abf6747 --- /dev/null +++ b/hyperkitty/templates/thread.html @@ -0,0 +1,99 @@ +{% extends "base.html" %} + +{% load gravatar %} + +{% block additional_stylesheets %} + +{% endblock %} + +{% block content %} + + {% include 'threads/right_col.html' %} + + +
+ + + {% include 'messages/first_email.html' %} + + + {% for email in threads %} +
+ + {% include 'messages/message.html' %} + +
+ {% endfor %} + +
+ + +{% endblock %} + +{% block additionaljs %} + + + + + + +{% endblock %} diff --git a/hyperkitty/templates/threads/add_tag_form.html b/hyperkitty/templates/threads/add_tag_form.html new file mode 100644 index 0000000..65f6577 --- /dev/null +++ b/hyperkitty/templates/threads/add_tag_form.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% block header %} {% endblock %} + +{% block content %} +
+ {% csrf_token %} + {{ addtag_form }} + +
+{% endblock %} diff --git a/hyperkitty/templates/threads/right_col.html b/hyperkitty/templates/threads/right_col.html new file mode 100644 index 0000000..9a1e448 --- /dev/null +++ b/hyperkitty/templates/threads/right_col.html @@ -0,0 +1,60 @@ +{% load gravatar %} + + +
+ +
+
+ 21 +
+
+ days +
+ inactive +
+
+ 24 +
+
+ days +
+ old +
+
+

+ Add to favorite discussions +

+ +
+
+ tags ({{tags|length}}) +
    + {% for tag in tags %} +
  • + {{ tag }} | +
  • + {% endfor %} +
+
+
+
+ {% csrf_token %} + {{ addtag_form.as_p }} + +
+
+
+ participants ({{participants|length}}) +
    + {% for key,value in participants.items %} +
  • + {% gravatar_img_for_email value.email 20%} + {{key}} +
  • + {% endfor %} +
+
+
+ diff --git a/hyperkitty/templates/user_profile.html b/hyperkitty/templates/user_profile.html new file mode 100644 index 0000000..f0aa27e --- /dev/null +++ b/hyperkitty/templates/user_profile.html @@ -0,0 +1,69 @@ +{% extends "base.html" %} +{% load i18n %} +{% load poll_extras %} + +{% block content %} +

User Profile - {{ user }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans 'User name' %}{{ user.username}}
{% trans 'Firstname' %}{{ user.first_name }}
{% trans 'Lastname' %}{{ user.last_name }}
{% trans 'Email' %}{{ user.email }}
{% trans 'Karma' %}{{ user_profile.karma }}
{% trans 'Date Joined' %}{{ user.date_joined }}
+

Up Votes :

+ + + +

Down Votes :

+ + + + +{% endblock %} diff --git a/hyperkitty/templatetags/__init__.py b/hyperkitty/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hyperkitty/templatetags/poll_extras.py b/hyperkitty/templatetags/poll_extras.py new file mode 100644 index 0000000..a5f1032 --- /dev/null +++ b/hyperkitty/templatetags/poll_extras.py @@ -0,0 +1,55 @@ +from django import template +from django.http import HttpRequest +from django.utils.datastructures import SortedDict +import re + +register = template.Library() + +@register.filter(name="trimString") +def trimString(str): + return re.sub('\s+', ' ', str) + +@register.filter(name='sort') +def listsort(value): + if isinstance(value, dict): + new_dict = SortedDict() + key_list = value.keys() + key_list.sort() + key_list.reverse() + for key in key_list: + values = value[key] + values.sort() + values.reverse() + new_dict[key] = values + return new_dict.items() + elif isinstance(value, list): + new_list = list(value) + new_list.sort() + return new_list + else: + return value + listsort.is_safe = True + +@register.filter(name="tomonth") +def to_month(value): + months = ('January', 'February', 'March', 'April', 'May', 'June', + 'July', 'August', 'September', 'October', 'November', 'December') + return months[value -1] + +@register.filter(name="strip_page") +def strip_page(value): + print repr(value), repr(value)[-2] + if not value: + return value + if value.endswith('/') and value[-3] == '/': + end_with_number = False + try: + if int(value[-2]) in range(0,10): + end_with_number = True + if end_with_number: + output = value.rsplit('/', 2) + except ValueError: + output = value.rsplit('/', 1) + else: + output = value.rsplit('/', 1) + return output[0] diff --git a/hyperkitty/tests/__init__.py b/hyperkitty/tests/__init__.py new file mode 100644 index 0000000..4cf4941 --- /dev/null +++ b/hyperkitty/tests/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# + +from gsoc.tests.test_views import * +from gsoc.tests.test_models import * +from gsoc.tests.test_forms import * diff --git a/hyperkitty/tests/test_forms.py b/hyperkitty/tests/test_forms.py new file mode 100644 index 0000000..9a056cd --- /dev/null +++ b/hyperkitty/tests/test_forms.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# diff --git a/hyperkitty/tests/test_models.py b/hyperkitty/tests/test_models.py new file mode 100644 index 0000000..9a056cd --- /dev/null +++ b/hyperkitty/tests/test_models.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# diff --git a/hyperkitty/tests/test_views.py b/hyperkitty/tests/test_views.py new file mode 100644 index 0000000..b94ef43 --- /dev/null +++ b/hyperkitty/tests/test_views.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# + +from django.test import TestCase +from django.test.client import Client +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse + +class AccountViewsTestCase(TestCase): + + def setUp(self): + self.client = Client() + + def test_login(self): + # Try to access user profile (private data) without logging in + response = self.client.get(reverse('user_profile')) + self.assertRedirects(response, "%s?next=%s" % (reverse('user_login'),reverse('user_profile'))) + + def test_profile(self): + User.objects.create_user('testuser', 'syst3m.w0rm+test@gmail.com', 'testPass') + user = self.client.login(username='testuser', password='testPass') + + response = self.client.get(reverse('user_profile')) + self.assertEqual(response.status_code, 200) + + # Verify that user_profile is present in request context + self.assertTrue('user_profile' in response.context) + + # Verify karma for newly created user is 1 + self.assertEqual(response.context['user_profile'].karma, 1) + + + def test_registration(self): + + User.objects.create_user('testuser', 'syst3m.w0rm+test@gmail.com', 'testPass') + user = self.client.login(username='testuser', password='testPass') + + # If the user if already logged in, redirect to index page..don't let him register again + response = self.client.get(reverse('user_registration')) + self.assertRedirects(response, reverse('index')) + self.client.logout() + + # Access the user registration page after logging out and try to register now + response = self.client.get(reverse('user_registration')) + self.assertEqual(response.status_code, 200) + + # @TODO: Try to register a user and verify its working + + + + + + + \ No newline at end of file diff --git a/hyperkitty/todo b/hyperkitty/todo new file mode 100644 index 0000000..ee1603e --- /dev/null +++ b/hyperkitty/todo @@ -0,0 +1,5 @@ +1. Better error handling -> log everything and handle exception gracefully. +2. write test cases +3. look at social-authentication application from detailed point of view. +4. make user profile editable. +5. setup.py to install this as a python application - code refactoring diff --git a/hyperkitty/urls.py b/hyperkitty/urls.py new file mode 100644 index 0000000..29edb06 --- /dev/null +++ b/hyperkitty/urls.py @@ -0,0 +1,106 @@ +from django.conf.urls.defaults import patterns, include, url +from django.conf import settings +from django.views.generic.simple import direct_to_template +from api import EmailResource, ThreadResource, SearchResource + +from django.contrib.staticfiles.urls import staticfiles_urlpatterns + +# Uncomment the next two lines to enable the admin: +from django.contrib import admin +admin.autodiscover() + +urlpatterns = patterns('', + # Account + url(r'^accounts/login/$', 'views.accounts.user_login', name='user_login'), + url(r'^accounts/logout/$', 'views.accounts.user_logout', name='user_logout'), + url(r'^accounts/profile/$', 'views.accounts.user_profile', name='user_profile'), + url(r'^accounts/register/$', 'views.accounts.user_registration', name='user_registration'), + + + # Index + url(r'^/$', 'views.pages.index', name='index'), + url(r'^$', 'views.pages.index', name='index'), + + # Archives + url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/(?P\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/$', + 'views.list.archives'), + url(r'^archives/(?P.*@.*)/$', + 'views.list.archives'), + + # Threads + url(r'^thread/(?P.*@.*)/(?P.+)/$', + 'views.thread.thread_index'), + + + # Lists + url(r'^list/$', 'views.pages.index'), # Can I remove this URL? + url(r'^list/(?P.*@.*)/$', + 'views.list.list'), + + # Search Tag + url(r'^tag/(?P.*@.*)\/(?P.*)\/(?P\d+)/$', + 'views.list.search_tag'), + url(r'^tag/(?P.*@.*)\/(?P.*)/$', + 'views.list.search_tag'), + + # Search + # If page number is present in URL + url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)\/(?P\d+)/$', + 'views.list.search_keyword'), + # Show the first page as default when no page number is present in URL + url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)/$', + 'views.list.search_keyword'), + url(r'^search/(?P.*@.*)/$', + 'views.list.search'), + + + ### MESSAGE LEVEL VIEWS ### + # Vote a message + url(r'^message/(?P.*@.*)/(?P.+)/$', + 'views.message.index'), + + url(r'^vote/(?P.*@.*)/$', + 'views.message.vote'), + ### MESSAGE LEVEL VIEW ENDS ### + + + + ### THREAD LEVEL VIEWS ### + # Thread view page + url(r'^thread/(?P.*@.*)/(?P.+)/$', + 'views.thread.thread_index'), + + # Add Tag to a thread + url(r'^addtag/(?P.*@.*)\/(?P.*)/$', + 'views.thread.add_tag'), + ### THREAD LEVEL VIEW ENDS ### + + + # REST API + url(r'^api/$', 'views.api.api'), + url(r'^api/email\/(?P.*@.*)\/(?P.*)/', + EmailResource.as_view()), + url(r'^api/thread\/(?P.*@.*)\/(?P.*)/', + ThreadResource.as_view()), + url(r'^api/search\/(?P.*@.*)\/(?P.*)\/(?P.*)/', + SearchResource.as_view()), + + # Uncomment the admin/doc line below to enable admin documentation: + # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), + + # Admin + url(r'^admin/', include(admin.site.urls)), + + # Robots.txt + url(r'^robots\.txt$', direct_to_template, + {'template': 'robots.txt', 'mimetype': 'text/plain'}), + + # Social Auth + url(r'', include('social_auth.urls')), + +) +#) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +urlpatterns += staticfiles_urlpatterns() + diff --git a/hyperkitty/utils.py b/hyperkitty/utils.py new file mode 100644 index 0000000..ae55826 --- /dev/null +++ b/hyperkitty/utils.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# + +import logging +import traceback + +from django.conf import settings +from django.core import mail +from django.views.debug import ExceptionReporter, get_exception_reporter_filter + +LOG_FILE = 'hk.log' + +def log(level, *args, **kwargs): + """Small wrapper around logger functions.""" + { + 'debug': logger.debug, + 'error': logger.error, + 'exception': logger.exception, + 'warn': logger.warn + }[level](*args, **kwargs) + + +class HyperKittyLogHandler(logging.Handler): + """A custom HyperKitty log handler. + + If the request is passed as the first argument to the log record, + request data will be provided in the email report. + """ + + def __init__(self, log_to_file=True, email_admins=True): + logging.Handler.__init__(self) + self.log_to_file = log_to_file + self.email_admins = email_admins + + def emit(self, record): + try: + request = record.request + subject = '%s (%s IP): %s' % ( + record.levelname, + (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS + and 'internal' or 'EXTERNAL'), + record.getMessage() + ) + filter = get_exception_reporter_filter(request) + request_repr = filter.get_request_repr(request) + except Exception: + subject = '%s: %s' % ( + record.levelname, + record.getMessage() + ) + request = None + request_repr = "Request repr() unavailable." + subject = self.format_subject(subject) + + if record.exc_info: + exc_info = record.exc_info + stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) + else: + exc_info = (None, record.getMessage(), None) + stack_trace = 'No stack trace available' + + message = "%s\n\n%s" % (stack_trace, request_repr) + reporter = ExceptionReporter(request, is_email=True, *exc_info) + html_message = reporter.get_traceback_html() + + if self.email_admins: + mail.mail_admins(subject, message, fail_silently=True, html_message=html_message) + + if self.log_to_file: + log_file = open(LOG_FILE, 'a') + log_file.write(message) + log_file.close() + + def format_subject(self, subject): + """ + Escape CR and LF characters, and limit length. + RFC 2822's hard limit is 998 characters per line. So, minus "Subject: " + the actual subject must be no longer than 989 characters. + """ + formatted_subject = subject.replace('\n', '\\n').replace('\r', '\\r') + return formatted_subject[:989] + + +logger = None +if not logger: + logger = logging.getLogger('HyperKitty') + +if not logger.handlers: + logger.addHandler(HyperKittyLogHandler(True, True)) diff --git a/hyperkitty/views/__init__.py b/hyperkitty/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hyperkitty/views/accounts.py b/hyperkitty/views/accounts.py new file mode 100644 index 0000000..3154563 --- /dev/null +++ b/hyperkitty/views/accounts.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. +# +# This file is part of HyperKitty. +# +# HyperKitty is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# HyperKitty 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 General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# HyperKitty. If not, see . +# +# Author: Aamir Khan +# + +import re +import sys + +from django.conf import settings +from django.contrib import messages +from django.contrib.auth import logout, authenticate, login +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from django.contrib.auth.forms import AuthenticationForm +from gsoc.models import UserProfile +from django.contrib.auth.models import User +from django.core.urlresolvers import reverse +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render_to_response, redirect +from django.template import Context, loader, RequestContext +from django.utils.translation import gettext as _ +from urllib2 import HTTPError +from urlparse import urlparse + +from forms import RegistrationForm +from gsoc.utils import log + +def user_logout(request): + logout(request) + return redirect('user_login') + +def user_login(request,template = 'login.html'): + + parse_r = urlparse(request.META.get('HTTP_REFERER', 'index')) + previous = '%s%s' % (parse_r.path, parse_r.query) + + next_var = request.POST.get('next', request.GET.get('next', previous)) + + if request.method == 'POST': + form = AuthenticationForm(request.POST) + user = authenticate(username=request.POST.get('username'), + password=request.POST.get('password')) + + if user is not None: + log('debug', user) + if user.is_active: + login(request,user) + return redirect(next_var) + + else: + form = AuthenticationForm() + return render_to_response(template, {'form': form, 'next' : next_var}, + context_instance=RequestContext(request)) + +@login_required +def user_profile(request, user_email = None): + if not request.user.is_authenticated(): + return redirect('user_login') + # try to render the user profile. + try: + user_profile = request.user.get_profile() + # @TODO: Include the error name e.g, ProfileDoesNotExist? + except: + user_profile = UserProfile.objects.create(user=request.user) + + t = loader.get_template('user_profile.html') + + c = RequestContext(request, { + 'user_profile' : user_profile, + }) + + return HttpResponse(t.render(c)) + + +def user_registration(request): + if request.user.is_authenticated(): + # Already registered, redirect back to index page + return redirect('index') + + if request.POST: + form = RegistrationForm(request.POST) + if form.is_valid(): + # Save the user data. + form.save(form.cleaned_data) + user = authenticate(username=form.cleaned_data['username'], + password=form.cleaned_data['password1']) + + if user is not None: + log('debug', user) + if user.is_active: + login(request,user) + return redirect('index') + else: + form = RegistrationForm() + + return render_to_response('register.html', {'form': form}, context_instance=RequestContext(request)) + diff --git a/hyperkitty/views/api.py b/hyperkitty/views/api.py new file mode 100644 index 0000000..3a764db --- /dev/null +++ b/hyperkitty/views/api.py @@ -0,0 +1,27 @@ +import re +import os +import json +import urllib +import django.utils.simplejson as simplejson + +from calendar import timegm +from datetime import datetime, timedelta + +from urlparse import urljoin +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from gsoc.utils import log + + +def api(request): + t = loader.get_template('api.html') + c = RequestContext(request, { + }) + return HttpResponse(t.render(c)) + + diff --git a/hyperkitty/views/forms.py b/hyperkitty/views/forms.py new file mode 100644 index 0000000..97e5550 --- /dev/null +++ b/hyperkitty/views/forms.py @@ -0,0 +1,59 @@ +from django import forms +from django.core import validators +from django.contrib.auth.models import User + +def isValidUsername(username): + try: + User.objects.get(username=username) + except User.DoesNotExist: + return + raise validators.ValidationError('The username "%s" is already taken.' % username) + +class RegistrationForm(forms.Form): + + username = forms.CharField(label='username', help_text=None, + widget=forms.TextInput( + attrs={'placeholder': 'username...'} + ), required = True, validators=[isValidUsername] + ) + + email = forms.EmailField(required=True) + + password1 = forms.CharField(widget=forms.PasswordInput) + + password2 = forms.CharField(widget=forms.PasswordInput) + + def save(self, new_user_data): + u = User.objects.create_user(new_user_data['username'], + new_user_data['email'], + new_user_data['password1']) + u.is_active = True + u.save() + return u + + +class AddTagForm(forms.Form): + tag = forms.CharField(label='', help_text=None, + widget=forms.TextInput( + attrs={'placeholder': 'Add a tag...'} + ) + ) + from_url = forms.CharField(widget=forms.HiddenInput, required=False) + +class SearchForm(forms.Form): + target = forms.CharField(label='', help_text=None, + widget=forms.Select( + choices=(('Subject', 'Subject'), + ('Content', 'Content'), + ('SubjectContent', 'Subject & Content'), + ('From', 'From')) + ) + ) + + keyword = forms.CharField(max_length=100,label='', help_text=None, + widget=forms.TextInput( + attrs={'placeholder': 'Search this list.'} + ) + ) + + diff --git a/hyperkitty/views/list.py b/hyperkitty/views/list.py new file mode 100644 index 0000000..17da2c6 --- /dev/null +++ b/hyperkitty/views/list.py @@ -0,0 +1,291 @@ +import re +import os +import json +import urllib +import django.utils.simplejson as simplejson + +from calendar import timegm +from datetime import datetime, timedelta + +from urlparse import urljoin +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from kittystore.kittysastore import KittySAStore + +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +# @TODO : Move this into settings.py +MONTH_PARTICIPANTS = 284 +MONTH_DISCUSSIONS = 82 + + + +def archives(request, mlist_fqdn, year=None, month=None, day=None): + # No year/month: past 32 days + # year and month: find the 32 days for that month + # @TODO : modify url.py to account for page number + + end_date = None + if year or month or day: + try: + start_day = 1 + end_day = 1 + start_month = int(month) + end_month = int(month) + 1 + start_year = int(year) + end_year = int(year) + if day: + start_day = int(day) + end_day = start_day + 1 + end_month = start_month + if start_month == 12: + end_month = 1 + end_year = start_year + 1 + + begin_date = datetime(start_year, start_month, start_day) + end_date = datetime(end_year, end_month, end_day) + month_string = begin_date.strftime('%B %Y') + except ValueError, err: + print err + logger.error('Wrong format given for the date') + + if not end_date: + today = datetime.utcnow() + begin_date = datetime(today.year, today.month, 1) + end_date = datetime(today.year, today.month+1, 1) + month_string = 'Past thirty days' + list_name = mlist_fqdn.split('@')[0] + + search_form = SearchForm(auto_id=False) + t = loader.get_template('month_view.html') + threads = STORE.get_archives(list_name, start=begin_date, + end=end_date) + + participants = set() + cnt = 0 + for msg in threads: + # Statistics on how many participants and threads this month + participants.add(msg.sender) + msg.participants = STORE.get_thread_participants(list_name, + msg.thread_id) + msg.answers = STORE.get_thread_length(list_name, + msg.thread_id) + threads[cnt] = msg + cnt = cnt + 1 + #print msg + + paginator = Paginator(threads, 10) + pageNo = request.GET.get('page') + + try: + threads = paginator.page(pageNo) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + threads = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + threads = paginator.page(paginator.num_pages) + + + archives_length = STORE.get_archives_length(list_name) + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'month': month_string, + 'month_participants': len(participants), + 'month_discussions': len(threads), + 'threads': threads, + 'archives_length': archives_length, + }) + return HttpResponse(t.render(c)) + +def list(request, mlist_fqdn=None): + if not mlist_fqdn: + return HttpResponseRedirect('/') + t = loader.get_template('recent_activities.html') + search_form = SearchForm(auto_id=False) + list_name = mlist_fqdn.split('@')[0] + + # Get stats for last 30 days + today = datetime.utcnow() + end_date = datetime(today.year, today.month, today.day) + begin_date = end_date - timedelta(days=32) + + threads = STORE.get_archives(list_name=list_name,start=begin_date, + end=end_date) + + participants = set() + dates = {} + cnt = 0 + for msg in threads: + month = msg.date.month + if month < 10: + month = '0%s' % month + day = msg.date.day + if day < 10: + day = '0%s' % day + key = '%s%s%s' % (msg.date.year, month, day) + if key in dates: + dates[key] = dates[key] + 1 + else: + dates[key] = 1 + # Statistics on how many participants and threads this month + participants.add(msg.sender) + msg.participants = STORE.get_thread_participants(list_name, + msg.thread_id) + msg.answers = STORE.get_thread_length(list_name, + msg.thread_id) + threads[cnt] = msg + cnt = cnt + 1 + + # top threads are the one with the most answers + top_threads = sorted(threads, key=lambda entry: entry.answers, reverse=True) + + # active threads are the ones that have the most recent posting + active_threads = sorted(threads, key=lambda entry: entry.date, reverse=True) + + archives_length = STORE.get_archives_length(list_name) + + # top authors are the ones that have the most kudos. How do we determine + # that? Most likes for their post? + authors = generate_top_author() + authors = sorted(authors, key=lambda author: author.kudos) + authors.reverse() + + # Get the list activity per day + days = dates.keys() + days.sort() + dates_string = ["%s/%s/%s" % (key[0:4], key[4:6], key[6:8]) for key in days] + #print days + #print dates_string + evolution = [dates[key] for key in days] + if not evolution: + evolution.append(0) + + # threads per category is the top thread titles in each category + threads_per_category = generate_thread_per_category() + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'month': 'Recent activity', + 'month_participants': len(participants), + 'month_discussions': len(threads), + 'top_threads': top_threads[:5], + 'most_active_threads': active_threads[:5], + 'top_author': authors, + 'threads_per_category': threads_per_category, + 'archives_length': archives_length, + 'evolution': evolution, + 'dates_string': dates_string, + }) + return HttpResponse(t.render(c)) + + +def _search_results_page(request, mlist_fqdn, threads, search_type, + page=1, num_threads=25, limit=None): + search_form = SearchForm(auto_id=False) + t = loader.get_template('search.html') + list_name = mlist_fqdn.split('@')[0] + res_num = len(threads) + + participants = set() + for msg in threads: + participants.add(msg.sender) + + paginator = Paginator(threads, num_threads) + + #If page request is out of range, deliver last page of results. + try: + threads = paginator.page(page) + except (EmptyPage, InvalidPage): + threads = paginator.page(paginator.num_pages) + + cnt = 0 + for msg in threads.object_list: + msg.email = msg.email.strip() + # Statistics on how many participants and threads this month + participants.add(msg.sender) + if msg.thread_id: + msg.participants = STORE.get_thread_participants(list_name, + msg.thread_id) + msg.answers = STORE.get_thread_length(list_name, + msg.thread_id) + else: + msg.participants = 0 + msg.answers = 0 + threads.object_list[cnt] = msg + cnt = cnt + 1 + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'month': search_type, + 'month_participants': len(participants), + 'month_discussions': res_num, + 'threads': threads, + 'full_path': request.get_full_path(), + }) + return HttpResponse(t.render(c)) + + +def search(request, mlist_fqdn): + keyword = request.GET.get('keyword') + target = request.GET.get('target') + page = request.GET.get('page') + if keyword and target: + url = '/search/%s/%s/%s/' % (mlist_fqdn, target, keyword) + if page: + url += '%s/' % page + else: + url = '/search/%s' % (mlist_fqdn) + return HttpResponseRedirect(url) + + +def search_keyword(request, mlist_fqdn, target, keyword, page=1): + ## Should we remove the code below? + ## If urls.py does it job we should never need it + if not keyword: + keyword = request.GET.get('keyword') + if not target: + target = request.GET.get('target') + if not target: + target = 'Subject' + regex = '%%%s%%' % keyword + list_name = mlist_fqdn.split('@')[0] + if target.lower() == 'subjectcontent': + threads = STORE.search_content_subject(list_name, keyword) + elif target.lower() == 'subject': + threads = STORE.search_subject(list_name, keyword) + elif target.lower() == 'content': + threads = STORE.search_content(list_name, keyword) + elif target.lower() == 'from': + threads = STORE.search_sender(list_name, keyword) + + return _search_results_page(request, mlist_fqdn, threads, 'Search', page) + + +def search_tag(request, mlist_fqdn, tag=None, page=1): + '''Searches both tag and topic''' + if tag: + query_string = {'Category': tag.capitalize()} + else: + query_string = None + return _search_results_page(request, mlist_fqdn, query_string, + 'Tag search', page, limit=50) + diff --git a/hyperkitty/views/message.py b/hyperkitty/views/message.py new file mode 100644 index 0000000..0b70969 --- /dev/null +++ b/hyperkitty/views/message.py @@ -0,0 +1,82 @@ +import re +import os +import django.utils.simplejson as simplejson + +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) + +from kittystore.kittysastore import KittySAStore + +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + +def index (request, mlist_fqdn, messageid): + ''' Displays a single message identified by its messageid ''' + list_name = mlist_fqdn.split('@')[0] + + search_form = SearchForm(auto_id=False) + t = loader.get_template('message.html') + message = STORE.get_email(list_name, messageid) + message.email = message.email.strip() + # Extract all the votes for this message + try: + votes = Rating.objects.filter(messageid = messageid) + except Rating.DoesNotExist: + votes = {} + + likes = 0 + dislikes = 0 + + for vote in votes: + if vote.vote == 1: + likes = likes + 1 + elif vote.vote == -1: + dislikes = dislikes + 1 + else: + pass + + message.votes = votes + message.likes = likes + message.dislikes = dislikes + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'message': message, + 'messageid' : messageid, + }) + return HttpResponse(t.render(c)) + + + +@login_required +def vote (request, mlist_fqdn): + """ Add a rating to a given message identified by messageid. """ + if not request.user.is_authenticated(): + return redirect('user_login') + + value = request.POST['vote'] + messageid = request.POST['messageid'] + + # Checks if the user has already voted for a this message. If yes modify db entry else create a new one. + try: + v = Rating.objects.get(user = request.user, messageid = messageid, list_address = mlist_fqdn) + except Rating.DoesNotExist: + v = Rating(list_address=mlist_fqdn, messageid = messageid, vote = value) + + v.user = request.user + v.vote = value + v.save() + response_dict = { } + + return HttpResponse(simplejson.dumps(response_dict), mimetype='application/javascript') diff --git a/hyperkitty/views/pages.py b/hyperkitty/views/pages.py new file mode 100644 index 0000000..189d574 --- /dev/null +++ b/hyperkitty/views/pages.py @@ -0,0 +1,38 @@ +#-*- coding: utf-8 -*- + +import re +import os +import json +import urllib +import django.utils.simplejson as simplejson + +from calendar import timegm +from datetime import datetime, timedelta + +from urlparse import urljoin +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + +def index(request): + t = loader.get_template('index.html') + search_form = SearchForm(auto_id=False) + + base_url = settings.MAILMAN_API_URL % { + 'username': settings.MAILMAN_USER, 'password': settings.MAILMAN_PASS} + + list_data = ['devel@fp.o', 'packaging@fp.o', 'fr-users@fp.o'] + + c = RequestContext(request, { + 'lists': list_data, + 'search_form': search_form, + }) + return HttpResponse(t.render(c)) diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py new file mode 100644 index 0000000..06a1bda --- /dev/null +++ b/hyperkitty/views/thread.py @@ -0,0 +1,113 @@ +import django.utils.simplejson as simplejson + +from django.http import HttpResponse, HttpResponseRedirect +from django.template import RequestContext, loader +from django.conf import settings +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage +from django.contrib.auth.decorators import (login_required, + permission_required, + user_passes_test) +from kittystore.kittysastore import KittySAStore + +from gsoc.models import Rating +from lib.mockup import * +from forms import * +from gsoc.utils import log + +STORE = KittySAStore(settings.KITTYSTORE_URL) + + + +def thread_index (request, mlist_fqdn, threadid): + ''' Displays all the email for a given thread identifier ''' + list_name = mlist_fqdn.split('@')[0] + + search_form = SearchForm(auto_id=False) + t = loader.get_template('thread.html') + threads = STORE.get_thread(list_name, threadid) + #prev_thread = mongo.get_thread_name(list_name, int(threadid) - 1) + prev_thread = [] + if len(prev_thread) > 30: + prev_thread = '%s...' % prev_thread[:31] + #next_thread = mongo.get_thread_name(list_name, int(threadid) + 1) + next_thread = [] + if len(next_thread) > 30: + next_thread = '%s...' % next_thread[:31] + + participants = {} + cnt = 0 + + for message in threads: + # @TODO: Move this logic inside KittyStore? + message.email = message.email.strip() + + # Extract all the votes for this message + try: + votes = Rating.objects.filter(messageid = message.message_id) + except Rating.DoesNotExist: + votes = {} + + likes = 0 + dislikes = 0 + + for vote in votes: + if vote.vote == 1: + likes = likes + 1 + elif vote.vote == -1: + dislikes = dislikes + 1 + else: + pass + + message.votes = votes + message.likes = likes + message.dislikes = dislikes + + # Statistics on how many participants and threads this month + participants[message.sender] = {'email': message.email} + cnt = cnt + 1 + + archives_length = STORE.get_archives_length(list_name) + from_url = '/thread/%s/%s/' %(mlist_fqdn, threadid) + tag_form = AddTagForm(initial={'from_url' : from_url}) + + c = RequestContext(request, { + 'list_name' : list_name, + 'list_address': mlist_fqdn, + 'search_form': search_form, + 'addtag_form': tag_form, + 'month': 'Thread', + 'participants': participants, + 'answers': cnt, + 'first_mail': threads[0], + 'threads': threads[1:], + 'next_thread': next_thread, + 'next_thread_id': 0, + 'prev_thread': prev_thread, + 'prev_thread_id': 0, + 'archives_length': archives_length, + }) + return HttpResponse(t.render(c)) + + +@login_required +def add_tag(request, mlist_fqdn, email_id): + """ Add a tag to a given thread. """ + t = loader.get_template('threads/add_tag_form.html') + if request.method == 'POST': + form = AddTagForm(request.POST) + if form.is_valid(): + print "THERE WE ARE" + # TODO: Add the logic to add the tag + if form.data['from_url']: + return HttpResponseRedirect(form.data['from_url']) + else: + return HttpResponseRedirect('/') + else: + form = AddTagForm() + c = RequestContext(request, { + 'list_address': mlist_fqdn, + 'email_id': email_id, + 'addtag_form': form, + }) + return HttpResponse(t.render(c)) + diff --git a/lib/__init__.py b/lib/__init__.py deleted file mode 100644 index aa5a3d9..0000000 --- a/lib/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -#-*- coding: utf-8 -*- - -from hashlib import md5 -import urllib - - -def gravatar_url(email): - '''Return a gravatar url for an email address''' - size = 64 - default = "http://fedoraproject.org/static/images/" + \ - "fedora_infinity_%ix%i.png" % (size, size) - query_string = urllib.urlencode({'s': size, 'd': default}) - identifier = md5(email).hexdigest() - return 'http://www.gravatar.com/avatar/%s?%s' % (identifier, query_string) diff --git a/lib/mockup.py b/lib/mockup.py deleted file mode 100644 index 6dfe298..0000000 --- a/lib/mockup.py +++ /dev/null @@ -1,167 +0,0 @@ -#-*- coding: utf-8 -*- - - -class Email(object): - """ Email class containing the information needed to store and - display email threads. - """ - - def __init__(self): - """ Constructor. - Instanciate the default attributes of the object. - """ - self.email_id = '' - self.title = '' - self.body = '' - self.tags = [] - self.category = 'question' - self.category_tag = None - self.participants = set(['Pierre-Yves Chibon']) - self.answers = [] - self.liked = 0 - self.author = '' - self.avatar = None - -class Author(object): - """ Author class containing the information needed to get the top - author of the month! - """ - - def __init__(self): - """ Constructor. - Instanciate the default attributes of the object. - """ - self.name = None - self.kudos = 0 - self.avatar = None - - -def get_email_tag(tag): - threads = generate_random_thread() - output = [] - for email in threads: - if tag in email.tags or tag in email.category: - output.append(email) - elif email.category_tag and tag in email.category_tag: - output.append(email) - return output - - -def generate_thread_per_category(): - threads = generate_random_thread() - categories = {} - for thread in threads: - category = thread.category - if thread.category_tag: - category = thread.category_tag - if category in categories.keys(): - categories[category].append(thread) - else: - categories[category] = [thread] - return categories - -def generate_top_author(): - authors = [] - - author = Author() - author.name = 'Pierre-Yves Chibon' - author.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' - author.kudos = 3 - authors.append(author) - - author = Author() - author.name = 'Stanislav Ochotnický' - author.avatar = 'http://sochotni.fedorapeople.org/sochotni.jpg' - author.kudos = 4 - authors.append(author) - - author = Author() - author.name = 'Toshio Kuratomi' - author.avatar = 'https://secure.gravatar.com/avatar/7a9c1d88f484c9806bceca0d6d91e948' - author.kudos = 5 - authors.append(author) - - return authors - -def generate_random_thread(): - threads = [] - - ## 1 - email = Email() - email.email_id = 1 - email.title = 'Headsup! krb5 ccache defaults are changing in Rawhide' - email.age = '6 days' - email.body = '''Dear fellow developers, -with the upcoming Fedora 18 release (currently Rawhide) we are going to change the place where krb5 credential cache files are saved by default. - -The new default for credential caches will be the /run/user/username directory. -''' - email.tags.extend(['rawhide', 'krb5']) - email.participants = set(['Stephen Gallagher', 'Toshio Kuratomi', 'Kevin Fenzi', 'Seth Vidal']) - email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) - email.liked = 1 - email.author = 'Stephen Gallagher' - email.avatar = 'http://fedorapeople.org/~sgallagh/karrde712.png' - threads.append(email) - - ## 2 - email = Email() - email.email_id = 2 - email.title = 'Problem in packaging kicad' - email.age = '6 days' - email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' - email.tags.extend(['packaging', 'kicad']) - email.participants = set(['Pierre-Yves Chibon', 'Tom "spot" Callaway', 'Toshio Kuratomi', 'Kevin Fenzi']) - email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) - email.liked = 0 - email.author = 'Pierre-Yves Chibon' - email.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' - threads.append(email) - - ## 3 - email = Email() - email.email_id = 3 - email.title = 'Update Java Guideline' - email.age = '6 days' - email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' - email.tags.extend(['rawhide', 'krb5']) - email.participants = set(['Stanislav Ochotnický', 'Tom "spot" Callaway', 'Stephen Gallagher', 'Jason Tibbitts', 'Rex Dieter', 'Toshio Kuratomi']) - email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]) - email.liked = 5 - email.category = 'todo' - email.author = 'Stanislav Ochotnický' - email.avatar = 'http://sochotni.fedorapeople.org/sochotni.jpg' - threads.append(email) - - ## 4 - email = Email() - email.email_id = 4 - email.title = 'Agenda for the next Board Meeting' - email.age = '6 days' - email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' - email.tags.extend(['agenda', 'board']) - email.participants = set(['Toshio Kuratomi', 'Tom "spot" Callaway', 'Robyn Bergeron', 'Max Spevack']) - email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) - email.liked = 20 - email.category = 'agenda' - email.author = 'Toshio Kuratomi' - email.avatar = 'https://secure.gravatar.com/avatar/7a9c1d88f484c9806bceca0d6d91e948' - threads.append(email) - - ## 5 - email = Email() - email.email_id = 5 - email.title = 'I told you so! ' - email.age = '6 days' - email.body = '''Paragraph 1: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ''' - email.tags.extend(['systemd', 'mp3', 'pulseaudio']) - email.participants = set(['Pierre-Yves Chibon']) - email.answers.extend([1,2,3,4,5,6,7,8,9,10,11,12]) - email.liked = 0 - email.author = 'Pierre-Yves Chibon' - email.avatar = 'https://secure.gravatar.com/avatar/072b4416fbfad867a44bc7a5be5eddb9' - email.category = 'shut down' - email.category_tag = 'dead' - threads.append(email) - - return threads diff --git a/models.py b/models.py deleted file mode 100644 index 588363d..0000000 --- a/models.py +++ /dev/null @@ -1,77 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# - -from django.db import models -from django.contrib.auth.models import User -from django.conf import settings - -from kittystore.kittysastore import KittySAStore - -from gsoc.utils import log - -STORE = KittySAStore(settings.KITTYSTORE_URL) - - -class Rating(models.Model): - # @TODO: instead of list_address, user list model from kittystore? - list_address = models.CharField(max_length=50) - - # @TODO: instead of messsageid, use message model from kittystore? - messageid = models.CharField(max_length=100) - - user = models.ForeignKey(User) - - vote = models.SmallIntegerField() - - def __unicode__(self): - """Unicode representation""" - if self.vote == 1: - return u'id = %s : %s voted up %s' % (self.id, unicode(self.user), self.messageid) - else: - return u'id = %s : %s voted down %s' % (self.id, unicode(self.user), self.messageid) - - -class UserProfile(models.Model): - # User Object - user = models.OneToOneField(User) - - karma = models.IntegerField(default=1) - - def _get_votes(self): - "Returns all the votes by a user" - # Extract all the votes by this user - try: - votes = Rating.objects.filter(user = self.user) - except Rating.DoesNotExist: - votes = {} - - for vote in votes: - list_name = vote.list_address.split('@')[0] - message = STORE.get_email(list_name, vote.messageid) - vote.message = message - - return votes - - votes = property(_get_votes) - - def __unicode__(self): - """Unicode representation""" - return u'%s' % (unicode(self.user)) diff --git a/robots.txt b/robots.txt deleted file mode 100644 index 5fe5da1..0000000 --- a/robots.txt +++ /dev/null @@ -1,4 +0,0 @@ -User-agent: * -Disallow: /accounts/ -Disallow: /admin/ -Disallow: /vote/ diff --git a/settings.py b/settings.py deleted file mode 100644 index 23e3578..0000000 --- a/settings.py +++ /dev/null @@ -1,195 +0,0 @@ -import os - -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -# Django settings for hyperkitty project. - -DEBUG = True -TEMPLATE_DEBUG = DEBUG - -ADMINS = ( - ('Aamir Khan', 'syst3m.w0rm+hk@gmail.com'), -) - -MANAGERS = ADMINS - -MAILMAN_API_URL=r'http://%(username)s:%(password)s@localhost:8001/3.0/' -MAILMAN_USER='mailmanapi' -MAILMAN_PASS='88ffd62d1094a6248415c59d7538793f3df5de2f04d244087952394e689e902a' - -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': 'hk', # Or path to database file if using sqlite3. - 'USER': 'root', # Not used with sqlite3. - 'PASSWORD': 'rootroot', # Not used with sqlite3. - 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3. - } -} - -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# On Unix systems, a value of None will cause Django to use the same -# timezone as the operating system. -# If running in a Windows environment this must be set to the same as your -# system time zone. -TIME_ZONE = 'America/Chicago' - -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html -LANGUAGE_CODE = 'en-us' - -SITE_ID = 1 - -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True - -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale -USE_L10N = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/home/media/media.lawrence.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://media.lawrence.com/media/", "http://example.com/media/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/home/media/media.lawrence.com/static/" -#STATIC_ROOT = '' -STATIC_ROOT = BASE_DIR + '/static_files/' - -# URL prefix for static files. -# Example: "http://media.lawrence.com/static/" -STATIC_URL = '/static/' - -# URL prefix for admin static files -- CSS, JavaScript and images. -# Make sure to use a trailing slash. -# Examples: "http://foo.com/static/admin/", "/static/admin/". -ADMIN_MEDIA_PREFIX = '/static/admin/' - -# Additional locations of static files -STATICFILES_DIRS = ( - # Put strings here, like "/home/html/static" or "C:/www/django/static". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - BASE_DIR + '/static/', -) - -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) - -# Make this unique, and don't share it with anybody. -SECRET_KEY = 'dtc3%x(k#mzpe32dmhtsb6!3p(izk84f7nuw1-+4x8zsxwsa^z' - -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - - -TEMPLATE_CONTEXT_PROCESSORS = ( - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.core.context_processors.debug", - "django.core.context_processors.i18n", - "django.core.context_processors.media", - "django.core.context_processors.static", - "django.core.context_processors.csrf", - "django.contrib.messages.context_processors.messages", - "gsoc.context_processors.app_name", -) - - -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -) - -ROOT_URLCONF = 'urls' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. - BASE_DIR + '/templates', -) - -AUTHENTICATION_BACKENDS = ( - 'social_auth.backends.google.GoogleBackend', - 'social_auth.backends.yahoo.YahooBackend', - 'social_auth.backends.browserid.BrowserIDBackend', - 'social_auth.backends.OpenIDBackend', - 'django.contrib.auth.backends.ModelBackend', -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', -# 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin', - # 'django.contrib.admindocs', - 'gsoc', - 'social_auth', - 'djangorestframework', - 'gravatar', -) - - -LOGIN_URL = '/accounts/login/' -LOGIN_REDIRECT_URL = '/' -LOGIN_ERROR_URL = '/accounts/login/' -SOCIAL_AUTH_COMPLETE_URL_NAME = 'socialauth_complete' -SOCIAL_AUTH_ASSOCIATE_URL_NAME = 'socialauth_associate_complete' -SOCIAL_AUTH_DEFAULT_USERNAME = 'new_social_auth_user' -SOCIAL_AUTH_UUID_LENGTH = 16 - -AUTH_PROFILE_MODULE = 'gsoc.UserProfile' - - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } -} - -SOCIAL_AUTH_LAST_LOGIN = 'social_auth_last_login_backend' -APP_NAME = 'Fedora Mailman App' -KITTYSTORE_URL = 'postgres://mm3:mm3@localhost/mm3' diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..935d985 --- /dev/null +++ b/setup.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import ez_setup +ez_setup.use_setuptools() + +from setuptools import setup, find_packages + +setup( + name="HyperKitty", + version="0.1", + description="A web interface to access GNU Mailman archives", + long_description=open('README.rst').read(), + url="https://fedorahosted.org/hyperkitty/", + packages=find_packages(exclude=["*.test", "test", "*.test.*"]), + include_package_data=True, + install_requires=open('requirements.txt').read(), + ) diff --git a/static/css/bootstrap.css b/static/css/bootstrap.css deleted file mode 100644 index c3e0c00..0000000 --- a/static/css/bootstrap.css +++ /dev/null @@ -1,3496 +0,0 @@ -/*! - * Bootstrap v2.0.1 - * - * Copyright 2012 Twitter, Inc - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Designed and built with all the love in the world @twitter by @mdo and @fat. - */ -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section { - display: block; -} -audio, canvas, video { - display: inline-block; - *display: inline; - *zoom: 1; -} -audio:not([controls]) { - display: none; -} -html { - font-size: 100%; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} -a:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -a:hover, a:active { - outline: 0; -} -sub, sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} -sup { - top: -0.5em; -} -sub { - bottom: -0.25em; -} -img { - max-width: 100%; - height: auto; - border: 0; - -ms-interpolation-mode: bicubic; -} -button, -input, -select, -textarea { - margin: 0; - font-size: 100%; - vertical-align: middle; -} -button, input { - *overflow: visible; - line-height: normal; -} -button::-moz-focus-inner, input::-moz-focus-inner { - padding: 0; - border: 0; -} -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; - -webkit-appearance: button; -} -input[type="search"] { - -webkit-appearance: textfield; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} -input[type="search"]::-webkit-search-decoration, input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} -textarea { - overflow: auto; - vertical-align: top; -} -.clearfix { - *zoom: 1; -} -.clearfix:before, .clearfix:after { - display: table; - content: ""; -} -.clearfix:after { - clear: both; -} -body { - margin: 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 18px; - color: #333333; - background-color: #ffffff; -} -a { - color: #0088cc; - text-decoration: none; -} -a:hover { - color: #005580; - text-decoration: underline; -} -.row { - margin-left: -20px; - *zoom: 1; -} -.row:before, .row:after { - display: table; - content: ""; -} -.row:after { - clear: both; -} -[class*="span"] { - float: left; - margin-left: 20px; -} -.span1 { - width: 60px; -} -.span2 { - width: 140px; -} -.span3 { - width: 220px; -} -.span4 { - width: 300px; -} -.span5 { - width: 380px; -} -.span6 { - width: 460px; -} -.span7 { - width: 540px; -} -.span8 { - width: 620px; -} -.span9 { - width: 700px; -} -.span10 { - width: 780px; -} -.span11 { - width: 860px; -} -.span12, .container { - width: 940px; -} -.offset1 { - margin-left: 100px; -} -.offset2 { - margin-left: 180px; -} -.offset3 { - margin-left: 260px; -} -.offset4 { - margin-left: 340px; -} -.offset5 { - margin-left: 420px; -} -.offset6 { - margin-left: 500px; -} -.offset7 { - margin-left: 580px; -} -.offset8 { - margin-left: 660px; -} -.offset9 { - margin-left: 740px; -} -.offset10 { - margin-left: 820px; -} -.offset11 { - margin-left: 900px; -} -.row-fluid { - width: 100%; - *zoom: 1; -} -.row-fluid:before, .row-fluid:after { - display: table; - content: ""; -} -.row-fluid:after { - clear: both; -} -.row-fluid > [class*="span"] { - float: left; - margin-left: 2.127659574%; -} -.row-fluid > [class*="span"]:first-child { - margin-left: 0; -} -.row-fluid > .span1 { - width: 6.382978723%; -} -.row-fluid > .span2 { - width: 14.89361702%; -} -.row-fluid > .span3 { - width: 23.404255317%; -} -.row-fluid > .span4 { - width: 31.914893614%; -} -.row-fluid > .span5 { - width: 40.425531911%; -} -.row-fluid > .span6 { - width: 48.93617020799999%; -} -.row-fluid > .span7 { - width: 57.446808505%; -} -.row-fluid > .span8 { - width: 65.95744680199999%; -} -.row-fluid > .span9 { - width: 74.468085099%; -} -.row-fluid > .span10 { - width: 82.97872339599999%; -} -.row-fluid > .span11 { - width: 91.489361693%; -} -.row-fluid > .span12 { - width: 99.99999998999999%; -} -.container { - width: 940px; - margin-left: auto; - margin-right: auto; - *zoom: 1; -} -.container:before, .container:after { - display: table; - content: ""; -} -.container:after { - clear: both; -} -.container-fluid { - padding-left: 20px; - padding-right: 20px; - *zoom: 1; -} -.container-fluid:before, .container-fluid:after { - display: table; - content: ""; -} -.container-fluid:after { - clear: both; -} -p { - margin: 0 0 9px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 18px; -} -p small { - font-size: 11px; - color: #999999; -} -.lead { - margin-bottom: 18px; - font-size: 20px; - font-weight: 200; - line-height: 27px; -} -h1, -h2, -h3, -h4, -h5, -h6 { - margin: 0; - font-weight: bold; - color: #333333; - text-rendering: optimizelegibility; -} -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small { - font-weight: normal; - color: #999999; -} -h1 { - font-size: 30px; - line-height: 36px; -} -h1 small { - font-size: 18px; -} -h2 { - font-size: 24px; - line-height: 36px; -} -h2 small { - font-size: 18px; -} -h3 { - line-height: 27px; - font-size: 18px; -} -h3 small { - font-size: 14px; -} -h4, h5, h6 { - line-height: 18px; -} -h4 { - font-size: 14px; -} -h4 small { - font-size: 12px; -} -h5 { - font-size: 12px; -} -h6 { - font-size: 11px; - color: #999999; - text-transform: uppercase; -} -.page-header { - padding-bottom: 17px; - margin: 18px 0; - border-bottom: 1px solid #eeeeee; -} -.page-header h1 { - line-height: 1; -} -ul, ol { - padding: 0; - margin: 0 0 9px 25px; -} -ul ul, -ul ol, -ol ol, -ol ul { - margin-bottom: 0; -} -ul { - list-style: disc; -} -ol { - list-style: decimal; -} -li { - line-height: 18px; -} -ul.unstyled, ol.unstyled { - margin-left: 0; - list-style: none; -} -dl { - margin-bottom: 18px; -} -dt, dd { - line-height: 18px; -} -dt { - font-weight: bold; -} -dd { - margin-left: 9px; -} -hr { - margin: 18px 0; - border: 0; - border-top: 1px solid #eeeeee; - border-bottom: 1px solid #ffffff; -} -strong { - font-weight: bold; -} -em { - font-style: italic; -} -.muted { - color: #999999; -} -abbr { - font-size: 90%; - text-transform: uppercase; - border-bottom: 1px dotted #ddd; - cursor: help; -} -blockquote { - padding: 0 0 0 15px; - margin: 0 0 18px; - border-left: 5px solid #eeeeee; -} -blockquote p { - margin-bottom: 0; - font-size: 16px; - font-weight: 300; - line-height: 22.5px; -} -blockquote small { - display: block; - line-height: 18px; - color: #999999; -} -blockquote small:before { - content: '\2014 \00A0'; -} -blockquote.pull-right { - float: right; - padding-left: 0; - padding-right: 15px; - border-left: 0; - border-right: 5px solid #eeeeee; -} -blockquote.pull-right p, blockquote.pull-right small { - text-align: right; -} -q:before, -q:after, -blockquote:before, -blockquote:after { - content: ""; -} -address { - display: block; - margin-bottom: 18px; - line-height: 18px; - font-style: normal; -} -small { - font-size: 100%; -} -cite { - font-style: normal; -} -code, pre { - padding: 0 3px 2px; - font-family: Menlo, Monaco, "Courier New", monospace; - font-size: 12px; - color: #333333; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} -code { - padding: 3px 4px; - color: #d14; - background-color: #f7f7f9; - border: 1px solid #e1e1e8; -} -pre { - display: block; - padding: 8.5px; - margin: 0 0 9px; - font-size: 12px; - line-height: 18px; - background-color: #f5f5f5; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, 0.15); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - white-space: pre; - white-space: pre-wrap; - word-break: break-all; - word-wrap: break-word; -} -pre.prettyprint { - margin-bottom: 18px; -} -pre code { - padding: 0; - color: inherit; - background-color: transparent; - border: 0; -} -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} -form { - margin: 0 0 18px; -} -fieldset { - padding: 0; - margin: 0; - border: 0; -} -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 27px; - font-size: 19.5px; - line-height: 36px; - color: #333333; - border: 0; - border-bottom: 1px solid #eee; -} -legend small { - font-size: 13.5px; - color: #999999; -} -label, -input, -button, -select, -textarea { - font-size: 13px; - font-weight: normal; - line-height: 18px; -} -input, -button, -select, -textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; -} -label { - display: block; - margin-bottom: 5px; - color: #333333; -} -input, -textarea, -select, -.uneditable-input { - display: inline-block; - width: 210px; - height: 18px; - padding: 4px; - margin-bottom: 9px; - font-size: 13px; - line-height: 18px; - color: #555555; - border: 1px solid #ccc; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} -.uneditable-textarea { - width: auto; - height: auto; -} -label input, label textarea, label select { - display: block; -} -input[type="image"], input[type="checkbox"], input[type="radio"] { - width: auto; - height: auto; - padding: 0; - margin: 3px 0; - *margin-top: 0; - /* IE7 */ - - line-height: normal; - cursor: pointer; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; - border: 0 \9; - /* IE9 and down */ - -} -input[type="image"] { - border: 0; -} -input[type="file"] { - width: auto; - padding: initial; - line-height: initial; - border: initial; - background-color: #ffffff; - background-color: initial; - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} -input[type="button"], input[type="reset"], input[type="submit"] { - width: auto; - height: auto; -} -select, input[type="file"] { - height: 28px; - /* In IE7, the height of the select element cannot be changed by height, only font-size */ - - *margin-top: 4px; - /* For IE7, add top margin to align select with labels */ - - line-height: 28px; -} -input[type="file"] { - line-height: 18px \9; -} -select { - width: 220px; - background-color: #ffffff; -} -select[multiple], select[size] { - height: auto; -} -input[type="image"] { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} -textarea { - height: auto; -} -input[type="hidden"] { - display: none; -} -.radio, .checkbox { - padding-left: 18px; -} -.radio input[type="radio"], .checkbox input[type="checkbox"] { - float: left; - margin-left: -18px; -} -.controls > .radio:first-child, .controls > .checkbox:first-child { - padding-top: 5px; -} -.radio.inline, .checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} -.radio.inline + .radio.inline, .checkbox.inline + .checkbox.inline { - margin-left: 10px; -} -input, textarea { - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border linear 0.2s, box-shadow linear 0.2s; - -moz-transition: border linear 0.2s, box-shadow linear 0.2s; - -ms-transition: border linear 0.2s, box-shadow linear 0.2s; - -o-transition: border linear 0.2s, box-shadow linear 0.2s; - transition: border linear 0.2s, box-shadow linear 0.2s; -} -input:focus, textarea:focus { - border-color: rgba(82, 168, 236, 0.8); - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6); - outline: 0; - outline: thin dotted \9; - /* IE6-9 */ - -} -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus, -select:focus { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.input-mini { - width: 60px; -} -.input-small { - width: 90px; -} -.input-medium { - width: 150px; -} -.input-large { - width: 210px; -} -.input-xlarge { - width: 270px; -} -.input-xxlarge { - width: 530px; -} -input[class*="span"], -select[class*="span"], -textarea[class*="span"], -.uneditable-input { - float: none; - margin-left: 0; -} -input.span1, textarea.span1, .uneditable-input.span1 { - width: 50px; -} -input.span2, textarea.span2, .uneditable-input.span2 { - width: 130px; -} -input.span3, textarea.span3, .uneditable-input.span3 { - width: 210px; -} -input.span4, textarea.span4, .uneditable-input.span4 { - width: 290px; -} -input.span5, textarea.span5, .uneditable-input.span5 { - width: 370px; -} -input.span6, textarea.span6, .uneditable-input.span6 { - width: 450px; -} -input.span7, textarea.span7, .uneditable-input.span7 { - width: 530px; -} -input.span8, textarea.span8, .uneditable-input.span8 { - width: 610px; -} -input.span9, textarea.span9, .uneditable-input.span9 { - width: 690px; -} -input.span10, textarea.span10, .uneditable-input.span10 { - width: 770px; -} -input.span11, textarea.span11, .uneditable-input.span11 { - width: 850px; -} -input.span12, textarea.span12, .uneditable-input.span12 { - width: 930px; -} -input[disabled], -select[disabled], -textarea[disabled], -input[readonly], -select[readonly], -textarea[readonly] { - background-color: #f5f5f5; - border-color: #ddd; - cursor: not-allowed; -} -.control-group.warning > label, .control-group.warning .help-block, .control-group.warning .help-inline { - color: #c09853; -} -.control-group.warning input, .control-group.warning select, .control-group.warning textarea { - color: #c09853; - border-color: #c09853; -} -.control-group.warning input:focus, .control-group.warning select:focus, .control-group.warning textarea:focus { - border-color: #a47e3c; - -webkit-box-shadow: 0 0 6px #dbc59e; - -moz-box-shadow: 0 0 6px #dbc59e; - box-shadow: 0 0 6px #dbc59e; -} -.control-group.warning .input-prepend .add-on, .control-group.warning .input-append .add-on { - color: #c09853; - background-color: #fcf8e3; - border-color: #c09853; -} -.control-group.error > label, .control-group.error .help-block, .control-group.error .help-inline { - color: #b94a48; -} -.control-group.error input, .control-group.error select, .control-group.error textarea { - color: #b94a48; - border-color: #b94a48; -} -.control-group.error input:focus, .control-group.error select:focus, .control-group.error textarea:focus { - border-color: #953b39; - -webkit-box-shadow: 0 0 6px #d59392; - -moz-box-shadow: 0 0 6px #d59392; - box-shadow: 0 0 6px #d59392; -} -.control-group.error .input-prepend .add-on, .control-group.error .input-append .add-on { - color: #b94a48; - background-color: #f2dede; - border-color: #b94a48; -} -.control-group.success > label, .control-group.success .help-block, .control-group.success .help-inline { - color: #468847; -} -.control-group.success input, .control-group.success select, .control-group.success textarea { - color: #468847; - border-color: #468847; -} -.control-group.success input:focus, .control-group.success select:focus, .control-group.success textarea:focus { - border-color: #356635; - -webkit-box-shadow: 0 0 6px #7aba7b; - -moz-box-shadow: 0 0 6px #7aba7b; - box-shadow: 0 0 6px #7aba7b; -} -.control-group.success .input-prepend .add-on, .control-group.success .input-append .add-on { - color: #468847; - background-color: #dff0d8; - border-color: #468847; -} -input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { - color: #b94a48; - border-color: #ee5f5b; -} -input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { - border-color: #e9322d; - -webkit-box-shadow: 0 0 6px #f8b9b7; - -moz-box-shadow: 0 0 6px #f8b9b7; - box-shadow: 0 0 6px #f8b9b7; -} -.form-actions { - padding: 17px 20px 18px; - margin-top: 18px; - margin-bottom: 18px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; -} -.uneditable-input { - display: block; - background-color: #ffffff; - border-color: #eee; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); - cursor: not-allowed; -} -:-moz-placeholder { - color: #999999; -} -::-webkit-input-placeholder { - color: #999999; -} -.help-block { - display: block; - margin-top: 5px; - margin-bottom: 0; - color: #999999; -} -.help-inline { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; - margin-bottom: 9px; - vertical-align: middle; - padding-left: 5px; -} -.input-prepend, .input-append { - margin-bottom: 5px; - *zoom: 1; -} -.input-prepend:before, -.input-append:before, -.input-prepend:after, -.input-append:after { - display: table; - content: ""; -} -.input-prepend:after, .input-append:after { - clear: both; -} -.input-prepend input, -.input-append input, -.input-prepend .uneditable-input, -.input-append .uneditable-input { - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} -.input-prepend input:focus, -.input-append input:focus, -.input-prepend .uneditable-input:focus, -.input-append .uneditable-input:focus { - position: relative; - z-index: 2; -} -.input-prepend .uneditable-input, .input-append .uneditable-input { - border-left-color: #ccc; -} -.input-prepend .add-on, .input-append .add-on { - float: left; - display: block; - width: auto; - min-width: 16px; - height: 18px; - margin-right: -1px; - padding: 4px 5px; - font-weight: normal; - line-height: 18px; - color: #999999; - text-align: center; - text-shadow: 0 1px 0 #ffffff; - background-color: #f5f5f5; - border: 1px solid #ccc; - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} -.input-prepend .active, .input-append .active { - background-color: #a9dba9; - border-color: #46a546; -} -.input-prepend .add-on { - *margin-top: 1px; - /* IE6-7 */ - -} -.input-append input, .input-append .uneditable-input { - float: left; - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} -.input-append .uneditable-input { - border-left-color: #eee; - border-right-color: #ccc; -} -.input-append .add-on { - margin-right: 0; - margin-left: -1px; - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} -.input-append input:first-child { - *margin-left: -160px; -} -.input-append input:first-child + .add-on { - *margin-left: -21px; -} -.search-query { - padding-left: 14px; - padding-right: 14px; - margin-bottom: 0; - -webkit-border-radius: 14px; - -moz-border-radius: 14px; - border-radius: 14px; -} -.form-search input, -.form-inline input, -.form-horizontal input, -.form-search textarea, -.form-inline textarea, -.form-horizontal textarea, -.form-search select, -.form-inline select, -.form-horizontal select, -.form-search .help-inline, -.form-inline .help-inline, -.form-horizontal .help-inline, -.form-search .uneditable-input, -.form-inline .uneditable-input, -.form-horizontal .uneditable-input { - display: inline-block; - margin-bottom: 0; -} -.form-search .hide, .form-inline .hide, .form-horizontal .hide { - display: none; -} -.form-search label, -.form-inline label, -.form-search .input-append, -.form-inline .input-append, -.form-search .input-prepend, -.form-inline .input-prepend { - display: inline-block; -} -.form-search .input-append .add-on, -.form-inline .input-prepend .add-on, -.form-search .input-append .add-on, -.form-inline .input-prepend .add-on { - vertical-align: middle; -} -.form-search .radio, -.form-inline .radio, -.form-search .checkbox, -.form-inline .checkbox { - margin-bottom: 0; - vertical-align: middle; -} -.control-group { - margin-bottom: 9px; -} -legend + .control-group { - margin-top: 18px; - -webkit-margin-top-collapse: separate; -} -.form-horizontal .control-group { - margin-bottom: 18px; - *zoom: 1; -} -.form-horizontal .control-group:before, .form-horizontal .control-group:after { - display: table; - content: ""; -} -.form-horizontal .control-group:after { - clear: both; -} -.form-horizontal .control-label { - float: left; - width: 140px; - padding-top: 5px; - text-align: right; -} -.form-horizontal .controls { - margin-left: 160px; -} -.form-horizontal .form-actions { - padding-left: 160px; -} -table { - max-width: 100%; - border-collapse: collapse; - border-spacing: 0; -} -.table { - width: 100%; - margin-bottom: 18px; -} -.table th, .table td { - padding: 8px; - line-height: 18px; - text-align: left; - vertical-align: top; - border-top: 1px solid #ddd; -} -.table th { - font-weight: bold; -} -.table thead th { - vertical-align: bottom; -} -.table thead:first-child tr th, .table thead:first-child tr td { - border-top: 0; -} -.table tbody + tbody { - border-top: 2px solid #ddd; -} -.table-condensed th, .table-condensed td { - padding: 4px 5px; -} -.table-bordered { - border: 1px solid #ddd; - border-collapse: separate; - *border-collapse: collapsed; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.table-bordered th + th, -.table-bordered td + td, -.table-bordered th + td, -.table-bordered td + th { - border-left: 1px solid #ddd; -} -.table-bordered thead:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child th, .table-bordered tbody:first-child tr:first-child td { - border-top: 0; -} -.table-bordered thead:first-child tr:first-child th:first-child, .table-bordered tbody:first-child tr:first-child td:first-child { - -webkit-border-radius: 4px 0 0 0; - -moz-border-radius: 4px 0 0 0; - border-radius: 4px 0 0 0; -} -.table-bordered thead:first-child tr:first-child th:last-child, .table-bordered tbody:first-child tr:first-child td:last-child { - -webkit-border-radius: 0 4px 0 0; - -moz-border-radius: 0 4px 0 0; - border-radius: 0 4px 0 0; -} -.table-bordered thead:last-child tr:last-child th:first-child, .table-bordered tbody:last-child tr:last-child td:first-child { - -webkit-border-radius: 0 0 0 4px; - -moz-border-radius: 0 0 0 4px; - border-radius: 0 0 0 4px; -} -.table-bordered thead:last-child tr:last-child th:last-child, .table-bordered tbody:last-child tr:last-child td:last-child { - -webkit-border-radius: 0 0 4px 0; - -moz-border-radius: 0 0 4px 0; - border-radius: 0 0 4px 0; -} -.table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th { - background-color: #f9f9f9; -} -.table tbody tr:hover td, .table tbody tr:hover th { - background-color: #f5f5f5; -} -table .span1 { - float: none; - width: 44px; - margin-left: 0; -} -table .span2 { - float: none; - width: 124px; - margin-left: 0; -} -table .span3 { - float: none; - width: 204px; - margin-left: 0; -} -table .span4 { - float: none; - width: 284px; - margin-left: 0; -} -table .span5 { - float: none; - width: 364px; - margin-left: 0; -} -table .span6 { - float: none; - width: 444px; - margin-left: 0; -} -table .span7 { - float: none; - width: 524px; - margin-left: 0; -} -table .span8 { - float: none; - width: 604px; - margin-left: 0; -} -table .span9 { - float: none; - width: 684px; - margin-left: 0; -} -table .span10 { - float: none; - width: 764px; - margin-left: 0; -} -table .span11 { - float: none; - width: 844px; - margin-left: 0; -} -table .span12 { - float: none; - width: 924px; - margin-left: 0; -} -[class^="icon-"], [class*=" icon-"] { - display: inline-block; - width: 14px; - height: 14px; - line-height: 14px; - vertical-align: text-top; - background-image: url("../img/glyphicons-halflings.png"); - background-position: 14px 14px; - background-repeat: no-repeat; - *margin-right: .3em; -} -[class^="icon-"]:last-child, [class*=" icon-"]:last-child { - *margin-left: 0; -} -.icon-white { - background-image: url("../img/glyphicons-halflings-white.png"); -} -.icon-glass { - background-position: 0 0; -} -.icon-music { - background-position: -24px 0; -} -.icon-search { - background-position: -48px 0; -} -.icon-envelope { - background-position: -72px 0; -} -.icon-heart { - background-position: -96px 0; -} -.icon-star { - background-position: -120px 0; -} -.icon-star-empty { - background-position: -144px 0; -} -.icon-user { - background-position: -168px 0; -} -.icon-film { - background-position: -192px 0; -} -.icon-th-large { - background-position: -216px 0; -} -.icon-th { - background-position: -240px 0; -} -.icon-th-list { - background-position: -264px 0; -} -.icon-ok { - background-position: -288px 0; -} -.icon-remove { - background-position: -312px 0; -} -.icon-zoom-in { - background-position: -336px 0; -} -.icon-zoom-out { - background-position: -360px 0; -} -.icon-off { - background-position: -384px 0; -} -.icon-signal { - background-position: -408px 0; -} -.icon-cog { - background-position: -432px 0; -} -.icon-trash { - background-position: -456px 0; -} -.icon-home { - background-position: 0 -24px; -} -.icon-file { - background-position: -24px -24px; -} -.icon-time { - background-position: -48px -24px; -} -.icon-road { - background-position: -72px -24px; -} -.icon-download-alt { - background-position: -96px -24px; -} -.icon-download { - background-position: -120px -24px; -} -.icon-upload { - background-position: -144px -24px; -} -.icon-inbox { - background-position: -168px -24px; -} -.icon-play-circle { - background-position: -192px -24px; -} -.icon-repeat { - background-position: -216px -24px; -} -.icon-refresh { - background-position: -240px -24px; -} -.icon-list-alt { - background-position: -264px -24px; -} -.icon-lock { - background-position: -287px -24px; -} -.icon-flag { - background-position: -312px -24px; -} -.icon-headphones { - background-position: -336px -24px; -} -.icon-volume-off { - background-position: -360px -24px; -} -.icon-volume-down { - background-position: -384px -24px; -} -.icon-volume-up { - background-position: -408px -24px; -} -.icon-qrcode { - background-position: -432px -24px; -} -.icon-barcode { - background-position: -456px -24px; -} -.icon-tag { - background-position: 0 -48px; -} -.icon-tags { - background-position: -25px -48px; -} -.icon-book { - background-position: -48px -48px; -} -.icon-bookmark { - background-position: -72px -48px; -} -.icon-print { - background-position: -96px -48px; -} -.icon-camera { - background-position: -120px -48px; -} -.icon-font { - background-position: -144px -48px; -} -.icon-bold { - background-position: -167px -48px; -} -.icon-italic { - background-position: -192px -48px; -} -.icon-text-height { - background-position: -216px -48px; -} -.icon-text-width { - background-position: -240px -48px; -} -.icon-align-left { - background-position: -264px -48px; -} -.icon-align-center { - background-position: -288px -48px; -} -.icon-align-right { - background-position: -312px -48px; -} -.icon-align-justify { - background-position: -336px -48px; -} -.icon-list { - background-position: -360px -48px; -} -.icon-indent-left { - background-position: -384px -48px; -} -.icon-indent-right { - background-position: -408px -48px; -} -.icon-facetime-video { - background-position: -432px -48px; -} -.icon-picture { - background-position: -456px -48px; -} -.icon-pencil { - background-position: 0 -72px; -} -.icon-map-marker { - background-position: -24px -72px; -} -.icon-adjust { - background-position: -48px -72px; -} -.icon-tint { - background-position: -72px -72px; -} -.icon-edit { - background-position: -96px -72px; -} -.icon-share { - background-position: -120px -72px; -} -.icon-check { - background-position: -144px -72px; -} -.icon-move { - background-position: -168px -72px; -} -.icon-step-backward { - background-position: -192px -72px; -} -.icon-fast-backward { - background-position: -216px -72px; -} -.icon-backward { - background-position: -240px -72px; -} -.icon-play { - background-position: -264px -72px; -} -.icon-pause { - background-position: -288px -72px; -} -.icon-stop { - background-position: -312px -72px; -} -.icon-forward { - background-position: -336px -72px; -} -.icon-fast-forward { - background-position: -360px -72px; -} -.icon-step-forward { - background-position: -384px -72px; -} -.icon-eject { - background-position: -408px -72px; -} -.icon-chevron-left { - background-position: -432px -72px; -} -.icon-chevron-right { - background-position: -456px -72px; -} -.icon-plus-sign { - background-position: 0 -96px; -} -.icon-minus-sign { - background-position: -24px -96px; -} -.icon-remove-sign { - background-position: -48px -96px; -} -.icon-ok-sign { - background-position: -72px -96px; -} -.icon-question-sign { - background-position: -96px -96px; -} -.icon-info-sign { - background-position: -120px -96px; -} -.icon-screenshot { - background-position: -144px -96px; -} -.icon-remove-circle { - background-position: -168px -96px; -} -.icon-ok-circle { - background-position: -192px -96px; -} -.icon-ban-circle { - background-position: -216px -96px; -} -.icon-arrow-left { - background-position: -240px -96px; -} -.icon-arrow-right { - background-position: -264px -96px; -} -.icon-arrow-up { - background-position: -289px -96px; -} -.icon-arrow-down { - background-position: -312px -96px; -} -.icon-share-alt { - background-position: -336px -96px; -} -.icon-resize-full { - background-position: -360px -96px; -} -.icon-resize-small { - background-position: -384px -96px; -} -.icon-plus { - background-position: -408px -96px; -} -.icon-minus { - background-position: -433px -96px; -} -.icon-asterisk { - background-position: -456px -96px; -} -.icon-exclamation-sign { - background-position: 0 -120px; -} -.icon-gift { - background-position: -24px -120px; -} -.icon-leaf { - background-position: -48px -120px; -} -.icon-fire { - background-position: -72px -120px; -} -.icon-eye-open { - background-position: -96px -120px; -} -.icon-eye-close { - background-position: -120px -120px; -} -.icon-warning-sign { - background-position: -144px -120px; -} -.icon-plane { - background-position: -168px -120px; -} -.icon-calendar { - background-position: -192px -120px; -} -.icon-random { - background-position: -216px -120px; -} -.icon-comment { - background-position: -240px -120px; -} -.icon-magnet { - background-position: -264px -120px; -} -.icon-chevron-up { - background-position: -288px -120px; -} -.icon-chevron-down { - background-position: -313px -119px; -} -.icon-retweet { - background-position: -336px -120px; -} -.icon-shopping-cart { - background-position: -360px -120px; -} -.icon-folder-close { - background-position: -384px -120px; -} -.icon-folder-open { - background-position: -408px -120px; -} -.icon-resize-vertical { - background-position: -432px -119px; -} -.icon-resize-horizontal { - background-position: -456px -118px; -} -.dropdown { - position: relative; -} -.dropdown-toggle { - *margin-bottom: -3px; -} -.dropdown-toggle:active, .open .dropdown-toggle { - outline: 0; -} -.caret { - display: inline-block; - width: 0; - height: 0; - text-indent: -99999px; - *text-indent: 0; - vertical-align: top; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid #000000; - opacity: 0.3; - filter: alpha(opacity=30); - content: "\2193"; -} -.dropdown .caret { - margin-top: 8px; - margin-left: 2px; -} -.dropdown:hover .caret, .open.dropdown .caret { - opacity: 1; - filter: alpha(opacity=100); -} -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - float: left; - display: none; - min-width: 160px; - _width: 160px; - padding: 4px 0; - margin: 0; - list-style: none; - background-color: #ffffff; - border-color: #ccc; - border-color: rgba(0, 0, 0, 0.2); - border-style: solid; - border-width: 1px; - -webkit-border-radius: 0 0 5px 5px; - -moz-border-radius: 0 0 5px 5px; - border-radius: 0 0 5px 5px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - -webkit-background-clip: padding-box; - -moz-background-clip: padding; - background-clip: padding-box; - *border-right-width: 2px; - *border-bottom-width: 2px; -} -.dropdown-menu.bottom-up { - top: auto; - bottom: 100%; - margin-bottom: 2px; -} -.dropdown-menu .divider { - height: 1px; - margin: 5px 1px; - overflow: hidden; - background-color: #e5e5e5; - border-bottom: 1px solid #ffffff; - *width: 100%; - *margin: -5px 0 5px; -} -.dropdown-menu a { - display: block; - padding: 3px 15px; - clear: both; - font-weight: normal; - line-height: 18px; - color: #555555; - white-space: nowrap; -} -.dropdown-menu li > a:hover, .dropdown-menu .active > a, .dropdown-menu .active > a:hover { - color: #ffffff; - text-decoration: none; - background-color: #0088cc; -} -.dropdown.open { - *z-index: 1000; -} -.dropdown.open .dropdown-toggle { - color: #ffffff; - background: #ccc; - background: rgba(0, 0, 0, 0.3); -} -.dropdown.open .dropdown-menu { - display: block; -} -.typeahead { - margin-top: 2px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #eee; - border: 1px solid rgba(0, 0, 0, 0.05); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} -.fade { - -webkit-transition: opacity 0.15s linear; - -moz-transition: opacity 0.15s linear; - -ms-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; - opacity: 0; -} -.fade.in { - opacity: 1; -} -.collapse { - -webkit-transition: height 0.35s ease; - -moz-transition: height 0.35s ease; - -ms-transition: height 0.35s ease; - -o-transition: height 0.35s ease; - transition: height 0.35s ease; - position: relative; - overflow: hidden; - height: 0; -} -.collapse.in { - height: auto; -} -.close { - float: right; - font-size: 20px; - font-weight: bold; - line-height: 18px; - color: #000000; - text-shadow: 0 1px 0 #ffffff; - opacity: 0.2; - filter: alpha(opacity=20); -} -.close:hover { - color: #000000; - text-decoration: none; - opacity: 0.4; - filter: alpha(opacity=40); - cursor: pointer; -} -.btn { - display: inline-block; - padding: 4px 10px 4px; - margin-bottom: 0; - font-size: 13px; - line-height: 18px; - color: #333333; - text-align: center; - text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); - vertical-align: middle; - background-color: #f5f5f5; - background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -ms-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); - background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); - background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); - background-image: linear-gradient(top, #ffffff, #e6e6e6); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0); - border-color: #e6e6e6 #e6e6e6 #bfbfbf; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - border: 1px solid #ccc; - border-bottom-color: #bbb; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - cursor: pointer; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - *margin-left: .3em; -} -.btn:hover, -.btn:active, -.btn.active, -.btn.disabled, -.btn[disabled] { - background-color: #e6e6e6; -} -.btn:active, .btn.active { - background-color: #cccccc \9; -} -.btn:first-child { - *margin-left: 0; -} -.btn:hover { - color: #333333; - text-decoration: none; - background-color: #e6e6e6; - background-position: 0 -15px; - -webkit-transition: background-position 0.1s linear; - -moz-transition: background-position 0.1s linear; - -ms-transition: background-position 0.1s linear; - -o-transition: background-position 0.1s linear; - transition: background-position 0.1s linear; -} -.btn:focus { - outline: thin dotted #333; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.btn.active, .btn:active { - background-image: none; - -webkit-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - background-color: #e6e6e6; - background-color: #d9d9d9 \9; - outline: 0; -} -.btn.disabled, .btn[disabled] { - cursor: default; - background-image: none; - background-color: #e6e6e6; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; -} -.btn-large { - padding: 9px 14px; - font-size: 15px; - line-height: normal; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} -.btn-large [class^="icon-"] { - margin-top: 1px; -} -.btn-small { - padding: 5px 9px; - font-size: 11px; - line-height: 16px; -} -.btn-small [class^="icon-"] { - margin-top: -1px; -} -.btn-mini { - padding: 2px 6px; - font-size: 11px; - line-height: 14px; -} -.btn-primary, -.btn-primary:hover, -.btn-warning, -.btn-warning:hover, -.btn-danger, -.btn-danger:hover, -.btn-success, -.btn-success:hover, -.btn-info, -.btn-info:hover, -.btn-inverse, -.btn-inverse:hover { - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - color: #ffffff; -} -.btn-primary.active, -.btn-warning.active, -.btn-danger.active, -.btn-success.active, -.btn-info.active, -.btn-dark.active { - color: rgba(255, 255, 255, 0.75); -} -.btn-primary { - background-color: #006dcc; - background-image: -moz-linear-gradient(top, #0088cc, #0044cc); - background-image: -ms-linear-gradient(top, #0088cc, #0044cc); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc)); - background-image: -webkit-linear-gradient(top, #0088cc, #0044cc); - background-image: -o-linear-gradient(top, #0088cc, #0044cc); - background-image: linear-gradient(top, #0088cc, #0044cc); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); - border-color: #0044cc #0044cc #002a80; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.btn-primary:hover, -.btn-primary:active, -.btn-primary.active, -.btn-primary.disabled, -.btn-primary[disabled] { - background-color: #0044cc; -} -.btn-primary:active, .btn-primary.active { - background-color: #003399 \9; -} -.btn-warning { - background-color: #faa732; - background-image: -moz-linear-gradient(top, #fbb450, #f89406); - background-image: -ms-linear-gradient(top, #fbb450, #f89406); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406)); - background-image: -webkit-linear-gradient(top, #fbb450, #f89406); - background-image: -o-linear-gradient(top, #fbb450, #f89406); - background-image: linear-gradient(top, #fbb450, #f89406); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0); - border-color: #f89406 #f89406 #ad6704; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.btn-warning:hover, -.btn-warning:active, -.btn-warning.active, -.btn-warning.disabled, -.btn-warning[disabled] { - background-color: #f89406; -} -.btn-warning:active, .btn-warning.active { - background-color: #c67605 \9; -} -.btn-danger { - background-color: #da4f49; - background-image: -moz-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -ms-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #bd362f); - background-image: -o-linear-gradient(top, #ee5f5b, #bd362f); - background-image: linear-gradient(top, #ee5f5b, #bd362f); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0); - border-color: #bd362f #bd362f #802420; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.btn-danger:hover, -.btn-danger:active, -.btn-danger.active, -.btn-danger.disabled, -.btn-danger[disabled] { - background-color: #bd362f; -} -.btn-danger:active, .btn-danger.active { - background-color: #942a25 \9; -} -.btn-success { - background-color: #5bb75b; - background-image: -moz-linear-gradient(top, #62c462, #51a351); - background-image: -ms-linear-gradient(top, #62c462, #51a351); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351)); - background-image: -webkit-linear-gradient(top, #62c462, #51a351); - background-image: -o-linear-gradient(top, #62c462, #51a351); - background-image: linear-gradient(top, #62c462, #51a351); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0); - border-color: #51a351 #51a351 #387038; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.btn-success:hover, -.btn-success:active, -.btn-success.active, -.btn-success.disabled, -.btn-success[disabled] { - background-color: #51a351; -} -.btn-success:active, .btn-success.active { - background-color: #408140 \9; -} -.btn-info { - background-color: #49afcd; - background-image: -moz-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -ms-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4)); - background-image: -webkit-linear-gradient(top, #5bc0de, #2f96b4); - background-image: -o-linear-gradient(top, #5bc0de, #2f96b4); - background-image: linear-gradient(top, #5bc0de, #2f96b4); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0); - border-color: #2f96b4 #2f96b4 #1f6377; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.btn-info:hover, -.btn-info:active, -.btn-info.active, -.btn-info.disabled, -.btn-info[disabled] { - background-color: #2f96b4; -} -.btn-info:active, .btn-info.active { - background-color: #24748c \9; -} -.btn-inverse { - background-color: #393939; - background-image: -moz-linear-gradient(top, #454545, #262626); - background-image: -ms-linear-gradient(top, #454545, #262626); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#454545), to(#262626)); - background-image: -webkit-linear-gradient(top, #454545, #262626); - background-image: -o-linear-gradient(top, #454545, #262626); - background-image: linear-gradient(top, #454545, #262626); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#454545', endColorstr='#262626', GradientType=0); - border-color: #262626 #262626 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.btn-inverse:hover, -.btn-inverse:active, -.btn-inverse.active, -.btn-inverse.disabled, -.btn-inverse[disabled] { - background-color: #262626; -} -.btn-inverse:active, .btn-inverse.active { - background-color: #0c0c0c \9; -} -button.btn, input[type="submit"].btn { - *padding-top: 2px; - *padding-bottom: 2px; -} -button.btn::-moz-focus-inner, input[type="submit"].btn::-moz-focus-inner { - padding: 0; - border: 0; -} -button.btn.large, input[type="submit"].btn.large { - *padding-top: 7px; - *padding-bottom: 7px; -} -button.btn.small, input[type="submit"].btn.small { - *padding-top: 3px; - *padding-bottom: 3px; -} -.btn-group { - position: relative; - *zoom: 1; - *margin-left: .3em; -} -.btn-group:before, .btn-group:after { - display: table; - content: ""; -} -.btn-group:after { - clear: both; -} -.btn-group:first-child { - *margin-left: 0; -} -.btn-group + .btn-group { - margin-left: 5px; -} -.btn-toolbar { - margin-top: 9px; - margin-bottom: 9px; -} -.btn-toolbar .btn-group { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; -} -.btn-group .btn { - position: relative; - float: left; - margin-left: -1px; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} -.btn-group .btn:first-child { - margin-left: 0; - -webkit-border-top-left-radius: 4px; - -moz-border-radius-topleft: 4px; - border-top-left-radius: 4px; - -webkit-border-bottom-left-radius: 4px; - -moz-border-radius-bottomleft: 4px; - border-bottom-left-radius: 4px; -} -.btn-group .btn:last-child, .btn-group .dropdown-toggle { - -webkit-border-top-right-radius: 4px; - -moz-border-radius-topright: 4px; - border-top-right-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - -moz-border-radius-bottomright: 4px; - border-bottom-right-radius: 4px; -} -.btn-group .btn.large:first-child { - margin-left: 0; - -webkit-border-top-left-radius: 6px; - -moz-border-radius-topleft: 6px; - border-top-left-radius: 6px; - -webkit-border-bottom-left-radius: 6px; - -moz-border-radius-bottomleft: 6px; - border-bottom-left-radius: 6px; -} -.btn-group .btn.large:last-child, .btn-group .large.dropdown-toggle { - -webkit-border-top-right-radius: 6px; - -moz-border-radius-topright: 6px; - border-top-right-radius: 6px; - -webkit-border-bottom-right-radius: 6px; - -moz-border-radius-bottomright: 6px; - border-bottom-right-radius: 6px; -} -.btn-group .btn:hover, -.btn-group .btn:focus, -.btn-group .btn:active, -.btn-group .btn.active { - z-index: 2; -} -.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle { - outline: 0; -} -.btn-group .dropdown-toggle { - padding-left: 8px; - padding-right: 8px; - -webkit-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); - *padding-top: 5px; - *padding-bottom: 5px; -} -.btn-group.open { - *z-index: 1000; -} -.btn-group.open .dropdown-menu { - display: block; - margin-top: 1px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} -.btn-group.open .dropdown-toggle { - background-image: none; - -webkit-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 6px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); -} -.btn .caret { - margin-top: 7px; - margin-left: 0; -} -.btn:hover .caret, .open.btn-group .caret { - opacity: 1; - filter: alpha(opacity=100); -} -.btn-primary .caret, -.btn-danger .caret, -.btn-info .caret, -.btn-success .caret, -.btn-inverse .caret { - border-top-color: #ffffff; - opacity: 0.75; - filter: alpha(opacity=75); -} -.btn-small .caret { - margin-top: 4px; -} -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 18px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.alert, .alert-heading { - color: #c09853; -} -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 18px; -} -.alert-success { - background-color: #dff0d8; - border-color: #d6e9c6; -} -.alert-success, .alert-success .alert-heading { - color: #468847; -} -.alert-danger, .alert-error { - background-color: #f2dede; - border-color: #eed3d7; -} -.alert-danger, -.alert-error, -.alert-danger .alert-heading, -.alert-error .alert-heading { - color: #b94a48; -} -.alert-info { - background-color: #d9edf7; - border-color: #bce8f1; -} -.alert-info, .alert-info .alert-heading { - color: #3a87ad; -} -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} -.alert-block > p, .alert-block > ul { - margin-bottom: 0; -} -.alert-block p + p { - margin-top: 5px; -} -.nav { - margin-left: 0; - margin-bottom: 18px; - list-style: none; -} -.nav > li > a { - display: block; -} -.nav > li > a:hover { - text-decoration: none; - background-color: #eeeeee; -} -.nav .nav-header { - display: block; - padding: 3px 15px; - font-size: 11px; - font-weight: bold; - line-height: 18px; - color: #999999; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - text-transform: uppercase; -} -.nav li + .nav-header { - margin-top: 9px; -} -.nav-list { - padding-left: 14px; - padding-right: 14px; - margin-bottom: 0; -} -.nav-list > li > a, .nav-list .nav-header { - margin-left: -15px; - margin-right: -15px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); -} -.nav-list > li > a { - padding: 3px 15px; -} -.nav-list .active > a, .nav-list .active > a:hover { - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2); - background-color: #0088cc; -} -.nav-list [class^="icon-"] { - margin-right: 2px; -} -.nav-tabs, .nav-pills { - *zoom: 1; -} -.nav-tabs:before, -.nav-pills:before, -.nav-tabs:after, -.nav-pills:after { - display: table; - content: ""; -} -.nav-tabs:after, .nav-pills:after { - clear: both; -} -.nav-tabs > li, .nav-pills > li { - float: left; -} -.nav-tabs > li > a, .nav-pills > li > a { - padding-right: 12px; - padding-left: 12px; - margin-right: 2px; - line-height: 14px; -} -.nav-tabs { - border-bottom: 1px solid #ddd; -} -.nav-tabs > li { - margin-bottom: -1px; -} -.nav-tabs > li > a { - padding-top: 9px; - padding-bottom: 9px; - border: 1px solid transparent; - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} -.nav-tabs > li > a:hover { - border-color: #eeeeee #eeeeee #dddddd; -} -.nav-tabs > .active > a, .nav-tabs > .active > a:hover { - color: #555555; - background-color: #ffffff; - border: 1px solid #ddd; - border-bottom-color: transparent; - cursor: default; -} -.nav-pills > li > a { - padding-top: 8px; - padding-bottom: 8px; - margin-top: 2px; - margin-bottom: 2px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; -} -.nav-pills .active > a, .nav-pills .active > a:hover { - color: #ffffff; - background-color: #0088cc; -} -.nav-stacked > li { - float: none; -} -.nav-stacked > li > a { - margin-right: 0; -} -.nav-tabs.nav-stacked { - border-bottom: 0; -} -.nav-tabs.nav-stacked > li > a { - border: 1px solid #ddd; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} -.nav-tabs.nav-stacked > li:first-child > a { - -webkit-border-radius: 4px 4px 0 0; - -moz-border-radius: 4px 4px 0 0; - border-radius: 4px 4px 0 0; -} -.nav-tabs.nav-stacked > li:last-child > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} -.nav-tabs.nav-stacked > li > a:hover { - border-color: #ddd; - z-index: 2; -} -.nav-pills.nav-stacked > li > a { - margin-bottom: 3px; -} -.nav-pills.nav-stacked > li:last-child > a { - margin-bottom: 1px; -} -.nav-tabs .dropdown-menu, .nav-pills .dropdown-menu { - margin-top: 1px; - border-width: 1px; -} -.nav-pills .dropdown-menu { - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.nav-tabs .dropdown-toggle .caret, .nav-pills .dropdown-toggle .caret { - border-top-color: #0088cc; - margin-top: 6px; -} -.nav-tabs .dropdown-toggle:hover .caret, .nav-pills .dropdown-toggle:hover .caret { - border-top-color: #005580; -} -.nav-tabs .active .dropdown-toggle .caret, .nav-pills .active .dropdown-toggle .caret { - border-top-color: #333333; -} -.nav > .dropdown.active > a:hover { - color: #000000; - cursor: pointer; -} -.nav-tabs .open .dropdown-toggle, .nav-pills .open .dropdown-toggle, .nav > .open.active > a:hover { - color: #ffffff; - background-color: #999999; - border-color: #999999; -} -.nav .open .caret, .nav .open.active .caret, .nav .open a:hover .caret { - border-top-color: #ffffff; - opacity: 1; - filter: alpha(opacity=100); -} -.tabs-stacked .open > a:hover { - border-color: #999999; -} -.tabbable { - *zoom: 1; -} -.tabbable:before, .tabbable:after { - display: table; - content: ""; -} -.tabbable:after { - clear: both; -} -.tab-content { - overflow: hidden; -} -.tabs-below .nav-tabs, .tabs-right .nav-tabs, .tabs-left .nav-tabs { - border-bottom: 0; -} -.tab-content > .tab-pane, .pill-content > .pill-pane { - display: none; -} -.tab-content > .active, .pill-content > .active { - display: block; -} -.tabs-below .nav-tabs { - border-top: 1px solid #ddd; -} -.tabs-below .nav-tabs > li { - margin-top: -1px; - margin-bottom: 0; -} -.tabs-below .nav-tabs > li > a { - -webkit-border-radius: 0 0 4px 4px; - -moz-border-radius: 0 0 4px 4px; - border-radius: 0 0 4px 4px; -} -.tabs-below .nav-tabs > li > a:hover { - border-bottom-color: transparent; - border-top-color: #ddd; -} -.tabs-below .nav-tabs .active > a, .tabs-below .nav-tabs .active > a:hover { - border-color: transparent #ddd #ddd #ddd; -} -.tabs-left .nav-tabs > li, .tabs-right .nav-tabs > li { - float: none; -} -.tabs-left .nav-tabs > li > a, .tabs-right .nav-tabs > li > a { - min-width: 74px; - margin-right: 0; - margin-bottom: 3px; -} -.tabs-left .nav-tabs { - float: left; - margin-right: 19px; - border-right: 1px solid #ddd; -} -.tabs-left .nav-tabs > li > a { - margin-right: -1px; - -webkit-border-radius: 4px 0 0 4px; - -moz-border-radius: 4px 0 0 4px; - border-radius: 4px 0 0 4px; -} -.tabs-left .nav-tabs > li > a:hover { - border-color: #eeeeee #dddddd #eeeeee #eeeeee; -} -.tabs-left .nav-tabs .active > a, .tabs-left .nav-tabs .active > a:hover { - border-color: #ddd transparent #ddd #ddd; - *border-right-color: #ffffff; -} -.tabs-right .nav-tabs { - float: right; - margin-left: 19px; - border-left: 1px solid #ddd; -} -.tabs-right .nav-tabs > li > a { - margin-left: -1px; - -webkit-border-radius: 0 4px 4px 0; - -moz-border-radius: 0 4px 4px 0; - border-radius: 0 4px 4px 0; -} -.tabs-right .nav-tabs > li > a:hover { - border-color: #eeeeee #eeeeee #eeeeee #dddddd; -} -.tabs-right .nav-tabs .active > a, .tabs-right .nav-tabs .active > a:hover { - border-color: #ddd #ddd #ddd transparent; - *border-left-color: #ffffff; -} -.navbar { - overflow: visible; - margin-bottom: 18px; -} -.navbar-inner { - padding-left: 20px; - padding-right: 20px; - background-color: #2c2c2c; - background-image: -moz-linear-gradient(top, #333333, #222222); - background-image: -ms-linear-gradient(top, #333333, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); - background-image: -webkit-linear-gradient(top, #333333, #222222); - background-image: -o-linear-gradient(top, #333333, #222222); - background-image: linear-gradient(top, #333333, #222222); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); - -moz-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); -} -.btn-navbar { - display: none; - float: right; - padding: 7px 10px; - margin-left: 5px; - margin-right: 5px; - background-color: #2c2c2c; - background-image: -moz-linear-gradient(top, #333333, #222222); - background-image: -ms-linear-gradient(top, #333333, #222222); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222)); - background-image: -webkit-linear-gradient(top, #333333, #222222); - background-image: -o-linear-gradient(top, #333333, #222222); - background-image: linear-gradient(top, #333333, #222222); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0); - border-color: #222222 #222222 #000000; - border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075); -} -.btn-navbar:hover, -.btn-navbar:active, -.btn-navbar.active, -.btn-navbar.disabled, -.btn-navbar[disabled] { - background-color: #222222; -} -.btn-navbar:active, .btn-navbar.active { - background-color: #080808 \9; -} -.btn-navbar .icon-bar { - display: block; - width: 18px; - height: 2px; - background-color: #f5f5f5; - -webkit-border-radius: 1px; - -moz-border-radius: 1px; - border-radius: 1px; - -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - -moz-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); - box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25); -} -.btn-navbar .icon-bar + .icon-bar { - margin-top: 3px; -} -.nav-collapse.collapse { - height: auto; -} -.navbar .brand:hover { - text-decoration: none; -} -.navbar .brand { - float: left; - display: block; - padding: 8px 20px 12px; - margin-left: -20px; - font-size: 20px; - font-weight: 200; - line-height: 1; - color: #ffffff; -} -.navbar .navbar-text { - margin-bottom: 0; - line-height: 40px; - color: #999999; -} -.navbar .navbar-text a:hover { - color: #ffffff; - background-color: transparent; -} -.navbar .btn, .navbar .btn-group { - margin-top: 5px; -} -.navbar .btn-group .btn { - margin-top: 0; -} -.navbar-form { - margin-bottom: 0; - *zoom: 1; -} -.navbar-form:before, .navbar-form:after { - display: table; - content: ""; -} -.navbar-form:after { - clear: both; -} -.navbar-form input, .navbar-form select { - display: inline-block; - margin-top: 5px; - margin-bottom: 0; -} -.navbar-form .radio, .navbar-form .checkbox { - margin-top: 5px; -} -.navbar-form input[type="image"], .navbar-form input[type="checkbox"], .navbar-form input[type="radio"] { - margin-top: 3px; -} -.navbar-form .input-append, .navbar-form .input-prepend { - margin-top: 6px; - white-space: nowrap; -} -.navbar-form .input-append input, .navbar-form .input-prepend input { - margin-top: 0; -} -.navbar-search { - position: relative; - float: left; - margin-top: 6px; - margin-bottom: 0; -} -.navbar-search .search-query { - padding: 4px 9px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - font-weight: normal; - line-height: 1; - color: #ffffff; - color: rgba(255, 255, 255, 0.75); - background: #666; - background: rgba(255, 255, 255, 0.3); - border: 1px solid #111; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0px rgba(255, 255, 255, 0.15); - -webkit-transition: none; - -moz-transition: none; - -ms-transition: none; - -o-transition: none; - transition: none; -} -.navbar-search .search-query :-moz-placeholder { - color: #eeeeee; -} -.navbar-search .search-query::-webkit-input-placeholder { - color: #eeeeee; -} -.navbar-search .search-query:hover { - color: #ffffff; - background-color: #999999; - background-color: rgba(255, 255, 255, 0.5); -} -.navbar-search .search-query:focus, .navbar-search .search-query.focused { - padding: 5px 10px; - color: #333333; - text-shadow: 0 1px 0 #ffffff; - background-color: #ffffff; - border: 0; - -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - -moz-box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - box-shadow: 0 0 3px rgba(0, 0, 0, 0.15); - outline: 0; -} -.navbar-fixed-top { - position: fixed; - top: 0; - right: 0; - left: 0; - z-index: 1030; -} -.navbar-fixed-top .navbar-inner { - padding-left: 0; - padding-right: 0; - -webkit-border-radius: 0; - -moz-border-radius: 0; - border-radius: 0; -} -.navbar .nav { - position: relative; - left: 0; - display: block; - float: left; - margin: 0 10px 0 0; -} -.navbar .nav.pull-right { - float: right; -} -.navbar .nav > li { - display: block; - float: left; -} -.navbar .nav > li > a { - float: none; - padding: 10px 10px 11px; - line-height: 19px; - color: #999999; - text-decoration: none; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); -} -.navbar .nav > li > a:hover { - background-color: transparent; - color: #ffffff; - text-decoration: none; -} -.navbar .nav .active > a, .navbar .nav .active > a:hover { - color: #ffffff; - text-decoration: none; - background-color: #222222; -} -.navbar .divider-vertical { - height: 40px; - width: 1px; - margin: 0 9px; - overflow: hidden; - background-color: #222222; - border-right: 1px solid #333333; -} -.navbar .nav.pull-right { - margin-left: 10px; - margin-right: 0; -} -.navbar .dropdown-menu { - margin-top: 1px; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.navbar .dropdown-menu:before { - content: ''; - display: inline-block; - border-left: 7px solid transparent; - border-right: 7px solid transparent; - border-bottom: 7px solid #ccc; - border-bottom-color: rgba(0, 0, 0, 0.2); - position: absolute; - top: -7px; - left: 9px; -} -.navbar .dropdown-menu:after { - content: ''; - display: inline-block; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #ffffff; - position: absolute; - top: -6px; - left: 10px; -} -.navbar .nav .dropdown-toggle .caret, .navbar .nav .open.dropdown .caret { - border-top-color: #ffffff; -} -.navbar .nav .active .caret { - opacity: 1; - filter: alpha(opacity=100); -} -.navbar .nav .open > .dropdown-toggle, .navbar .nav .active > .dropdown-toggle, .navbar .nav .open.active > .dropdown-toggle { - background-color: transparent; -} -.navbar .nav .active > .dropdown-toggle:hover { - color: #ffffff; -} -.navbar .nav.pull-right .dropdown-menu { - left: auto; - right: 0; -} -.navbar .nav.pull-right .dropdown-menu:before { - left: auto; - right: 12px; -} -.navbar .nav.pull-right .dropdown-menu:after { - left: auto; - right: 13px; -} -.breadcrumb { - padding: 7px 14px; - margin: 0 0 18px; - background-color: #fbfbfb; - background-image: -moz-linear-gradient(top, #ffffff, #f5f5f5); - background-image: -ms-linear-gradient(top, #ffffff, #f5f5f5); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f5f5f5)); - background-image: -webkit-linear-gradient(top, #ffffff, #f5f5f5); - background-image: -o-linear-gradient(top, #ffffff, #f5f5f5); - background-image: linear-gradient(top, #ffffff, #f5f5f5); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0); - border: 1px solid #ddd; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; -} -.breadcrumb li { - display: inline-block; - text-shadow: 0 1px 0 #ffffff; -} -.breadcrumb .divider { - padding: 0 5px; - color: #999999; -} -.breadcrumb .active a { - color: #333333; -} -.pagination { - height: 36px; - margin: 18px 0; -} -.pagination ul { - display: inline-block; - *display: inline; - /* IE7 inline-block hack */ - - *zoom: 1; - margin-left: 0; - margin-bottom: 0; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} -.pagination li { - display: inline; -} -.pagination a { - float: left; - padding: 0 14px; - line-height: 34px; - text-decoration: none; - border: 1px solid #ddd; - border-left-width: 0; -} -.pagination a:hover, .pagination .active a { - background-color: #f5f5f5; -} -.pagination .active a { - color: #999999; - cursor: default; -} -.pagination .disabled a, .pagination .disabled a:hover { - color: #999999; - background-color: transparent; - cursor: default; -} -.pagination li:first-child a { - border-left-width: 1px; - -webkit-border-radius: 3px 0 0 3px; - -moz-border-radius: 3px 0 0 3px; - border-radius: 3px 0 0 3px; -} -.pagination li:last-child a { - -webkit-border-radius: 0 3px 3px 0; - -moz-border-radius: 0 3px 3px 0; - border-radius: 0 3px 3px 0; -} -.pagination-centered { - text-align: center; -} -.pagination-right { - text-align: right; -} -.pager { - margin-left: 0; - margin-bottom: 18px; - list-style: none; - text-align: center; - *zoom: 1; -} -.pager:before, .pager:after { - display: table; - content: ""; -} -.pager:after { - clear: both; -} -.pager li { - display: inline; -} -.pager a { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - -webkit-border-radius: 15px; - -moz-border-radius: 15px; - border-radius: 15px; -} -.pager a:hover { - text-decoration: none; - background-color: #f5f5f5; -} -.pager .next a { - float: right; -} -.pager .previous a { - float: left; -} -.modal-open .dropdown-menu { - z-index: 2050; -} -.modal-open .dropdown.open { - *z-index: 2050; -} -.modal-open .popover { - z-index: 2060; -} -.modal-open .tooltip { - z-index: 2070; -} -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} -.modal-backdrop.fade { - opacity: 0; -} -.modal-backdrop, .modal-backdrop.fade.in { - opacity: 0.8; - filter: alpha(opacity=80); -} -.modal { - position: fixed; - top: 50%; - left: 50%; - z-index: 1050; - max-height: 500px; - overflow: auto; - width: 560px; - margin: -250px 0 0 -280px; - background-color: #ffffff; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, 0.3); - *border: 1px solid #999; - /* IE6-7 */ - - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} -.modal.fade { - -webkit-transition: opacity .3s linear, top .3s ease-out; - -moz-transition: opacity .3s linear, top .3s ease-out; - -ms-transition: opacity .3s linear, top .3s ease-out; - -o-transition: opacity .3s linear, top .3s ease-out; - transition: opacity .3s linear, top .3s ease-out; - top: -25%; -} -.modal.fade.in { - top: 50%; -} -.modal-header { - padding: 9px 15px; - border-bottom: 1px solid #eee; -} -.modal-header .close { - margin-top: 2px; -} -.modal-body { - padding: 15px; -} -.modal-body .modal-form { - margin-bottom: 0; -} -.modal-footer { - padding: 14px 15px 15px; - margin-bottom: 0; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - -webkit-border-radius: 0 0 6px 6px; - -moz-border-radius: 0 0 6px 6px; - border-radius: 0 0 6px 6px; - -webkit-box-shadow: inset 0 1px 0 #ffffff; - -moz-box-shadow: inset 0 1px 0 #ffffff; - box-shadow: inset 0 1px 0 #ffffff; - *zoom: 1; -} -.modal-footer:before, .modal-footer:after { - display: table; - content: ""; -} -.modal-footer:after { - clear: both; -} -.modal-footer .btn { - float: right; - margin-left: 5px; - margin-bottom: 0; -} -.tooltip { - position: absolute; - z-index: 1020; - display: block; - visibility: visible; - padding: 5px; - font-size: 11px; - opacity: 0; - filter: alpha(opacity=0); -} -.tooltip.in { - opacity: 0.8; - filter: alpha(opacity=80); -} -.tooltip.top { - margin-top: -2px; -} -.tooltip.right { - margin-left: 2px; -} -.tooltip.bottom { - margin-top: 2px; -} -.tooltip.left { - margin-left: -2px; -} -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 5px solid #000000; -} -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid #000000; -} -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-bottom: 5px solid #000000; -} -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-right: 5px solid #000000; -} -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #ffffff; - text-align: center; - text-decoration: none; - background-color: #000000; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; -} -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1010; - display: none; - padding: 5px; -} -.popover.top { - margin-top: -5px; -} -.popover.right { - margin-left: 5px; -} -.popover.bottom { - margin-top: 5px; -} -.popover.left { - margin-left: -5px; -} -.popover.top .arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 5px solid #000000; -} -.popover.right .arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-right: 5px solid #000000; -} -.popover.bottom .arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-bottom: 5px solid #000000; -} -.popover.left .arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; - border-left: 5px solid #000000; -} -.popover .arrow { - position: absolute; - width: 0; - height: 0; -} -.popover-inner { - padding: 3px; - width: 280px; - overflow: hidden; - background: #000000; - background: rgba(0, 0, 0, 0.8); - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; - -webkit-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - -moz-box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); - box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3); -} -.popover-title { - padding: 9px 15px; - line-height: 1; - background-color: #f5f5f5; - border-bottom: 1px solid #eee; - -webkit-border-radius: 3px 3px 0 0; - -moz-border-radius: 3px 3px 0 0; - border-radius: 3px 3px 0 0; -} -.popover-content { - padding: 14px; - background-color: #ffffff; - -webkit-border-radius: 0 0 3px 3px; - -moz-border-radius: 0 0 3px 3px; - border-radius: 0 0 3px 3px; - -webkit-background-clip: padding-box; - -moz-background-clip: padding-box; - background-clip: padding-box; -} -.popover-content p, .popover-content ul, .popover-content ol { - margin-bottom: 0; -} -.thumbnails { - margin-left: -20px; - list-style: none; - *zoom: 1; -} -.thumbnails:before, .thumbnails:after { - display: table; - content: ""; -} -.thumbnails:after { - clear: both; -} -.thumbnails > li { - float: left; - margin: 0 0 18px 20px; -} -.thumbnail { - display: block; - padding: 4px; - line-height: 1; - border: 1px solid #ddd; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); - -moz-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.075); -} -a.thumbnail:hover { - border-color: #0088cc; - -webkit-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - -moz-box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); - box-shadow: 0 1px 4px rgba(0, 105, 214, 0.25); -} -.thumbnail > img { - display: block; - max-width: 100%; - margin-left: auto; - margin-right: auto; -} -.thumbnail .caption { - padding: 9px; -} -.label { - padding: 2px 4px 3px; - font-size: 11.049999999999999px; - font-weight: bold; - color: #ffffff; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #999999; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; -} -.label:hover { - color: #ffffff; - text-decoration: none; -} -.label-important { - background-color: #b94a48; -} -.label-important:hover { - background-color: #953b39; -} -.label-warning { - background-color: #f89406; -} -.label-warning:hover { - background-color: #c67605; -} -.label-success { - background-color: #468847; -} -.label-success:hover { - background-color: #356635; -} -.label-info { - background-color: #3a87ad; -} -.label-info:hover { - background-color: #2d6987; -} -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} -@-moz-keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} -@keyframes progress-bar-stripes { - from { - background-position: 0 0; - } - to { - background-position: 40px 0; - } -} -.progress { - overflow: hidden; - height: 18px; - margin-bottom: 18px; - background-color: #f7f7f7; - background-image: -moz-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -ms-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9)); - background-image: -webkit-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: -o-linear-gradient(top, #f5f5f5, #f9f9f9); - background-image: linear-gradient(top, #f5f5f5, #f9f9f9); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f5f5f5', endColorstr='#f9f9f9', GradientType=0); - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.progress .bar { - width: 0%; - height: 18px; - color: #ffffff; - font-size: 12px; - text-align: center; - text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); - background-color: #0e90d2; - background-image: -moz-linear-gradient(top, #149bdf, #0480be); - background-image: -ms-linear-gradient(top, #149bdf, #0480be); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be)); - background-image: -webkit-linear-gradient(top, #149bdf, #0480be); - background-image: -o-linear-gradient(top, #149bdf, #0480be); - background-image: linear-gradient(top, #149bdf, #0480be); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#149bdf', endColorstr='#0480be', GradientType=0); - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -moz-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - -webkit-transition: width 0.6s ease; - -moz-transition: width 0.6s ease; - -ms-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} -.progress-striped .bar { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; -} -.progress.active .bar { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} -.progress-danger .bar { - background-color: #dd514c; - background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35)); - background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); - background-image: linear-gradient(top, #ee5f5b, #c43c35); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0); -} -.progress-danger.progress-striped .bar { - background-color: #ee5f5b; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-success .bar { - background-color: #5eb95e; - background-image: -moz-linear-gradient(top, #62c462, #57a957); - background-image: -ms-linear-gradient(top, #62c462, #57a957); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957)); - background-image: -webkit-linear-gradient(top, #62c462, #57a957); - background-image: -o-linear-gradient(top, #62c462, #57a957); - background-image: linear-gradient(top, #62c462, #57a957); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0); -} -.progress-success.progress-striped .bar { - background-color: #62c462; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-info .bar { - background-color: #4bb1cf; - background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); - background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9)); - background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); - background-image: -o-linear-gradient(top, #5bc0de, #339bb9); - background-image: linear-gradient(top, #5bc0de, #339bb9); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0); -} -.progress-info.progress-striped .bar { - background-color: #5bc0de; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -ms-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(-45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.accordion { - margin-bottom: 18px; -} -.accordion-group { - margin-bottom: 2px; - border: 1px solid #e5e5e5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.accordion-heading { - border-bottom: 0; -} -.accordion-heading .accordion-toggle { - display: block; - padding: 8px 15px; -} -.accordion-inner { - padding: 9px 15px; - border-top: 1px solid #e5e5e5; -} -.carousel { - position: relative; - margin-bottom: 18px; - line-height: 1; -} -.carousel-inner { - overflow: hidden; - width: 100%; - position: relative; -} -.carousel .item { - display: none; - position: relative; - -webkit-transition: 0.6s ease-in-out left; - -moz-transition: 0.6s ease-in-out left; - -ms-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} -.carousel .item > img { - display: block; - line-height: 1; -} -.carousel .active, .carousel .next, .carousel .prev { - display: block; -} -.carousel .active { - left: 0; -} -.carousel .next, .carousel .prev { - position: absolute; - top: 0; - width: 100%; -} -.carousel .next { - left: 100%; -} -.carousel .prev { - left: -100%; -} -.carousel .next.left, .carousel .prev.right { - left: 0; -} -.carousel .active.left { - left: -100%; -} -.carousel .active.right { - left: 100%; -} -.carousel-control { - position: absolute; - top: 40%; - left: 15px; - width: 40px; - height: 40px; - margin-top: -20px; - font-size: 60px; - font-weight: 100; - line-height: 30px; - color: #ffffff; - text-align: center; - background: #222222; - border: 3px solid #ffffff; - -webkit-border-radius: 23px; - -moz-border-radius: 23px; - border-radius: 23px; - opacity: 0.5; - filter: alpha(opacity=50); -} -.carousel-control.right { - left: auto; - right: 15px; -} -.carousel-control:hover { - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} -.carousel-caption { - position: absolute; - left: 0; - right: 0; - bottom: 0; - padding: 10px 15px 5px; - background: #333333; - background: rgba(0, 0, 0, 0.75); -} -.carousel-caption h4, .carousel-caption p { - color: #ffffff; -} -.hero-unit { - padding: 60px; - margin-bottom: 30px; - background-color: #f5f5f5; - -webkit-border-radius: 6px; - -moz-border-radius: 6px; - border-radius: 6px; -} -.hero-unit h1 { - margin-bottom: 0; - font-size: 60px; - line-height: 1; - letter-spacing: -1px; -} -.hero-unit p { - font-size: 18px; - font-weight: 200; - line-height: 27px; -} -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.hide { - display: none; -} -.show { - display: block; -} -.invisible { - visibility: hidden; -} diff --git a/static/css/normalize.css b/static/css/normalize.css deleted file mode 100644 index f056d58..0000000 --- a/static/css/normalize.css +++ /dev/null @@ -1,504 +0,0 @@ -/*! normalize.css 2012-03-06T10:21 UTC - http://github.com/necolas/normalize.css */ - -/* ============================================================================= - HTML5 display definitions - ========================================================================== */ - -/* - * Corrects block display not defined in IE6/7/8/9 & FF3 - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section, -summary { - display: block; -} - -/* - * Corrects inline-block display not defined in IE6/7/8/9 & FF3 - */ - -audio, -canvas, -video { - display: inline-block; - *display: inline; - *zoom: 1; -} - -/* - * Prevents modern browsers from displaying 'audio' without controls - * Remove excess height in iOS5 devices - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/* - * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 - * Known issue: no IE6 support - */ - -[hidden] { - display: none; -} - - -/* ============================================================================= - Base - ========================================================================== */ - -/* - * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units - * http://clagnut.com/blog/348/#c790 - * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom - * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ - */ - -html { - font-size: 100%; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - -ms-text-size-adjust: 100%; /* 2 */ -} - -/* - * Addresses font-family inconsistency between 'textarea' and other form elements. - */ - -html, -button, -input, -select, -textarea { - font-family: sans-serif; -} - -/* - * Addresses margins handled incorrectly in IE6/7 - */ - -body { - margin: 0; -} - - -/* ============================================================================= - Links - ========================================================================== */ - -/* - * Addresses outline displayed oddly in Chrome - */ - -a:focus { - outline: thin dotted; -} - -/* - * Improves readability when focused and also mouse hovered in all browsers - * people.opera.com/patrickl/experiments/keyboard/test - */ - -a:hover, -a:active { - outline: 0; -} - - -/* ============================================================================= - Typography - ========================================================================== */ - -/* - * Addresses font sizes and margins set differently in IE6/7 - * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -h2 { - font-size: 1.5em; - margin: 0.83em 0; -} - -h3 { - font-size: 1.17em; - margin: 1em 0; -} - -h4 { - font-size: 1em; - margin: 1.33em 0; -} - -h5 { - font-size: 0.83em; - margin: 1.67em 0; -} - -h6 { - font-size: 0.75em; - margin: 2.33em 0; -} - -/* - * Addresses styling not present in IE7/8/9, S5, Chrome - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/* - * Addresses style set to 'bolder' in FF3+, S4/5, Chrome -*/ - -b, -strong { - font-weight: bold; -} - -blockquote { - margin: 1em 40px; -} - -/* - * Addresses styling not present in S5, Chrome - */ - -dfn { - font-style: italic; -} - -/* - * Addresses styling not present in IE6/7/8/9 - */ - -mark { - background: #ff0; - color: #000; -} - -/* - * Addresses margins set differently in IE6/7 - */ - -p, -pre { - margin: 1em 0; -} - -/* - * Corrects font family set oddly in IE6, S4/5, Chrome - * en.wikipedia.org/wiki/User:Davidgothberg/Test59 - */ - -pre, -code, -kbd, -samp { - font-family: monospace, serif; - _font-family: 'courier new', monospace; - font-size: 1em; -} - -/* - * Improves readability of pre-formatted text in all browsers - */ - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; -} - -/* - * 1. Addresses CSS quotes not supported in IE6/7 - * 2. Addresses quote property not supported in S4 - */ - -/* 1 */ - -q { - quotes: none; -} - -/* 2 */ - -q:before, -q:after { - content: ''; - content: none; -} - -small { - font-size: 75%; -} - -/* - * Prevents sub and sup affecting line-height in all browsers - * gist.github.com/413930 - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - - -/* ============================================================================= - Lists - ========================================================================== */ - -/* - * Addresses margins set differently in IE6/7 - */ - -dl, -menu, -ol, -ul { - margin: 1em 0; -} - -dd { - margin: 0 0 0 40px; -} - -/* - * Addresses paddings set differently in IE6/7 - */ - -menu, -ol, -ul { - padding: 0 0 0 40px; -} - -/* - * Corrects list images handled incorrectly in IE7 - */ - -nav ul, -nav ol { - list-style: none; - list-style-image: none; -} - - -/* ============================================================================= - Embedded content - ========================================================================== */ - -/* - * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 - * 2. Improves image quality when scaled in IE7 - * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ - */ - -img { - border: 0; /* 1 */ - -ms-interpolation-mode: bicubic; /* 2 */ -} - -/* - * Corrects overflow displayed oddly in IE9 - */ - -svg:not(:root) { - overflow: hidden; -} - - -/* ============================================================================= - Figures - ========================================================================== */ - -/* - * Addresses margin not present in IE6/7/8/9, S5, O11 - */ - -figure { - margin: 0; -} - - -/* ============================================================================= - Forms - ========================================================================== */ - -/* - * Corrects margin displayed oddly in IE6/7 - */ - -form { - margin: 0; -} - -/* - * Define consistent border, margin, and padding - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/* - * 1. Corrects color not being inherited in IE6/7/8/9 - * 2. Corrects text not wrapping in FF3 - * 3. Corrects alignment displayed oddly in IE6/7 - */ - -legend { - border: 0; /* 1 */ - padding: 0; - white-space: normal; /* 2 */ - *margin-left: -7px; /* 3 */ -} - -/* - * 1. Corrects font size not being inherited in all browsers - * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome - * 3. Improves appearance and consistency in all browsers - */ - -button, -input, -select, -textarea { - font-size: 100%; /* 1 */ - margin: 0; /* 2 */ - vertical-align: baseline; /* 3 */ - *vertical-align: middle; /* 3 */ -} - -/* - * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet - */ - -button, -input { - line-height: normal; /* 1 */ -} - -/* - * 1. Improves usability and consistency of cursor style between image-type 'input' and others - * 2. Corrects inability to style clickable 'input' types in iOS - * 3. Removes inner spacing in IE7 without affecting normal text inputs - * Known issue: inner spacing remains in IE6 - */ - -button, -input[type="button"], -input[type="reset"], -input[type="submit"] { - cursor: pointer; /* 1 */ - -webkit-appearance: button; /* 2 */ - *overflow: visible; /* 3 */ -} - -/* - * Re-set default cursor for disabled elements - */ - -button[disabled], -input[disabled] { - cursor: default; -} - -/* - * 1. Addresses box sizing set to content-box in IE8/9 - * 2. Removes excess padding in IE8/9 - * 3. Removes excess padding in IE7 - Known issue: excess padding remains in IE6 - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ - *height: 13px; /* 3 */ - *width: 13px; /* 3 */ -} - -/* - * 1. Addresses appearance set to searchfield in S5, Chrome - * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/* - * Removes inner padding and search cancel button in S5, Chrome on OS X - */ - -input[type="search"]::-webkit-search-decoration, -input[type="search"]::-webkit-search-cancel-button { - -webkit-appearance: none; -} - -/* - * Removes inner padding and border in FF3+ - * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/* - * 1. Removes default vertical scrollbar in IE6/7/8/9 - * 2. Improves readability and alignment in all browsers - */ - -textarea { - overflow: auto; /* 1 */ - vertical-align: top; /* 2 */ -} - - -/* ============================================================================= - Tables - ========================================================================== */ - -/* - * Remove most spacing between table cells - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} diff --git a/static/css/stats.css b/static/css/stats.css deleted file mode 100644 index ec1bf4d..0000000 --- a/static/css/stats.css +++ /dev/null @@ -1,137 +0,0 @@ -h2 { - margin-top: 0px; - padding-top: 10px; -} -/* Add icons to some text */ -.neutral { - background: url("../img/neutral.png") no-repeat scroll left; - padding-left: 20px; - padding-right: 20px; - font-weight: bold; -} - -.like { - background: url("../img/like.png") no-repeat scroll left; - padding-left: 20px; - padding-right: 20px; - font-weight: bold; -} - -.likealot { - background: url("../img/likealot.png") no-repeat scroll left; - padding-left: 20px; - padding-right: 20px; - font-weight: bold; -} - -/* The content section of the page */ -.content { - width: 1024px; - margin: auto; -} - -#graph { - vertical-align: middle; -} - -#fig { - position: relative; - margin: auto; - width: 540px; - height: 330px; -} - -#top_discussion { - width: 45%; - margin-right: 22px; - margin-left: 10px; -} - -#discussion_by_topic { - width: 45%; - margin-top: 20px; - margin-right: 22px; - margin-left: 10px; -} - -#most_active { - float: right; - width: 45%; -} - -#discussion_marker { - float: right; - width: 45%; - margin-top: 20px; -} - -.thread { - white-space: nowrap; -} - -.thread * { - white-space: normal; -} - -.thread_id { - font-weight: bold; - font-size: 125%; - color: rgb(102, 102, 102); - vertical-align: top; - padding-right: 10px; -} - -.thread_title{ - padding-right:20px; - color: rgb(102, 102, 102); - display: inline-block; -} - -.thread_stats ul li { - margin-right:10px; -} - -.category { - font-variant: small-caps; - font-weight: bold; - color: white; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - border-radius: 5px 5px 5px 5px; - vertical-align: top; - margin-bottom: 10px; - padding-top: 0px; - padding-left: 10px; -} - -.category_entry { - list-style-type: circle; - margin-top: 0px; - padding-bottom: 10px; - padding-left: 25px; -} - -.category_entry li { - padding-bottom: 10px; -} - -.maker { - color: rgb(102, 102, 102); - padding-right: 10px; - padding-bottom: 20px; -} - -.maker_id, .marker_name{ - font-weight: bold; - font-size: 115%; - vertical-align: top; - padding-right: 20px; -} - -.gravatar { - padding-right: 20px; -} - -.score{ - font-weight: bold; -} diff --git a/static/css/style.css b/static/css/style.css deleted file mode 100644 index 30535a2..0000000 --- a/static/css/style.css +++ /dev/null @@ -1,382 +0,0 @@ -body { - position: relative; - padding-top: 90px; - background-color: white; -} - -.Sb { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - background-color: white; - clear: both; - font-size: 13px; - line-height: 1.4; - margin: 20px 0 20px 68px; - outline: none; - position: relative; - width: 497px; - word-wrap: break-word; -} - -.ZX { - color: #999; - height: 40px; - margin: 0 2px; - position: relative; - bottom: -3px; - background-color: #F8F8F8; - border: 1px solid #CCC; - -} - -.socialLogin { - list-style: none; - margin: 0px; -} - -.socialLogin li { - float: left; - padding: 5px; -} - -.right { - text-align: right; -} - -.inline-block { - display: inline-block; -} - -.inline li, .inline-block li { - display: inline-block; - list-style-type: none; -} - -/* Add icons to some text */ -.participant { - background: url("../img/participant.png") no-repeat scroll left top; - padding-left: 20px; -} - -.discussion { - background: url("../img/discussion.png") no-repeat scroll left top; - padding-left: 20px; -} - -.saved { - background: url("../img/saved.png") no-repeat scroll left top; - padding-left: 20px; -} - -.notsaved { - background: url("../img/notsaved.png") no-repeat scroll left top; - padding-left: 20px; -} - -.gravatar { - vertical-align: top; - width:40px; - font-size: 70%; -} - -.gravatar img { - width: 40px; -} - -.neutral { - background: url("../img/neutral.png") no-repeat scroll left; - padding-left: 20px; - padding-right: 20px; - font-weight: bold; -} - -.like { - background: url("../img/like.png") no-repeat scroll left; - padding-left: 20px; - padding-right: 20px; - font-weight: bold; -} - -.likealot { - background: url("../img/likealot.png") no-repeat scroll left; - padding-left: 20px; - padding-right: 20px; - font-weight: bold; -} - -.youlike { - background: url("../img/youlike.png") no-repeat scroll left; - padding-left: 15px; - padding-right: 5px; -} - -.youdislike { - background: url("../img/youdislike.png") no-repeat scroll left; - padding-left: 15px; - padding-right: 5px; -} - -.showdiscussion { - background-image: linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); - background-image: -o-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); - background-image: -moz-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); - background-image: -webkit-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); - background-image: -ms-linear-gradient(bottom, rgb(204,204,204) 11%, rgb(255,255,255) 100%); - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.11, rgb(204,204,204)), - color-stop(1, rgb(255,255,255)) - ); - padding: 3px 7px 3px 7px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - border-radius: 5px 5px 5px 5px; - border-style: solid; - border-width: 1px; - border-color: rgb(179, 179, 179); -} - -.showdiscussion a { - color: rgb(77, 77, 77); -} - - -/* Top of the page -- header */ -.header { - background-color: rgb(236, 236, 236); - min-height : 100px; -} - -#white { - color: rgb(255, 255, 255); - background-color: rgb(255, 255, 255); - margin-bottom: 0px; -} - -#headline { - min-height: 50px; -} - -.list_nav { - float: left; - list-style: none; - margin: 0; - padding: 5px 0 0 0; -} - -.list_nav li { - float: left; - margin-left: 20px; -} - -.user_nav { - float: right; - list-style: none; - margin: 0; - padding: 5px 0 0 0; -} - -.user_nav a { - float: none; - padding: 10px 10px 11px; - line-height: 19px; - color: #999; - text-decoration: none; - text-shadow: 0 -1px 0 - rgba(0, 0, 0, 0.25); -} - -.user_nav li { - float: left; - margin-left: 20px; -} - -#thread_content { - width: 70%; -} - -.email_date .date { - font-weight: bold; -} - -#top_right { - position: absolute; - right: 20px; - bottom: 0; - color: rgb(102, 102, 102); -} - -#top_right li { - margin-left:10px; -} - -#list_name { - font-weight: bold; -} - -#page_date { - font-size: 150%; -} - - -#searchbox { - text-align:right; - padding-right: 20px; -} - -#searchbox input { - width: 250px -} - -#searchbox input::-webkit-input-placeholder { - font-style: italic; -} - -#searchbox input:-moz-placeholder { - font-style: italic; -} - -#recent_activities{ - width: 88%; - margin-top: 20px; - margin-right: 10px; - float: right; -} - -#archives{ - width: 9%; - margin-left: 10px; - margin-top: 20px; - float: left; -/* - margin-right: 2px; -*/ -} - -#archives ul { - padding: 0; - margin: 0; -} - -#archives li { - list-style-type: none; -} - -/* Thread list */ - -.thread_title { - font-weight: bold; - font-size: 125%; -} - -.thread_date { - font-style: italic; - font-size: 70%; - color: rgb(128, 0, 0); -} - -.thread_content { - margin-top:10px; -} - -.thread_info { - text-align:right; - padding-right: 50px; -} - -.tags { - text-align:left; - margin: 0px 0px 0px 200px; - padding: 0px; -} - -/* Part containing the body of the mail which can be shown/hidden */ -.expander { - width: 665px; - background-image: linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); - background-image: -o-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); - background-image: -moz-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); - background-image: -webkit-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); - background-image: -ms-linear-gradient(bottom, rgb(236,236,236) 11%, rgb(255,255,255) 100%); - - background-image: -webkit-gradient( - linear, - left bottom, - left top, - color-stop(0.11, rgb(236,236,236)), - color-stop(1, rgb(255,255,255)) - ); - border-style: solid; - border-width: 1px; - border-color: rgb(236,236,236); - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - border-radius: 5px 5px 5px 5px; - padding-left: 20px; - margin-left: 21px; - display: inline-block; - vertical-align: top; - white-space: pre; -} - -.expander a { - float: right; - padding: 20px 10px 0px 0px; -} - -/* Thread types */ -.type { - font-variant: small-caps; - font-weight: bold; - color: white; - padding: 3px; - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - border-radius: 5px 5px 5px 5px; - vertical-align: top; - width: 110px; - text-align:center; -} - -.type a { - color: white; -} - -.type_question { - background-color: rgb(179, 128, 255); -} - -.type_agenda { - background-color: rgb(42, 127, 255); -} - -.type_todo { - background-color: rgb(200, 171, 55); -} - -.type_dead { - background-color: rgb(0, 0, 0); -} - -.type_announcement { - background-color: rgb(170, 212, 0); -} - -.type_policy { - background-color: rgb(200, 55, 171); -} - -.type_test { - background-color: rgb(200, 171, 55); -} - -.invisible { - visibility: hidden; -} - -.removed { - display: none; -}[ diff --git a/static/css/thread.css b/static/css/thread.css deleted file mode 100644 index 4815ace..0000000 --- a/static/css/thread.css +++ /dev/null @@ -1,239 +0,0 @@ -#thread_nav{ - margin:auto; - width:100%; - text-align:center; -} - -#thread_nav * { - vertical-align: middle; -} - -#thread_nav .thread_title{ - margin:auto; - width: 50%; -} - -#thread_nav br { - margin-top: 10px; -} - -#thread_nav .thread_info { - margin-top:10px; - margin-bottom:10px; - font-size: 70%; - font-weight: normal; - text-align: center; -} - -#thread_nav .thread_info li { - margin-left:3em; -} - -#olderhread, #newewthread { - font-size: 70%; - color: rgb(167, 169, 172); -} - -#olderhread { - float: right; - margin-top: 2em; - margin-right: 20px; -} - -/* Define the two columns */ -#thread_content { - width: 70%; - margin-right: 22px; -} - -#thread_overview_info { - float: right; - width: 22%; -} - -/* Thread general information column */ -#days_old { - margin-left:1em; -} - -.days_text { - font-size: 70%; -} - -.days_num { - font-size: 200%; -} - -#add_to_fav a{ - color: rgb(167, 169, 172); -} - -#grey { - color: rgb(167, 169, 172); - background-color: rgb(167, 169, 172); - margin: 0px; - border: 0 none; - height: 1px; -} - -#tags { - color: rgb(167, 169, 172); - margin-top: 20px; -} - -#tag_title { - color: rgb(77, 77, 77); - text-transform: uppercase; -} - -#tags ul { - padding: 10px 0px 10px 0px; - margin: 0; -} - -#add_tag_field { - width:70%; -} - -#participants { - margin-top: 20px; - color: rgb(167, 169, 172); -} - -#participants_title { - color: rgb(77, 77, 77); - text-transform: uppercase; -} - -#participants ul { - padding: 10px 0px 10px 0px; - margin: 0; -} - -#participants li { - list-style-type: none; -} - -#participants img { - width: 20px; -} - -/* Main section with the whole thread */ - -/* First email of the thread. */ - -.first_email { -} - -.email_header { - position:relative; - margin-top: 20px; - margin-bottom: 20px; -} - -.email_header img { - width: 40px; -} - -.email_author .name{ - color: rgb(55, 113, 200); - font-weight: bold; -} - -.email_author .rank{ - color: rgb(167, 169, 172); - font-size: 80%; - font-weight: bold; -} - -.email_date { - position: absolute; - right: 20px; - bottom: 0px; -} - -.email_date .date { - font-weight: bold; -} - -.email_date .time { - color: rgb(167, 169, 172); -} - -.email_info { - padding: 0px; -} - -.add_comment { - float: right; -} - -/* The email thread */ -.even { - background-color: rgb(246, 246, 246); - border-top: 1px solid rgb(179, 179, 179); - padding-left: 20px; - margin: 20px 0px 20px 0px; -} - -.odd { - background-color: rgb(238, 238, 238); - border-top: 1px solid rgb(179, 179, 179); - padding-left: 20px; - margin: 20px 0px 20px 0px; -} - -.email { -} - -.email .email_header { - margin-top: 10px; - margin-bottom: 10px; -} - -.email .email_author { - font-size: 90%; -} - -.email .email_date, .email .email_date .date { - font-size: 90%; -} - -.email_body{ - -webkit-border-radius: 5px 5px 5px 5px; - -moz-border-radius: 5px 5px 5px 5px; - border-radius: 5px 5px 5px 5px; - border-style: solid; - border-width: 1px; - border-color: rgb(179, 179, 179); - padding: 5px; - min-height: 40px; - background-color: rgb(255, 255, 255); - white-space: pre; - display: inline-block; -} - -#first_email_body { - white-space: pre; - display: inline-block; -} - - -.email_body a { - float: right; - padding: 3px 10px 0px 0px; -} - -.email_info { - padding: 0px; - margin-top: 5px; -} - -.thread_email { - padding-left: 20px; - margin-left: 21px; - display: inline-block; - vertical-align: top; - white-space: pre; -} - diff --git a/static/img/button_newer.png b/static/img/button_newer.png deleted file mode 100644 index 14cfaa6..0000000 Binary files a/static/img/button_newer.png and /dev/null differ diff --git a/static/img/button_older.png b/static/img/button_older.png deleted file mode 100644 index 6c3c950..0000000 Binary files a/static/img/button_older.png and /dev/null differ diff --git a/static/img/discussion.png b/static/img/discussion.png deleted file mode 100644 index 26e60d9..0000000 Binary files a/static/img/discussion.png and /dev/null differ diff --git a/static/img/email_bg.png b/static/img/email_bg.png deleted file mode 100644 index f3ae7b7..0000000 Binary files a/static/img/email_bg.png and /dev/null differ diff --git a/static/img/like.png b/static/img/like.png deleted file mode 100644 index 7406cdd..0000000 Binary files a/static/img/like.png and /dev/null differ diff --git a/static/img/likealot.png b/static/img/likealot.png deleted file mode 100644 index 5ce4b88..0000000 Binary files a/static/img/likealot.png and /dev/null differ diff --git a/static/img/login/browserid.png b/static/img/login/browserid.png deleted file mode 100644 index 919a5c7..0000000 Binary files a/static/img/login/browserid.png and /dev/null differ diff --git a/static/img/login/facebook.png b/static/img/login/facebook.png deleted file mode 100644 index 551100d..0000000 Binary files a/static/img/login/facebook.png and /dev/null differ diff --git a/static/img/login/google.png b/static/img/login/google.png deleted file mode 100644 index b840860..0000000 Binary files a/static/img/login/google.png and /dev/null differ diff --git a/static/img/login/openid.png b/static/img/login/openid.png deleted file mode 100644 index bc81687..0000000 Binary files a/static/img/login/openid.png and /dev/null differ diff --git a/static/img/login/twitter.png b/static/img/login/twitter.png deleted file mode 100644 index 14960b8..0000000 Binary files a/static/img/login/twitter.png and /dev/null differ diff --git a/static/img/login/yahoo.png b/static/img/login/yahoo.png deleted file mode 100644 index e9deaf2..0000000 Binary files a/static/img/login/yahoo.png and /dev/null differ diff --git a/static/img/neutral.png b/static/img/neutral.png deleted file mode 100644 index 392f8c7..0000000 Binary files a/static/img/neutral.png and /dev/null differ diff --git a/static/img/newthread.png b/static/img/newthread.png deleted file mode 100644 index e61b871..0000000 Binary files a/static/img/newthread.png and /dev/null differ diff --git a/static/img/notsaved.png b/static/img/notsaved.png deleted file mode 100644 index a427a91..0000000 Binary files a/static/img/notsaved.png and /dev/null differ diff --git a/static/img/participant.png b/static/img/participant.png deleted file mode 100644 index f2d700b..0000000 Binary files a/static/img/participant.png and /dev/null differ diff --git a/static/img/saved.png b/static/img/saved.png deleted file mode 100644 index b240cd5..0000000 Binary files a/static/img/saved.png and /dev/null differ diff --git a/static/img/show_discussion.png b/static/img/show_discussion.png deleted file mode 100644 index f7f42f1..0000000 Binary files a/static/img/show_discussion.png and /dev/null differ diff --git a/static/img/youdislike.png b/static/img/youdislike.png deleted file mode 100644 index 0c6387b..0000000 Binary files a/static/img/youdislike.png and /dev/null differ diff --git a/static/img/youlike.png b/static/img/youlike.png deleted file mode 100644 index affe451..0000000 Binary files a/static/img/youlike.png and /dev/null differ diff --git a/static/jquery.expander.js b/static/jquery.expander.js deleted file mode 100644 index 9eabab4..0000000 --- a/static/jquery.expander.js +++ /dev/null @@ -1,382 +0,0 @@ -/*! - * jQuery Expander Plugin v1.4 - * - * Date: Sun Dec 11 15:08:42 2011 EST - * Requires: jQuery v1.3+ - * - * Copyright 2011, Karl Swedberg - * Dual licensed under the MIT and GPL licenses (just like jQuery): - * http://www.opensource.org/licenses/mit-license.php - * http://www.gnu.org/licenses/gpl.html - * - * - * - * -*/ - -(function($) { - $.expander = { - version: '1.4', - defaults: { - // the number of characters at which the contents will be sliced into two parts. - slicePoint: 100, - - // whether to keep the last word of the summary whole (true) or let it slice in the middle of a word (false) - preserveWords: true, - - // a threshold of sorts for whether to initially hide/collapse part of the element's contents. - // If after slicing the contents in two there are fewer words in the second part than - // the value set by widow, we won't bother hiding/collapsing anything. - widow: 4, - - // text displayed in a link instead of the hidden part of the element. - // clicking this will expand/show the hidden/collapsed text - expandText: 'read more', - expandPrefix: '… ', - - expandAfterSummary: false, - - // class names for summary element and detail element - summaryClass: 'summary', - detailClass: 'details', - - // class names for around "read-more" link and "read-less" link - moreClass: 'read-more', - lessClass: 'read-less', - - // number of milliseconds after text has been expanded at which to collapse the text again. - // when 0, no auto-collapsing - collapseTimer: 0, - - // effects for expanding and collapsing - expandEffect: 'fadeIn', - expandSpeed: 250, - collapseEffect: 'fadeOut', - collapseSpeed: 200, - - // allow the user to re-collapse the expanded text. - userCollapse: true, - - // text to use for the link to re-collapse the text - userCollapseText: 'read less', - userCollapsePrefix: ' ', - - - // all callback functions have the this keyword mapped to the element in the jQuery set when .expander() is called - - onSlice: null, // function() {} - beforeExpand: null, // function() {}, - afterExpand: null, // function() {}, - onCollapse: null // function(byUser) {} - } - }; - - $.fn.expander = function(options) { - var meth = 'init'; - - if (typeof options == 'string') { - meth = options; - options = {}; - } - - var opts = $.extend({}, $.expander.defaults, options), - rSelfClose = /^<(?:area|br|col|embed|hr|img|input|link|meta|param).*>$/i, - rAmpWordEnd = /(&(?:[^;]+;)?|\w+)$/, - rOpenCloseTag = /<\/?(\w+)[^>]*>/g, - rOpenTag = /<(\w+)[^>]*>/g, - rCloseTag = /<\/(\w+)>/g, - rLastCloseTag = /(<\/[^>]+>)\s*$/, - rTagPlus = /^<[^>]+>.?/, - delayedCollapse; - - var methods = { - init: function() { - this.each(function() { - var i, l, tmp, summTagLess, summOpens, summCloses, lastCloseTag, detailText, - $thisDetails, $readMore, - openTagsForDetails = [], - closeTagsForsummaryText = [], - defined = {}, - thisEl = this, - $this = $(this), - $summEl = $([]), - o = $.meta ? $.extend({}, opts, $this.data()) : opts, - hasDetails = !!$this.find('.' + o.detailClass).length, - hasBlocks = !!$this.find('*').filter(function() { - var display = $(this).css('display'); - return (/^block|table|list/).test(display); - }).length, - el = hasBlocks ? 'div' : 'span', - detailSelector = el + '.' + o.detailClass, - moreSelector = 'span.' + o.moreClass, - expandSpeed = o.expandSpeed || 0, - allHtml = $.trim( $this.html() ), - allText = $.trim( $this.text() ), - summaryText = allHtml.slice(0, o.slicePoint); - - // bail out if we've already set up the expander on this element - if ( $.data(this, 'expander') ) { - return; - } - $.data(this, 'expander', true); - - // determine which callback functions are defined - $.each(['onSlice','beforeExpand', 'afterExpand', 'onCollapse'], function(index, val) { - defined[val] = $.isFunction(o[val]); - }); - - // back up if we're in the middle of a tag or word - summaryText = backup(summaryText); - - // summary text sans tags length - summTagless = summaryText.replace(rOpenCloseTag, '').length; - - // add more characters to the summary, one for each character in the tags - while (summTagless < o.slicePoint) { - newChar = allHtml.charAt(summaryText.length); - if (newChar == '<') { - newChar = allHtml.slice(summaryText.length).match(rTagPlus)[0]; - } - summaryText += newChar; - summTagless++; - } - - summaryText = backup(summaryText, o.preserveWords); - - // separate open tags from close tags and clean up the lists - summOpens = summaryText.match(rOpenTag) || []; - summCloses = summaryText.match(rCloseTag) || []; - - // filter out self-closing tags - tmp = []; - $.each(summOpens, function(index, val) { - if ( !rSelfClose.test(val) ) { - tmp.push(val); - } - }); - summOpens = tmp; - - // strip close tags to just the tag name - l = summCloses.length; - for (i = 0; i < l; i++) { - summCloses[i] = summCloses[i].replace(rCloseTag, '$1'); - } - - // tags that start in summary and end in detail need: - // a). close tag at end of summary - // b). open tag at beginning of detail - $.each(summOpens, function(index, val) { - var thisTagName = val.replace(rOpenTag, '$1'); - var closePosition = $.inArray(thisTagName, summCloses); - if (closePosition === -1) { - openTagsForDetails.push(val); - closeTagsForsummaryText.push(''); - - } else { - summCloses.splice(closePosition, 1); - } - }); - - // reverse the order of the close tags for the summary so they line up right - closeTagsForsummaryText.reverse(); - - // create necessary summary and detail elements if they don't already exist - if ( !hasDetails ) { - - // end script if there is no detail text or if detail has fewer words than widow option - detailText = allHtml.slice(summaryText.length); - - if ( detailText === '' || detailText.split(/\s+/).length < o.widow ) { - return; - } - - // otherwise, continue... - lastCloseTag = closeTagsForsummaryText.pop() || ''; - summaryText += closeTagsForsummaryText.join(''); - detailText = openTagsForDetails.join('') + detailText; - - } else { - // assume that even if there are details, we still need readMore/readLess/summary elements - // (we already bailed out earlier when readMore el was found) - // but we need to create els differently - - // remove the detail from the rest of the content - detailText = $this.find(detailSelector).remove().html(); - - // The summary is what's left - summaryText = $this.html(); - - // allHtml is the summary and detail combined (this is needed when content has block-level elements) - allHtml = summaryText + detailText; - - lastCloseTag = ''; - } - o.moreLabel = $this.find(moreSelector).length ? '' : buildMoreLabel(o); - - if (hasBlocks) { - detailText = allHtml; - } - summaryText += lastCloseTag; - - // onSlice callback - o.summary = summaryText; - o.details = detailText; - o.lastCloseTag = lastCloseTag; - - if (defined.onSlice) { - // user can choose to return a modified options object - // one last chance for user to change the options. sneaky, huh? - // but could be tricky so use at your own risk. - tmp = o.onSlice.call(thisEl, o); - - // so, if the returned value from the onSlice function is an object with a details property, we'll use that! - o = tmp && tmp.details ? tmp : o; - } - - // build the html with summary and detail and use it to replace old contents - var html = buildHTML(o, hasBlocks); - - $this.html( html ); - - // set up details and summary for expanding/collapsing - $thisDetails = $this.find(detailSelector); - $readMore = $this.find(moreSelector); - $thisDetails.hide(); - $readMore.find('a').unbind('click.expander').bind('click.expander', expand); - - $summEl = $this.find('div.' + o.summaryClass); - - if ( o.userCollapse && !$this.find('span.' + o.lessClass).length ) { - $this - .find(detailSelector) - .append('' + o.userCollapsePrefix + '' + o.userCollapseText + ''); - } - - $this - .find('span.' + o.lessClass + ' a') - .unbind('click.expander') - .bind('click.expander', function(event) { - event.preventDefault(); - clearTimeout(delayedCollapse); - var $detailsCollapsed = $(this).closest(detailSelector); - reCollapse(o, $detailsCollapsed); - if (defined.onCollapse) { - o.onCollapse.call(thisEl, true); - } - }); - - function expand(event) { - event.preventDefault(); - $readMore.hide(); - $summEl.hide(); - if (defined.beforeExpand) { - o.beforeExpand.call(thisEl); - } - - $thisDetails.stop(false, true)[o.expandEffect](expandSpeed, function() { - $thisDetails.css({zoom: ''}); - if (defined.afterExpand) {o.afterExpand.call(thisEl);} - delayCollapse(o, $thisDetails, thisEl); - }); - } - - }); // this.each - }, - destroy: function() { - if ( !this.data('expander') ) { - return; - } - this.removeData('expander'); - this.each(function() { - var $this = $(this), - o = $.meta ? $.extend({}, opts, $this.data()) : opts, - details = $this.find('.' + o.detailClass).contents(); - - $this.find('.' + o.moreClass).remove(); - $this.find('.' + o.summaryClass).remove(); - $this.find('.' + o.detailClass).after(details).remove(); - $this.find('.' + o.lessClass).remove(); - - }); - } - }; - - // run the methods (almost always "init") - if ( methods[meth] ) { - methods[ meth ].call(this); - } - - // utility functions - function buildHTML(o, blocks) { - var el = 'span', - summary = o.summary; - if ( blocks ) { - el = 'div'; - // if summary ends with a close tag, tuck the moreLabel inside it - if ( rLastCloseTag.test(summary) && !o.expandAfterSummary) { - summary = summary.replace(rLastCloseTag, o.moreLabel + '$1'); - } else { - // otherwise (e.g. if ends with self-closing tag) just add moreLabel after summary - // fixes #19 - summary += o.moreLabel; - } - - // and wrap it in a div - summary = '
' + summary + '
'; - } else { - summary += o.moreLabel; - } - - return [ - summary, - '<', - el + ' class="' + o.detailClass + '"', - '>', - o.details, - '' - ].join(''); - } - - function buildMoreLabel(o) { - var ret = '' + o.expandPrefix; - ret += '' + o.expandText + ''; - return ret; - } - - function backup(txt, preserveWords) { - if ( txt.lastIndexOf('<') > txt.lastIndexOf('>') ) { - txt = txt.slice( 0, txt.lastIndexOf('<') ); - } - if (preserveWords) { - txt = txt.replace(rAmpWordEnd,''); - } - return txt; - } - - function reCollapse(o, el) { - el.stop(true, true)[o.collapseEffect](o.collapseSpeed, function() { - var prevMore = el.prev('span.' + o.moreClass).show(); - if (!prevMore.length) { - el.parent().children('div.' + o.summaryClass).show() - .find('span.' + o.moreClass).show(); - } - }); - } - - function delayCollapse(option, $collapseEl, thisEl) { - if (option.collapseTimer) { - delayedCollapse = setTimeout(function() { - reCollapse(option, $collapseEl); - if ( $.isFunction(option.onCollapse) ) { - option.onCollapse.call(thisEl, false); - } - }, option.collapseTimer); - } - } - - return this; - }; - - // plugin defaults - $.fn.expander.defaults = $.expander.defaults; -})(jQuery); diff --git a/static/js/libs/jquery-1.7.1.min.js b/static/js/libs/jquery-1.7.1.min.js deleted file mode 100644 index 198b3ff..0000000 --- a/static/js/libs/jquery-1.7.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v1.7.1 jquery.com | jquery.org/license */ -(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cv(a){if(!ck[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){cl||(cl=c.createElement("iframe"),cl.frameBorder=cl.width=cl.height=0),b.appendChild(cl);if(!cm||!cl.createElement)cm=(cl.contentWindow||cl.contentDocument).document,cm.write((c.compatMode==="CSS1Compat"?"":"")+""),cm.close();d=cm.createElement(a),cm.body.appendChild(d),e=f.css(d,"display"),b.removeChild(cl)}ck[a]=e}return ck[a]}function cu(a,b){var c={};f.each(cq.concat.apply([],cq.slice(0,b)),function(){c[this]=a});return c}function ct(){cr=b}function cs(){setTimeout(ct,0);return cr=f.now()}function cj(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ci(){try{return new a.XMLHttpRequest}catch(b){}}function cc(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;g=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?parseFloat(d):j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.1",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=q.getElementsByTagName("*"),e=q.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=q.getElementsByTagName("input")[0],b={leadingWhitespace:q.firstChild.nodeType===3,tbody:!q.getElementsByTagName("tbody").length,htmlSerialize:!!q.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:q.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete q.test}catch(s){b.deleteExpando=!1}!q.addEventListener&&q.attachEvent&&q.fireEvent&&(q.attachEvent("onclick",function(){b.noCloneEvent=!1}),q.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),q.appendChild(i),k=c.createDocumentFragment(),k.appendChild(q.lastChild),b.checkClone=k.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,k.removeChild(i),k.appendChild(q),q.innerHTML="",a.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",q.style.width="2px",q.appendChild(j),b.reliableMarginRight=(parseInt((a.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0);if(q.attachEvent)for(o in{submit:1,change:1,focusin:1})n="on"+o,p=n in q,p||(q.setAttribute(n,"return;"),p=typeof q[n]=="function"),b[o+"Bubbles"]=p;k.removeChild(q),k=g=h=j=q=i=null,f(function(){var a,d,e,g,h,i,j,k,m,n,o,r=c.getElementsByTagName("body")[0];!r||(j=1,k="position:absolute;top:0;left:0;width:1px;height:1px;margin:0;",m="visibility:hidden;border:0;",n="style='"+k+"border:5px solid #000;padding:0;'",o="
"+""+"
",a=c.createElement("div"),a.style.cssText=m+"width:0;height:0;position:static;top:0;margin-top:"+j+"px",r.insertBefore(a,r.firstChild),q=c.createElement("div"),a.appendChild(q),q.innerHTML="
t
",l=q.getElementsByTagName("td"),p=l[0].offsetHeight===0,l[0].style.display="",l[1].style.display="none",b.reliableHiddenOffsets=p&&l[0].offsetHeight===0,q.innerHTML="",q.style.width=q.style.paddingLeft="1px",f.boxModel=b.boxModel=q.offsetWidth===2,typeof q.style.zoom!="undefined"&&(q.style.display="inline",q.style.zoom=1,b.inlineBlockNeedsLayout=q.offsetWidth===2,q.style.display="",q.innerHTML="
",b.shrinkWrapBlocks=q.offsetWidth!==2),q.style.cssText=k+m,q.innerHTML=o,d=q.firstChild,e=d.firstChild,h=d.nextSibling.firstChild.firstChild,i={doesNotAddBorder:e.offsetTop!==5,doesAddBorderForTableAndCells:h.offsetTop===5},e.style.position="fixed",e.style.top="20px",i.fixedPosition=e.offsetTop===20||e.offsetTop===15,e.style.position=e.style.top="",d.style.overflow="hidden",d.style.position="relative",i.subtractsBorderForOverflowNotVisible=e.offsetTop===-5,i.doesNotIncludeMarginInBodyOffset=r.offsetTop!==j,r.removeChild(a),q=a=null,f.extend(b,i))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.nodeName.toLowerCase()]||f.valHooks[g.type];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;h=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/\bhover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function(a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")}; -f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&i.push({elem:this,matches:d.slice(e)});for(j=0;j0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function() -{for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||!bc.test("<"+a.nodeName)?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=bg[l]||bg._default,n=m[0],o=b.createElement("div");b===c?bh.appendChild(o):U(b).appendChild(o),o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return br.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bq,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bq.test(g)?g.replace(bq,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bz(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bA=function(a,b){var c,d,e;b=b.replace(bs,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b)));return c}),c.documentElement.currentStyle&&(bB=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f===null&&g&&(e=g[b])&&(f=e),!bt.test(f)&&bu.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f||0,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),bz=bA||bB,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bD=/%20/g,bE=/\[\]$/,bF=/\r?\n/g,bG=/#.*$/,bH=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bI=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bJ=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bK=/^(?:GET|HEAD)$/,bL=/^\/\//,bM=/\?/,bN=/)<[^<]*)*<\/script>/gi,bO=/^(?:select|textarea)/i,bP=/\s+/,bQ=/([?&])_=[^&]*/,bR=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bS=f.fn.load,bT={},bU={},bV,bW,bX=["*/"]+["*"];try{bV=e.href}catch(bY){bV=c.createElement("a"),bV.href="",bV=bV.href}bW=bR.exec(bV.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bS)return bS.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bN,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bO.test(this.nodeName)||bI.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bF,"\r\n")}}):{name:b.name,value:c.replace(bF,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b_(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b_(a,b);return a},ajaxSettings:{url:bV,isLocal:bJ.test(bW[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bX},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bZ(bT),ajaxTransport:bZ(bU),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?cb(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cc(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bH.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bG,"").replace(bL,bW[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bP),d.crossDomain==null&&(r=bR.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bW[1]&&r[2]==bW[2]&&(r[3]||(r[1]==="http:"?80:443))==(bW[3]||(bW[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),b$(bT,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bK.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bM.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bQ,"$1_="+x);d.url=y+(y===d.url?(bM.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bX+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=b$(bU,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)ca(g,a[g],c,e);return d.join("&").replace(bD,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cd=f.now(),ce=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cd++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=b.contentType==="application/x-www-form-urlencoded"&&typeof b.data=="string";if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(ce.test(b.url)||e&&ce.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(ce,l),b.url===j&&(e&&(k=k.replace(ce,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var cf=a.ActiveXObject?function(){for(var a in ch)ch[a](0,1)}:!1,cg=0,ch;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ci()||cj()}:ci,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,cf&&delete ch[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n),m.text=h.responseText;try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cg,cf&&(ch||(ch={},f(a).unload(cf)),ch[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var ck={},cl,cm,cn=/^(?:toggle|show|hide)$/,co=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,cp,cq=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cr;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(cu("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each(["Left","Top"],function(a,c){var d="scroll"+c;f.fn[d]=function(c){var e,g;if(c===b){e=this[0];if(!e)return null;g=cy(e);return g?"pageXOffset"in g?g[a?"pageYOffset":"pageXOffset"]:f.support.boxModel&&g.document.documentElement[d]||g.document.body[d]:e[d]}return this.each(function(){g=cy(this),g?g.scrollTo(a?f(g).scrollLeft():c,a?c:f(g).scrollTop()):this[d]=c})}}),f.each(["Height","Width"],function(a,c){var d=c.toLowerCase();f.fn["inner"+c]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,d,"padding")):this[d]():null},f.fn["outer"+c]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,d,a?"margin":"border")):this[d]():null},f.fn[d]=function(a){var e=this[0];if(!e)return a==null?null:this;if(f.isFunction(a))return this.each(function(b){var c=f(this);c[d](a.call(this,b,c[d]()))});if(f.isWindow(e)){var g=e.document.documentElement["client"+c],h=e.document.body;return e.document.compatMode==="CSS1Compat"&&g||h&&h["client"+c]||g}if(e.nodeType===9)return Math.max(e.documentElement["client"+c],e.body["scroll"+c],e.documentElement["scroll"+c],e.body["offset"+c],e.documentElement["offset"+c]);if(a===b){var i=f.css(e,d),j=parseFloat(i);return f.isNumeric(j)?j:i}return this.css(d,typeof a=="string"?a:a+"px")}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/static/protovis-d3.1.js b/static/protovis-d3.1.js deleted file mode 100644 index af56eac..0000000 --- a/static/protovis-d3.1.js +++ /dev/null @@ -1,7725 +0,0 @@ -/** - * @class The built-in Array class. - * @name Array - */ - -if (!Array.prototype.map) { - /** - * Creates a new array with the results of calling a provided function on - * every element in this array. Implemented in Javascript 1.6. - * - * @see map - * documentation. - * @param {function} f function that produces an element of the new Array from - * an element of the current one. - * @param [o] object to use as this when executing f. - */ - Array.prototype.map = function(f, o) { - var n = this.length; - var result = new Array(n); - for (var i = 0; i < n; i++) { - if (i in this) { - result[i] = f.call(o, this[i], i, this); - } - } - return result; - }; -} - -if (!Array.prototype.filter) { - /** - * Creates a new array with all elements that pass the test implemented by the - * provided function. Implemented in Javascript 1.6. - * - * @see filter - * documentation. - * @param {function} f function to test each element of the array. - * @param [o] object to use as this when executing f. - */ - Array.prototype.filter = function(f, o) { - var n = this.length; - var result = new Array(); - for (var i = 0; i < n; i++) { - if (i in this) { - var v = this[i]; - if (f.call(o, v, i, this)) result.push(v); - } - } - return result; - }; -} - -if (!Array.prototype.forEach) { - /** - * Executes a provided function once per array element. Implemented in - * Javascript 1.6. - * - * @see forEach - * documentation. - * @param {function} f function to execute for each element. - * @param [o] object to use as this when executing f. - */ - Array.prototype.forEach = function(f, o) { - var n = this.length >>> 0; - for (var i = 0; i < n; i++) { - if (i in this) f.call(o, this[i], i, this); - } - }; -} - -if (!Array.prototype.reduce) { - /** - * Apply a function against an accumulator and each value of the array (from - * left-to-right) as to reduce it to a single value. Implemented in Javascript - * 1.8. - * - * @see reduce - * documentation. - * @param {function} f function to execute on each value in the array. - * @param [v] object to use as the first argument to the first call of - * t. - */ - Array.prototype.reduce = function(f, v) { - var len = this.length; - if (!len && (arguments.length == 1)) { - throw new Error("reduce: empty array, no initial value"); - } - - var i = 0; - if (arguments.length < 2) { - while (true) { - if (i in this) { - v = this[i++]; - break; - } - if (++i >= len) { - throw new Error("reduce: no values, no initial value"); - } - } - } - - for (; i < len; i++) { - if (i in this) { - v = f(v, this[i], i, this); - } - } - return v; - }; -} -/** - * @class The built-in Date class. - * @name Date - */ - -Date.__parse__ = Date.parse; - -/** - * Parses a date from a string, optionally using the specified formatting. If - * only a single argument is specified (i.e., format is not specified), - * this method invokes the native implementation to guarantee - * backwards-compatibility. - * - *

The format string is in the same format expected by the strptime - * function in C. The following conversion specifications are supported:

    - * - *
  • %b - abbreviated month names.
  • - *
  • %B - full month names.
  • - *
  • %h - same as %b.
  • - *
  • %d - day of month [1,31].
  • - *
  • %e - same as %d.
  • - *
  • %H - hour (24-hour clock) [0,23].
  • - *
  • %m - month number [1,12].
  • - *
  • %M - minute [0,59].
  • - *
  • %S - second [0,61].
  • - *
  • %y - year with century [0,99].
  • - *
  • %Y - year including century.
  • - *
  • %% - %.
  • - * - *
The following conversion specifications are unsupported (for now):
    - * - *
  • %a - day of week, either abbreviated or full name.
  • - *
  • %A - same as %a.
  • - *
  • %c - locale's appropriate date and time.
  • - *
  • %C - century number.
  • - *
  • %D - same as %m/%d/%y.
  • - *
  • %I - hour (12-hour clock) [1,12].
  • - *
  • %j - day number [1,366].
  • - *
  • %n - any white space.
  • - *
  • %p - locale's equivalent of a.m. or p.m.
  • - *
  • %r - same as %I:%M:%S %p.
  • - *
  • %R - same as %H:%M.
  • - *
  • %t - same as %n.
  • - *
  • %T - same as %H:%M:%S.
  • - *
  • %U - week number [0,53].
  • - *
  • %w - weekday [0,6].
  • - *
  • %W - week number [0,53].
  • - *
  • %x - locale's equivalent to %m/%d/%y.
  • - *
  • %X - locale's equivalent to %I:%M:%S %p.
  • - * - *
- * - * @see strptime - * documentation. - * @param {string} s the string to parse as a date. - * @param {string} [format] an optional format string. - * @returns {Date} the parsed date. - */ -Date.parse = function(s, format) { - if (arguments.length == 1) { - return Date.__parse__(s); - } - - var year = 1970, month = 0, date = 1, hour = 0, minute = 0, second = 0; - var fields = [function() {}]; - format = format.replace(/[\\\^\$\*\+\?\[\]\(\)\.\{\}]/g, "\\$&"); - format = format.replace(/%[a-zA-Z0-9]/g, function(s) { - switch (s) { - // TODO %a: day of week, either abbreviated or full name - // TODO %A: same as %a - case '%b': { - fields.push(function(x) { month = { - Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5, Jul: 6, Aug: 7, - Sep: 8, Oct: 9, Nov: 10, Dec: 11 - }[x]; }); - return "([A-Za-z]+)"; - } - case '%h': - case '%B': { - fields.push(function(x) { month = { - January: 0, February: 1, March: 2, April: 3, May: 4, June: 5, - July: 6, August: 7, September: 8, October: 9, November: 10, - December: 11 - }[x]; }); - return "([A-Za-z]+)"; - } - // TODO %c: locale's appropriate date and time - // TODO %C: century number[0,99] - case '%e': - case '%d': { - fields.push(function(x) { date = x; }); - return "([0-9]+)"; - } - // TODO %D: same as %m/%d/%y - case '%H': { - fields.push(function(x) { hour = x; }); - return "([0-9]+)"; - } - // TODO %I: hour (12-hour clock) [1,12] - // TODO %j: day number [1,366] - case '%m': { - fields.push(function(x) { month = x - 1; }); - return "([0-9]+)"; - } - case '%M': { - fields.push(function(x) { minute = x; }); - return "([0-9]+)"; - } - // TODO %n: any white space - // TODO %p: locale's equivalent of a.m. or p.m. - // TODO %r: %I:%M:%S %p - // TODO %R: %H:%M - case '%S': { - fields.push(function(x) { second = x; }); - return "([0-9]+)"; - } - // TODO %t: any white space - // TODO %T: %H:%M:%S - // TODO %U: week number [00,53] - // TODO %w: weekday [0,6] - // TODO %W: week number [00, 53] - // TODO %x: locale date (%m/%d/%y) - // TODO %X: locale time (%I:%M:%S %p) - case '%y': { - fields.push(function(x) { - x = Number(x); - year = x + (((0 <= x) && (x < 69)) ? 2000 - : (((x >= 69) && (x < 100) ? 1900 : 0))); - }); - return "([0-9]+)"; - } - case '%Y': { - fields.push(function(x) { year = x; }); - return "([0-9]+)"; - } - case '%%': { - fields.push(function() {}); - return "%"; - } - } - return s; - }); - - var match = s.match(format); - if (match) match.forEach(function(m, i) { fields[i](m); }); - return new Date(year, month, date, hour, minute, second); -}; - -if (Date.prototype.toLocaleFormat) { - Date.prototype.format = Date.prototype.toLocaleFormat; -} else { - -/** - * Converts a date to a string using the specified formatting. If the - * Date object already supports the toLocaleFormat method, as - * in Firefox, this is simply an alias to the built-in method. - * - *

The format string is in the same format expected by the strftime - * function in C. The following conversion specifications are supported:

    - * - *
  • %a - abbreviated weekday name.
  • - *
  • %A - full weekday name.
  • - *
  • %b - abbreviated month names.
  • - *
  • %B - full month names.
  • - *
  • %c - locale's appropriate date and time.
  • - *
  • %C - century number.
  • - *
  • %d - day of month [01,31] (zero padded).
  • - *
  • %D - same as %m/%d/%y.
  • - *
  • %e - day of month [ 1,31] (space padded).
  • - *
  • %h - same as %b.
  • - *
  • %H - hour (24-hour clock) [00,23] (zero padded).
  • - *
  • %I - hour (12-hour clock) [01,12] (zero padded).
  • - *
  • %m - month number [01,12] (zero padded).
  • - *
  • %M - minute [0,59] (zero padded).
  • - *
  • %n - newline character.
  • - *
  • %p - locale's equivalent of a.m. or p.m.
  • - *
  • %r - same as %I:%M:%S %p.
  • - *
  • %R - same as %H:%M.
  • - *
  • %S - second [00,61] (zero padded).
  • - *
  • %t - tab character.
  • - *
  • %T - same as %H:%M:%S.
  • - *
  • %x - same as %m/%d/%y.
  • - *
  • %X - same as %I:%M:%S %p.
  • - *
  • %y - year with century [00,99] (zero padded).
  • - *
  • %Y - year including century.
  • - *
  • %% - %.
  • - * - *
The following conversion specifications are unsupported (for now):
    - * - *
  • %j - day number [1,366].
  • - *
  • %u - weekday number [1,7].
  • - *
  • %U - week number [00,53].
  • - *
  • %V - week number [01,53].
  • - *
  • %w - weekday number [0,6].
  • - *
  • %W - week number [00,53].
  • - *
  • %Z - timezone name or abbreviation.
  • - * - *
- * - * @see Date.toLocaleFormat - * documentation. - * @see strftime - * documentation. - * @param {string} format a format string. - * @returns {string} the formatted date. - */ -Date.prototype.format = function(format) { - function pad(n, p) { return (n < 10) ? (p || "0") + n : n; } - var d = this; - return format.replace(/%[a-zA-Z0-9]/g, function(s) { - switch (s) { - case '%a': return [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" - ][d.getDay()]; - case '%A': return [ - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", - "Saturday" - ][d.getDay()]; - case '%h': - case '%b': return [ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", - "Oct", "Nov", "Dec" - ][d.getMonth()]; - case '%B': return [ - "January", "February", "March", "April", "May", "June", "July", - "August", "September", "October", "November", "December" - ][d.getMonth()]; - case '%c': return d.toLocaleString(); - case '%C': return pad(Math.floor(d.getFullYear() / 100) % 100); - case '%d': return pad(d.getDate()); - case '%x': - case '%D': return pad(d.getMonth() + 1) - + "/" + pad(d.getDate()) - + "/" + pad(d.getFullYear() % 100); - case '%e': return pad(d.getDate(), " "); - case '%H': return pad(d.getHours()); - case '%I': { - var h = d.getHours() % 12; - return h ? pad(h) : 12; - } - // TODO %j: day of year as a decimal number [001,366] - case '%m': return pad(d.getMonth() + 1); - case '%M': return pad(d.getMinutes()); - case '%n': return "\n"; - case '%p': return d.getHours() < 12 ? "AM" : "PM"; - case '%T': - case '%X': - case '%r': { - var h = d.getHours() % 12; - return (h ? pad(h) : 12) - + ":" + pad(d.getMinutes()) - + ":" + pad(d.getSeconds()) - + " " + (d.getHours() < 12 ? "AM" : "PM"); - } - case '%R': return pad(d.getHours()) + ":" + pad(d.getMinutes()); - case '%S': return pad(d.getSeconds()); - case '%t': return "\t"; - case '%u': { - var w = d.getDay(); - return w ? w : 1; - } - // TODO %U: week number (sunday first day) [00,53] - // TODO %V: week number (monday first day) [01,53] ... with weirdness - case '%w': return d.getDay(); - // TODO %W: week number (monday first day) [00,53] ... with weirdness - case '%y': return pad(d.getFullYear() % 100); - case '%Y': return d.getFullYear(); - // TODO %Z: timezone name or abbreviation - case '%%': return "%"; - } - return s; - }); - }; -} -var pv = function() {/** - * The top-level Protovis namespace. All public methods and fields should be - * registered on this object. Note that core Protovis source is surrounded by an - * anonymous function, so any other declared globals will not be visible outside - * of core methods. This also allows multiple versions of Protovis to coexist, - * since each version will see their own pv namespace. - * - * @namespace The top-level Protovis namespace, pv. - */ -var pv = {}; - -/** - * @private Returns a prototype object suitable for extending the given class - * f. Rather than constructing a new instance of f to serve as - * the prototype (which unnecessarily runs the constructor on the created - * prototype object, potentially polluting it), an anonymous function is - * generated internally that shares the same prototype: - * - *
function g() {}
- * g.prototype = f.prototype;
- * return new g();
- * - * For more details, see Douglas Crockford's essay on prototypal inheritance. - * - * @param {function} f a constructor. - * @returns a suitable prototype object. - * @see Douglas Crockford's essay on prototypal - * inheritance. - */ -pv.extend = function(f) { - function g() {} - g.prototype = f.prototype || f; - return new g(); -}; - -try { - eval("pv.parse = function(x) x;"); // native support -} catch (e) { - -/** - * @private Parses a Protovis specification, which may use JavaScript 1.8 - * function expresses, replacing those function expressions with proper - * functions such that the code can be run by a JavaScript 1.6 interpreter. This - * hack only supports function expressions (using clumsy regular expressions, no - * less), and not other JavaScript 1.8 features such as let expressions. - * - * @param {string} s a Protovis specification (i.e., a string of JavaScript 1.8 - * source code). - * @returns {string} a conformant JavaScript 1.6 source code. - */ - pv.parse = function(js) { // hacky regex support - var re = new RegExp("function(\\s+\\w+)?\\([^)]*\\)\\s*", "mg"), m, d, i = 0, s = ""; - while (m = re.exec(js)) { - var j = m.index + m[0].length; - if (js.charAt(j--) != '{') { - s += js.substring(i, j) + "{return "; - i = j; - for (var p = 0; p >= 0 && j < js.length; j++) { - var c = js.charAt(j); - switch (c) { - case '"': case '\'': { - while (++j < js.length && (d = js.charAt(j)) != c) { - if (d == '\\') j++; - } - break; - } - case '[': case '(': p++; break; - case ']': case ')': p--; break; - case ';': - case ',': if (p == 0) p--; break; - } - } - s += pv.parse(js.substring(i, --j)) + ";}"; - i = j; - } - re.lastIndex = j; - } - s += js.substring(i); - return s; - }; -} - -/** - * Returns the passed-in argument, x; the identity function. This method - * is provided for convenience since it is used as the default behavior for a - * number of property functions. - * - * @param x a value. - * @returns the value x. - */ -pv.identity = function(x) { return x; }; - -/** - * Returns this.index. This method is provided for convenience for use - * with scales. For example, to color bars by their index, say: - * - *
.fillStyle(pv.Colors.category10().by(pv.index))
- * - * This method is equivalent to function() this.index, but more - * succinct. Note that the index property is also supported for - * accessor functions with {@link pv.max}, {@link pv.min} and other array - * utility methods. - * - * @see pv.Scale - * @see pv.Mark#index - */ -pv.index = function() { return this.index; }; - -/** - * Returns this.childIndex. This method is provided for convenience for - * use with scales. For example, to color bars by their child index, say: - * - *
.fillStyle(pv.Colors.category10().by(pv.child))
- * - * This method is equivalent to function() this.childIndex, but more - * succinct. - * - * @see pv.Scale - * @see pv.Mark#childIndex - */ -pv.child = function() { return this.childIndex; }; - -/** - * Returns this.parent.index. This method is provided for convenience - * for use with scales. This method is provided for convenience for use with - * scales. For example, to color bars by their parent index, say: - * - *
.fillStyle(pv.Colors.category10().by(pv.parent))
- * - * Tthis method is equivalent to function() this.parent.index, but more - * succinct. - * - * @see pv.Scale - * @see pv.Mark#index - */ -pv.parent = function() { return this.parent.index; }; - -/** - * Returns an array of numbers, starting at start, incrementing by - * step, until stop is reached. The stop value is exclusive. If - * only a single argument is specified, this value is interpeted as the - * stop value, with the start value as zero. If only two arguments - * are specified, the step value is implied to be one. - * - *

The method is modeled after the built-in range method from - * Python. See the Python documentation for more details. - * - * @see Python range - * @param {number} [start] the start value. - * @param {number} stop the stop value. - * @param {number} [step] the step value. - * @returns {number[]} an array of numbers. - */ -pv.range = function(start, stop, step) { - if (arguments.length == 1) { - stop = start; - start = 0; - } - if (step == undefined) step = 1; - else if (!step) throw new Error("step must be non-zero"); - var array = [], i = 0, j; - if (step < 0) { - while ((j = start + step * i++) > stop) { - array.push(j); - } - } else { - while ((j = start + step * i++) < stop) { - array.push(j); - } - } - return array; -}; - -/** - * Returns a random number in the range [min, max) that is a - * multiple of step. More specifically, the returned number is of the - * form min + n * step, where n is a nonnegative - * integer. If step is not specified, it defaults to 1, returning a - * random integer if min is also an integer. - * - * @param min {number} minimum value. - * @param [max] {number} maximum value. - * @param [step] {numbeR} step value. - */ -pv.random = function(min, max, step) { - if (arguments.length == 1) { - max = min; - min = 0; - } - if (step == undefined) { - step = 1; - } - return step - ? (Math.floor(Math.random() * (max - min) / step) * step + min) - : (Math.random() * (max - min) + min); -}; - -/** - * Concatenates the specified array with itself n times. For example, - * pv.repeat([1, 2]) returns [1, 2, 1, 2]. - * - * @param {array} a an array. - * @param {number} [n] the number of times to repeat; defaults to two. - * @returns {array} an array that repeats the specified array. - */ -pv.repeat = function(array, n) { - if (arguments.length == 1) n = 2; - return pv.blend(pv.range(n).map(function() { return array; })); -}; - -/** - * Given two arrays a and b, returns an array of all possible - * pairs of elements [ai, bj]. The outer loop is on array - * a, while the inner loop is on b, such that the order of - * returned elements is [a0, b0], [a0, - * b1], ... [a0, bm], [a1, - * b0], [a1, b1], ... [a1, - * bm], ... [an, bm]. If either array is empty, - * an empty array is returned. - * - * @param {array} a an array. - * @param {array} b an array. - * @returns {array} an array of pairs of elements in a and b. - */ -pv.cross = function(a, b) { - var array = []; - for (var i = 0, n = a.length, m = b.length; i < n; i++) { - for (var j = 0, x = a[i]; j < m; j++) { - array.push([x, b[j]]); - } - } - return array; -}; - -/** - * Given the specified array of arrays, concatenates the arrays into a single - * array. If the individual arrays are explicitly known, an alternative to blend - * is to use JavaScript's concat method directly. These two equivalent - * expressions:

    - * - *
  • pv.blend([[1, 2, 3], ["a", "b", "c"]]) - *
  • [1, 2, 3].concat(["a", "b", "c"]) - * - *
return [1, 2, 3, "a", "b", "c"]. - * - * @param {array[]} arrays an array of arrays. - * @returns {array} an array containing all the elements of each array in - * arrays. - */ -pv.blend = function(arrays) { - return Array.prototype.concat.apply([], arrays); -}; - -/** - * Given the specified array of arrays, transposes each element - * arrayij with arrayji. If the array has dimensions - * n×m, it will have dimensions m×n - * after this method returns. This method transposes the elements of the array - * in place, mutating the array, and returning a reference to the array. - * - * @param {array[]} arrays an array of arrays. - * @returns {array[]} the passed-in array, after transposing the elements. - */ -pv.transpose = function(arrays) { - var n = arrays.length, m = pv.max(arrays, function(d) { return d.length; }); - - if (m > n) { - arrays.length = m; - for (var i = n; i < m; i++) { - arrays[i] = new Array(n); - } - for (var i = 0; i < n; i++) { - for (var j = i + 1; j < m; j++) { - var t = arrays[i][j]; - arrays[i][j] = arrays[j][i]; - arrays[j][i] = t; - } - } - } else { - for (var i = 0; i < m; i++) { - arrays[i].length = n; - } - for (var i = 0; i < n; i++) { - for (var j = 0; j < i; j++) { - var t = arrays[i][j]; - arrays[i][j] = arrays[j][i]; - arrays[j][i] = t; - } - } - } - - arrays.length = m; - for (var i = 0; i < m; i++) { - arrays[i].length = n; - } - - return arrays; -}; - -/** - * Returns all of the property names (keys) of the specified object (a map). The - * order of the returned array is not defined. - * - * @param map an object. - * @returns {string[]} an array of strings corresponding to the keys. - * @see #entries - */ -pv.keys = function(map) { - var array = []; - for (var key in map) { - array.push(key); - } - return array; -}; - -/** - * Returns all of the entries (key-value pairs) of the specified object (a - * map). The order of the returned array is not defined. Each key-value pair is - * represented as an object with key and value attributes, - * e.g., {key: "foo", value: 42}. - * - * @param map an object. - * @returns {array} an array of key-value pairs corresponding to the keys. - */ -pv.entries = function(map) { - var array = []; - for (var key in map) { - array.push({ key: key, value: map[key] }); - } - return array; -}; - -/** - * Returns all of the values (attribute values) of the specified object (a - * map). The order of the returned array is not defined. - * - * @param map an object. - * @returns {array} an array of objects corresponding to the values. - * @see #entries - */ -pv.values = function(map) { - var array = []; - for (var key in map) { - array.push(map[key]); - } - return array; -}; - -/** - * @private A private variant of Array.prototype.map that supports the index - * property. - */ -function map(array, f) { - var o = {}; - return f - ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) - : array.slice(); -}; - -/** - * Returns a normalized copy of the specified array, such that the sum of the - * returned elements sum to one. If the specified array is not an array of - * numbers, an optional accessor function f can be specified to map the - * elements to numbers. For example, if array is an array of objects, - * and each object has a numeric property "foo", the expression - * - *
pv.normalize(array, function(d) d.foo)
- * - * returns a normalized array on the "foo" property. If an accessor function is - * not specified, the identity function is used. Accessor functions can refer to - * this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number[]} an array of numbers that sums to one. - */ -pv.normalize = function(array, f) { - var norm = map(array, f), sum = pv.sum(norm); - for (var i = 0; i < norm.length; i++) norm[i] /= sum; - return norm; -}; - -/** - * Returns the sum of the specified array. If the specified array is not an - * array of numbers, an optional accessor function f can be specified - * to map the elements to numbers. See {@link #normalize} for an example. - * Accessor functions can refer to this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the sum of the specified array. - */ -pv.sum = function(array, f) { - var o = {}; - return array.reduce(f - ? function(p, d, i) { o.index = i; return p + f.call(o, d); } - : function(p, d) { return p + d; }, 0); -}; - -/** - * Returns the maximum value of the specified array. If the specified array is - * not an array of numbers, an optional accessor function f can be - * specified to map the elements to numbers. See {@link #normalize} for an - * example. Accessor functions can refer to this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the maximum value of the specified array. - */ -pv.max = function(array, f) { - if (f == pv.index) return array.length - 1; - return Math.max.apply(null, f ? map(array, f) : array); -}; - -/** - * Returns the index of the maximum value of the specified array. If the - * specified array is not an array of numbers, an optional accessor function - * f can be specified to map the elements to numbers. See - * {@link #normalize} for an example. Accessor functions can refer to - * this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the index of the maximum value of the specified array. - */ -pv.max.index = function(array, f) { - if (f == pv.index) return array.length - 1; - if (!f) f = pv.identity; - var maxi = -1, maxx = -Infinity, o = {}; - for (var i = 0; i < array.length; i++) { - o.index = i; - var x = f.call(o, array[i]); - if (x > maxx) { - maxx = x; - maxi = i; - } - } - return maxi; -} - -/** - * Returns the minimum value of the specified array of numbers. If the specified - * array is not an array of numbers, an optional accessor function f - * can be specified to map the elements to numbers. See {@link #normalize} for - * an example. Accessor functions can refer to this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the minimum value of the specified array. - */ -pv.min = function(array, f) { - if (f == pv.index) return 0; - return Math.min.apply(null, f ? map(array, f) : array); -}; - -/** - * Returns the index of the minimum value of the specified array. If the - * specified array is not an array of numbers, an optional accessor function - * f can be specified to map the elements to numbers. See - * {@link #normalize} for an example. Accessor functions can refer to - * this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the index of the minimum value of the specified array. - */ -pv.min.index = function(array, f) { - if (f == pv.index) return 0; - if (!f) f = pv.identity; - var mini = -1, minx = Infinity, o = {}; - for (var i = 0; i < array.length; i++) { - o.index = i; - var x = f.call(o, array[i]); - if (x < minx) { - minx = x; - mini = i; - } - } - return mini; -} - -/** - * Returns the arithmetic mean, or average, of the specified array. If the - * specified array is not an array of numbers, an optional accessor function - * f can be specified to map the elements to numbers. See - * {@link #normalize} for an example. Accessor functions can refer to - * this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the mean of the specified array. - */ -pv.mean = function(array, f) { - return pv.sum(array, f) / array.length; -}; - -/** - * Returns the median of the specified array. If the specified array is not an - * array of numbers, an optional accessor function f can be specified - * to map the elements to numbers. See {@link #normalize} for an example. - * Accessor functions can refer to this.index. - * - * @param {array} array an array of objects, or numbers. - * @param {function} [f] an optional accessor function. - * @returns {number} the median of the specified array. - */ -pv.median = function(array, f) { - if (f == pv.index) return (array.length - 1) / 2; - array = map(array, f).sort(pv.naturalOrder); - if (array.length % 2) return array[Math.floor(array.length / 2)]; - var i = array.length / 2; - return (array[i - 1] + array[i]) / 2; -}; - -/** - * Returns a map constructed from the specified keys, using the - * function f to compute the value for each key. The single argument to - * the value function is the key. The callback is invoked only for indexes of - * the array which have assigned values; it is not invoked for indexes which - * have been deleted or which have never been assigned values. - * - *

For example, this expression creates a map from strings to string length: - * - *

pv.dict(["one", "three", "seventeen"], function(s) s.length)
- * - * The returned value is {one: 3, three: 5, seventeen: 9}. Accessor - * functions can refer to this.index. - * - * @param {array} keys an array. - * @param {function} f a value function. - * @returns a map from keys to values. - */ -pv.dict = function(keys, f) { - var m = {}, o = {}; - for (var i = 0; i < keys.length; i++) { - if (i in keys) { - var k = keys[i]; - o.index = i; - m[k] = f.call(o, k); - } - } - return m; -}; - -/** - * Returns a permutation of the specified array, using the specified array of - * indexes. The returned array contains the corresponding element in - * array for each index in indexes, in order. For example, - * - *
pv.permute(["a", "b", "c"], [1, 2, 0])
- * - * returns ["b", "c", "a"]. It is acceptable for the array of indexes - * to be a different length from the array of elements, and for indexes to be - * duplicated or omitted. The optional accessor function f can be used - * to perform a simultaneous mapping of the array elements. Accessor functions - * can refer to this.index. - * - * @param {array} array an array. - * @param {number[]} indexes an array of indexes into array. - * @param {function} [f] an optional accessor function. - * @returns {array} an array of elements from array; a permutation. - */ -pv.permute = function(array, indexes, f) { - if (!f) f = pv.identity; - var p = new Array(indexes.length), o = {}; - indexes.forEach(function(j, i) { o.index = j; p[i] = f.call(o, array[j]); }); - return p; -}; - -/** - * Returns a map from key to index for the specified keys array. For - * example, - * - *
pv.numerate(["a", "b", "c"])
- * - * returns {a: 0, b: 1, c: 2}. Note that since JavaScript maps only - * support string keys, keys must contain strings, or other values that - * naturally map to distinct string values. Alternatively, an optional accessor - * function f can be specified to compute the string key for the given - * element. Accessor functions can refer to this.index. - * - * @param {array} keys an array, usually of string keys. - * @param {function} [f] an optional key function. - * @returns a map from key to index. - */ -pv.numerate = function(keys, f) { - if (!f) f = pv.identity; - var map = {}, o = {}; - keys.forEach(function(x, i) { o.index = i; map[f.call(o, x)] = i; }); - return map; -}; - -/** - * The comparator function for natural order. This can be used in conjunction with - * the built-in array sort method to sort elements by their natural - * order, ascending. Note that if no comparator function is specified to the - * built-in sort method, the default order is lexicographic, not - * natural! - * - * @see Array.sort. - * @param a an element to compare. - * @param b an element to compare. - * @returns {number} negative if a < b; positive if a > b; otherwise 0. - */ -pv.naturalOrder = function(a, b) { - return (a < b) ? -1 : ((a > b) ? 1 : 0); -}; - -/** - * The comparator function for reverse natural order. This can be used in - * conjunction with the built-in array sort method to sort elements by - * their natural order, descending. Note that if no comparator function is - * specified to the built-in sort method, the default order is - * lexicographic, not natural! - * - * @see #naturalOrder - * @param a an element to compare. - * @param b an element to compare. - * @returns {number} negative if a < b; positive if a > b; otherwise 0. - */ -pv.reverseOrder = function(b, a) { - return (a < b) ? -1 : ((a > b) ? 1 : 0); -}; - -/** - * @private Computes the value of the specified CSS property p on the - * specified element e. - * - * @param {string} p the name of the CSS property. - * @param e the element on which to compute the CSS property. - */ -pv.css = function(e, p) { - return window.getComputedStyle - ? window.getComputedStyle(e, null).getPropertyValue(p) - : e.currentStyle[p]; -}; - -/** - * Namespace constants for SVG, XMLNS, and XLINK. - * - * @namespace Namespace constants for SVG, XMLNS, and XLINK. - */ -pv.ns = { - /** - * The SVG namespace, "http://www.w3.org/2000/svg". - * - * @type string - * @constant - */ - svg: "http://www.w3.org/2000/svg", - - /** - * The XMLNS namespace, "http://www.w3.org/2000/xmlns". - * - * @type string - * @constant - */ - xmlns: "http://www.w3.org/2000/xmlns", - - /** - * The XLINK namespace, "http://www.w3.org/1999/xlink". - * - * @type string - * @constant - */ - xlink: "http://www.w3.org/1999/xlink" -}; - -/** - * Protovis major and minor version numbers. - * - * @namespace Protovis major and minor version numbers. - */ -pv.version = { - /** - * The major version number. - * - * @type number - * @constant - */ - major: 3, - - /** - * The minor version number. - * - * @type number - * @constant - */ - minor: 1 -}; - -/** - * @private Reports the specified error to the JavaScript console. Mozilla only - * allows logging to the console for privileged code; if the console is - * unavailable, the alert dialog box is used instead. - * - * @param e the exception that triggered the error. - */ -pv.error = function(e) { - (typeof console == "undefined") ? alert(e) : console.error(e); -}; - -/** - * @private Registers the specified listener for events of the specified type on - * the specified target. For standards-compliant browsers, this method uses - * addEventListener; for Internet Explorer, attachEvent. - * - * @param target a DOM element. - * @param {string} type the type of event, such as "click". - * @param {function} the listener callback function. - */ -pv.listen = function(target, type, listener) { - return target.addEventListener - ? target.addEventListener(type, listener, false) - : target.attachEvent("on" + type, listener); -}; - -/** - * Returns the logarithm with a given base value. - * - * @param {number} x the number for which to compute the logarithm. - * @param {number} b the base of the logarithm. - * @returns {number} the logarithm value. - */ -pv.log = function(x, b) { - return Math.log(x) / Math.log(b); -}; - -/** - * Computes a zero-symmetric logarithm. Computes the logarithm of the absolute - * value of the input, and determines the sign of the output according to the - * sign of the input value. - * - * @param {number} x the number for which to compute the logarithm. - * @param {number} b the base of the logarithm. - * @returns {number} the symmetric log value. - */ -pv.logSymmetric = function(x, b) { - return (x == 0) ? 0 : ((x < 0) ? -pv.log(-x, b) : pv.log(x, b)); -}; - -/** - * Computes a zero-symmetric logarithm, with adjustment to values between zero - * and the logarithm base. This adjustment introduces distortion for values less - * than the base number, but enables simultaneous plotting of log-transformed - * data involving both positive and negative numbers. - * - * @param {number} x the number for which to compute the logarithm. - * @param {number} b the base of the logarithm. - * @returns {number} the adjusted, symmetric log value. - */ -pv.logAdjusted = function(x, b) { - var negative = x < 0; - if (x < b) x += (b - x) / b; - return negative ? -pv.log(x, b) : pv.log(x, b); -}; - -/** - * Rounds an input value down according to its logarithm. The method takes the - * floor of the logarithm of the value and then uses the resulting value as an - * exponent for the base value. - * - * @param {number} x the number for which to compute the logarithm floor. - * @param {number} b the base of the logarithm. - * @return {number} the rounded-by-logarithm value. - */ -pv.logFloor = function(x, b) { - return (x > 0) - ? Math.pow(b, Math.floor(pv.log(x, b))) - : -Math.pow(b, -Math.floor(-pv.log(-x, b))); -}; - -/** - * Rounds an input value up according to its logarithm. The method takes the - * ceiling of the logarithm of the value and then uses the resulting value as an - * exponent for the base value. - * - * @param {number} x the number for which to compute the logarithm ceiling. - * @param {number} b the base of the logarithm. - * @return {number} the rounded-by-logarithm value. - */ -pv.logCeil = function(x, b) { - return (x > 0) - ? Math.pow(b, Math.ceil(pv.log(x, b))) - : -Math.pow(b, -Math.ceil(-pv.log(-x, b))); -}; - -/** - * Searches the specified array of numbers for the specified value using the - * binary search algorithm. The array must be sorted (as by the sort - * method) prior to making this call. If it is not sorted, the results are - * undefined. If the array contains multiple elements with the specified value, - * there is no guarantee which one will be found. - * - *

The insertion point is defined as the point at which the value - * would be inserted into the array: the index of the first element greater than - * the value, or array.length, if all elements in the array are less - * than the specified value. Note that this guarantees that the return value - * will be nonnegative if and only if the value is found. - * - * @param {number[]} array the array to be searched. - * @param {number} value the value to be searched for. - * @returns the index of the search value, if it is contained in the array; - * otherwise, (-(insertion point) - 1). - * @param {function} [f] an optional key function. - */ -pv.search = function(array, value, f) { - if (!f) f = pv.identity; - var low = 0, high = array.length - 1; - while (low <= high) { - var mid = (low + high) >> 1, midValue = f(array[mid]); - if (midValue < value) low = mid + 1; - else if (midValue > value) high = mid - 1; - else return mid; - } - return -low - 1; -}; - -pv.search.index = function(array, value, f) { - var i = pv.search(array, value, f); - return (i < 0) ? (-i - 1) : i; -}; -/** - * Returns a {@link pv.Tree} operator for the specified array. This is a - * convenience factory method, equivalent to new pv.Tree(array). - * - * @see pv.Tree - * @param {array} array an array from which to construct a tree. - * @returns {pv.Tree} a tree operator for the specified array. - */ -pv.tree = function(array) { - return new pv.Tree(array); -}; - -/** - * Constructs a tree operator for the specified array. This constructor should - * not be invoked directly; use {@link pv.tree} instead. - * - * @class Represents a tree operator for the specified array. The tree operator - * allows a hierarchical map to be constructed from an array; it is similar to - * the {@link pv.Nest} operator, except the hierarchy is derived dynamically - * from the array elements. - * - *

For example, given an array of size information for ActionScript classes: - * - *

{ name: "flare.flex.FlareVis", size: 4116 },
- * { name: "flare.physics.DragForce", size: 1082 },
- * { name: "flare.physics.GravityForce", size: 1336 }, ...
- * - * To facilitate visualization, it may be useful to nest the elements by their - * package hierarchy: - * - *
var tree = pv.tree(classes)
- *     .keys(function(d) d.name.split("."))
- *     .map();
- * - * The resulting tree is: - * - *
{ flare: {
- *     flex: {
- *       FlareVis: {
- *         name: "flare.flex.FlareVis",
- *         size: 4116 } },
- *     physics: {
- *       DragForce: {
- *         name: "flare.physics.DragForce",
- *         size: 1082 },
- *       GravityForce: {
- *         name: "flare.physics.GravityForce",
- *         size: 1336 } },
- *     ... } }
- * - * By specifying a value function, - * - *
var tree = pv.tree(classes)
- *     .keys(function(d) d.name.split("."))
- *     .value(function(d) d.size)
- *     .map();
- * - * we can further eliminate redundant data: - * - *
{ flare: {
- *     flex: {
- *       FlareVis: 4116 },
- *     physics: {
- *       DragForce: 1082,
- *       GravityForce: 1336 },
- *   ... } }
- * - * For visualizations with large data sets, performance improvements may be seen - * by storing the data in a tree format, and then flattening it into an array at - * runtime with {@link pv.Flatten}. - * - * @param {array} array an array from which to construct a tree. - */ -pv.Tree = function(array) { - this.array = array; -}; - -/** - * Assigns a keys function to this operator; required. The keys function - * returns an array of strings for each element in the associated - * array; these keys determine how the elements are nested in the tree. The - * returned keys should be unique for each element in the array; otherwise, the - * behavior of this operator is undefined. - * - * @param {function} k the keys function. - * @returns {pv.Tree} this. - */ -pv.Tree.prototype.keys = function(k) { - this.k = k; - return this; -}; - -/** - * Assigns a value function to this operator; optional. The value - * function specifies an optional transformation of the element in the array - * before it is inserted into the map. If no value function is specified, it is - * equivalent to using the identity function. - * - * @param {function} k the value function. - * @returns {pv.Tree} this. - */ -pv.Tree.prototype.value = function(v) { - this.v = v; - return this; -}; - -/** - * Returns a hierarchical map of values. The hierarchy is determined by the keys - * function; the values in the map are determined by the value function. - * - * @returns a hierarchical map of values. - */ -pv.Tree.prototype.map = function() { - var map = {}, o = {}; - for (var i = 0; i < this.array.length; i++) { - o.index = i; - var value = this.array[i], keys = this.k.call(o, value), node = map; - for (var j = 0; j < keys.length - 1; j++) { - node = node[keys[j]] || (node[keys[j]] = {}); - } - node[keys[j]] = this.v ? this.v.call(o, value) : value; - } - return map; -}; -/** - * Returns a {@link pv.Nest} operator for the specified array. This is a - * convenience factory method, equivalent to new pv.Nest(array). - * - * @see pv.Nest - * @param {array} array an array of elements to nest. - * @returns {pv.Nest} a nest operator for the specified array. - */ -pv.nest = function(array) { - return new pv.Nest(array); -}; - -/** - * Constructs a nest operator for the specified array. This constructor should - * not be invoked directly; use {@link pv.nest} instead. - * - * @class Represents a {@link Nest} operator for the specified array. Nesting - * allows elements in an array to be grouped into a hierarchical tree - * structure. The levels in the tree are specified by key functions. The - * leaf nodes of the tree can be sorted by value, while the internal nodes can - * be sorted by key. Finally, the tree can be returned either has a - * multidimensional array via {@link #entries}, or as a hierarchical map via - * {@link #map}. The {@link #rollup} routine similarly returns a map, collapsing - * the elements in each leaf node using a summary function. - * - *

For example, consider the following tabular data structure of Barley - * yields, from various sites in Minnesota during 1931-2: - * - *

{ yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" },
- * { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" },
- * { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" }, ...
- * - * To facilitate visualization, it may be useful to nest the elements first by - * year, and then by variety, as follows: - * - *
var nest = pv.nest(yields)
- *     .key(function(d) d.year)
- *     .key(function(d) d.variety)
- *     .entries();
- * - * This returns a nested array. Each element of the outer array is a key-values - * pair, listing the values for each distinct key: - * - *
{ key: 1931, values: [
- *   { key: "Manchuria", values: [
- *       { yield: 27.00, variety: "Manchuria", year: 1931, site: "University Farm" },
- *       { yield: 48.87, variety: "Manchuria", year: 1931, site: "Waseca" },
- *       { yield: 27.43, variety: "Manchuria", year: 1931, site: "Morris" },
- *       ...
- *     ] },
- *   { key: "Glabron", values: [
- *       { yield: 43.07, variety: "Glabron", year: 1931, site: "University Farm" },
- *       { yield: 55.20, variety: "Glabron", year: 1931, site: "Waseca" },
- *       ...
- *     ] },
- *   ] },
- * { key: 1932, values: ... }
- * - * Further details, including sorting and rollup, is provided below on the - * corresponding methods. - * - * @param {array} array an array of elements to nest. - */ -pv.Nest = function(array) { - this.array = array; - this.keys = []; -}; - -/** - * Nests using the specified key function. Multiple keys may be added to the - * nest; the array elements will be nested in the order keys are specified. - * - * @param {function} key a key function; must return a string or suitable map - * key. - * @return {pv.Nest} this. - */ -pv.Nest.prototype.key = function(key) { - this.keys.push(key); - return this; -}; - -/** - * Sorts the previously-added keys. The natural sort order is used by default - * (see {@link pv.naturalOrder}); if an alternative order is desired, - * order should be a comparator function. If this method is not called - * (i.e., keys are unsorted), keys will appear in the order they appear - * in the underlying elements array. For example, - * - *
pv.nest(yields)
- *     .key(function(d) d.year)
- *     .key(function(d) d.variety)
- *     .sortKeys()
- *     .entries()
- * - * groups yield data by year, then variety, and sorts the variety groups - * lexicographically (since the variety attribute is a string). - * - *

Key sort order is only used in conjunction with {@link #entries}, which - * returns an array of key-values pairs. If the nest is used to construct a - * {@link #map} instead, keys are unsorted. - * - * @param {function} [order] an optional comparator function. - * @returns {pv.Nest} this. - */ -pv.Nest.prototype.sortKeys = function(order) { - this.keys[this.keys.length - 1].order = order || pv.naturalOrder; - return this; -}; - -/** - * Sorts the leaf values. The natural sort order is used by default (see - * {@link pv.naturalOrder}); if an alternative order is desired, order - * should be a comparator function. If this method is not called (i.e., values - * are unsorted), values will appear in the order they appear in the - * underlying elements array. For example, - * - *

pv.nest(yields)
- *     .key(function(d) d.year)
- *     .key(function(d) d.variety)
- *     .sortValues(function(a, b) a.yield - b.yield)
- *     .entries()
- * - * groups yield data by year, then variety, and sorts the values for each - * variety group by yield. - * - *

Value sort order, unlike keys, applies to both {@link #entries} and - * {@link #map}. It has no effect on {@link #rollup}. - * - * @param {function} [order] an optional comparator function. - * @return {pv.Nest} this. - */ -pv.Nest.prototype.sortValues = function(order) { - this.order = order || pv.naturalOrder; - return this; -}; - -/** - * Returns a hierarchical map of values. Each key adds one level to the - * hierarchy. With only a single key, the returned map will have a key for each - * distinct value of the key function; the correspond value with be an array of - * elements with that key value. If a second key is added, this will be a nested - * map. For example: - * - *

pv.nest(yields)
- *     .key(function(d) d.variety)
- *     .key(function(d) d.site)
- *     .map()
- * - * returns a map m such that m[variety][site] is an array, a subset of - * yields, with each element having the given variety and site. - * - * @returns a hierarchical map of values. - */ -pv.Nest.prototype.map = function() { - var map = {}, values = []; - - /* Build the map. */ - for (var i, j = 0; j < this.array.length; j++) { - var x = this.array[j]; - var m = map; - for (i = 0; i < this.keys.length - 1; i++) { - var k = this.keys[i](x); - if (!m[k]) m[k] = {}; - m = m[k]; - } - k = this.keys[i](x); - if (!m[k]) { - var a = []; - values.push(a); - m[k] = a; - } - m[k].push(x); - } - - /* Sort each leaf array. */ - if (this.order) { - for (var i = 0; i < values.length; i++) { - values[i].sort(this.order); - } - } - - return map; -}; - -/** - * Returns a hierarchical nested array. This method is similar to - * {@link pv.entries}, but works recursively on the entire hierarchy. Rather - * than returning a map like {@link #map}, this method returns a nested - * array. Each element of the array has a key and values - * field. For leaf nodes, the values array will be a subset of the - * underlying elements array; for non-leaf nodes, the values array will - * contain more key-values pairs. - * - *

For an example usage, see the {@link Nest} constructor. - * - * @returns a hierarchical nested array. - */ -pv.Nest.prototype.entries = function() { - - /** Recursively extracts the entries for the given map. */ - function entries(map) { - var array = []; - for (var k in map) { - var v = map[k]; - array.push({ key: k, values: (v instanceof Array) ? v : entries(v) }); - }; - return array; - } - - /** Recursively sorts the values for the given key-values array. */ - function sort(array, i) { - var o = this.keys[i].order; - if (o) array.sort(function(a, b) { return o(a.key, b.key); }); - if (++i < this.keys.length) { - for (var j = 0; j < array.length; j++) { - sort.call(this, array[j].values, i); - } - } - return array; - } - - return sort.call(this, entries(this.map()), 0); -}; - -/** - * Returns a rollup map. The behavior of this method is the same as - * {@link #map}, except that the leaf values are replaced with the return value - * of the specified rollup function f. For example, - * - *

pv.nest(yields)
- *      .key(function(d) d.site)
- *      .rollup(function(v) pv.median(v, function(d) d.yield))
- * - * first groups yield data by site, and then returns a map from site to median - * yield for the given site. - * - * @see #map - * @param {function} f a rollup function. - * @returns a hierarchical map, with the leaf values computed by f. - */ -pv.Nest.prototype.rollup = function(f) { - - /** Recursively descends to the leaf nodes (arrays) and does rollup. */ - function rollup(map) { - for (var key in map) { - var value = map[key]; - if (value instanceof Array) { - map[key] = f(value); - } else { - rollup(value); - } - } - return map; - } - - return rollup(this.map()); -}; -/** - * Returns a {@link pv.Flatten} operator for the specified map. This is a - * convenience factory method, equivalent to new pv.Flatten(map). - * - * @see pv.Flatten - * @param map a map to flatten. - * @returns {pv.Flatten} a flatten operator for the specified map. - */ -pv.flatten = function(map) { - return new pv.Flatten(map); -}; - -/** - * Constructs a flatten operator for the specified map. This constructor should - * not be invoked directly; use {@link pv.flatten} instead. - * - * @class Represents a flatten operator for the specified array. Flattening - * allows hierarchical maps to be flattened into an array. The levels in the - * input tree are specified by key functions. - * - *

For example, consider the following hierarchical data structure of Barley - * yields, from various sites in Minnesota during 1931-2: - * - *

{ 1931: {
- *     Manchuria: {
- *       "University Farm": 27.00,
- *       "Waseca": 48.87,
- *       "Morris": 27.43,
- *       ... },
- *     Glabron: {
- *       "University Farm": 43.07,
- *       "Waseca": 55.20,
- *       ... } },
- *   1932: {
- *     ... } }
- * - * To facilitate visualization, it may be useful to flatten the tree into a - * tabular array: - * - *
var array = pv.flatten(yields)
- *     .key("year")
- *     .key("variety")
- *     .key("site")
- *     .key("yield")
- *     .array();
- * - * This returns an array of object elements. Each element in the array has - * attributes corresponding to this flatten operator's keys: - * - *
{ site: "University Farm", variety: "Manchuria", year: 1931, yield: 27 },
- * { site: "Waseca", variety: "Manchuria", year: 1931, yield: 48.87 },
- * { site: "Morris", variety: "Manchuria", year: 1931, yield: 27.43 },
- * { site: "University Farm", variety: "Glabron", year: 1931, yield: 43.07 },
- * { site: "Waseca", variety: "Glabron", year: 1931, yield: 55.2 }, ...
- * - *

The flatten operator is roughly the inverse of the {@link pv.Nest} and - * {@link pv.Tree} operators. - * - * @param map a map to flatten. - */ -pv.Flatten = function(map) { - this.map = map; - this.keys = []; -}; - -/** - * Flattens using the specified key function. Multiple keys may be added to the - * flatten; the tiers of the underlying tree must correspond to the specified - * keys, in order. The order of the returned array is undefined; however, you - * can easily sort it. - * - * @param {string} key the key name. - * @param {function} [f] an optional value map function. - * @return {pv.Nest} this. - */ -pv.Flatten.prototype.key = function(key, f) { - this.keys.push({name: key, value: f}); - return this; -}; - -/** - * Returns the flattened array. Each entry in the array is an object; each - * object has attributes corresponding to this flatten operator's keys. - * - * @returns an array of elements from the flattened map. - */ -pv.Flatten.prototype.array = function() { - var entries = [], stack = [], keys = this.keys; - - /* Recursively visits the specified value. */ - function visit(value, i) { - if (i < keys.length - 1) { - for (var key in value) { - stack.push(key); - visit(value[key], i + 1); - stack.pop(); - } - } else { - entries.push(stack.concat(value)); - } - } - - visit(this.map, 0); - return entries.map(function(stack) { - var m = {}; - for (var i = 0; i < keys.length; i++) { - var k = keys[i], v = stack[i]; - m[k.name] = k.value ? k.value.call(null, v) : v; - } - return m; - }); -}; -/** - * Returns a {@link pv.Vector} for the specified x and y - * coordinate. This is a convenience factory method, equivalent to new - * pv.Vector(x, y). - * - * @see pv.Vector - * @param {number} x the x coordinate. - * @param {number} y the y coordinate. - * @returns {pv.Vector} a vector for the specified coordinates. - */ -pv.vector = function(x, y) { - return new pv.Vector(x, y); -}; - -/** - * Constructs a {@link pv.Vector} for the specified x and y - * coordinate. This constructor should not be invoked directly; use - * {@link pv.vector} instead. - * - * @class Represents a two-dimensional vector; a 2-tuple ⟨x, - * y⟩. - * - * @param {number} x the x coordinate. - * @param {number} y the y coordinate. - */ -pv.Vector = function(x, y) { - this.x = x; - this.y = y; -}; - -/** - * Returns a vector perpendicular to this vector: ⟨-y, x⟩. - * - * @returns {pv.Vector} a perpendicular vector. - */ -pv.Vector.prototype.perp = function() { - return new pv.Vector(-this.y, this.x); -}; - -/** - * Returns a normalized copy of this vector: a vector with the same direction, - * but unit length. If this vector has zero length this method returns a copy of - * this vector. - * - * @returns {pv.Vector} a unit vector. - */ -pv.Vector.prototype.norm = function() { - var l = this.length(); - return this.times(l ? (1 / l) : 1); -}; - -/** - * Returns the magnitude of this vector, defined as sqrt(x * x + y * y). - * - * @returns {number} a length. - */ -pv.Vector.prototype.length = function() { - return Math.sqrt(this.x * this.x + this.y * this.y); -}; - -/** - * Returns a scaled copy of this vector: ⟨x * k, y * k⟩. - * To perform the equivalent divide operation, use 1 / k. - * - * @param {number} k the scale factor. - * @returns {pv.Vector} a scaled vector. - */ -pv.Vector.prototype.times = function(k) { - return new pv.Vector(this.x * k, this.y * k); -}; - -/** - * Returns this vector plus the vector v: ⟨x + v.x, y + - * v.y⟩. If only one argument is specified, it is interpreted as the - * vector v. - * - * @param {number} x the x coordinate to add. - * @param {number} y the y coordinate to add. - * @returns {pv.Vector} a new vector. - */ -pv.Vector.prototype.plus = function(x, y) { - return (arguments.length == 1) - ? new pv.Vector(this.x + x.x, this.y + x.y) - : new pv.Vector(this.x + x, this.y + y); -}; - -/** - * Returns this vector minus the vector v: ⟨x - v.x, y - - * v.y⟩. If only one argument is specified, it is interpreted as the - * vector v. - * - * @param {number} x the x coordinate to subtract. - * @param {number} y the y coordinate to subtract. - * @returns {pv.Vector} a new vector. - */ -pv.Vector.prototype.minus = function(x, y) { - return (arguments.length == 1) - ? new pv.Vector(this.x - x.x, this.y - x.y) - : new pv.Vector(this.x - x, this.y - y); -}; - -/** - * Returns the dot product of this vector and the vector v: x * v.x + - * y * v.y. If only one argument is specified, it is interpreted as the - * vector v. - * - * @param {number} x the x coordinate to dot. - * @param {number} y the y coordinate to dot. - * @returns {number} a dot product. - */ -pv.Vector.prototype.dot = function(x, y) { - return (arguments.length == 1) - ? this.x * x.x + this.y * x.y - : this.x * x + this.y * y; -}; -// TODO code-sharing between scales - -/** - * @ignore - * @class - */ -pv.Scale = function() {}; - -/** - * @private Returns a function that interpolators from the start value to the - * end value, given a parameter t in [0, 1]. - * - * @param start the start value. - * @param end the end value. - */ -pv.Scale.interpolator = function(start, end) { - if (typeof start == "number") { - return function(t) { - return t * (end - start) + start; - }; - } - - /* For now, assume color. */ - start = pv.color(start).rgb(); - end = pv.color(end).rgb(); - return function(t) { - var a = start.a * (1 - t) + end.a * t; - if (a < 1e-5) a = 0; // avoid scientific notation - return (start.a == 0) ? pv.rgb(end.r, end.g, end.b, a) - : ((end.a == 0) ? pv.rgb(start.r, start.g, start.b, a) - : pv.rgb( - Math.round(start.r * (1 - t) + end.r * t), - Math.round(start.g * (1 - t) + end.g * t), - Math.round(start.b * (1 - t) + end.b * t), a)); - }; -}; -/** - * Returns a linear scale for the specified domain. The arguments to this - * constructor are optional, and equivalent to calling {@link #domain}. - * - * @class Represents a linear scale. Most commonly, a linear scale - * represents a 1-dimensional linear transformation from a numeric domain of - * input data [d0, d1] to a numeric range of - * pixels [r0, r1]. The equation for such a - * scale is: - * - *

f(x) = (x - d0) / (d1 - d0) * - * (r1 - r0) + r0
- * - * For example, a linear scale from the domain [0, 100] to range [0, 640]: - * - *
f(x) = (x - 0) / (100 - 0) * (640 - 0) + 0
- * f(x) = x / 100 * 640
- * f(x) = x * 6.4
- *
- * - * Thus, saying - * - *
.height(function(d) d * 6.4)
- * - * is identical to - * - *
.height(pv.Scale.linear(0, 100).range(0, 640))
- * - * As you can see, scales do not always make code smaller, but they should make - * code more explicit and easier to maintain. In addition to readability, scales - * offer several useful features: - * - *

1. The range can be expressed in colors, rather than pixels. Changing the - * example above to - * - *

.fillStyle(pv.Scale.linear(0, 100).range("red", "green"))
- * - * will cause it to fill the marks "red" on an input value of 0, "green" on an - * input value of 100, and some color in-between for intermediate values. - * - *

2. The domain and range can be subdivided for a "poly-linear" - * transformation. For example, you may want a diverging color scale that is - * increasingly red for negative values, and increasingly green for positive - * values: - * - *

.fillStyle(pv.Scale.linear(-1, 0, 1).range("red", "white", "green"))
- * - * The domain can be specified as a series of n monotonically-increasing - * values; the range must also be specified as n values, resulting in - * n - 1 contiguous linear scales. - * - *

3. Linear scales can be inverted for interaction. The {@link #invert} - * method takes a value in the output range, and returns the corresponding value - * in the input domain. This is frequently used to convert the mouse location - * (see {@link pv.Mark#mouse}) to a value in the input domain. Note that - * inversion is only supported for numeric ranges, and not colors. - * - *

4. A scale can be queried for reasonable "tick" values. The {@link #ticks} - * method provides a convenient way to get a series of evenly-spaced rounded - * values in the input domain. Frequently these are used in conjunction with - * {@link pv.Rule} to display tick marks or grid lines. - * - *

5. A scale can be "niced" to extend the domain to suitable rounded - * numbers. If the minimum and maximum of the domain are messy because they are - * derived from data, you can use {@link #nice} to round these values down and - * up to even numbers. - * - * @param {number...} domain... domain values. - * @returns {pv.Scale.linear} a linear scale. - */ -pv.Scale.linear = function() { - var d = [0, 1], r = [0, 1], i = [pv.identity], precision = 0; - - /** @private */ - function scale(x) { - var j = pv.search(d, x); - if (j < 0) j = -j - 2; - j = Math.max(0, Math.min(i.length - 1, j)); - return i[j]((x - d[j]) / (d[j + 1] - d[j])); - } - - /** - * Sets or gets the input domain. This method can be invoked several ways: - * - *

1. domain(min, ..., max) - * - *

Specifying the domain as a series of numbers is the most explicit and - * recommended approach. Most commonly, two numbers are specified: the minimum - * and maximum value. However, for a diverging scale, or other subdivided - * poly-linear scales, multiple values can be specified. Values can be derived - * from data using {@link pv.min} and {@link pv.max}. For example: - * - *

.domain(0, pv.max(array))
- * - * An alternative method for deriving minimum and maximum values from data - * follows. - * - *

2. domain(array, minf, maxf) - * - *

When both the minimum and maximum value are derived from data, the - * arguments to the domain method can be specified as the array of - * data, followed by zero, one or two accessor functions. For example, if the - * array of data is just an array of numbers: - * - *

.domain(array)
- * - * On the other hand, if the array elements are objects representing stock - * values per day, and the domain should consider the stock's daily low and - * daily high: - * - *
.domain(array, function(d) d.low, function(d) d.high)
- * - * The first method of setting the domain is preferred because it is more - * explicit; setting the domain using this second method should be used only - * if brevity is required. - * - *

3. domain() - * - *

Invoking the domain method with no arguments returns the - * current domain as an array of numbers. - * - * @function - * @name pv.Scale.linear.prototype.domain - * @param {number...} domain... domain values. - * @returns {pv.Scale.linear} this, or the current domain. - */ - scale.domain = function(array, min, max) { - if (arguments.length) { - if (array instanceof Array) { - if (arguments.length < 2) min = pv.identity; - if (arguments.length < 3) max = min; - d = [pv.min(array, min), pv.max(array, max)]; - } else { - d = Array.prototype.slice.call(arguments); - } - return this; - } - return d; - }; - - /** - * Sets or gets the output range. This method can be invoked several ways: - * - *

1. range(min, ..., max) - * - *

The range may be specified as a series of numbers or colors. Most - * commonly, two numbers are specified: the minimum and maximum pixel values. - * For a color scale, values may be specified as {@link pv.Color}s or - * equivalent strings. For a diverging scale, or other subdivided poly-linear - * scales, multiple values can be specified. For example: - * - *

.range("red", "white", "green")
- * - *

Currently, only numbers and colors are supported as range values. The - * number of range values must exactly match the number of domain values, or - * the behavior of the scale is undefined. - * - *

2. range() - * - *

Invoking the range method with no arguments returns the current - * range as an array of numbers or colors. - * - * @function - * @name pv.Scale.linear.prototype.range - * @param {...} range... range values. - * @returns {pv.Scale.linear} this, or the current range. - */ - scale.range = function() { - if (arguments.length) { - r = Array.prototype.slice.call(arguments); - i = []; - for (var j = 0; j < r.length - 1; j++) { - i.push(pv.Scale.interpolator(r[j], r[j + 1])); - } - return this; - } - return r; - }; - - /** - * Inverts the specified value in the output range, returning the - * corresponding value in the input domain. This is frequently used to convert - * the mouse location (see {@link pv.Mark#mouse}) to a value in the input - * domain. Inversion is only supported for numeric ranges, and not colors. - * - *

Note that this method does not do any rounding or bounds checking. If - * the input domain is discrete (e.g., an array index), the returned value - * should be rounded. If the specified y value is outside the range, - * the returned value may be equivalently outside the input domain. - * - * @function - * @name pv.Scale.linear.prototype.invert - * @param {number} y a value in the output range (a pixel location). - * @returns {number} a value in the input domain. - */ - scale.invert = function(y) { - var j = pv.search(r, y); - if (j < 0) j = -j - 2; - j = Math.max(0, Math.min(i.length - 1, j)); - return (y - r[j]) / (r[j + 1] - r[j]) * (d[j + 1] - d[j]) + d[j]; - }; - - /** - * Returns an array of evenly-spaced, suitably-rounded values in the input - * domain. This method attempts to return between 5 and 10 tick values. These - * values are frequently used in conjunction with {@link pv.Rule} to display - * tick marks or grid lines. - * - * @function - * @name pv.Scale.linear.prototype.ticks - * @returns {number[]} an array input domain values to use as ticks. - */ - scale.ticks = function() { - var min = d[0], - max = d[d.length - 1], - span = max - min, - step = pv.logCeil(span / 10, 10); - if (span / step < 2) step /= 5; - else if (span / step < 5) step /= 2; - var start = Math.ceil(min / step) * step, - end = Math.floor(max / step) * step; - precision = Math.max(0, -Math.floor(pv.log(step, 10) + .01)); - return pv.range(start, end + step, step); - }; - - /** - * Formats the specified tick value using the appropriate precision, based on - * the step interval between tick marks. - * - * @function - * @name pv.Scale.linear.prototype.tickFormat - * @param {number} t a tick value. - * @return {string} a formatted tick value. - */ - scale.tickFormat = function(t) { - return t.toFixed(precision); - }; - - /** - * "Nices" this scale, extending the bounds of the input domain to - * evenly-rounded values. Nicing is useful if the domain is computed - * dynamically from data, and may be irregular. For example, given a domain of - * [0.20147987687960267, 0.996679553296417], a call to nice() might - * extend the domain to [0.2, 1]. - * - *

This method must be invoked each time after setting the domain. - * - * @function - * @name pv.Scale.linear.prototype.nice - * @returns {pv.Scale.linear} this. - */ - scale.nice = function() { - var min = d[0], - max = d[d.length - 1], - step = Math.pow(10, Math.round(Math.log(max - min) / Math.log(10)) - 1); - d = [Math.floor(min / step) * step, Math.ceil(max / step) * step]; - return this; - }; - - /** - * Returns a view of this scale by the specified accessor function f. - * Given a scale y, y.by(function(d) d.foo) is equivalent to - * function(d) y(d.foo). - * - *

This method is provided for convenience, such that scales can be - * succinctly defined inline. For example, given an array of data elements - * that have a score attribute with the domain [0, 1], the height - * property could be specified as: - * - *

.height(pv.Scale.linear().range(0, 480).by(function(d) d.score))
- * - * This is equivalent to: - * - *
.height(function(d) d.score * 480)
- * - * This method should be used judiciously; it is typically more clear to - * invoke the scale directly, passing in the value to be scaled. - * - * @function - * @name pv.Scale.linear.prototype.by - * @param {function} f an accessor function. - * @returns {pv.Scale.linear} a view of this scale by the specified accessor - * function. - */ - scale.by = function(f) { - function by() { return scale(f.apply(this, arguments)); } - for (var method in scale) by[method] = scale[method]; - return by; - }; - - scale.domain.apply(scale, arguments); - return scale; -}; -/** - * Returns a log scale for the specified domain. The arguments to this - * constructor are optional, and equivalent to calling {@link #domain}. - * - * @class Represents a log scale. Most commonly, a log scale represents - * a 1-dimensional log transformation from a numeric domain of input data - * [d0, d1] to a numeric range of pixels - * [r0, r1]. The equation for such a scale - * is: - * - *
f(x) = (log(x) - log(d0)) / (log(d1) - - * log(d0)) * (r1 - r0) + - * r0
- * - * where log(x) represents the zero-symmetric logarthim of x using - * the scale's associated base (default: 10, see {@link pv.logSymmetric}). For - * example, a log scale from the domain [1, 100] to range [0, 640]: - * - *
f(x) = (log(x) - log(1)) / (log(100) - log(1)) * (640 - 0) + 0
- * f(x) = log(x) / 2 * 640
- * f(x) = log(x) * 320
- *
- * - * Thus, saying - * - *
.height(function(d) Math.log(d) * 138.974)
- * - * is equivalent to - * - *
.height(pv.Scale.log(1, 100).range(0, 640))
- * - * As you can see, scales do not always make code smaller, but they should make - * code more explicit and easier to maintain. In addition to readability, scales - * offer several useful features: - * - *

1. The range can be expressed in colors, rather than pixels. Changing the - * example above to - * - *

.fillStyle(pv.Scale.log(1, 100).range("red", "green"))
- * - * will cause it to fill the marks "red" on an input value of 1, "green" on an - * input value of 100, and some color in-between for intermediate values. - * - *

2. The domain and range can be subdivided for a "poly-log" - * transformation. For example, you may want a diverging color scale that is - * increasingly red for small values, and increasingly green for large values: - * - *

.fillStyle(pv.Scale.log(1, 10, 100).range("red", "white", "green"))
- * - * The domain can be specified as a series of n monotonically-increasing - * values; the range must also be specified as n values, resulting in - * n - 1 contiguous log scales. - * - *

3. Log scales can be inverted for interaction. The {@link #invert} method - * takes a value in the output range, and returns the corresponding value in the - * input domain. This is frequently used to convert the mouse location (see - * {@link pv.Mark#mouse}) to a value in the input domain. Note that inversion is - * only supported for numeric ranges, and not colors. - * - *

4. A scale can be queried for reasonable "tick" values. The {@link #ticks} - * method provides a convenient way to get a series of evenly-spaced rounded - * values in the input domain. Frequently these are used in conjunction with - * {@link pv.Rule} to display tick marks or grid lines. - * - *

5. A scale can be "niced" to extend the domain to suitable rounded - * numbers. If the minimum and maximum of the domain are messy because they are - * derived from data, you can use {@link #nice} to round these values down and - * up to even numbers. - * - * @param {number...} domain... domain values. - * @returns {pv.Scale.log} a log scale. - */ -pv.Scale.log = function() { - var d = [1, 10], l = [0, 1], b = 10, r = [0, 1], i = [pv.identity]; - - /** @private */ - function scale(x) { - var j = pv.search(d, x); - if (j < 0) j = -j - 2; - j = Math.max(0, Math.min(i.length - 1, j)); - return i[j]((log(x) - l[j]) / (l[j + 1] - l[j])); - } - - /** @private */ - function log(x) { - return pv.logSymmetric(x, b); - } - - /** - * Sets or gets the input domain. This method can be invoked several ways: - * - *

1. domain(min, ..., max) - * - *

Specifying the domain as a series of numbers is the most explicit and - * recommended approach. Most commonly, two numbers are specified: the minimum - * and maximum value. However, for a diverging scale, or other subdivided - * poly-log scales, multiple values can be specified. Values can be derived - * from data using {@link pv.min} and {@link pv.max}. For example: - * - *

.domain(1, pv.max(array))
- * - * An alternative method for deriving minimum and maximum values from data - * follows. - * - *

2. domain(array, minf, maxf) - * - *

When both the minimum and maximum value are derived from data, the - * arguments to the domain method can be specified as the array of - * data, followed by zero, one or two accessor functions. For example, if the - * array of data is just an array of numbers: - * - *

.domain(array)
- * - * On the other hand, if the array elements are objects representing stock - * values per day, and the domain should consider the stock's daily low and - * daily high: - * - *
.domain(array, function(d) d.low, function(d) d.high)
- * - * The first method of setting the domain is preferred because it is more - * explicit; setting the domain using this second method should be used only - * if brevity is required. - * - *

3. domain() - * - *

Invoking the domain method with no arguments returns the - * current domain as an array of numbers. - * - * @function - * @name pv.Scale.log.prototype.domain - * @param {number...} domain... domain values. - * @returns {pv.Scale.log} this, or the current domain. - */ - scale.domain = function(array, min, max) { - if (arguments.length) { - if (array instanceof Array) { - if (arguments.length < 2) min = pv.identity; - if (arguments.length < 3) max = min; - d = [pv.min(array, min), pv.max(array, max)]; - } else { - d = Array.prototype.slice.call(arguments); - } - l = d.map(log); - return this; - } - return d; - }; - - /** - * @function - * @name pv.Scale.log.prototype.range - * @param {...} range... range values. - * @returns {pv.Scale.log} this. - */ - scale.range = function() { - if (arguments.length) { - r = Array.prototype.slice.call(arguments); - i = []; - for (var j = 0; j < r.length - 1; j++) { - i.push(pv.Scale.interpolator(r[j], r[j + 1])); - } - return this; - } - return r; - }; - - /** - * Sets or gets the output range. This method can be invoked several ways: - * - *

1. range(min, ..., max) - * - *

The range may be specified as a series of numbers or colors. Most - * commonly, two numbers are specified: the minimum and maximum pixel values. - * For a color scale, values may be specified as {@link pv.Color}s or - * equivalent strings. For a diverging scale, or other subdivided poly-log - * scales, multiple values can be specified. For example: - * - *

.range("red", "white", "green")
- * - *

Currently, only numbers and colors are supported as range values. The - * number of range values must exactly match the number of domain values, or - * the behavior of the scale is undefined. - * - *

2. range() - * - *

Invoking the range method with no arguments returns the current - * range as an array of numbers or colors. - * - * @function - * @name pv.Scale.log.prototype.invert - * @param {...} range... range values. - * @returns {pv.Scale.log} this, or the current range. - */ - scale.invert = function(y) { - var j = pv.search(r, y); - if (j < 0) j = -j - 2; - j = Math.max(0, Math.min(i.length - 1, j)); - var t = l[j] + (y - r[j]) / (r[j + 1] - r[j]) * (l[j + 1] - l[j]); - return (d[j] < 0) ? -Math.pow(b, -t) : Math.pow(b, t); - }; - - /** - * Returns an array of evenly-spaced, suitably-rounded values in the input - * domain. These values are frequently used in conjunction with {@link - * pv.Rule} to display tick marks or grid lines. - * - * @function - * @name pv.Scale.log.prototype.ticks - * @returns {number[]} an array input domain values to use as ticks. - */ - scale.ticks = function() { - // TODO: support multiple domains - var start = Math.floor(l[0]), - end = Math.ceil(l[1]), - ticks = []; - for (var i = start; i < end; i++) { - var x = Math.pow(b, i); - if (d[0] < 0) x = -x; - for (var j = 1; j < b; j++) { - ticks.push(x * j); - } - } - ticks.push(Math.pow(b, end)); - if (ticks[0] < d[0]) ticks.shift(); - if (ticks[ticks.length - 1] > d[1]) ticks.pop(); - return ticks; - }; - - /** - * Formats the specified tick value using the appropriate precision, assuming - * base 10. - * - * @function - * @name pv.Scale.log.prototype.tickFormat - * @param {number} t a tick value. - * @return {string} a formatted tick value. - */ - scale.tickFormat = function(t) { - return t.toPrecision(1); - }; - - /** - * "Nices" this scale, extending the bounds of the input domain to - * evenly-rounded values. This method uses {@link pv.logFloor} and {@link - * pv.logCeil}. Nicing is useful if the domain is computed dynamically from - * data, and may be irregular. For example, given a domain of - * [0.20147987687960267, 0.996679553296417], a call to nice() might - * extend the domain to [0.1, 1]. - * - *

This method must be invoked each time after setting the domain (and - * base). - * - * @function - * @name pv.Scale.log.prototype.nice - * @returns {pv.Scale.log} this. - */ - scale.nice = function() { - // TODO: support multiple domains - d = [pv.logFloor(d[0], b), pv.logCeil(d[1], b)]; - l = d.map(log); - return this; - }; - - /** - * Sets or gets the logarithm base. Defaults to 10. - * - * @function - * @name pv.Scale.log.prototype.base - * @param {number} [v] the new base. - * @returns {pv.Scale.log} this, or the current base. - */ - scale.base = function(v) { - if (arguments.length) { - b = v; - l = d.map(log); - return this; - } - return b; - }; - - /** - * Returns a view of this scale by the specified accessor function f. - * Given a scale y, y.by(function(d) d.foo) is equivalent to - * function(d) y(d.foo). - * - *

This method is provided for convenience, such that scales can be - * succinctly defined inline. For example, given an array of data elements - * that have a score attribute with the domain [0, 1], the height - * property could be specified as: - * - *

.height(pv.Scale.log().range(0, 480).by(function(d) d.score))
- * - * This is equivalent to: - * - *
.height(function(d) d.score * 480)
- * - * This method should be used judiciously; it is typically more clear to - * invoke the scale directly, passing in the value to be scaled. - * - * @function - * @name pv.Scale.log.prototype.by - * @param {function} f an accessor function. - * @returns {pv.Scale.log} a view of this scale by the specified accessor - * function. - */ - scale.by = function(f) { - function by() { return scale(f.apply(this, arguments)); } - for (var method in scale) by[method] = scale[method]; - return by; - }; - - scale.domain.apply(scale, arguments); - return scale; -}; -/** - * Returns an ordinal scale for the specified domain. The arguments to this - * constructor are optional, and equivalent to calling {@link #domain}. - * - * @class Represents an ordinal scale. An ordinal scale represents a - * pairwise mapping from n discrete values in the input domain to - * n discrete values in the output range. For example, an ordinal scale - * might map a domain of species ["setosa", "versicolor", "virginica"] to colors - * ["red", "green", "blue"]. Thus, saying - * - *
.fillStyle(function(d) {
- *     switch (d.species) {
- *       case "setosa": return "red";
- *       case "versicolor": return "green";
- *       case "virginica": return "blue";
- *     }
- *   })
- * - * is equivalent to - * - *
.fillStyle(pv.Scale.ordinal("setosa", "versicolor", "virginica")
- *     .range("red", "green", "blue")
- *     .by(function(d) d.species))
- * - * If the mapping from species to color does not need to be specified - * explicitly, the domain can be omitted. In this case it will be inferred - * lazily from the data: - * - *
.fillStyle(pv.colors("red", "green", "blue")
- *     .by(function(d) d.species))
- * - * When the domain is inferred, the first time the scale is invoked, the first - * element from the range will be returned. Subsequent calls with unique values - * will return subsequent elements from the range. If the inferred domain grows - * larger than the range, range values will be reused. However, it is strongly - * recommended that the domain and the range contain the same number of - * elements. - * - *

A range can be discretized from a continuous interval (e.g., for pixel - * positioning) by using {@link #split}, {@link #splitFlush} or - * {@link #splitBanded} after the domain has been set. For example, if - * states is an array of the fifty U.S. state names, the state name can - * be encoded in the left position: - * - *

.left(pv.Scale.ordinal(states)
- *     .split(0, 640)
- *     .by(function(d) d.state))
- * - *

N.B.: ordinal scales are not invertible (at least not yet), since the - * domain and range and discontinuous. A workaround is to use a linear scale. - * - * @param {...} domain... domain values. - * @returns {pv.Scale.ordinal} an ordinal scale. - * @see pv.colors - */ -pv.Scale.ordinal = function() { - var d = [], i = {}, r = [], band = 0; - - /** @private */ - function scale(x) { - if (!(x in i)) i[x] = d.push(x) - 1; - return r[i[x] % r.length]; - } - - /** - * Sets or gets the input domain. This method can be invoked several ways: - * - *

1. domain(values...) - * - *

Specifying the domain as a series of values is the most explicit and - * recommended approach. However, if the domain values are derived from data, - * you may find the second method more appropriate. - * - *

2. domain(array, f) - * - *

Rather than enumerating the domain values as explicit arguments to this - * method, you can specify a single argument of an array. In addition, you can - * specify an optional accessor function to extract the domain values from the - * array. - * - *

3. domain() - * - *

Invoking the domain method with no arguments returns the - * current domain as an array. - * - * @function - * @name pv.Scale.ordinal.prototype.domain - * @param {...} domain... domain values. - * @returns {pv.Scale.ordinal} this, or the current domain. - */ - scale.domain = function(array, f) { - if (arguments.length) { - array = (array instanceof Array) - ? ((arguments.length > 1) ? map(array, f) : array) - : Array.prototype.slice.call(arguments); - - /* Filter the specified ordinals to their unique values. */ - d = []; - var seen = {}; - for (var j = 0; j < array.length; j++) { - var o = array[j]; - if (!(o in seen)) { - seen[o] = true; - d.push(o); - } - } - - i = pv.numerate(d); - return this; - } - return d; - }; - - /** - * Sets or gets the output range. This method can be invoked several ways: - * - *

1. range(values...) - * - *

Specifying the range as a series of values is the most explicit and - * recommended approach. However, if the range values are derived from data, - * you may find the second method more appropriate. - * - *

2. range(array, f) - * - *

Rather than enumerating the range values as explicit arguments to this - * method, you can specify a single argument of an array. In addition, you can - * specify an optional accessor function to extract the range values from the - * array. - * - *

3. range() - * - *

Invoking the range method with no arguments returns the - * current range as an array. - * - * @function - * @name pv.Scale.ordinal.prototype.range - * @param {...} range... range values. - * @returns {pv.Scale.ordinal} this, or the current range. - */ - scale.range = function(array, f) { - if (arguments.length) { - r = (array instanceof Array) - ? ((arguments.length > 1) ? map(array, f) : array) - : Array.prototype.slice.call(arguments); - if (typeof r[0] == "string") r = r.map(pv.color); - return this; - } - return r; - }; - - /** - * Sets the range from the given continuous interval. The interval - * [min, max] is subdivided into n equispaced points, - * where n is the number of (unique) values in the domain. The first - * and last point are offset from the edge of the range by half the distance - * between points. - * - *

This method must be called after the domain is set. - * - * @function - * @name pv.Scale.ordinal.prototype.split - * @param {number} min minimum value of the output range. - * @param {number} max maximum value of the output range. - * @returns {pv.Scale.ordinal} this. - * @see #splitFlush - * @see #splitBanded - */ - scale.split = function(min, max) { - var step = (max - min) / this.domain().length; - r = pv.range(min + step / 2, max, step); - return this; - }; - - /** - * Sets the range from the given continuous interval. The interval - * [min, max] is subdivided into n equispaced points, - * where n is the number of (unique) values in the domain. The first - * and last point are exactly on the edge of the range. - * - *

This method must be called after the domain is set. - * - * @function - * @name pv.Scale.ordinal.prototype.splitFlush - * @param {number} min minimum value of the output range. - * @param {number} max maximum value of the output range. - * @returns {pv.Scale.ordinal} this. - * @see #split - */ - scale.splitFlush = function(min, max) { - var n = this.domain().length, step = (max - min) / (n - 1); - r = (n == 1) ? [(min + max) / 2] - : pv.range(min, max + step / 2, step); - return this; - }; - - /** - * Sets the range from the given continuous interval. The interval - * [min, max] is subdivided into n equispaced bands, - * where n is the number of (unique) values in the domain. The first - * and last band are offset from the edge of the range by the distance between - * bands. - * - *

The band width argument, band, is typically in the range [0, 1] - * and defaults to 1. This fraction corresponds to the amount of space in the - * range to allocate to the bands, as opposed to padding. A value of 0.5 means - * that the band width will be equal to the padding width. The computed - * absolute band width can be retrieved from the range as - * scale.range().band. - * - *

If the band width argument is negative, this method will allocate bands - * of a fixed width -band, rather than a relative fraction of - * the available space. - * - *

Tip: to inset the bands by a fixed amount p, specify a minimum - * value of min + p (or simply p, if min is - * 0). Then set the mark width to scale.range().band - p. - * - *

This method must be called after the domain is set. - * - * @function - * @name pv.Scale.ordinal.prototype.splitBanded - * @param {number} min minimum value of the output range. - * @param {number} max maximum value of the output range. - * @param {number} [band] the fractional band width in [0, 1]; defaults to 1. - * @returns {pv.Scale.ordinal} this. - * @see #split - */ - scale.splitBanded = function(min, max, band) { - if (arguments.length < 3) band = 1; - if (band < 0) { - var n = this.domain().length, - total = -band * n, - remaining = max - min - total, - padding = remaining / (n + 1); - r = pv.range(min + padding, max, padding - band); - r.band = -band; - } else { - var step = (max - min) / (this.domain().length + (1 - band)); - r = pv.range(min + step * (1 - band), max, step); - r.band = step * band; - } - return this; - }; - - /** - * Returns a view of this scale by the specified accessor function f. - * Given a scale y, y.by(function(d) d.foo) is equivalent to - * function(d) y(d.foo). This method should be used judiciously; it - * is typically more clear to invoke the scale directly, passing in the value - * to be scaled. - * - * @function - * @name pv.Scale.ordinal.prototype.by - * @param {function} f an accessor function. - * @returns {pv.Scale.ordinal} a view of this scale by the specified accessor - * function. - */ - scale.by = function(f) { - function by() { return scale(f.apply(this, arguments)); } - for (var method in scale) by[method] = scale[method]; - return by; - }; - - scale.domain.apply(scale, arguments); - return scale; -}; -/** - * Returns the {@link pv.Color} for the specified color format string. Colors - * may have an associated opacity, or alpha channel. Color formats are specified - * by CSS Color Modular Level 3, using either in RGB or HSL color space. For - * example:

    - * - *
  • #f00 // #rgb - *
  • #ff0000 // #rrggbb - *
  • rgb(255, 0, 0) - *
  • rgb(100%, 0%, 0%) - *
  • hsl(0, 100%, 50%) - *
  • rgba(0, 0, 255, 0.5) - *
  • hsla(120, 100%, 50%, 1) - * - *
The SVG 1.0 color keywords names are also supported, such as "aliceblue" - * and "yellowgreen". The "transparent" keyword is supported for a - * fully-transparent color. - * - *

If the format argument is already an instance of Color, - * the argument is returned with no further processing. - * - * @param {string} format the color specification string, such as "#f00". - * @returns {pv.Color} the corresponding Color. - * @see SVG color - * keywords - * @see CSS3 color module - */ -pv.color = function(format) { - if (!format || (format == "transparent")) { - return pv.rgb(0, 0, 0, 0); - } - if (format instanceof pv.Color) { - return format; - } - - /* Handle hsl, rgb. */ - var m1 = /([a-z]+)\((.*)\)/i.exec(format); - if (m1) { - var m2 = m1[2].split(","), a = 1; - switch (m1[1]) { - case "hsla": - case "rgba": { - a = parseFloat(m2[3]); - break; - } - } - switch (m1[1]) { - case "hsla": - case "hsl": { - var h = parseFloat(m2[0]), // degrees - s = parseFloat(m2[1]) / 100, // percentage - l = parseFloat(m2[2]) / 100; // percentage - return (new pv.Color.Hsl(h, s, l, a)).rgb(); - } - case "rgba": - case "rgb": { - function parse(c) { // either integer or percentage - var f = parseFloat(c); - return (c[c.length - 1] == '%') ? Math.round(f * 2.55) : f; - } - var r = parse(m2[0]), g = parse(m2[1]), b = parse(m2[2]); - return pv.rgb(r, g, b, a); - } - } - } - - /* Named colors. */ - format = pv.Color.names[format] || format; - - /* Hexadecimal colors: #rgb and #rrggbb. */ - if (format.charAt(0) == "#") { - var r, g, b; - if (format.length == 4) { - r = format.charAt(1); r += r; - g = format.charAt(2); g += g; - b = format.charAt(3); b += b; - } else if (format.length == 7) { - r = format.substring(1, 3); - g = format.substring(3, 5); - b = format.substring(5, 7); - } - return pv.rgb(parseInt(r, 16), parseInt(g, 16), parseInt(b, 16), 1); - } - - /* Otherwise, assume named colors. TODO allow lazy conversion to RGB. */ - return new pv.Color(format, 1); -}; - -/** - * Constructs a color with the specified color format string and opacity. This - * constructor should not be invoked directly; use {@link pv.color} instead. - * - * @class Represents an abstract (possibly translucent) color. The color is - * divided into two parts: the color attribute, an opaque color format - * string, and the opacity attribute, a float in [0, 1]. The color - * space is dependent on the implementing class; all colors support the - * {@link #rgb} method to convert to RGB color space for interpolation. - * - *

See also the Color guide. - * - * @param {string} color an opaque color format string, such as "#f00". - * @param {number} opacity the opacity, in [0,1]. - * @see pv.color - */ -pv.Color = function(color, opacity) { - /** - * An opaque color format string, such as "#f00". - * - * @type string - * @see SVG color - * keywords - * @see CSS3 color module - */ - this.color = color; - - /** - * The opacity, a float in [0, 1]. - * - * @type number - */ - this.opacity = opacity; -}; - -/** - * Returns a new color that is a brighter version of this color. The behavior of - * this method may vary slightly depending on the underlying color space. - * Although brighter and darker are inverse operations, the results of a series - * of invocations of these two methods might be inconsistent because of rounding - * errors. - * - * @param [k] {number} an optional scale factor; defaults to 1. - * @see #darker - * @returns {pv.Color} a brighter color. - */ -pv.Color.prototype.brighter = function(k) { - return this.rgb().brighter(k); -}; - -/** - * Returns a new color that is a brighter version of this color. The behavior of - * this method may vary slightly depending on the underlying color space. - * Although brighter and darker are inverse operations, the results of a series - * of invocations of these two methods might be inconsistent because of rounding - * errors. - * - * @param [k] {number} an optional scale factor; defaults to 1. - * @see #brighter - * @returns {pv.Color} a darker color. - */ -pv.Color.prototype.darker = function(k) { - return this.rgb().darker(k); -}; - -/** - * Constructs a new RGB color with the specified channel values. - * - * @param {number} r the red channel, an integer in [0,255]. - * @param {number} g the green channel, an integer in [0,255]. - * @param {number} b the blue channel, an integer in [0,255]. - * @param {number} [a] the alpha channel, a float in [0,1]. - * @returns pv.Color.Rgb - */ -pv.rgb = function(r, g, b, a) { - return new pv.Color.Rgb(r, g, b, (arguments.length == 4) ? a : 1); -}; - -/** - * Constructs a new RGB color with the specified channel values. - * - * @class Represents a color in RGB space. - * - * @param {number} r the red channel, an integer in [0,255]. - * @param {number} g the green channel, an integer in [0,255]. - * @param {number} b the blue channel, an integer in [0,255]. - * @param {number} a the alpha channel, a float in [0,1]. - * @extends pv.Color - */ -pv.Color.Rgb = function(r, g, b, a) { - pv.Color.call(this, a ? ("rgb(" + r + "," + g + "," + b + ")") : "none", a); - - /** - * The red channel, an integer in [0, 255]. - * - * @type number - */ - this.r = r; - - /** - * The green channel, an integer in [0, 255]. - * - * @type number - */ - this.g = g; - - /** - * The blue channel, an integer in [0, 255]. - * - * @type number - */ - this.b = b; - - /** - * The alpha channel, a float in [0, 1]. - * - * @type number - */ - this.a = a; -}; -pv.Color.Rgb.prototype = pv.extend(pv.Color); - -/** - * Constructs a new RGB color with the same green, blue and alpha channels as - * this color, with the specified red channel. - * - * @param {number} r the red channel, an integer in [0,255]. - */ -pv.Color.Rgb.prototype.red = function(r) { - return pv.rgb(r, this.g, this.b, this.a); -}; - -/** - * Constructs a new RGB color with the same red, blue and alpha channels as this - * color, with the specified green channel. - * - * @param {number} g the green channel, an integer in [0,255]. - */ -pv.Color.Rgb.prototype.green = function(g) { - return pv.rgb(this.r, g, this.b, this.a); -}; - -/** - * Constructs a new RGB color with the same red, green and alpha channels as - * this color, with the specified blue channel. - * - * @param {number} b the blue channel, an integer in [0,255]. - */ -pv.Color.Rgb.prototype.blue = function(b) { - return pv.rgb(this.r, this.g, b, this.a); -}; - -/** - * Constructs a new RGB color with the same red, green and blue channels as this - * color, with the specified alpha channel. - * - * @param {number} a the alpha channel, a float in [0,1]. - */ -pv.Color.Rgb.prototype.alpha = function(a) { - return pv.rgb(this.r, this.g, this.b, a); -}; - -/** - * Returns the RGB color equivalent to this color. This method is abstract and - * must be implemented by subclasses. - * - * @returns {pv.Color.Rgb} an RGB color. - * @function - * @name pv.Color.prototype.rgb - */ - -/** - * Returns this. - * - * @returns {pv.Color.Rgb} this. - */ -pv.Color.Rgb.prototype.rgb = function() { return this; }; - -/** - * Returns a new color that is a brighter version of this color. This method - * applies an arbitrary scale factor to each of the three RGB components of this - * color to create a brighter version of this color. Although brighter and - * darker are inverse operations, the results of a series of invocations of - * these two methods might be inconsistent because of rounding errors. - * - * @param [k] {number} an optional scale factor; defaults to 1. - * @see #darker - * @returns {pv.Color.Rgb} a brighter color. - */ -pv.Color.Rgb.prototype.brighter = function(k) { - k = Math.pow(0.7, arguments.length ? k : 1); - var r = this.r, g = this.g, b = this.b, i = 30; - if (!r && !g && !b) return pv.rgb(i, i, i, this.a); - if (r && (r < i)) r = i; - if (g && (g < i)) g = i; - if (b && (b < i)) b = i; - return pv.rgb( - Math.min(255, Math.floor(r / k)), - Math.min(255, Math.floor(g / k)), - Math.min(255, Math.floor(b / k)), - this.a); -}; - -/** - * Returns a new color that is a darker version of this color. This method - * applies an arbitrary scale factor to each of the three RGB components of this - * color to create a darker version of this color. Although brighter and darker - * are inverse operations, the results of a series of invocations of these two - * methods might be inconsistent because of rounding errors. - * - * @param [k] {number} an optional scale factor; defaults to 1. - * @see #brighter - * @returns {pv.Color.Rgb} a darker color. - */ -pv.Color.Rgb.prototype.darker = function(k) { - k = Math.pow(0.7, arguments.length ? k : 1); - return pv.rgb( - Math.max(0, Math.floor(k * this.r)), - Math.max(0, Math.floor(k * this.g)), - Math.max(0, Math.floor(k * this.b)), - this.a); -}; - -/** - * Constructs a new HSL color with the specified values. - * - * @param {number} h the hue, an integer in [0, 360]. - * @param {number} s the saturation, a float in [0, 1]. - * @param {number} l the lightness, a float in [0, 1]. - * @param {number} [a] the opacity, a float in [0, 1]. - * @returns pv.Color.Hsl - */ -pv.hsl = function(h, s, l, a) { - return new pv.Color.Hsl(h, s, l, (arguments.length == 4) ? a : 1); -}; - -/** - * Constructs a new HSL color with the specified values. - * - * @class Represents a color in HSL space. - * - * @param {number} h the hue, an integer in [0, 360]. - * @param {number} s the saturation, a float in [0, 1]. - * @param {number} l the lightness, a float in [0, 1]. - * @param {number} a the opacity, a float in [0, 1]. - * @extends pv.Color - */ -pv.Color.Hsl = function(h, s, l, a) { - pv.Color.call(this, "hsl(" + h + "," + (s * 100) + "%," + (l * 100) + "%)", a); - - /** - * The hue, an integer in [0, 360]. - * - * @type number - */ - this.h = h; - - /** - * The saturation, a float in [0, 1]. - * - * @type number - */ - this.s = s; - - /** - * The lightness, a float in [0, 1]. - * - * @type number - */ - this.l = l; - - /** - * The opacity, a float in [0, 1]. - * - * @type number - */ - this.a = a; -}; -pv.Color.Hsl.prototype = pv.extend(pv.Color); - -/** - * Constructs a new HSL color with the same saturation, lightness and alpha as - * this color, and the specified hue. - * - * @param {number} h the hue, an integer in [0, 360]. - */ -pv.Color.Hsl.prototype.hue = function(h) { - return pv.hsl(h, this.s, this.l, this.a); -}; - -/** - * Constructs a new HSL color with the same hue, lightness and alpha as this - * color, and the specified saturation. - * - * @param {number} s the saturation, a float in [0, 1]. - */ -pv.Color.Hsl.prototype.saturation = function(s) { - return pv.hsl(this.h, s, this.l, this.a); -}; - -/** - * Constructs a new HSL color with the same hue, saturation and alpha as this - * color, and the specified lightness. - * - * @param {number} l the lightness, a float in [0, 1]. - */ -pv.Color.Hsl.prototype.lightness = function(l) { - return pv.hsl(this.h, this.s, l, this.a); -}; - -/** - * Constructs a new HSL color with the same hue, saturation and lightness as - * this color, and the specified alpha. - * - * @param {number} a the opacity, a float in [0, 1]. - */ -pv.Color.Hsl.prototype.alpha = function(a) { - return pv.hsl(this.h, this.s, this.l, a); -}; - -/** - * Returns the RGB color equivalent to this HSL color. - * - * @returns {pv.Color.Rgb} an RGB color. - */ -pv.Color.Hsl.prototype.rgb = function() { - var h = this.h, s = this.s, l = this.l; - - /* Some simple corrections for h, s and l. */ - h = h % 360; if (h < 0) h += 360; - s = Math.max(0, Math.min(s, 1)); - l = Math.max(0, Math.min(l, 1)); - - /* From FvD 13.37, CSS Color Module Level 3 */ - var m2 = (l <= .5) ? (l * (1 + s)) : (l + s - l * s); - var m1 = 2 * l - m2; - function v(h) { - if (h > 360) h -= 360; - else if (h < 0) h += 360; - if (h < 60) return m1 + (m2 - m1) * h / 60; - if (h < 180) return m2; - if (h < 240) return m1 + (m2 - m1) * (240 - h) / 60; - return m1; - } - function vv(h) { - return Math.round(v(h) * 255); - } - - return pv.rgb(vv(h + 120), vv(h), vv(h - 120), this.a); -}; - -/** - * @private SVG color keywords, per CSS Color Module Level 3. - * - * @see SVG color - * keywords - */ -pv.Color.names = { - aliceblue: "#f0f8ff", - antiquewhite: "#faebd7", - aqua: "#00ffff", - aquamarine: "#7fffd4", - azure: "#f0ffff", - beige: "#f5f5dc", - bisque: "#ffe4c4", - black: "#000000", - blanchedalmond: "#ffebcd", - blue: "#0000ff", - blueviolet: "#8a2be2", - brown: "#a52a2a", - burlywood: "#deb887", - cadetblue: "#5f9ea0", - chartreuse: "#7fff00", - chocolate: "#d2691e", - coral: "#ff7f50", - cornflowerblue: "#6495ed", - cornsilk: "#fff8dc", - crimson: "#dc143c", - cyan: "#00ffff", - darkblue: "#00008b", - darkcyan: "#008b8b", - darkgoldenrod: "#b8860b", - darkgray: "#a9a9a9", - darkgreen: "#006400", - darkgrey: "#a9a9a9", - darkkhaki: "#bdb76b", - darkmagenta: "#8b008b", - darkolivegreen: "#556b2f", - darkorange: "#ff8c00", - darkorchid: "#9932cc", - darkred: "#8b0000", - darksalmon: "#e9967a", - darkseagreen: "#8fbc8f", - darkslateblue: "#483d8b", - darkslategray: "#2f4f4f", - darkslategrey: "#2f4f4f", - darkturquoise: "#00ced1", - darkviolet: "#9400d3", - deeppink: "#ff1493", - deepskyblue: "#00bfff", - dimgray: "#696969", - dimgrey: "#696969", - dodgerblue: "#1e90ff", - firebrick: "#b22222", - floralwhite: "#fffaf0", - forestgreen: "#228b22", - fuchsia: "#ff00ff", - gainsboro: "#dcdcdc", - ghostwhite: "#f8f8ff", - gold: "#ffd700", - goldenrod: "#daa520", - gray: "#808080", - green: "#008000", - greenyellow: "#adff2f", - grey: "#808080", - honeydew: "#f0fff0", - hotpink: "#ff69b4", - indianred: "#cd5c5c", - indigo: "#4b0082", - ivory: "#fffff0", - khaki: "#f0e68c", - lavender: "#e6e6fa", - lavenderblush: "#fff0f5", - lawngreen: "#7cfc00", - lemonchiffon: "#fffacd", - lightblue: "#add8e6", - lightcoral: "#f08080", - lightcyan: "#e0ffff", - lightgoldenrodyellow: "#fafad2", - lightgray: "#d3d3d3", - lightgreen: "#90ee90", - lightgrey: "#d3d3d3", - lightpink: "#ffb6c1", - lightsalmon: "#ffa07a", - lightseagreen: "#20b2aa", - lightskyblue: "#87cefa", - lightslategray: "#778899", - lightslategrey: "#778899", - lightsteelblue: "#b0c4de", - lightyellow: "#ffffe0", - lime: "#00ff00", - limegreen: "#32cd32", - linen: "#faf0e6", - magenta: "#ff00ff", - maroon: "#800000", - mediumaquamarine: "#66cdaa", - mediumblue: "#0000cd", - mediumorchid: "#ba55d3", - mediumpurple: "#9370db", - mediumseagreen: "#3cb371", - mediumslateblue: "#7b68ee", - mediumspringgreen: "#00fa9a", - mediumturquoise: "#48d1cc", - mediumvioletred: "#c71585", - midnightblue: "#191970", - mintcream: "#f5fffa", - mistyrose: "#ffe4e1", - moccasin: "#ffe4b5", - navajowhite: "#ffdead", - navy: "#000080", - oldlace: "#fdf5e6", - olive: "#808000", - olivedrab: "#6b8e23", - orange: "#ffa500", - orangered: "#ff4500", - orchid: "#da70d6", - palegoldenrod: "#eee8aa", - palegreen: "#98fb98", - paleturquoise: "#afeeee", - palevioletred: "#db7093", - papayawhip: "#ffefd5", - peachpuff: "#ffdab9", - peru: "#cd853f", - pink: "#ffc0cb", - plum: "#dda0dd", - powderblue: "#b0e0e6", - purple: "#800080", - red: "#ff0000", - rosybrown: "#bc8f8f", - royalblue: "#4169e1", - saddlebrown: "#8b4513", - salmon: "#fa8072", - sandybrown: "#f4a460", - seagreen: "#2e8b57", - seashell: "#fff5ee", - sienna: "#a0522d", - silver: "#c0c0c0", - skyblue: "#87ceeb", - slateblue: "#6a5acd", - slategray: "#708090", - slategrey: "#708090", - snow: "#fffafa", - springgreen: "#00ff7f", - steelblue: "#4682b4", - tan: "#d2b48c", - teal: "#008080", - thistle: "#d8bfd8", - tomato: "#ff6347", - turquoise: "#40e0d0", - violet: "#ee82ee", - wheat: "#f5deb3", - white: "#ffffff", - whitesmoke: "#f5f5f5", - yellow: "#ffff00", - yellowgreen: "#9acd32" -}; -/** - * Returns a new categorical color encoding using the specified colors. The - * arguments to this method are an array of colors; see {@link pv.color}. For - * example, to create a categorical color encoding using the species - * attribute: - * - *

pv.colors("red", "green", "blue").by(function(d) d.species)
- * - * The result of this expression can be used as a fill- or stroke-style - * property. This assumes that the data's species attribute is a - * string. - * - * @param {string} colors... categorical colors. - * @see pv.Scale.ordinal - * @returns {pv.Scale.ordinal} an ordinal color scale. - */ -pv.colors = function() { - var scale = pv.Scale.ordinal(); - scale.range.apply(scale, arguments); - return scale; -}; - -/** - * A collection of standard color palettes for categorical encoding. - * - * @namespace A collection of standard color palettes for categorical encoding. - */ -pv.Colors = {}; - -/** - * Returns a new 10-color scheme. The arguments to this constructor are - * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The - * following colors are used: - * - *
#1f77b4
- *
#ff7f0e
- *
#2ca02c
- *
#d62728
- *
#9467bd
- *
#8c564b
- *
#e377c2
- *
#7f7f7f
- *
#bcbd22
- *
#17becf
- * - * @param {number...} domain... domain values. - * @returns {pv.Scale.ordinal} a new ordinal color scale. - * @see pv.color - */ -pv.Colors.category10 = function() { - var scale = pv.colors( - "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", - "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf"); - scale.domain.apply(scale, arguments); - return scale; -}; - -/** - * Returns a new 20-color scheme. The arguments to this constructor are - * optional, and equivalent to calling {@link pv.Scale.OrdinalScale#domain}. The - * following colors are used: - * - *
#1f77b4
- *
#aec7e8
- *
#ff7f0e
- *
#ffbb78
- *
#2ca02c
- *
#98df8a
- *
#d62728
- *
#ff9896
- *
#9467bd
- *
#c5b0d5
- *
#8c564b
- *
#c49c94
- *
#e377c2
- *
#f7b6d2
- *
#7f7f7f
- *
#c7c7c7
- *
#bcbd22
- *
#dbdb8d
- *
#17becf
- *
#9edae5
- * - * @param {number...} domain... domain values. - * @returns {pv.Scale.ordinal} a new ordinal color scale. - * @see pv.color -*/ -pv.Colors.category20 = function() { - var scale = pv.colors( - "#1f77b4", "#aec7e8", "#ff7f0e", "#ffbb78", "#2ca02c", - "#98df8a", "#d62728", "#ff9896", "#9467bd", "#c5b0d5", - "#8c564b", "#c49c94", "#e377c2", "#f7b6d2", "#7f7f7f", - "#c7c7c7", "#bcbd22", "#dbdb8d", "#17becf", "#9edae5"); - scale.domain.apply(scale, arguments); - return scale; -}; - -/** - * Returns a new alternative 19-color scheme. The arguments to this constructor - * are optional, and equivalent to calling - * {@link pv.Scale.OrdinalScale#domain}. The following colors are used: - * - *
#9c9ede
- *
#7375b5
- *
#4a5584
- *
#cedb9c
- *
#b5cf6b
- *
#8ca252
- *
#637939
- *
#e7cb94
- *
#e7ba52
- *
#bd9e39
- *
#8c6d31
- *
#e7969c
- *
#d6616b
- *
#ad494a
- *
#843c39
- *
#de9ed6
- *
#ce6dbd
- *
#a55194
- *
#7b4173
- * - * @param {number...} domain... domain values. - * @returns {pv.Scale.ordinal} a new ordinal color scale. - * @see pv.color - */ -pv.Colors.category19 = function() { - var scale = pv.colors( - "#9c9ede", "#7375b5", "#4a5584", "#cedb9c", "#b5cf6b", - "#8ca252", "#637939", "#e7cb94", "#e7ba52", "#bd9e39", - "#8c6d31", "#e7969c", "#d6616b", "#ad494a", "#843c39", - "#de9ed6", "#ce6dbd", "#a55194", "#7b4173"); - scale.domain.apply(scale, arguments); - return scale; -}; -/** - * Returns a linear color ramp from the specified start color to the - * specified end color. The color arguments may be specified either as - * strings or as {@link pv.Color}s. - * - * @param {string} start the start color; may be a pv.Color. - * @param {string} end the end color; may be a pv.Color. - * @returns {Function} a color ramp from start to end. - * @see pv.Scale.linear - */ -pv.ramp = function(start, end) { - var scale = pv.Scale.linear(); - scale.range.apply(scale, arguments); - return scale; -}; -// TODO don't populate default attributes? - -/** - * @private - * @namespace - */ -pv.Scene = pv.SvgScene = {}; - -/** - * Updates the display for the specified array of scene nodes. - * - * @param scenes {array} an array of scene nodes. - */ -pv.SvgScene.updateAll = function(scenes) { - if (!scenes.length) return; - if ((scenes[0].reverse) - && (scenes.type != "line") - && (scenes.type != "area")) { - var reversed = pv.extend(scenes); - for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { - reversed[i] = scenes[j]; - } - scenes = reversed; - } - this.removeSiblings(this[scenes.type](scenes)); -}; - -/** - * Creates a new SVG element of the specified type. - * - * @param type {string} an SVG element type, such as "rect". - * @return a new SVG element. - */ -pv.SvgScene.create = function(type) { - return document.createElementNS(pv.ns.svg, type); -}; - -/** - * Expects the element e to be the specified type. If the element does - * not exist, a new one is created. If the element does exist but is the wrong - * type, it is replaced with the specified element. - * - * @param type {string} an SVG element type, such as "rect". - * @return a new SVG element. - */ -pv.SvgScene.expect = function(type, e) { - if (!e) return this.create(type); - if (e.tagName == "a") e = e.firstChild; - if (e.tagName == type) return e; - var n = this.create(type); - e.parentNode.replaceChild(n, e); - return n; -}; - -/** TODO */ -pv.SvgScene.append = function(e, scenes, index) { - e.$scene = {scenes:scenes, index:index}; - e = this.title(e, scenes[index]); - if (!e.parentNode) scenes.$g.appendChild(e); - return e.nextSibling; -}; - -/** - * Applies a title tooltip to the specified element e, using the - * title property of the specified scene node s. Note that - * this implementation does not create an SVG title element as a child - * of e; although this is the recommended standard, it is only - * supported in Opera. Instead, an anchor element is created around the element - * e, and the xlink:title attribute is set accordingly. - * - * @param e an SVG element. - * @param s a scene node. - */ -pv.SvgScene.title = function(e, s) { - var a = e.parentNode, t = String(s.title); - if (a && (a.tagName != "a")) a = null; - if (t) { - if (!a) { - a = this.create("a"); - if (e.parentNode) e.parentNode.replaceChild(a, e); - a.appendChild(e); - } - a.setAttributeNS(pv.ns.xlink, "title", t); - return a; - } - if (a) a.parentNode.replaceChild(e, a); - return e; -}; - -/** TODO */ -pv.SvgScene.dispatch = function(e) { - var t = e.target.$scene; - if (t) { - t.scenes.mark.dispatch(e.type, t.scenes, t.index); - e.preventDefault(); - } -}; - -/** TODO */ -pv.SvgScene.removeSiblings = function(e) { - while (e) { - var n = e.nextSibling; - e.parentNode.removeChild(e); - e = n; - } -}; -// TODO strokeStyle for areaSegment? - -pv.SvgScene.area = function(scenes) { - var e = scenes.$g.firstChild; - if (!scenes.length) return e; - var s = scenes[0]; - - /* segmented */ - if (s.segmented) return this.areaSegment(scenes); - - /* visible */ - if (!s.visible) return e; - var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); - if (!fill.opacity && !stroke.opacity) return e; - - /* points */ - var p1 = "", p2 = ""; - for (var i = 0, j = scenes.length - 1; j >= 0; i++, j--) { - var si = scenes[i], sj = scenes[j]; - p1 += si.left + "," + si.top + " "; - p2 += (sj.left + sj.width) + "," + (sj.top + sj.height) + " "; - - /* interpolate (assume linear by default) */ - if (i < scenes.length - 1) { - var sk = scenes[i + 1], sl = scenes[j - 1]; - switch (s.interpolate) { - case "step-before": { - p1 += si.left + "," + sk.top + " "; - p2 += (sl.left + sl.width) + "," + (sj.top + sj.height) + " "; - break; - } - case "step-after": { - p1 += sk.left + "," + si.top + " "; - p2 += (sj.left + sj.width) + "," + (sl.top + sl.height) + " "; - break; - } - } - } - } - - e = this.expect("polygon", e); - e.setAttribute("cursor", s.cursor); - e.setAttribute("points", p1 + p2); - var fill = pv.color(s.fillStyle); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - var stroke = pv.color(s.strokeStyle); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - return this.append(e, scenes, 0); -}; - -pv.SvgScene.areaSegment = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0, n = scenes.length - 1; i < n; i++) { - var s1 = scenes[i], s2 = scenes[i + 1]; - - /* visible */ - if (!s1.visible || !s2.visible) continue; - var fill = pv.color(s1.fillStyle), stroke = pv.color(s1.strokeStyle); - if (!fill.opacity && !stroke.opacity) continue; - - /* points */ - var p = s1.left + "," + s1.top + " " - + s2.left + "," + s2.top + " " - + (s2.left + s2.width) + "," + (s2.top + s2.height) + " " - + (s1.left + s1.width) + "," + (s1.top + s1.height); - - e = this.expect("polygon", e); - e.setAttribute("cursor", s1.cursor); - e.setAttribute("points", p); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s1.lineWidth); - e = this.append(e, scenes, i); - } - return e; -}; -pv.SvgScene.bar = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); - if (!fill.opacity && !stroke.opacity) continue; - - e = this.expect("rect", e); - e.setAttribute("cursor", s.cursor); - e.setAttribute("x", s.left); - e.setAttribute("y", s.top); - e.setAttribute("width", Math.max(1E-10, s.width)); - e.setAttribute("height", Math.max(1E-10, s.height)); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - e = this.append(e, scenes, i); - } - return e; -}; -pv.SvgScene.dot = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); - if (!fill.opacity && !stroke.opacity) continue; - - /* points */ - var radius = Math.sqrt(s.size), fillPath = "", strokePath = ""; - switch (s.shape) { - case "cross": { - fillPath = "M" + -radius + "," + -radius - + "L" + radius + "," + radius - + "M" + radius + "," + -radius - + "L" + -radius + "," + radius; - break; - } - case "triangle": { - var h = radius, w = radius * 2 / Math.sqrt(3); - fillPath = "M0," + h - + "L" + w +"," + -h - + " " + -w + "," + -h - + "Z"; - break; - } - case "diamond": { - radius *= Math.sqrt(2); - fillPath = "M0," + -radius - + "L" + radius + ",0" - + " 0," + radius - + " " + -radius + ",0" - + "Z"; - break; - } - case "square": { - fillPath = "M" + -radius + "," + -radius - + "L" + radius + "," + -radius - + " " + radius + "," + radius - + " " + -radius + "," + radius - + "Z"; - break; - } - case "tick": { - fillPath = "M0,0L0," + -s.size; - break; - } - default: { - function circle(r) { - return "M0," + r - + "A" + r + "," + r + " 0 1,1 0," + (-r) - + "A" + r + "," + r + " 0 1,1 0," + r - + "Z"; - } - if (s.lineWidth / 2 > radius) strokePath = circle(s.lineWidth); - fillPath = circle(radius); - break; - } - } - - /* transform */ - var transform = "translate(" + s.left + "," + s.top + ")" - + (s.angle ? " rotate(" + 180 * s.angle / Math.PI + ")" : ""); - - /* The normal fill path. */ - e = this.expect("path", e); - e.setAttribute("d", fillPath); - e.setAttribute("transform", transform); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e.setAttribute("cursor", s.cursor); - if (strokePath) { - e.setAttribute("stroke", "none"); - } else { - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - } - e = this.append(e, scenes, i); - - /* The special-case stroke path. */ - if (strokePath) { - e = this.expect("path", e); - e.setAttribute("d", strokePath); - e.setAttribute("transform", transform); - e.setAttribute("fill", stroke.color); - e.setAttribute("fill-opacity", stroke.opacity); - e.setAttribute("cursor", s.cursor); - e = this.append(e, scenes, i); - } - } - return e; -}; -pv.SvgScene.image = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - - /* fill */ - e = this.fill(e, scenes, i); - - /* image */ - e = this.expect("image", e); - e.setAttribute("preserveAspectRatio", "none"); - e.setAttribute("x", s.left); - e.setAttribute("y", s.top); - e.setAttribute("width", s.width); - e.setAttribute("height", s.height); - e.setAttribute("cursor", s.cursor); - e.setAttributeNS(pv.ns.xlink, "href", s.url); - e = this.append(e, scenes, i); - - /* stroke */ - e = this.stroke(e, scenes, i); - } - return e; -}; -pv.SvgScene.label = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - var fill = pv.color(s.textStyle); - if (!fill.opacity) continue; - - /* text-baseline, text-align */ - var x = 0, y = 0, dy = 0, anchor = "start"; - switch (s.textBaseline) { - case "middle": dy = ".35em"; break; - case "top": dy = ".71em"; y = s.textMargin; break; - case "bottom": y = "-" + s.textMargin; break; - } - switch (s.textAlign) { - case "right": anchor = "end"; x = "-" + s.textMargin; break; - case "center": anchor = "middle"; break; - case "left": x = s.textMargin; break; - } - - e = this.expect("text", e); - e.setAttribute("pointer-events", "none"); - e.setAttribute("x", x); - e.setAttribute("y", y); - e.setAttribute("dy", dy); - e.setAttribute("text-anchor", anchor); - e.setAttribute("transform", - "translate(" + s.left + "," + s.top + ")" - + (s.textAngle ? " rotate(" + 180 * s.textAngle / Math.PI + ")" : "")); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e.style.font = s.font; - e.style.textShadow = s.textShadow; - if (e.firstChild) e.firstChild.nodeValue = s.text; - else e.appendChild(document.createTextNode(s.text)); - e = this.append(e, scenes, i); - } - return e; -}; -// TODO fillStyle for lineSegment? -// TODO lineOffset for flow maps? - -pv.SvgScene.line = function(scenes) { - var e = scenes.$g.firstChild; - if (scenes.length < 2) return e; - var s = scenes[0]; - - /* segmented */ - if (s.segmented) return this.lineSegment(scenes); - - /* visible */ - if (!s.visible) return e; - var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); - if (!fill.opacity && !stroke.opacity) return e; - - /* points */ - var p = ""; - for (var i = 0; i < scenes.length; i++) { - var si = scenes[i]; - p += si.left + "," + si.top + " "; - - /* interpolate (assume linear by default) */ - if (i < scenes.length - 1) { - var sj = scenes[i + 1]; - switch (s.interpolate) { - case "step-before": { - p += si.left + "," + sj.top + " "; - break; - } - case "step-after": { - p += sj.left + "," + si.top + " "; - break; - } - } - } - } - - - e = this.expect("polyline", e); - e.setAttribute("cursor", s.cursor); - e.setAttribute("points", p); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - return this.append(e, scenes, 0); -}; - -pv.SvgScene.lineSegment = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0, n = scenes.length - 1; i < n; i++) { - var s1 = scenes[i], s2 = scenes[i + 1]; - - /* visible */ - if (!s1.visible || !s2.visible) continue; - var stroke = pv.color(s1.strokeStyle); - if (!stroke.opacity) continue; - - /* Line-line intersection, per Akenine-Moller 16.16.1. */ - function intersect(o1, d1, o2, d2) { - return o1.plus(d1.times(o2.minus(o1).dot(d2.perp()) / d1.dot(d2.perp()))); - } - - /* - * P1-P2 is the current line segment. V is a vector that is perpendicular to - * the line segment, and has length lineWidth / 2. ABCD forms the initial - * bounding box of the line segment (i.e., the line segment if we were to do - * no joins). - */ - var p1 = pv.vector(s1.left, s1.top), - p2 = pv.vector(s2.left, s2.top), - p = p2.minus(p1), - v = p.perp().norm(), - w = v.times(s1.lineWidth / 2), - a = p1.plus(w), - b = p2.plus(w), - c = p2.minus(w), - d = p1.minus(w); - - /* - * Start join. P0 is the previous line segment's start point. We define the - * cutting plane as the average of the vector perpendicular to P0-P1, and - * the vector perpendicular to P1-P2. This insures that the cross-section of - * the line on the cutting plane is equal if the line-width is unchanged. - * Note that we don't implement miter limits, so these can get wild. - */ - if (i > 0) { - var s0 = scenes[i - 1]; - if (s0.visible) { - var v1 = p1.minus(s0.left, s0.top).perp().norm().plus(v); - d = intersect(p1, v1, d, p); - a = intersect(p1, v1, a, p); - } - } - - /* Similarly, for end join. */ - if (i < (n - 1)) { - var s3 = scenes[i + 2]; - if (s3.visible) { - var v2 = pv.vector(s3.left, s3.top).minus(p2).perp().norm().plus(v); - c = intersect(p2, v2, c, p); - b = intersect(p2, v2, b, p); - } - } - - /* points */ - var p = a.x + "," + a.y + " " - + b.x + "," + b.y + " " - + c.x + "," + c.y + " " - + d.x + "," + d.y; - - e = this.expect("polygon", e); - e.setAttribute("cursor", s1.cursor); - e.setAttribute("points", p); - e.setAttribute("fill", stroke.color); - e.setAttribute("fill-opacity", stroke.opacity); - e = this.append(e, scenes, i); - } - return e; -}; -var guid = 0; - -pv.SvgScene.panel = function(scenes) { - var g = scenes.$g, e = g && g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - - /* svg */ - if (!scenes.parent) { - s.canvas.style.display = "inline-block"; - g = s.canvas.firstChild; - if (!g) { - g = s.canvas.appendChild(this.create("svg")); - g.onclick - = g.onmousedown - = g.onmouseup - = g.onmousemove - = g.onmouseout - = g.onmouseover - = pv.SvgScene.dispatch; - } - scenes.$g = g; - g.setAttribute("width", s.width + s.left + s.right); - g.setAttribute("height", s.height + s.top + s.bottom); - if (typeof e == "undefined") e = g.firstChild; - } - - /* clip (nest children) */ - if (s.overflow == "hidden") { - var c = this.expect("g", e), id = (guid++).toString(36); - c.setAttribute("clip-path", "url(#" + id + ")"); - if (!c.parentNode) g.appendChild(c); - scenes.$g = g = c; - e = c.firstChild; - - e = this.expect("clipPath", e); - e.setAttribute("id", id); - var r = e.firstChild || e.appendChild(this.create("rect")); - r.setAttribute("x", s.left); - r.setAttribute("y", s.top); - r.setAttribute("width", s.width); - r.setAttribute("height", s.height); - if (!e.parentNode) g.appendChild(e); - e = e.nextSibling; - } - - /* fill */ - e = this.fill(e, scenes, i); - - /* children */ - for (var j = 0; j < s.children.length; j++) { - s.children[j].$g = e = this.expect("g", e); - e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); - this.updateAll(s.children[j]); - if (!e.parentNode) g.appendChild(e); - e = e.nextSibling; - } - - /* stroke */ - e = this.stroke(e, scenes, i); - - /* clip (restore group) */ - if (s.overflow == "hidden") { - scenes.$g = g = c.parentNode; - e = c.nextSibling; - } - } - return e; -}; - -pv.SvgScene.fill = function(e, scenes, i) { - var s = scenes[i], fill = pv.color(s.fillStyle); - if (fill.opacity) { - e = this.expect("rect", e); - e.setAttribute("x", s.left); - e.setAttribute("y", s.top); - e.setAttribute("width", s.width); - e.setAttribute("height", s.height); - e.setAttribute("cursor", s.cursor); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e = this.append(e, scenes, i); - } - return e; -}; - -pv.SvgScene.stroke = function(e, scenes, i) { - var s = scenes[i], stroke = pv.color(s.strokeStyle); - if (stroke.opacity) { - e = this.expect("rect", e); - e.setAttribute("x", s.left); - e.setAttribute("y", s.top); - e.setAttribute("width", Math.max(1E-10, s.width)); - e.setAttribute("height", Math.max(1E-10, s.height)); - e.setAttribute("cursor", s.cursor); - e.setAttribute("fill", "none"); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - e = this.append(e, scenes, i); - } - return e; -}; -pv.SvgScene.rule = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - var stroke = pv.color(s.strokeStyle); - if (!stroke.opacity) continue; - - e = this.expect("line", e); - e.setAttribute("cursor", s.cursor); - e.setAttribute("x1", s.left); - e.setAttribute("y1", s.top); - e.setAttribute("x2", s.left + s.width); - e.setAttribute("y2", s.top + s.height); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - e = this.append(e, scenes, i); - } - return e; -}; -pv.SvgScene.wedge = function(scenes) { - var e = scenes.$g.firstChild; - for (var i = 0; i < scenes.length; i++) { - var s = scenes[i]; - - /* visible */ - if (!s.visible) continue; - var fill = pv.color(s.fillStyle), stroke = pv.color(s.strokeStyle); - if (!fill.opacity && !stroke.opacity) continue; - - /* points */ - var r1 = s.innerRadius, r2 = s.outerRadius, a = Math.abs(s.angle), p; - if (a >= 2 * Math.PI) { - if (r1) { - p = "M0," + r2 - + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) - + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 - + "M0," + r1 - + "A" + r1 + "," + r1 + " 0 1,1 0," + (-r1) - + "A" + r1 + "," + r1 + " 0 1,1 0," + r1 - + "Z"; - } else { - p = "M0," + r2 - + "A" + r2 + "," + r2 + " 0 1,1 0," + (-r2) - + "A" + r2 + "," + r2 + " 0 1,1 0," + r2 - + "Z"; - } - } else { - var sa = Math.min(s.startAngle, s.endAngle), - ea = Math.max(s.startAngle, s.endAngle), - c1 = Math.cos(sa), c2 = Math.cos(ea), - s1 = Math.sin(sa), s2 = Math.sin(ea); - if (r1) { - p = "M" + r2 * c1 + "," + r2 * s1 - + "A" + r2 + "," + r2 + " 0 " - + ((a < Math.PI) ? "0" : "1") + ",1 " - + r2 * c2 + "," + r2 * s2 - + "L" + r1 * c2 + "," + r1 * s2 - + "A" + r1 + "," + r1 + " 0 " - + ((a < Math.PI) ? "0" : "1") + ",0 " - + r1 * c1 + "," + r1 * s1 + "Z"; - } else { - p = "M" + r2 * c1 + "," + r2 * s1 - + "A" + r2 + "," + r2 + " 0 " - + ((a < Math.PI) ? "0" : "1") + ",1 " - + r2 * c2 + "," + r2 * s2 + "L0,0Z"; - } - } - - e = this.expect("path", e); - e.setAttribute("fill-rule", "evenodd"); - e.setAttribute("cursor", s.cursor); - e.setAttribute("transform", "translate(" + s.left + "," + s.top + ")"); - e.setAttribute("d", p); - e.setAttribute("fill", fill.color); - e.setAttribute("fill-opacity", fill.opacity); - e.setAttribute("stroke", stroke.color); - e.setAttribute("stroke-opacity", stroke.opacity); - e.setAttribute("stroke-width", s.lineWidth); - e = this.append(e, scenes, i); - } - return e; -}; -/** - * Constructs a new mark with default properties. Marks, with the exception of - * the root panel, are not typically constructed directly; instead, they are - * added to a panel or an existing mark via {@link pv.Mark#add}. - * - * @class Represents a data-driven graphical mark. The Mark class is - * the base class for all graphical marks in Protovis; it does not provide any - * specific rendering functionality, but together with {@link Panel} establishes - * the core framework. - * - *

Concrete mark types include familiar visual elements such as bars, lines - * and labels. Although a bar mark may be used to construct a bar chart, marks - * know nothing about charts; it is only through their specification and - * composition that charts are produced. These building blocks permit many - * combinatorial possibilities. - * - *

Marks are associated with data: a mark is generated once per - * associated datum, mapping the datum to visual properties such as - * position and color. Thus, a single mark specification represents a set of - * visual elements that share the same data and visual encoding. The type of - * mark defines the names of properties and their meaning. A property may be - * static, ignoring the associated datum and returning a constant; or, it may be - * dynamic, derived from the associated datum or index. Such dynamic encodings - * can be specified succinctly using anonymous functions. Special properties - * called event handlers can be registered to add interactivity. - * - *

Protovis uses inheritance to simplify the specification of related - * marks: a new mark can be derived from an existing mark, inheriting its - * properties. The new mark can then override properties to specify new - * behavior, potentially in terms of the old behavior. In this way, the old mark - * serves as the prototype for the new mark. Most mark types share the - * same basic properties for consistency and to facilitate inheritance. - * - *

The prioritization of redundant properties is as follows:

    - * - *
  1. If the width property is not specified (i.e., null), its value - * is the width of the parent panel, minus this mark's left and right margins; - * the left and right margins are zero if not specified. - * - *
  2. Otherwise, if the right margin is not specified, its value is - * the width of the parent panel, minus this mark's width and left margin; the - * left margin is zero if not specified. - * - *
  3. Otherwise, if the left property is not specified, its value is - * the width of the parent panel, minus this mark's width and the right margin. - * - *
This prioritization is then duplicated for the height, - * bottom and top properties, respectively. - * - *

While most properties are variable, some mark types, such as lines - * and areas, generate a single visual element rather than a distinct visual - * element per datum. With these marks, some properties may be fixed. - * Fixed properties can vary per mark, but not per datum! These - * properties are evaluated solely for the first (0-index) datum, and typically - * are specified as a constant. However, it is valid to use a function if the - * property varies between panels or is dynamically generated. - * - *

See also the Protovis guide. - */ -pv.Mark = function() { - /* - * TYPE 0 constant defs - * TYPE 1 function defs - * TYPE 2 constant properties - * TYPE 3 function properties - * in order of evaluation! - */ - this.$properties = []; -}; - -/** @private TOOD */ -pv.Mark.prototype.properties = {}; - -/** - * @private Defines and registers a property method for the property with the - * given name. This method should be called on a mark class prototype to define - * each exposed property. (Note this refers to the JavaScript - * prototype, not the Protovis mark prototype, which is the {@link - * #proto} field.) - * - *

The created property method supports several modes of invocation:

    - * - *
  1. If invoked with a Function argument, this function is evaluated - * for each associated datum. The return value of the function is used as the - * computed property value. The context of the function (this) is this - * mark. The arguments to the function are the associated data of this mark and - * any enclosing panels. For example, a linear encoding of numerical data to - * height is specified as - * - *
    m.height(function(d) d * 100);
    - * - * The expression d * 100 will be evaluated for the height property of - * each mark instance. The return value of the property method (e.g., - * m.height) is this mark (m)).

    - * - *

  2. If invoked with a non-function argument, the property is treated as a - * constant. The return value of the property method (e.g., m.height) - * is this mark.

    - * - *

  3. If invoked with no arguments, the computed property value for the current - * mark instance in the scene graph is returned. This facilitates property - * chaining, where one mark's properties are defined in terms of another's. - * For example, to offset a mark's location from its prototype, you might say - * - *
    m.top(function() this.proto.top() + 10);
    - * - * Note that the index of the mark being evaluated (in the above example, - * this.proto) is inherited from the Mark class and set by - * this mark. So, if the fifth element's top property is being evaluated, the - * fifth instance of this.proto will similarly be queried for the value - * of its top property. If the mark being evaluated has a different number of - * instances, or its data is unrelated, the behavior of this method is - * undefined. In these cases it may be better to index the scene - * explicitly to specify the exact instance. - * - *

Property names should follow standard JavaScript method naming - * conventions, using lowerCamel-style capitalization. - * - *

In addition to creating the property method, every property is registered - * in the {@link #properties} map on the prototype. Although this is an - * instance field, it is considered immutable and shared by all instances of a - * given mark type. The properties map can be queried to see if a mark - * type defines a particular property, such as width or height. - * - * @param {string} name the property name. - */ -pv.Mark.prototype.property = function(name) { - if (!this.hasOwnProperty("properties")) { - this.properties = pv.extend(this.properties); - } - this.properties[name] = true; - - /* - * Define the setter-getter globally, since the default behavior should be the - * same for all properties, and since the Protovis inheritance chain is - * independent of the JavaScript inheritance chain. For example, anchors - * define a "name" property that is evaluated on derived marks, even though - * those marks don't normally have a name. - */ - pv.Mark.prototype[name] = function(v) { - if (arguments.length) { - this.$properties.push({ - name: name, - type: (typeof v == "function") ? 3 : 2, - value: v - }); - return this; - } - return this.scene[this.index][name]; - }; - - return this; -}; - -/* Define all global properties. */ -pv.Mark.prototype - .property("data") - .property("visible") - .property("left") - .property("right") - .property("top") - .property("bottom") - .property("cursor") - .property("title") - .property("reverse"); - -/** - * The mark type; a lower camelCase name. The type name controls rendering - * behavior, and unless the rendering engine is extended, must be one of the - * built-in concrete mark types: area, bar, dot, image, label, line, panel, - * rule, or wedge. - * - * @type string - * @name pv.Mark.prototype.type - */ - -/** - * The mark prototype, possibly undefined, from which to inherit property - * functions. The mark prototype is not necessarily of the same type as this - * mark. Any properties defined on this mark will override properties inherited - * either from the prototype or from the type-specific defaults. - * - * @type pv.Mark - * @name pv.Mark.prototype.proto - */ - -/** - * The enclosing parent panel. The parent panel is generally undefined only for - * the root panel; however, it is possible to create "offscreen" marks that are - * used only for inheritance purposes. - * - * @type pv.Panel - * @name pv.Mark.prototype.parent - */ - -/** - * The child index. -1 if the enclosing parent panel is null; otherwise, the - * zero-based index of this mark into the parent panel's children array. - * - * @type number - */ -pv.Mark.prototype.childIndex = -1; - -/** - * The mark index. The value of this field depends on which instance (i.e., - * which element of the data array) is currently being evaluated. During the - * build phase, the index is incremented over each datum; when handling events, - * the index is set to the instance that triggered the event. - * - * @type number - */ -pv.Mark.prototype.index = -1; - -/** - * The scene graph. The scene graph is an array of objects; each object (or - * "node") corresponds to an instance of this mark and an element in the data - * array. The scene graph can be traversed to lookup previously-evaluated - * properties. - * - *

For instance, consider a stacked area chart. The bottom property of the - * area can be defined using the cousin instance, which is the current - * area instance in the previous instantiation of the parent panel. In this - * sample code, - * - *

new pv.Panel()
- *     .width(150).height(150)
- *   .add(pv.Panel)
- *     .data([[1, 1.2, 1.7, 1.5, 1.7],
- *            [.5, 1, .8, 1.1, 1.3],
- *            [.2, .5, .8, .9, 1]])
- *   .add(pv.Area)
- *     .data(function(d) d)
- *     .bottom(function() {
- *         var c = this.cousin();
- *         return c ? (c.bottom + c.height) : 0;
- *       })
- *     .height(function(d) d * 40)
- *     .left(function() this.index * 35)
- *   .root.render();
- * - * the bottom property is computed based on the upper edge of the corresponding - * datum in the previous series. The area's parent panel is instantiated once - * per series, so the cousin refers to the previous (below) area mark. (Note - * that the position of the upper edge is not the same as the top property, - * which refers to the top margin: the distance from the top edge of the panel - * to the top edge of the mark.) - * - * @see #first - * @see #last - * @see #sibling - * @see #cousin - * @name pv.Mark.prototype.scene - */ - -/** - * The root parent panel. This may be undefined for "offscreen" marks that are - * created for inheritance purposes only. - * - * @type pv.Panel - * @name pv.Mark.prototype.root - */ - -/** - * The data property; an array of objects. The size of the array determines the - * number of marks that will be instantiated; each element in the array will be - * passed to property functions to compute the property values. Typically, the - * data property is specified as a constant array, such as - * - *
m.data([1, 2, 3, 4, 5]);
- * - * However, it is perfectly acceptable to define the data property as a - * function. This function might compute the data dynamically, allowing - * different data to be used per enclosing panel. For instance, in the stacked - * area graph example (see {@link #scene}), the data function on the area mark - * dereferences each series. - * - * @type array - * @name pv.Mark.prototype.data - */ - -/** - * The visible property; a boolean determining whether or not the mark instance - * is visible. If a mark instance is not visible, its other properties will not - * be evaluated. Similarly, for panels no child marks will be rendered. - * - * @type boolean - * @name pv.Mark.prototype.visible - */ - -/** - * The left margin; the distance, in pixels, between the left edge of the - * enclosing panel and the left edge of this mark. Note that in some cases this - * property may be redundant with the right property, or with the conjunction of - * right and width. - * - * @type number - * @name pv.Mark.prototype.left - */ - -/** - * The right margin; the distance, in pixels, between the right edge of the - * enclosing panel and the right edge of this mark. Note that in some cases this - * property may be redundant with the left property, or with the conjunction of - * left and width. - * - * @type number - * @name pv.Mark.prototype.right - */ - -/** - * The top margin; the distance, in pixels, between the top edge of the - * enclosing panel and the top edge of this mark. Note that in some cases this - * property may be redundant with the bottom property, or with the conjunction - * of bottom and height. - * - * @type number - * @name pv.Mark.prototype.top - */ - -/** - * The bottom margin; the distance, in pixels, between the bottom edge of the - * enclosing panel and the bottom edge of this mark. Note that in some cases - * this property may be redundant with the top property, or with the conjunction - * of top and height. - * - * @type number - * @name pv.Mark.prototype.bottom - */ - -/** - * The cursor property; corresponds to the CSS cursor property. This is - * typically used in conjunction with event handlers to indicate interactivity. - * - * @type string - * @name pv.Mark.prototype.cursor - * @see CSS2 cursor - */ - -/** - * The title property; corresponds to the HTML/SVG title property, allowing the - * general of simple plain text tooltips. - * - * @type string - * @name pv.Mark.prototype.title - */ - -/** - * The reverse property; a boolean determining whether marks are ordered from - * front-to-back or back-to-front. SVG does not support explicit z-ordering; - * shapes are rendered in the order they appear. Thus, by default, marks are - * rendered in data order. Setting the reverse property to false reverses the - * order in which they are rendered; however, the properties are still evaluated - * (i.e., built) in forward order. - * - * @type boolean - * @name pv.Mark.prototype.reverse - */ - -/** - * Default properties for all mark types. By default, the data array is the - * parent data as a single-element array; if the data property is not specified, - * this causes each mark to be instantiated as a singleton with the parents - * datum. The visible property is true by default, and the reverse property is - * false. - * - * @type pv.Mark - */ -pv.Mark.prototype.defaults = new pv.Mark() - .data(function(d) { return [d]; }) - .visible(true) - .reverse(false) - .cursor("") - .title(""); - -/* Private categorical colors for default fill & stroke styles. */ -var defaultFillStyle = pv.Colors.category20().by(pv.parent), - defaultStrokeStyle = pv.Colors.category10().by(pv.parent); - -/** - * Sets the prototype of this mark to the specified mark. Any properties not - * defined on this mark may be inherited from the specified prototype mark, or - * its prototype, and so on. The prototype mark need not be the same type of - * mark as this mark. (Note that for inheritance to be useful, properties with - * the same name on different mark types should have equivalent meaning.) - * - * @param {pv.Mark} proto the new prototype. - * @return {pv.Mark} this mark. - * @see #add - */ -pv.Mark.prototype.extend = function(proto) { - this.proto = proto; - return this; -}; - -/** - * Adds a new mark of the specified type to the enclosing parent panel, whilst - * simultaneously setting the prototype of the new mark to be this mark. - * - * @param {function} type the type of mark to add; a constructor, such as - * pv.Bar. - * @return {pv.Mark} the new mark. - * @see #extend - */ -pv.Mark.prototype.add = function(type) { - return this.parent.add(type).extend(this); -}; - -/** - * Defines a local variable on this mark. Local variables are initialized once - * per mark (i.e., per parent panel instance), and can be used to store local - * state for the mark. Here are a few reasons you might want to use - * def: - * - *

1. To store local state. For example, say you were visualizing employment - * statistics, and your root panel had an array of occupations. In a child - * panel, you might want to initialize a local scale, and reference it from a - * property function: - * - *

.def("y", function(d) pv.Scale.linear(0, pv.max(d.values)).range(0, h))
- * .height(function(d) this.y()(d))
- * - * In this example, this.y() returns the defined local scale. We then - * invoke the scale function, passing in the datum, to compute the height. Note - * that defs are similar to fixed properties: they are only evaluated once per - * parent panel, and this.y() returns a function, rather than - * automatically evaluating this function as a property. - * - *

2. To store temporary state for interaction. Say you have an array of - * bars, and you want to color the bar differently if the mouse is over it. Use - * def to define a local variable, and event handlers to override this - * variable interactively: - * - *

.def("i", -1)
- * .event("mouseover", function() this.i(this.index))
- * .event("mouseout", function() this.i(-1))
- * .fillStyle(function() this.i() == this.index ? "red" : "blue")
- * - * Notice that this.i() can be used both to set the value of i - * (when an argument is specified), and to get the value of i (when no - * arguments are specified). In this way, it's like other property methods. - * - *

3. To specify fixed properties efficiently. Sometimes, the value of a - * property may be locally a constant, but dependent on parent panel data which - * is variable. In this scenario, you can use def to define a property; - * it will only get computed once per mark, rather than once per datum. - * - * @param {string} name the name of the local variable. - * @param {function} [value] an optional initializer; may be a constant or a - * function. - */ -pv.Mark.prototype.def = function(name, value) { - this.$properties.push({ - name: name, - type: (typeof value == "function") ? 1 : 0, - value: value - }); - return this; -}; - -/** - * Returns an anchor with the specified name. While anchor names are typically - * constants, the anchor name is a true property, which means you can specify a - * function to compute the anchor name dynamically. See the - * {@link pv.Anchor#name} property for details. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} the new anchor. - */ -pv.Mark.prototype.anchor = function(name) { - var anchor = new pv.Anchor().extend(this).name(name); - anchor.parent = this.parent; - return anchor; -}; - -/** - * Returns the anchor target of this mark, if it is derived from an anchor; - * otherwise returns null. For example, if a label is derived from a bar anchor, - * - *

bar.anchor("top").add(pv.Label);
- * - * then property functions on the label can refer to the bar via the - * anchorTarget method. This method is also useful for mark types - * defining properties on custom anchors. - * - * @returns {pv.Mark} the anchor target of this mark; possibly null. - */ -pv.Mark.prototype.anchorTarget = function() { - var target = this; - while (!(target instanceof pv.Anchor)) { - target = target.proto; - if (!target) return null; - } - return target.proto; -}; - -/** - * Returns the first instance of this mark in the scene graph. This method can - * only be called when the mark is bound to the scene graph (for example, from - * an event handler, or within a property function). - * - * @returns a node in the scene graph. - */ -pv.Mark.prototype.first = function() { - return this.scene[0]; -}; - -/** - * Returns the last instance of this mark in the scene graph. This method can - * only be called when the mark is bound to the scene graph (for example, from - * an event handler, or within a property function). In addition, note that mark - * instances are built sequentially, so the last instance of this mark may not - * yet be constructed. - * - * @returns a node in the scene graph. - */ -pv.Mark.prototype.last = function() { - return this.scene[this.scene.length - 1]; -}; - -/** - * Returns the previous instance of this mark in the scene graph, or null if - * this is the first instance. - * - * @returns a node in the scene graph, or null. - */ -pv.Mark.prototype.sibling = function() { - return (this.index == 0) ? null : this.scene[this.index - 1]; -}; - -/** - * Returns the current instance in the scene graph of this mark, in the previous - * instance of the enclosing parent panel. May return null if this instance - * could not be found. See the {@link pv.Layout.stack} function for an example - * property function using cousin. - * - * @see pv.Layout.stack - * @returns a node in the scene graph, or null. - */ -pv.Mark.prototype.cousin = function() { - var p = this.parent, s = p && p.sibling(); - return (s && s.children) ? s.children[this.childIndex][this.index] : null; -}; - -/** - * Renders this mark, including recursively rendering all child marks if this is - * a panel. - */ -pv.Mark.prototype.render = function() { - /* - * Rendering consists of three phases: bind, build and update. The update - * phase is decoupled to allow different rendering engines. - * - * In the bind phase, inherited property definitions are cached so they do not - * need to be queried during build. In the build phase, properties are - * evaluated, and the scene graph is generated. In the update phase, the scene - * is rendered by creating and updating elements and attributes in the SVG - * image. No properties are evaluated during the update phase; instead the - * values computed previously in the build phase are simply translated into - * SVG. - */ - this.bind(); - this.build(); - pv.Scene.updateAll(this.scene); -}; - -/** @private Computes the root data stack for the specified mark. */ -function argv(mark) { - var stack = []; - while (mark) { - stack.push(mark.scene[mark.index].data); - mark = mark.parent; - } - return stack; -} - -/** @private TODO */ -pv.Mark.prototype.bind = function() { - var seen = {}, types = [[], [], [], []], data, visible; - - /** TODO */ - function bind(mark) { - do { - var properties = mark.$properties; - for (var i = properties.length - 1; i >= 0 ; i--) { - var p = properties[i]; - if (!(p.name in seen)) { - seen[p.name] = 1; - switch (p.name) { - case "data": data = p; break; - case "visible": visible = p; break; - default: types[p.type].push(p); break; - } - } - } - } while (mark = mark.proto); - } - - /** TODO */ - function def(name) { - return function(v) { - var defs = this.scene.defs; - if (arguments.length) { - if (v == undefined) { - delete defs.locked[name]; - } else { - defs.locked[name] = true; - } - defs.values[name] = v; - return this; - } else { - return defs.values[name]; - } - }; - } - - /* Scan the proto chain for all defined properties. */ - bind(this); - bind(this.defaults); - types[1].reverse(); - types[3].reverse(); - - /* Any undefined properties are null. */ - var mark = this; - do for (var name in mark.properties) { - if (!(name in seen)) { - seen[name] = 1; - types[2].push({name: name, type: 2, value: null}); - } - } while (mark = mark.proto); - - /* Define setter-getter for inherited defs. */ - var defs = types[0].concat(types[1]); - for (var i = 0; i < defs.length; i++) { - var d = defs[i]; - this[d.name] = def(d.name); - } - - /* Setup binds to evaluate constants before functions. */ - this.binds = { - data: data, - visible: visible, - defs: defs, - properties: pv.blend(types) - }; -}; - -/** - * @private Evaluates properties and computes implied properties. Properties are - * stored in the {@link #scene} array for each instance of this mark. - * - *

As marks are built recursively, the {@link #index} property is updated to - * match the current index into the data array for each mark. Note that the - * index property is only set for the mark currently being built and its - * enclosing parent panels. The index property for other marks is unset, but is - * inherited from the global Mark class prototype. This allows mark - * properties to refer to properties on other marks in the same panel - * conveniently; however, in general it is better to reference mark instances - * specifically through the scene graph rather than depending on the magical - * behavior of {@link #index}. - * - *

The root scene array has a special property, data, which stores - * the current data stack. The first element in this stack is the current datum, - * followed by the datum of the enclosing parent panel, and so on. The data - * stack should not be accessed directly; instead, property functions are passed - * the current data stack as arguments. - * - *

The evaluation of the data and visible properties is - * special. The data property is evaluated first; unlike the other - * properties, the data stack is from the parent panel, rather than the current - * mark, since the data is not defined until the data property is evaluated. - * The visisble property is subsequently evaluated for each instance; - * only if true will the {@link #buildInstance} method be called, evaluating - * other properties and recursively building the scene graph. - * - *

If this mark is being re-built, any old instances of this mark that no - * longer exist (because the new data array contains fewer elements) will be - * cleared using {@link #clearInstance}. - * - * @param parent the instance of the parent panel from the scene graph. - */ -pv.Mark.prototype.build = function() { - var scene = this.scene; - if (!scene) { - scene = this.scene = []; - scene.mark = this; - scene.type = this.type; - scene.childIndex = this.childIndex; - if (this.parent) { - scene.parent = this.parent.scene; - scene.parentIndex = this.parent.index; - } - } - - /* Set the data stack. */ - var stack = this.root.scene.data; - if (!stack) this.root.scene.data = stack = argv(this.parent); - - /* Evaluate defs. */ - if (this.binds.defs.length) { - var defs = scene.defs; - if (!defs) scene.defs = defs = {values: {}, locked: {}}; - for (var i = 0; i < this.binds.defs.length; i++) { - var d = this.binds.defs[i]; - if (!(d.name in defs.locked)) { - var v = d.value; - if (d.type == 1) { - property = d.name; - v = v.apply(this, stack); - } - defs.values[d.name] = v; - } - } - } - - /* Evaluate special data property. */ - var data = this.binds.data; - switch (data.type) { - case 0: case 1: data = defs.values.data; break; - case 2: data = data.value; break; - case 3: { - property = "data"; - data = data.value.apply(this, stack); - break; - } - } - - /* Create, update and delete scene nodes. */ - stack.unshift(null); - scene.length = data.length; - for (var i = 0; i < data.length; i++) { - pv.Mark.prototype.index = this.index = i; - var s = scene[i]; - if (!s) scene[i] = s = {}; - s.data = stack[0] = data[i]; - - /* Evaluate special visible property. */ - var visible = this.binds.visible; - switch (visible.type) { - case 0: case 1: visible = defs.values.visible; break; - case 2: visible = visible.value; break; - case 3: { - property = "visible"; - visible = visible.value.apply(this, stack); - break; - } - } - - if (s.visible = visible) this.buildInstance(s); - } - stack.shift(); - delete this.index; - pv.Mark.prototype.index = -1; - if (!this.parent) scene.data = null; - - return this; -}; - -/** - * @private Evaluates the specified array of properties for the specified - * instance s in the scene graph. - * - * @param s a node in the scene graph; the instance of the mark to build. - * @param properties an array of properties. - */ -pv.Mark.prototype.buildProperties = function(s, properties) { - for (var i = 0, n = properties.length; i < n; i++) { - var p = properties[i], v = p.value; - switch (p.type) { - case 0: case 1: v = this.scene.defs.values[p.name]; break; - case 3: { - property = p.name; - v = v.apply(this, this.root.scene.data); - break; - } - } - s[p.name] = v; - } -}; - -/** - * @private Evaluates all of the properties for this mark for the specified - * instance s in the scene graph. The set of properties to evaluate is - * retrieved from the {@link #properties} array for this mark type (see {@link - * #type}). After these properties are evaluated, any implied properties - * may be computed by the mark and set on the scene graph; see - * {@link #buildImplied}. - * - *

For panels, this method recursively builds the scene graph for all child - * marks as well. In general, this method should not need to be overridden by - * concrete mark types. - * - * @param s a node in the scene graph; the instance of the mark to build. - */ -pv.Mark.prototype.buildInstance = function(s) { - this.buildProperties(s, this.binds.properties); - this.buildImplied(s); -}; - -/** - * @private Computes the implied properties for this mark for the specified - * instance s in the scene graph. Implied properties are those with - * dependencies on multiple other properties; for example, the width property - * may be implied if the left and right properties are set. This method can be - * overridden by concrete mark types to define new implied properties, if - * necessary. - * - * @param s a node in the scene graph; the instance of the mark to build. - */ -pv.Mark.prototype.buildImplied = function(s) { - var l = s.left; - var r = s.right; - var t = s.top; - var b = s.bottom; - - /* Assume width and height are zero if not supported by this mark type. */ - var p = this.properties; - var w = p.width ? s.width : 0; - var h = p.height ? s.height : 0; - - /* Compute implied width, right and left. */ - var width = this.parent ? this.parent.width() : (w + l + r); - if (w == null) { - w = width - (r = r || 0) - (l = l || 0); - } else if (r == null) { - r = width - w - (l = l || 0); - } else if (l == null) { - l = width - w - (r = r || 0); - } - - /* Compute implied height, bottom and top. */ - var height = this.parent ? this.parent.height() : (h + t + b); - if (h == null) { - h = height - (t = t || 0) - (b = b || 0); - } else if (b == null) { - b = height - h - (t = t || 0); - } else if (t == null) { - t = height - h - (b = b || 0); - } - - s.left = l; - s.right = r; - s.top = t; - s.bottom = b; - - /* Only set width and height if they are supported by this mark type. */ - if (p.width) s.width = w; - if (p.height) s.height = h; -}; - -/** - * @private The name of the property being evaluated, for so-called "smart" - * functions that change behavior depending on which property is being - * evaluated. This functionality is somewhat magical, so for now, this feature - * is not exposed outside the library. - * - * @type string - */ -var property; - -/** @private The current mouse location. */ -var pageX = 0, pageY = 0; -pv.listen(window, "mousemove", function(e) { pageX = e.pageX; pageY = e.pageY; }); - -/** - * Returns the current location of the mouse (cursor) relative to this mark's - * parent. The x coordinate corresponds to the left margin, while the - * y coordinate corresponds to the top margin. - * - * @returns {pv.Vector} the mouse location. - */ -pv.Mark.prototype.mouse = function() { - var x = 0, y = 0, mark = (this instanceof pv.Panel) ? this : this.parent; - do { - x += mark.left(); - y += mark.top(); - } while (mark = mark.parent); - var node = this.root.canvas(); - do { - x += node.offsetLeft; - y += node.offsetTop; - } while (node = node.offsetParent); - return pv.vector(pageX - x, pageY - y); -}; - -/** - * Registers an event handler for the specified event type with this mark. When - * an event of the specified type is triggered, the specified handler will be - * invoked. The handler is invoked in a similar method to property functions: - * the context is this mark instance, and the arguments are the full - * data stack. Event handlers can use property methods to manipulate the display - * properties of the mark: - * - *

m.event("click", function() this.fillStyle("red"));
- * - * Alternatively, the external data can be manipulated and the visualization - * redrawn: - * - *
m.event("click", function(d) {
- *     data = all.filter(function(k) k.name == d);
- *     vis.render();
- *   });
- * - * The return value of the event handler determines which mark gets re-rendered. - * Use defs ({@link #def}) to set temporary state from event handlers. - * - *

The complete set of event types is defined by SVG; see the reference - * below. The set of supported event types is:

    - * - *
  • click - *
  • mousedown - *
  • mouseup - *
  • mouseover - *
  • mousemove - *
  • mouseout - * - *
Since Protovis does not specify any concept of focus, it does not - * support key events; these should be handled outside the visualization using - * standard JavaScript. In the future, support for interaction may be extended - * to support additional event types, particularly those most relevant to - * interactive visualization, such as selection. - * - *

TODO In the current implementation, event handlers are not inherited from - * prototype marks. They must be defined explicitly on each interactive mark. In - * addition, only one event handler for a given event type can be defined; when - * specifying multiple event handlers for the same type, only the last one will - * be used. - * - * @see SVG events - * @param {string} type the event type. - * @param {function} handler the event handler. - * @returns {pv.Mark} this. - */ -pv.Mark.prototype.event = function(type, handler) { - if (!this.$handlers) this.$handlers = {}; - this.$handlers[type] = handler; - return this; -}; - -/** @private TODO */ -pv.Mark.prototype.dispatch = function(type, scenes, index) { - var l = this.$handlers && this.$handlers[type]; - if (!l) { - if (this.parent) { - this.parent.dispatch(type, scenes.parent, scenes.parentIndex); - } - return; - } - try { - - /* Setup the scene stack. */ - var mark = this; - do { - mark.index = index; - mark.scene = scenes; - index = scenes.parentIndex; - scenes = scenes.parent; - } while (mark = mark.parent); - - /* Execute the event listener. */ - try { - mark = l.apply(this, this.root.scene.data = argv(this)); - } finally { - this.root.scene.data = null; - } - - /* Update the display. TODO dirtying. */ - if (mark instanceof pv.Mark) mark.render(); - - } finally { - - /* Restore the scene stack. */ - var mark = this; - do { - if (mark.parent) delete mark.scene; - delete mark.index; - } while (mark = mark.parent); - } -}; -/** - * Constructs a new mark anchor with default properties. - * - * @class Represents an anchor on a given mark. An anchor is itself a mark, but - * without a visual representation. It serves only to provide useful default - * properties that can be inherited by other marks. Each type of mark can define - * any number of named anchors for convenience. If the concrete mark type does - * not define an anchor implementation specifically, one will be inherited from - * the mark's parent class. - * - *

For example, the bar mark provides anchors for its four sides: left, - * right, top and bottom. Adding a label to the top anchor of a bar, - * - *

bar.anchor("top").add(pv.Label);
- * - * will render a text label on the top edge of the bar; the top anchor defines - * the appropriate position properties (top and left), as well as text-rendering - * properties for convenience (textAlign and textBaseline). - * - * @extends pv.Mark - */ -pv.Anchor = function() { - pv.Mark.call(this); -}; - -pv.Anchor.prototype = pv.extend(pv.Mark) - .property("name"); - -/** - * The anchor name. The set of supported anchor names is dependent on the - * concrete mark type; see the mark type for details. For example, bars support - * left, right, top and bottom anchors. - * - *

While anchor names are typically constants, the anchor name is a true - * property, which means you can specify a function to compute the anchor name - * dynamically. For instance, if you wanted to alternate top and bottom anchors, - * saying - * - *

m.anchor(function() (this.index % 2) ? "top" : "bottom").add(pv.Dot);
- * - * would have the desired effect. - * - * @type string - * @name pv.Anchor.prototype.name - */ -/** - * Constructs a new area mark with default properties. Areas are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents an area mark: the solid area between two series of - * connected line segments. Unsurprisingly, areas are used most frequently for - * area charts. - * - *

Just as a line represents a polyline, the Area mark type - * represents a polygon. However, an area is not an arbitrary polygon; - * vertices are paired either horizontally or vertically into parallel - * spans, and each span corresponds to an associated datum. Either the - * width or the height must be specified, but not both; this determines whether - * the area is horizontally-oriented or vertically-oriented. Like lines, areas - * can be stroked and filled with arbitrary colors. - * - *

See also the Area guide. - * - * @extends pv.Mark - */ -pv.Area = function() { - pv.Mark.call(this); -}; - -pv.Area.prototype = pv.extend(pv.Mark) - .property("width") - .property("height") - .property("lineWidth") - .property("strokeStyle") - .property("fillStyle") - .property("segmented") - .property("interpolate"); - -pv.Area.prototype.type = "area"; - -/** - * The width of a given span, in pixels; used for horizontal spans. If the width - * is specified, the height property should be 0 (the default). Either the top - * or bottom property should be used to space the spans vertically, typically as - * a multiple of the index. - * - * @type number - * @name pv.Area.prototype.width - */ - -/** - * The height of a given span, in pixels; used for vertical spans. If the height - * is specified, the width property should be 0 (the default). Either the left - * or right property should be used to space the spans horizontally, typically - * as a multiple of the index. - * - * @type number - * @name pv.Area.prototype.height - */ - -/** - * The width of stroked lines, in pixels; used in conjunction with - * strokeStyle to stroke the perimeter of the area. Unlike the - * {@link Line} mark type, the entire perimeter is stroked, rather than just one - * edge. The default value of this property is 1.5, but since the default stroke - * style is null, area marks are not stroked by default. - * - *

This property is fixed for non-segmented areas. See - * {@link pv.Mark}. - * - * @type number - * @name pv.Area.prototype.lineWidth - */ - -/** - * The style of stroked lines; used in conjunction with lineWidth to - * stroke the perimeter of the area. Unlike the {@link Line} mark type, the - * entire perimeter is stroked, rather than just one edge. The default value of - * this property is null, meaning areas are not stroked by default. - * - *

This property is fixed for non-segmented areas. See - * {@link pv.Mark}. - * - * @type string - * @name pv.Area.prototype.strokeStyle - * @see pv.color - */ - -/** - * The area fill style; if non-null, the interior of the polygon forming the - * area is filled with the specified color. The default value of this property - * is a categorical color. - * - *

This property is fixed for non-segmented areas. See - * {@link pv.Mark}. - * - * @type string - * @name pv.Area.prototype.fillStyle - * @see pv.color - */ - -/** - * Whether the area is segmented; whether variations in fill style, stroke - * style, and the other properties are treated as fixed. Rendering segmented - * areas is noticeably slower than non-segmented areas. - * - *

This property is fixed. See {@link pv.Mark}. - * - * @type boolean - * @name pv.Area.prototype.segmented - */ - -/** - * How to interpolate between values. Linear interpolation ("linear") is the - * default, producing a straight line between points. For piecewise constant - * functions (i.e., step functions), either "step-before" or "step-after" can be - * specified. - * - *

Note: this property is currently supported only on non-segmented areas. - * - *

This property is fixed. See {@link pv.Mark}. - * - * @type string - * @name pv.Area.prototype.interpolate - */ - -/** - * Default properties for areas. By default, there is no stroke and the fill - * style is a categorical color. - * - * @type pv.Area - */ -pv.Area.prototype.defaults = new pv.Area() - .extend(pv.Mark.prototype.defaults) - .lineWidth(1.5) - .fillStyle(defaultFillStyle) - .interpolate("linear"); - -/** - * Constructs a new area anchor with default properties. Areas support five - * different anchors:

    - * - *
  • top - *
  • left - *
  • center - *
  • bottom - *
  • right - * - *
In addition to positioning properties (left, right, top bottom), the - * anchors support text rendering properties (text-align, text-baseline). Text is - * rendered to appear inside the area polygon. - * - *

To facilitate stacking of areas, the anchors are defined in terms of their - * opposite edge. For example, the top anchor defines the bottom property, such - * that the area grows upwards; the bottom anchor instead defines the top - * property, such that the area grows downwards. Of course, in general it is - * more robust to use panels and the cousin accessor to define stacked area - * marks; see {@link pv.Mark#scene} for an example. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} - */ -pv.Area.prototype.anchor = function(name) { - var area = this; - return pv.Mark.prototype.anchor.call(this, name) - .left(function() { - switch (this.name()) { - case "bottom": - case "top": - case "center": return area.left() + area.width() / 2; - case "right": return area.left() + area.width(); - } - return null; - }) - .right(function() { - switch (this.name()) { - case "bottom": - case "top": - case "center": return area.right() + area.width() / 2; - case "left": return area.right() + area.width(); - } - return null; - }) - .top(function() { - switch (this.name()) { - case "left": - case "right": - case "center": return area.top() + area.height() / 2; - case "bottom": return area.top() + area.height(); - } - return null; - }) - .bottom(function() { - switch (this.name()) { - case "left": - case "right": - case "center": return area.bottom() + area.height() / 2; - case "top": return area.bottom() + area.height(); - } - return null; - }) - .textAlign(function() { - switch (this.name()) { - case "bottom": - case "top": - case "center": return "center"; - case "right": return "right"; - } - return "left"; - }) - .textBaseline(function() { - switch (this.name()) { - case "right": - case "left": - case "center": return "middle"; - case "top": return "top"; - } - return "bottom"; - }); -}; - -/** - * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such - * that the width and height are set to zero if null. - * - * @param s a node in the scene graph; the instance of the mark to build. - */ -pv.Area.prototype.buildImplied = function(s) { - if (s.height == null) s.height = 0; - if (s.width == null) s.width = 0; - pv.Mark.prototype.buildImplied.call(this, s); -}; - -/** @private */ -var pv_Area_specials = {left:1, top:1, right:1, bottom:1, width:1, height:1, name:1}; - -/** @private */ -pv.Area.prototype.bind = function() { - pv.Mark.prototype.bind.call(this); - var binds = this.binds, - properties = binds.properties, - specials = binds.specials = []; - for (var i = 0, n = properties.length; i < n; i++) { - var p = properties[i]; - if (p.name in pv_Area_specials) specials.push(p); - } -}; - -/** @private */ -pv.Area.prototype.buildInstance = function(s) { - if (this.index && !this.scene[0].segmented) { - this.buildProperties(s, this.binds.specials); - this.buildImplied(s); - } else { - pv.Mark.prototype.buildInstance.call(this, s); - } -}; -/** - * Constructs a new bar mark with default properties. Bars are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents a bar: an axis-aligned rectangle that can be stroked and - * filled. Bars are used for many chart types, including bar charts, histograms - * and Gantt charts. Bars can also be used as decorations, for example to draw a - * frame border around a panel; in fact, a panel is a special type (a subclass) - * of bar. - * - *

Bars can be positioned in several ways. Most commonly, one of the four - * corners is fixed using two margins, and then the width and height properties - * determine the extent of the bar relative to this fixed location. For example, - * using the bottom and left properties fixes the bottom-left corner; the width - * then extends to the right, while the height extends to the top. As an - * alternative to the four corners, a bar can be positioned exclusively using - * margins; this is convenient as an inset from the containing panel, for - * example. See {@link pv.Mark} for details on the prioritization of redundant - * positioning properties. - * - *

See also the Bar guide. - * - * @extends pv.Mark - */ -pv.Bar = function() { - pv.Mark.call(this); -}; - -pv.Bar.prototype = pv.extend(pv.Mark) - .property("width") - .property("height") - .property("lineWidth") - .property("strokeStyle") - .property("fillStyle"); - -pv.Bar.prototype.type = "bar"; - -/** - * The width of the bar, in pixels. If the left position is specified, the bar - * extends rightward from the left edge; if the right position is specified, the - * bar extends leftward from the right edge. - * - * @type number - * @name pv.Bar.prototype.width - */ - -/** - * The height of the bar, in pixels. If the bottom position is specified, the - * bar extends upward from the bottom edge; if the top position is specified, - * the bar extends downward from the top edge. - * - * @type number - * @name pv.Bar.prototype.height - */ - -/** - * The width of stroked lines, in pixels; used in conjunction with - * strokeStyle to stroke the bar's border. - * - * @type number - * @name pv.Bar.prototype.lineWidth - */ - -/** - * The style of stroked lines; used in conjunction with lineWidth to - * stroke the bar's border. The default value of this property is null, meaning - * bars are not stroked by default. - * - * @type string - * @name pv.Bar.prototype.strokeStyle - * @see pv.color - */ - -/** - * The bar fill style; if non-null, the interior of the bar is filled with the - * specified color. The default value of this property is a categorical color. - * - * @type string - * @name pv.Bar.prototype.fillStyle - * @see pv.color - */ - -/** - * Default properties for bars. By default, there is no stroke and the fill - * style is a categorical color. - * - * @type pv.Bar - */ -pv.Bar.prototype.defaults = new pv.Bar() - .extend(pv.Mark.prototype.defaults) - .lineWidth(1.5) - .fillStyle(defaultFillStyle); - -/** - * Constructs a new bar anchor with default properties. Bars support five - * different anchors:

    - * - *
  • top - *
  • left - *
  • center - *
  • bottom - *
  • right - * - *
In addition to positioning properties (left, right, top bottom), the - * anchors support text rendering properties (text-align, text-baseline). Text - * is rendered to appear inside the bar. - * - *

To facilitate stacking of bars, the anchors are defined in terms of their - * opposite edge. For example, the top anchor defines the bottom property, such - * that the bar grows upwards; the bottom anchor instead defines the top - * property, such that the bar grows downwards. Of course, in general it is more - * robust to use panels and the cousin accessor to define stacked bars; see - * {@link pv.Mark#scene} for an example. - * - *

Bar anchors also "smartly" specify position properties based on whether - * the derived mark type supports the width and height properties. If the - * derived mark type does not support these properties (e.g., dots), the - * position will be centered on the corresponding edge. Otherwise (e.g., bars), - * the position will be in the opposite side. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} - */ -pv.Bar.prototype.anchor = function(name) { - var bar = this; - return pv.Mark.prototype.anchor.call(this, name) - .left(function() { - switch (this.name()) { - case "bottom": - case "top": - case "center": return bar.left() + (this.properties.width ? 0 : (bar.width() / 2)); - case "right": return bar.left() + bar.width(); - } - return null; - }) - .right(function() { - switch (this.name()) { - case "bottom": - case "top": - case "center": return bar.right() + (this.properties.width ? 0 : (bar.width() / 2)); - case "left": return bar.right() + bar.width(); - } - return null; - }) - .top(function() { - switch (this.name()) { - case "left": - case "right": - case "center": return bar.top() + (this.properties.height ? 0 : (bar.height() / 2)); - case "bottom": return bar.top() + bar.height(); - } - return null; - }) - .bottom(function() { - switch (this.name()) { - case "left": - case "right": - case "center": return bar.bottom() + (this.properties.height ? 0 : (bar.height() / 2)); - case "top": return bar.bottom() + bar.height(); - } - return null; - }) - .textAlign(function() { - switch (this.name()) { - case "bottom": - case "top": - case "center": return "center"; - case "right": return "right"; - } - return "left"; - }) - .textBaseline(function() { - switch (this.name()) { - case "right": - case "left": - case "center": return "middle"; - case "top": return "top"; - } - return "bottom"; - }); -}; -/** - * Constructs a new dot mark with default properties. Dots are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents a dot; a dot is simply a sized glyph centered at a given - * point that can also be stroked and filled. The size property is - * proportional to the area of the rendered glyph to encourage meaningful visual - * encodings. Dots can visually encode up to eight dimensions of data, though - * this may be unwise due to integrality. See {@link pv.Mark} for details on the - * prioritization of redundant positioning properties. - * - *

See also the Dot guide. - * - * @extends pv.Mark - */ -pv.Dot = function() { - pv.Mark.call(this); -}; - -pv.Dot.prototype = pv.extend(pv.Mark) - .property("size") - .property("shape") - .property("angle") - .property("lineWidth") - .property("strokeStyle") - .property("fillStyle"); - -pv.Dot.prototype.type = "dot"; - -/** - * The size of the dot, in square pixels. Square pixels are used such that the - * area of the dot is linearly proportional to the value of the size property, - * facilitating representative encodings. - * - * @see #radius - * @type number - * @name pv.Dot.prototype.size - */ - -/** - * The shape name. Several shapes are supported:

    - * - *
  • cross - *
  • triangle - *
  • diamond - *
  • square - *
  • tick - *
  • circle - * - *
These shapes can be further changed using the {@link #angle} property; - * for instance, a cross can be turned into a plus by rotating. Similarly, the - * tick, which is vertical by default, can be rotated horizontally. Note that - * some shapes (cross and tick) do not have interior areas, and thus do not - * support fill style meaningfully. - * - *

Note: it may be more natural to use the {@link pv.Rule} mark for - * horizontal and vertical ticks. The tick shape is only necessary if angled - * ticks are needed. - * - * @type string - * @name pv.Dot.prototype.shape - */ - -/** - * The rotation angle, in radians. Used to rotate shapes, such as to turn a - * cross into a plus. - * - * @type number - * @name pv.Dot.prototype.angle - */ - -/** - * The width of stroked lines, in pixels; used in conjunction with - * strokeStyle to stroke the dot's shape. - * - * @type number - * @name pv.Dot.prototype.lineWidth - */ - -/** - * The style of stroked lines; used in conjunction with lineWidth to - * stroke the dot's shape. The default value of this property is a categorical - * color. - * - * @type string - * @name pv.Dot.prototype.strokeStyle - * @see pv.color - */ - -/** - * The fill style; if non-null, the interior of the dot is filled with the - * specified color. The default value of this property is null, meaning dots are - * not filled by default. - * - * @type string - * @name pv.Dot.prototype.fillStyle - * @see pv.color - */ - -/** - * Default properties for dots. By default, there is no fill and the stroke - * style is a categorical color. The default shape is "circle" with size 20. - * - * @type pv.Dot - */ -pv.Dot.prototype.defaults = new pv.Dot() - .extend(pv.Mark.prototype.defaults) - .size(20) - .shape("circle") - .lineWidth(1.5) - .strokeStyle(defaultStrokeStyle); - -/** - * Constructs a new dot anchor with default properties. Dots support five - * different anchors:

    - * - *
  • top - *
  • left - *
  • center - *
  • bottom - *
  • right - * - *
In addition to positioning properties (left, right, top bottom), the - * anchors support text rendering properties (text-align, text-baseline). Text is - * rendered to appear outside the dot. Note that this behavior is different from - * other mark anchors, which default to rendering text inside the mark. - * - *

For consistency with the other mark types, the anchor positions are - * defined in terms of their opposite edge. For example, the top anchor defines - * the bottom property, such that a bar added to the top anchor grows upward. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} - */ -pv.Dot.prototype.anchor = function(name) { - var dot = this; - return pv.Mark.prototype.anchor.call(this, name) - .left(function(d) { - switch (this.name()) { - case "bottom": - case "top": - case "center": return dot.left(); - case "right": return dot.left() + dot.radius(); - } - return null; - }) - .right(function(d) { - switch (this.name()) { - case "bottom": - case "top": - case "center": return dot.right(); - case "left": return dot.right() + dot.radius(); - } - return null; - }) - .top(function(d) { - switch (this.name()) { - case "left": - case "right": - case "center": return dot.top(); - case "bottom": return dot.top() + dot.radius(); - } - return null; - }) - .bottom(function(d) { - switch (this.name()) { - case "left": - case "right": - case "center": return dot.bottom(); - case "top": return dot.bottom() + dot.radius(); - } - return null; - }) - .textAlign(function(d) { - switch (this.name()) { - case "left": return "right"; - case "bottom": - case "top": - case "center": return "center"; - } - return "left"; - }) - .textBaseline(function(d) { - switch (this.name()) { - case "right": - case "left": - case "center": return "middle"; - case "bottom": return "top"; - } - return "bottom"; - }); -}; - -/** - * Returns the radius of the dot, which is defined to be the square root of the - * {@link #size} property. - * - * @returns {number} the radius. - */ -pv.Dot.prototype.radius = function() { - return Math.sqrt(this.size()); -}; -/** - * Constructs a new label mark with default properties. Labels are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents a text label, allowing textual annotation of other marks or - * arbitrary text within the visualization. The character data must be plain - * text (unicode), though the text can be styled using the {@link #font} - * property. If rich text is needed, external HTML elements can be overlaid on - * the canvas by hand. - * - *

Labels are positioned using the box model, similarly to {@link Dot}. Thus, - * a label has no width or height, but merely a text anchor location. The text - * is positioned relative to this anchor location based on the - * {@link #textAlign}, {@link #textBaseline} and {@link #textMargin} properties. - * Furthermore, the text may be rotated using {@link #textAngle}. - * - *

Labels ignore events, so as to not interfere with event handlers on - * underlying marks, such as bars. In the future, we may support event handlers - * on labels. - * - *

See also the Label guide. - * - * @extends pv.Mark - */ -pv.Label = function() { - pv.Mark.call(this); -}; - -pv.Label.prototype = pv.extend(pv.Mark) - .property("text") - .property("font") - .property("textAngle") - .property("textStyle") - .property("textAlign") - .property("textBaseline") - .property("textMargin") - .property("textShadow"); - -pv.Label.prototype.type = "label"; - -/** - * The character data to render; a string. The default value of the text - * property is the identity function, meaning the label's associated datum will - * be rendered using its toString. - * - * @type string - * @name pv.Label.prototype.text - */ - -/** - * The font format, per the CSS Level 2 specification. The default font is "10px - * sans-serif", for consistency with the HTML 5 canvas element specification. - * Note that since text is not wrapped, any line-height property will be - * ignored. The other font-style, font-variant, font-weight, font-size and - * font-family properties are supported. - * - * @see CSS2 fonts - * @type string - * @name pv.Label.prototype.font - */ - -/** - * The rotation angle, in radians. Text is rotated clockwise relative to the - * anchor location. For example, with the default left alignment, an angle of - * Math.PI / 2 causes text to proceed downwards. The default angle is zero. - * - * @type number - * @name pv.Label.prototype.textAngle - */ - -/** - * The text color. The name "textStyle" is used for consistency with "fillStyle" - * and "strokeStyle", although it might be better to rename this property (and - * perhaps use the same name as "strokeStyle"). The default color is black. - * - * @type string - * @name pv.Label.prototype.textStyle - * @see pv.color - */ - -/** - * The horizontal text alignment. One of:

    - * - *
  • left - *
  • center - *
  • right - * - *
The default horizontal alignment is left. - * - * @type string - * @name pv.Label.prototype.textAlign - */ - -/** - * The vertical text alignment. One of:
    - * - *
  • top - *
  • middle - *
  • bottom - * - *
The default vertical alignment is bottom. - * - * @type string - * @name pv.Label.prototype.textBaseline - */ - -/** - * The text margin; may be specified in pixels, or in font-dependent units (such - * as ".1ex"). The margin can be used to pad text away from its anchor location, - * in a direction dependent on the horizontal and vertical alignment - * properties. For example, if the text is left- and middle-aligned, the margin - * shifts the text to the right. The default margin is 3 pixels. - * - * @type number - * @name pv.Label.prototype.textMargin - */ - -/** - * A list of shadow effects to be applied to text, per the CSS Text Level 3 - * text-shadow property. An example specification is "0.1em 0.1em 0.1em - * rgba(0,0,0,.5)"; the first length is the horizontal offset, the second the - * vertical offset, and the third the blur radius. - * - * @see CSS3 text - * @type string - * @name pv.Label.prototype.textShadow - */ - -/** - * Default properties for labels. See the individual properties for the default - * values. - * - * @type pv.Label - */ -pv.Label.prototype.defaults = new pv.Label() - .extend(pv.Mark.prototype.defaults) - .text(pv.identity) - .font("10px sans-serif") - .textAngle(0) - .textStyle("black") - .textAlign("left") - .textBaseline("bottom") - .textMargin(3); -/** - * Constructs a new line mark with default properties. Lines are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents a series of connected line segments, or polyline, - * that can be stroked with a configurable color and thickness. Each - * articulation point in the line corresponds to a datum; for n points, - * n-1 connected line segments are drawn. The point is positioned using - * the box model. Arbitrary paths are also possible, allowing radar plots and - * other custom visualizations. - * - *

Like areas, lines can be stroked and filled with arbitrary colors. In most - * cases, lines are only stroked, but the fill style can be used to construct - * arbitrary polygons. - * - *

See also the Line guide. - * - * @extends pv.Mark - */ -pv.Line = function() { - pv.Mark.call(this); -}; - -pv.Line.prototype = pv.extend(pv.Mark) - .property("lineWidth") - .property("strokeStyle") - .property("fillStyle") - .property("segmented") - .property("interpolate"); - -pv.Line.prototype.type = "line"; - -/** - * The width of stroked lines, in pixels; used in conjunction with - * strokeStyle to stroke the line. - * - * @type number - * @name pv.Line.prototype.lineWidth - */ - -/** - * The style of stroked lines; used in conjunction with lineWidth to - * stroke the line. The default value of this property is a categorical color. - * - * @type string - * @name pv.Line.prototype.strokeStyle - * @see pv.color - */ - -/** - * The line fill style; if non-null, the interior of the line is closed and - * filled with the specified color. The default value of this property is a - * null, meaning that lines are not filled by default. - * - * @type string - * @name pv.Line.prototype.fillStyle - * @see pv.color - */ - -/** - * Whether the line is segmented; whether variations in stroke style, line width - * and the other properties are treated as fixed. Rendering segmented lines is - * noticeably slower than non-segmented lines. - * - *

This property is fixed. See {@link pv.Mark}. - * - * @type boolean - * @name pv.Line.prototype.segmented - */ - -/** - * How to interpolate between values. Linear interpolation ("linear") is the - * default, producing a straight line between points. For piecewise constant - * functions (i.e., step functions), either "step-before" or "step-after" can be - * specified. - * - *

Note: this property is currently supported only on non-segmented lines. - * - *

This property is fixed. See {@link pv.Mark}. - * - * @type string - * @name pv.Line.prototype.interpolate - */ - -/** - * Default properties for lines. By default, there is no fill and the stroke - * style is a categorical color. The default interpolation is linear. - * - * @type pv.Line - */ -pv.Line.prototype.defaults = new pv.Line() - .extend(pv.Mark.prototype.defaults) - .lineWidth(1.5) - .strokeStyle(defaultStrokeStyle) - .interpolate("linear"); - -/** @private */ -var pv_Line_specials = {left:1, top:1, right:1, bottom:1, name:1}; - -/** @private */ -pv.Line.prototype.bind = function() { - pv.Mark.prototype.bind.call(this); - var binds = this.binds, - properties = binds.properties, - specials = binds.specials = []; - for (var i = 0, n = properties.length; i < n; i++) { - var p = properties[i]; - if (p.name in pv_Line_specials) specials.push(p); - } -}; - -/** @private */ -pv.Line.prototype.buildInstance = function(s) { - if (this.index && !this.scene[0].segmented) { - this.buildProperties(s, this.binds.specials); - this.buildImplied(s); - } else { - pv.Mark.prototype.buildInstance.call(this, s); - } -}; -/** - * Constructs a new rule with default properties. Rules are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents a horizontal or vertical rule. Rules are frequently used - * for axes and grid lines. For example, specifying only the bottom property - * draws horizontal rules, while specifying only the left draws vertical - * rules. Rules can also be used as thin bars. The visual style is controlled in - * the same manner as lines. - * - *

Rules are positioned exclusively the standard box model properties. The - * following combinations of properties are supported: - * - *

- * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
PropertiesOrientation
leftvertical
rightvertical
left, bottom, topvertical
right, bottom, topvertical
tophorizontal
bottomhorizontal
top, left, righthorizontal
bottom, left, righthorizontal
left, top, heightvertical
left, bottom, heightvertical
right, top, heightvertical
right, bottom, heightvertical
left, top, widthhorizontal
left, bottom, widthhorizontal
right, top, widthhorizontal
right, bottom, widthhorizontal
- * - *

Small rules can be used as tick marks; alternatively, a {@link Dot} with - * the "tick" shape can be used. - * - *

See also the Rule guide. - * - * @see pv.Line - * @extends pv.Mark - */ -pv.Rule = function() { - pv.Mark.call(this); -}; - -pv.Rule.prototype = pv.extend(pv.Mark) - .property("width") - .property("height") - .property("lineWidth") - .property("strokeStyle"); - -pv.Rule.prototype.type = "rule"; - -/** - * The width of the rule, in pixels. If the left position is specified, the rule - * extends rightward from the left edge; if the right position is specified, the - * rule extends leftward from the right edge. - * - * @type number - * @name pv.Rule.prototype.width - */ - -/** - * The height of the rule, in pixels. If the bottom position is specified, the - * rule extends upward from the bottom edge; if the top position is specified, - * the rule extends downward from the top edge. - * - * @type number - * @name pv.Rule.prototype.height - */ - -/** - * The width of stroked lines, in pixels; used in conjunction with - * strokeStyle to stroke the rule. The default value is 1 pixel. - * - * @type number - * @name pv.Rule.prototype.lineWidth - */ - -/** - * The style of stroked lines; used in conjunction with lineWidth to - * stroke the rule. The default value of this property is black. - * - * @type string - * @name pv.Rule.prototype.strokeStyle - * @see pv.color - */ - -/** - * Default properties for rules. By default, a single-pixel black line is - * stroked. - * - * @type pv.Rule - */ -pv.Rule.prototype.defaults = new pv.Rule() - .extend(pv.Mark.prototype.defaults) - .lineWidth(1) - .strokeStyle("black"); - -/** - * Constructs a new rule anchor with default properties. Rules support five - * different anchors:

    - * - *
  • top - *
  • left - *
  • center - *
  • bottom - *
  • right - * - *
In addition to positioning properties (left, right, top bottom), the - * anchors support text rendering properties (text-align, text-baseline). Text is - * rendered to appear outside the rule. Note that this behavior is different - * from other mark anchors, which default to rendering text inside the - * mark. - * - *

For consistency with the other mark types, the anchor positions are - * defined in terms of their opposite edge. For example, the top anchor defines - * the bottom property, such that a bar added to the top anchor grows upward. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} - */ -pv.Rule.prototype.anchor = function(name) { - return pv.Bar.prototype.anchor.call(this, name) - .textAlign(function(d) { - switch (this.name()) { - case "left": return "right"; - case "bottom": - case "top": - case "center": return "center"; - case "right": return "left"; - } - }) - .textBaseline(function(d) { - switch (this.name()) { - case "right": - case "left": - case "center": return "middle"; - case "top": return "bottom"; - case "bottom": return "top"; - } - }); -}; - -/** - * @private Overrides the default behavior of {@link pv.Mark.buildImplied} to - * determine the orientation (vertical or horizontal) of the rule. - * - * @param s a node in the scene graph; the instance of the rule to build. - */ -pv.Rule.prototype.buildImplied = function(s) { - var l = s.left, r = s.right, t = s.top, b = s.bottom; - - /* Determine horizontal or vertical orientation. */ - if ((s.width != null) - || ((l == null) && (r == null)) - || ((r != null) && (l != null))) { - s.height = 0; - } else { - s.width = 0; - } - - pv.Mark.prototype.buildImplied.call(this, s); -}; -/** - * Constructs a new, empty panel with default properties. Panels, with the - * exception of the root panel, are not typically constructed directly; instead, - * they are added to an existing panel or mark via {@link pv.Mark#add}. - * - * @class Represents a container mark. Panels allow repeated or nested - * structures, commonly used in small multiple displays where a small - * visualization is tiled to facilitate comparison across one or more - * dimensions. Other types of visualizations may benefit from repeated and - * possibly overlapping structure as well, such as stacked area charts. Panels - * can also offset the position of marks to provide padding from surrounding - * content. - * - *

All Protovis displays have at least one panel; this is the root panel to - * which marks are rendered. The box model properties (four margins, width and - * height) are used to offset the positions of contained marks. The data - * property determines the panel count: a panel is generated once per associated - * datum. When nested panels are used, property functions can declare additional - * arguments to access the data associated with enclosing panels. - * - *

Panels can be rendered inline, facilitating the creation of sparklines. - * This allows designers to reuse browser layout features, such as text flow and - * tables; designers can also overlay HTML elements such as rich text and - * images. - * - *

All panels have a children array (possibly empty) containing the - * child marks in the order they were added. Panels also have a root - * field which points to the root (outermost) panel; the root panel's root field - * points to itself. - * - *

See also the Protovis guide. - * - * @extends pv.Bar - */ -pv.Panel = function() { - pv.Bar.call(this); - - /** - * The child marks; zero or more {@link pv.Mark}s in the order they were - * added. - * - * @see #add - * @type pv.Mark[] - */ - this.children = []; - this.root = this; - - /** - * The internal $dom field is set by the Protovis loader; see lang/init.js. It - * refers to the script element that contains the Protovis specification, so - * that the panel knows where in the DOM to insert the generated SVG element. - * - * @private - */ - this.$dom = pv.Panel.$dom; -}; - -pv.Panel.prototype = pv.extend(pv.Bar) - .property("canvas") - .property("overflow"); - -pv.Panel.prototype.type = "panel"; - -/** - * The canvas element; either the string ID of the canvas element in the current - * document, or a reference to the canvas element itself. If null, a canvas - * element will be created and inserted into the document at the location of the - * script element containing the current Protovis specification. This property - * only applies to root panels and is ignored on nested panels. - * - *

Note: the "canvas" element here refers to a div (or other suitable - * HTML container element), not a canvas element. The name of - * this property is a historical anachronism from the first implementation that - * used HTML 5 canvas, rather than SVG. - * - * @type string - * @name pv.Panel.prototype.canvas - */ - -/** - * Default properties for panels. By default, the margins are zero, the fill - * style is transparent. - * - * @type pv.Panel - */ -pv.Panel.prototype.defaults = new pv.Panel() - .extend(pv.Bar.prototype.defaults) - .fillStyle(null) - .overflow("visible"); - -/** - * Returns an anchor with the specified name. This method is overridden since - * the behavior of Panel anchors is slightly different from normal anchors: - * adding to an anchor adds to the anchor target's, rather than the anchor - * target's parent. To avoid double margins, we override the anchor's proto so - * that the margins are zero. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} the new anchor. - */ -pv.Panel.prototype.anchor = function(name) { - - /* A "view" of this panel whose margins appear to be zero. */ - function z() { return 0; } - z.prototype = this; - z.prototype.left = z.prototype.right = z.prototype.top = z.prototype.bottom = z; - - var anchor = pv.Bar.prototype.anchor.call(new z(), name) - .data(function(d) { return [d]; }); - anchor.parent = this; - return anchor; -}; - -/** - * Adds a new mark of the specified type to this panel. Unlike the normal - * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark - * to inherit from the panel. Since the contained marks are offset by the panel - * margins already, inheriting properties is generally undesirable; of course, - * it is always possible to change this behavior by calling {@link Mark#extend} - * explicitly. - * - * @param {function} type the type of the new mark to add. - * @returns {pv.Mark} the new mark. - */ -pv.Panel.prototype.add = function(type) { - var child = new type(); - child.parent = this; - child.root = this.root; - child.childIndex = this.children.length; - this.children.push(child); - return child; -}; - -/** @private TODO */ -pv.Panel.prototype.bind = function() { - pv.Mark.prototype.bind.call(this); - for (var i = 0; i < this.children.length; i++) { - this.children[i].bind(); - } -}; - -/** - * @private Evaluates all of the properties for this panel for the specified - * instance s in the scene graph, including recursively building the - * scene graph for child marks. - * - * @param s a node in the scene graph; the instance of the panel to build. - * @see Mark#scene - */ -pv.Panel.prototype.buildInstance = function(s) { - pv.Bar.prototype.buildInstance.call(this, s); - if (!s.children) s.children = []; - - /* - * Build each child, passing in the parent (this panel) scene graph node. The - * child mark's scene is initialized from the corresponding entry in the - * existing scene graph, such that properties from the previous build can be - * reused; this is largely to facilitate the recycling of SVG elements. - */ - for (var i = 0; i < this.children.length; i++) { - this.children[i].scene = s.children[i]; // possibly undefined - this.children[i].build(); - } - - /* - * Once the child marks have been built, the new scene graph nodes are removed - * from the child marks and placed into the scene graph. The nodes cannot - * remain on the child nodes because this panel (or a parent panel) may be - * instantiated multiple times! - */ - for (var i = 0; i < this.children.length; i++) { - s.children[i] = this.children[i].scene; - delete this.children[i].scene; - } - - /* Delete any expired child scenes, should child marks have been removed. */ - s.children.length = this.children.length; -}; - -/** - * @private Computes the implied properties for this panel for the specified - * instance s in the scene graph. Panels have two implied - * properties:

    - * - *
  • The canvas property references the DOM element, typically a DIV, - * that contains the SVG element that is used to display the visualization. This - * property may be specified as a string, referring to the unique ID of the - * element in the DOM. The string is converted to a reference to the DOM - * element. The width and height of the SVG element is inferred from this DOM - * element. If no canvas property is specified, a new SVG element is created and - * inserted into the document, using the panel dimensions; see - * {@link #createCanvas}. - * - *
  • The children array, while not a property per se, contains the - * scene graph for each child mark. This array is initialized to be empty, and - * is populated above in {@link #buildInstance}. - * - *
The current implementation creates the SVG element, if necessary, during - * the build phase; in the future, it may be preferrable to move this to the - * update phase, although then the canvas property would be undefined. In - * addition, DOM inspection is necessary to define the implied width and height - * properties that may be inferred from the DOM. - * - * @param s a node in the scene graph; the instance of the panel to build. - */ -pv.Panel.prototype.buildImplied = function(s) { - if (!this.parent) { - var c = s.canvas; - if (c) { - if (typeof c == "string") c = document.getElementById(c); - - /* Clear the container if it's not associated with this panel. */ - if (c.$panel != this) { - c.$panel = this; - c.innerHTML = ""; - } - - /* If width and height weren't specified, inspect the container. */ - var w, h; - if (s.width == null) { - w = parseFloat(pv.css(c, "width")); - s.width = w - s.left - s.right; - } - if (s.height == null) { - h = parseFloat(pv.css(c, "height")); - s.height = h - s.top - s.bottom; - } - } else if (s.$canvas) { - - /* - * If the canvas property is null, and we previously created a canvas for - * this scene node, reuse the previous canvas rather than creating a new - * one. - */ - c = s.$canvas; - } else { - - /** - * Returns the last element in the current document's body. The canvas - * element is appended to this last element if another DOM element has not - * already been specified via the $dom field. - */ - function lastElement() { - var node = document.body; - while (node.lastChild && node.lastChild.tagName) { - node = node.lastChild; - } - return (node == document.body) ? node : node.parentNode; - } - - /* Insert a new container into the DOM. */ - c = s.$canvas = document.createElement("span"); - this.$dom // script element for text/javascript+protovis - ? this.$dom.parentNode.insertBefore(c, this.$dom) - : lastElement().appendChild(c); - } - s.canvas = c; - } - pv.Bar.prototype.buildImplied.call(this, s); -}; -/** - * Constructs a new dot mark with default properties. Images are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents an image. Images share the same layout and style properties as - * bars, in conjunction with an external image such as PNG or JPEG. The image is - * specified via the {@link #url} property. The fill, if specified, appears - * beneath the image, while the optional stroke appears above the image. - * - *

TODO Restore support for dynamic images (such as heatmaps). These were - * supported in the canvas implementation using the pixel buffer API; although - * SVG does not support pixel manipulation, it is possible to embed a canvas - * element in SVG using foreign objects. - * - *

TODO Allow different modes of image placement: "scale" -- scale and - * preserve aspect ratio, "tile" -- repeat the image, "center" -- center the - * image, "fill" -- scale without preserving aspect ratio. - * - *

See {@link pv.Bar} for details on positioning properties. - * - * @extends pv.Bar - */ -pv.Image = function() { - pv.Bar.call(this); -}; - -pv.Image.prototype = pv.extend(pv.Bar) - .property("url"); - -pv.Image.prototype.type = "image"; - -/** - * The URL of the image to display. The set of supported image types is - * browser-dependent; PNG and JPEG are recommended. - * - * @type string - * @name pv.Image.prototype.url - */ - -/** - * Default properties for images. By default, there is no stroke or fill style. - * - * @type pv.Image - */ -pv.Image.prototype.defaults = new pv.Image() - .extend(pv.Bar.prototype.defaults) - .fillStyle(null); -/** - * Constructs a new wedge with default properties. Wedges are not typically - * constructed directly, but by adding to a panel or an existing mark via - * {@link pv.Mark#add}. - * - * @class Represents a wedge, or pie slice. Specified in terms of start and end - * angle, inner and outer radius, wedges can be used to construct donut charts - * and polar bar charts as well. If the {@link #angle} property is used, the end - * angle is implied by adding this value to start angle. By default, the start - * angle is the previously-generated wedge's end angle. This design allows - * explicit control over the wedge placement if desired, while offering - * convenient defaults for the construction of radial graphs. - * - *

The center point of the circle is positioned using the standard box model. - * The wedge can be stroked and filled, similar to {link Bar}. - * - *

See also the Wedge guide. - * - * @extends pv.Mark - */ -pv.Wedge = function() { - pv.Mark.call(this); -}; - -pv.Wedge.prototype = pv.extend(pv.Mark) - .property("startAngle") - .property("endAngle") - .property("angle") - .property("innerRadius") - .property("outerRadius") - .property("lineWidth") - .property("strokeStyle") - .property("fillStyle"); - -pv.Wedge.prototype.type = "wedge"; - -/** - * The start angle of the wedge, in radians. The start angle is measured - * clockwise from the 3 o'clock position. The default value of this property is - * the end angle of the previous instance (the {@link Mark#sibling}), or -PI / 2 - * for the first wedge; for pie and donut charts, typically only the - * {@link #angle} property needs to be specified. - * - * @type number - * @name pv.Wedge.prototype.startAngle - */ - -/** - * The end angle of the wedge, in radians. If not specified, the end angle is - * implied as the start angle plus the {@link #angle}. - * - * @type number - * @name pv.Wedge.prototype.endAngle - */ - -/** - * The angular span of the wedge, in radians. This property is used if end angle - * is not specified. - * - * @type number - * @name pv.Wedge.prototype.angle - */ - -/** - * The inner radius of the wedge, in pixels. The default value of this property - * is zero; a positive value will produce a donut slice rather than a pie slice. - * The inner radius can vary per-wedge. - * - * @type number - * @name pv.Wedge.prototype.innerRadius - */ - -/** - * The outer radius of the wedge, in pixels. This property is required. For - * pies, only this radius is required; for donuts, the inner radius must be - * specified as well. The outer radius can vary per-wedge. - * - * @type number - * @name pv.Wedge.prototype.outerRadius - */ - -/** - * The width of stroked lines, in pixels; used in conjunction with - * strokeStyle to stroke the wedge's border. - * - * @type number - * @name pv.Wedge.prototype.lineWidth - */ - -/** - * The style of stroked lines; used in conjunction with lineWidth to - * stroke the wedge's border. The default value of this property is null, - * meaning wedges are not stroked by default. - * - * @type string - * @name pv.Wedge.prototype.strokeStyle - * @see pv.color - */ - -/** - * The wedge fill style; if non-null, the interior of the wedge is filled with - * the specified color. The default value of this property is a categorical - * color. - * - * @type string - * @name pv.Wedge.prototype.fillStyle - * @see pv.color - */ - -/** - * Default properties for wedges. By default, there is no stroke and the fill - * style is a categorical color. - * - * @type pv.Wedge - */ -pv.Wedge.prototype.defaults = new pv.Wedge() - .extend(pv.Mark.prototype.defaults) - .startAngle(function() { - var s = this.sibling(); - return s ? s.endAngle : -Math.PI / 2; - }) - .innerRadius(0) - .lineWidth(1.5) - .strokeStyle(null) - .fillStyle(defaultFillStyle.by(pv.index)); - -/** - * Returns the mid-radius of the wedge, which is defined as half-way between the - * inner and outer radii. - * - * @see #innerRadius - * @see #outerRadius - * @returns {number} the mid-radius, in pixels. - */ -pv.Wedge.prototype.midRadius = function() { - return (this.innerRadius() + this.outerRadius()) / 2; -}; - -/** - * Returns the mid-angle of the wedge, which is defined as half-way between the - * start and end angles. - * - * @see #startAngle - * @see #endAngle - * @returns {number} the mid-angle, in radians. - */ -pv.Wedge.prototype.midAngle = function() { - return (this.startAngle() + this.endAngle()) / 2; -}; - -/** - * Constructs a new wedge anchor with default properties. Wedges support five - * different anchors:

    - * - *
  • outer - *
  • inner - *
  • center - *
  • start - *
  • end - * - *
In addition to positioning properties (left, right, top bottom), the - * anchors support text rendering properties (text-align, text-baseline, - * textAngle). Text is rendered to appear inside the wedge. - * - * @param {string} name the anchor name; either a string or a property function. - * @returns {pv.Anchor} - */ -pv.Wedge.prototype.anchor = function(name) { - var w = this; - return pv.Mark.prototype.anchor.call(this, name) - .left(function() { - switch (this.name()) { - case "outer": return w.left() + w.outerRadius() * Math.cos(w.midAngle()); - case "inner": return w.left() + w.innerRadius() * Math.cos(w.midAngle()); - case "start": return w.left() + w.midRadius() * Math.cos(w.startAngle()); - case "center": return w.left() + w.midRadius() * Math.cos(w.midAngle()); - case "end": return w.left() + w.midRadius() * Math.cos(w.endAngle()); - } - }) - .right(function() { - switch (this.name()) { - case "outer": return w.right() + w.outerRadius() * Math.cos(w.midAngle()); - case "inner": return w.right() + w.innerRadius() * Math.cos(w.midAngle()); - case "start": return w.right() + w.midRadius() * Math.cos(w.startAngle()); - case "center": return w.right() + w.midRadius() * Math.cos(w.midAngle()); - case "end": return w.right() + w.midRadius() * Math.cos(w.endAngle()); - } - }) - .top(function() { - switch (this.name()) { - case "outer": return w.top() + w.outerRadius() * Math.sin(w.midAngle()); - case "inner": return w.top() + w.innerRadius() * Math.sin(w.midAngle()); - case "start": return w.top() + w.midRadius() * Math.sin(w.startAngle()); - case "center": return w.top() + w.midRadius() * Math.sin(w.midAngle()); - case "end": return w.top() + w.midRadius() * Math.sin(w.endAngle()); - } - }) - .bottom(function() { - switch (this.name()) { - case "outer": return w.bottom() + w.outerRadius() * Math.sin(w.midAngle()); - case "inner": return w.bottom() + w.innerRadius() * Math.sin(w.midAngle()); - case "start": return w.bottom() + w.midRadius() * Math.sin(w.startAngle()); - case "center": return w.bottom() + w.midRadius() * Math.sin(w.midAngle()); - case "end": return w.bottom() + w.midRadius() * Math.sin(w.endAngle()); - } - }) - .textAlign(function() { - switch (this.name()) { - case "outer": return pv.Wedge.upright(w.midAngle()) ? "right" : "left"; - case "inner": return pv.Wedge.upright(w.midAngle()) ? "left" : "right"; - } - return "center"; - }) - .textBaseline(function() { - switch (this.name()) { - case "start": return pv.Wedge.upright(w.startAngle()) ? "top" : "bottom"; - case "end": return pv.Wedge.upright(w.endAngle()) ? "bottom" : "top"; - } - return "middle"; - }) - .textAngle(function() { - var a = 0; - switch (this.name()) { - case "center": - case "inner": - case "outer": a = w.midAngle(); break; - case "start": a = w.startAngle(); break; - case "end": a = w.endAngle(); break; - } - return pv.Wedge.upright(a) ? a : (a + Math.PI); - }); -}; - -/** - * Returns true if the specified angle is considered "upright", as in, text - * rendered at that angle would appear upright. If the angle is not upright, - * text is rotated 180 degrees to be upright, and the text alignment properties - * are correspondingly changed. - * - * @param {number} angle an angle, in radius. - * @returns {boolean} true if the specified angle is upright. - */ -pv.Wedge.upright = function(angle) { - angle = angle % (2 * Math.PI); - angle = (angle < 0) ? (2 * Math.PI + angle) : angle; - return (angle < Math.PI / 2) || (angle > 3 * Math.PI / 2); -}; - -/** - * @private Overrides the default behavior of {@link pv.Mark.buildImplied} such - * that the end angle is computed from the start angle and angle (angular span) - * if not specified. - * - * @param s a node in the scene graph; the instance of the wedge to build. - */ -pv.Wedge.prototype.buildImplied = function(s) { - pv.Mark.prototype.buildImplied.call(this, s); - - /* - * TODO If the angle or endAngle is updated by an event handler, the implied - * properties won't recompute correctly, so this will lead to potentially - * buggy redraw. How to re-evaluate implied properties on update? - */ - if (s.endAngle == null) s.endAngle = s.startAngle + s.angle; - if (s.angle == null) s.angle = s.endAngle - s.startAngle; -}; -/** - * @ignore - * @namespace - */ -pv.Layout = {}; -/** - * Returns a new grid layout. - * - * @class A grid layout with regularly-sized rows and columns. The number of rows - * and columns are determined from the array, which should be in row-major - * order. For example, the 2×3 array: - * - *
1 2 3
- * 4 5 6
- * - * should be represented as: - * - *
[[1, 2, 3], [4, 5, 6]]
- * - * If your data is in column-major order, you can use {@link pv.transpose} to - * transpose it. - * - *

This layout defines left, top, width, height and data properties. The data - * property will be the associated element in the array. For example, if the - * array is a two-dimensional array of values in the range [0,1], a simple - * heatmap can be generated as: - * - *

.add(pv.Bar)
- *   .extend(pv.Layout.grid(array))
- *   .fillStyle(pv.ramp("white", "black"))
- * - * By default, the grid fills the full width and height of the parent panel. - * - * @param {array[]} arrays an array of arrays. - * @returns {pv.Layout.grid} a grid layout. - */ -pv.Layout.grid = function(arrays) { - var rows = arrays.length, cols = arrays[0].length; - - /** @private */ - function w() { return this.parent.width() / cols; } - - /** @private */ - function h() { return this.parent.height() / rows; } - - /* A dummy mark, like an anchor, which the caller extends. */ - return new pv.Mark() - .data(pv.blend(arrays)) - .left(function() { return w.call(this) * (this.index % cols); }) - .top(function() { return h.call(this) * Math.floor(this.index / cols); }) - .width(w) - .height(h); -}; -/** - * Returns a new stack layout. - * - * @class A layout for stacking marks vertically or horizontally, using the - * cousin instance. This layout is designed to be used for one of the - * four positional properties in the box model, and changes behavior depending - * on the property being evaluated:
    - * - *
  • bottom: cousin.bottom + cousin.height - *
  • top: cousin.top + cousin.height - *
  • left: cousin.left + cousin.width - *
  • right: cousin.right + cousin.width - * - *
If no cousin instance is available (for example, for first instance), - * the specified offset is used. If no offset is specified, zero is used. For - * example, - * - *
new pv.Panel()
- *     .width(150).height(150)
- *   .add(pv.Panel)
- *     .data([[1, 1.2, 1.7, 1.5, 1.7],
- *            [.5, 1, .8, 1.1, 1.3],
- *            [.2, .5, .8, .9, 1]])
- *   .add(pv.Area)
- *     .data(function(d) d)
- *     .bottom(pv.Layout.stack())
- *     .height(function(d) d * 40)
- *     .left(function() this.index * 35)
- *   .root.render();
- * - * specifies a vertically-stacked area chart. - * - * @returns {pv.Layout.stack} a stack property function. - * @see pv.Mark#cousin - */ -pv.Layout.stack = function() { - /** @private */ - var offset = function() { return 0; }; - - /** @private */ - function layout() { - - /* Find the previous visible parent instance. */ - var i = this.parent.index, p, c; - while ((i-- > 0) && !c) { - p = this.parent.scene[i]; - if (p.visible) c = p.children[this.childIndex][this.index]; - } - - if (c) { - switch (property) { - case "bottom": return c.bottom + c.height; - case "top": return c.top + c.height; - case "left": return c.left + c.width; - case "right": return c.right + c.width; - } - } - - return offset.apply(this, arguments); - } - - /** - * Sets the offset for this stack layout. The offset can either be specified - * as a function or as a constant. If a function, the function is invoked in - * the same context as a normal property function: this refers to the - * mark, and the arguments are the full data stack. By default the offset is - * zero. - * - * @function - * @name pv.Layout.stack.prototype.offset - * @param {function} f offset function, or constant value. - * @returns {pv.Layout.stack} this. - */ - layout.offset = function(f) { - offset = (f instanceof Function) ? f : function() { return f; }; - return this; - }; - - return layout; -}; -// TODO share code with Treemap -// TODO vertical / horizontal orientation? - -/** - * Returns a new icicle tree layout. - * - * @class A tree layout in the form of an icicle. The first row corresponds to the root - * of the tree; subsequent rows correspond to each tier. Rows are subdivided - * into cells based on the size of nodes, per {@link #size}. Within a row, cells - * are sorted by size. - * - *

This tree layout is intended to be extended (see {@link pv.Mark#extend}) - * by a {@link pv.Bar}. The data property returns an array of nodes for use by - * other property functions. The following node attributes are supported: - * - *

    - *
  • left - the cell left position. - *
  • top - the cell top position. - *
  • width - the cell width. - *
  • height - the cell height. - *
  • depth - the node depth (tier; the root is 0). - *
  • keys - an array of string keys for the node. - *
  • size - the aggregate node size. - *
  • children - child nodes, if any. - *
  • data - the associated tree element, for leaf nodes. - *
- * - * To produce a default icicle layout, say: - * - *
.add(pv.Bar)
- *   .extend(pv.Layout.icicle(tree))
- * - * To customize the tree to highlight leaf nodes bigger than 10,000 (1E4), you - * might say: - * - *
.add(pv.Bar)
- *   .extend(pv.Layout.icicle(tree))
- *   .fillStyle(function(n) n.data > 1e4 ? "#ff0" : "#fff")
- * - * The format of the tree argument is any hierarchical object whose - * leaf nodes are numbers corresponding to their size. For an example, and - * information on how to convert tabular data into such a tree, see - * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function - * can be specified to override how the tree is interpreted. This size function - * can also be used to transform the data. - * - *

By default, the icicle fills the full width and height of the parent - * panel. An optional root key can be specified using {@link #root} for - * convenience. - * - * @param tree a tree (an object) who leaf attributes have sizes. - * @returns {pv.Layout.icicle} a tree layout. - */ -pv.Layout.icicle = function(tree) { - var keys = [], sizeof = Number; - - /** @private */ - function accumulate(map) { - var node = {size: 0, children: [], keys: keys.slice()}; - for (var key in map) { - var child = map[key], size = sizeof(child); - keys.push(key); - if (isNaN(size)) { - child = accumulate(child); - } else { - child = {size: size, data: child, keys: keys.slice()}; - } - node.children.push(child); - node.size += child.size; - keys.pop(); - } - node.children.sort(function(a, b) { return b.size - a.size; }); - return node; - } - - /** @private */ - function scale(node, k) { - node.size *= k; - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - scale(node.children[i], k); - } - } - } - - /** @private */ - function depth(node, i) { - i = i ? (i + 1) : 1; - return node.children - ? pv.max(node.children, function(n) { return depth(n, i); }) - : i; - } - - /** @private */ - function layout(node) { - if (node.children) { - icify(node); - for (var i = 0; i < node.children.length; i++) { - layout(node.children[i]); - } - } - } - - /** @private */ - function icify(node) { - var left = node.left; - for (var i = 0; i < node.children.length; i++) { - var child = node.children[i], width = (child.size / node.size) * node.width; - child.left = left; - child.top = node.top + node.height; - child.width = width; - child.height = node.height; - child.depth = node.depth + 1; - left += width; - if (child.children) { - icify(child); - } - } - } - - /** @private */ - function flatten(node, array) { - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - flatten(node.children[i], array); - } - } - array.push(node) - return array; - } - - /** @private */ - function data() { - var root = accumulate(tree); - root.top = 0; - root.left = 0; - root.width = this.parent.width(); - root.height = this.parent.height() / depth(root); - root.depth = 0; - layout(root); - return flatten(root, []).reverse(); - } - - /* A dummy mark, like an anchor, which the caller extends. */ - var mark = new pv.Mark() - .data(data) - .left(function(n) { return n.left; }) - .top(function(n) { return n.top; }) - .width(function(n) { return n.width; }) - .height(function(n) { return n.height; }); - - /** - * Specifies the root key; optional. The root key is prepended to the - * keys attribute for all generated nodes. This method is provided - * for convenience and does not affect layout. - * - * @param {string} v the root key. - * @function - * @name pv.Layout.icicle.prototype.root - * @returns {pv.Layout.icicle} this. - */ - mark.root = function(v) { - keys = [v]; - return this; - }; - - /** - * Specifies the sizing function. By default, the sizing function is - * Number. The sizing function is invoked for each node in the tree - * (passed to the constructor): the sizing function must return - * undefined or NaN for internal nodes, and a number for - * leaf nodes. The aggregate sizes of internal nodes will be automatically - * computed by the layout. - * - *

For example, if the tree data structure represents a file system, with - * files as leaf nodes, and each file has a bytes attribute, you can - * specify a size function as: - * - *

.size(function(d) d.bytes)
- * - * This function will return undefined for internal nodes (since - * these do not have a bytes attribute), and a number for leaf nodes. - * - *

Note that the built-in Math.sqrt and Math.log methods - * can also be used as sizing functions. These function similarly to - * Number, except perform a root and log scale, respectively. - * - * @param {function} f the new sizing function. - * @function - * @name pv.Layout.icicle.prototype.size - * @returns {pv.Layout.icicle} this. - */ - mark.size = function(f) { - sizeof = f; - return this; - }; - - return mark; -}; -// TODO share code with Treemap -// TODO inspect parent panel dimensions to set inner and outer radii - -/** - * Returns a new sunburst tree layout. - * - * @class A tree layout in the form of a sunburst. The - * center circle corresponds to the root of the tree; subsequent rings - * correspond to each tier. Rings are subdivided into wedges based on the size - * of nodes, per {@link #size}. Within a ring, wedges are sorted by size. - * - *

The tree layout is intended to be extended (see {@link pv.Mark#extend} by - * a {@link pv.Wedge}. The data property returns an array of nodes for use by - * other property functions. The following node attributes are supported: - * - *

    - *
  • left - the wedge left position. - *
  • top - the wedge top position. - *
  • innerRadius - the wedge inner radius. - *
  • outerRadius - the wedge outer radius. - *
  • startAngle - the wedge start angle. - *
  • endAngle - the wedge end angle. - *
  • angle - the wedge angle. - *
  • depth - the node depth (tier; the root is 0). - *
  • keys - an array of string keys for the node. - *
  • size - the aggregate node size. - *
  • children - child nodes, if any. - *
  • data - the associated tree element, for leaf nodes. - *
- * - *

To produce a default sunburst layout, say: - * - *

.add(pv.Wedge)
- *   .extend(pv.Layout.sunburst(tree))
- * - * To only show nodes at a depth of two or greater, you might say: - * - *
.add(pv.Wedge)
- *   .extend(pv.Layout.sunburst(tree))
- *   .visible(function(n) n.depth > 1)
- * - * The format of the tree argument is a hierarchical object whose leaf - * nodes are numbers corresponding to their size. For an example, and - * information on how to convert tabular data into such a tree, see - * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function - * can be specified to override how the tree is interpreted. This size function - * can also be used to transform the data. - * - *

By default, the sunburst fills the full width and height of the parent - * panel. An optional root key can be specified using {@link #root} for - * convenience. - * - * @param tree a tree (an object) who leaf attributes have sizes. - * @returns {pv.Layout.sunburst} a tree layout. - */ -pv.Layout.sunburst = function(tree) { - var keys = [], sizeof = Number, w, h, r; - - /** @private */ - function accumulate(map) { - var node = {size: 0, children: [], keys: keys.slice()}; - for (var key in map) { - var child = map[key], size = sizeof(child); - keys.push(key); - if (isNaN(size)) { - child = accumulate(child); - } else { - child = {size: size, data: child, keys: keys.slice()}; - } - node.children.push(child); - node.size += child.size; - keys.pop(); - } - node.children.sort(function(a, b) { return b.size - a.size; }); - return node; - } - - /** @private */ - function scale(node, k) { - node.size *= k; - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - scale(node.children[i], k); - } - } - } - - /** @private */ - function depth(node, i) { - i = i ? (i + 1) : 1; - return node.children - ? pv.max(node.children, function(n) { return depth(n, i); }) - : i; - } - - /** @private */ - function layout(node) { - if (node.children) { - wedgify(node); - for (var i = 0; i < node.children.length; i++) { - layout(node.children[i]); - } - } - } - - /** @private */ - function wedgify(node) { - var startAngle = node.startAngle; - for (var i = 0; i < node.children.length; i++) { - var child = node.children[i], angle = (child.size / node.size) * node.angle; - child.startAngle = startAngle; - child.angle = angle; - child.endAngle = startAngle + angle; - child.depth = node.depth + 1; - child.left = w / 2; - child.top = h / 2; - child.innerRadius = Math.max(0, child.depth - .5) * r; - child.outerRadius = (child.depth + .5) * r; - startAngle += angle; - if (child.children) { - wedgify(child); - } - } - } - - /** @private */ - function flatten(node, array) { - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - flatten(node.children[i], array); - } - } - array.push(node) - return array; - } - - /** @private */ - function data() { - var root = accumulate(tree); - w = this.parent.width(); - h = this.parent.height(); - r = Math.min(w, h) / 2 / (depth(root) - .5); - root.left = w / 2; - root.top = h / 2; - root.startAngle = 0; - root.angle = 2 * Math.PI; - root.endAngle = 2 * Math.PI; - root.innerRadius = 0; - root.outerRadius = r; - root.depth = 0; - layout(root); - return flatten(root, []).reverse(); - } - - /* A dummy mark, like an anchor, which the caller extends. */ - var mark = new pv.Mark() - .data(data) - .left(function(n) { return n.left; }) - .top(function(n) { return n.top; }) - .startAngle(function(n) { return n.startAngle; }) - .angle(function(n) { return n.angle; }) - .innerRadius(function(n) { return n.innerRadius; }) - .outerRadius(function(n) { return n.outerRadius; }); - - /** - * Specifies the root key; optional. The root key is prepended to the - * keys attribute for all generated nodes. This method is provided - * for convenience and does not affect layout. - * - * @param {string} v the root key. - * @function - * @name pv.Layout.sunburst.prototype.root - * @returns {pv.Layout.sunburst} this. - */ - mark.root = function(v) { - keys = [v]; - return this; - }; - - /** - * Specifies the sizing function. By default, the sizing function is - * Number. The sizing function is invoked for each node in the tree - * (passed to the constructor): the sizing function must return - * undefined or NaN for internal nodes, and a number for - * leaf nodes. The aggregate sizes of internal nodes will be automatically - * computed by the layout. - * - *

For example, if the tree data structure represents a file system, with - * files as leaf nodes, and each file has a bytes attribute, you can - * specify a size function as: - * - *

.size(function(d) d.bytes)
- * - * This function will return undefined for internal nodes (since - * these do not have a bytes attribute), and a number for leaf nodes. - * - *

Note that the built-in Math.sqrt and Math.log methods - * can be used as sizing functions. These function similarly to - * Number, except perform a root and log scale, respectively. - * - * @param {function} f the new sizing function. - * @function - * @name pv.Layout.sunburst.prototype.size - * @returns {pv.Layout.sunburst} this. - */ - mark.size = function(f) { - sizeof = f; - return this; - }; - - return mark; -}; -// TODO add `by` function for determining size (and children?) - -/** - * Returns a new treemap tree layout. - * - * @class A tree layout in the form of an treemap. Treemaps - * are a form of space-filling layout that represents nodes as boxes, with child - * nodes placed within parent boxes. The size of each box is proportional to the - * size of the node in the tree. - * - *

This particular algorithm is taken from Bruls, D.M., C. Huizing, and - * J.J. van Wijk, "Squarified - * Treemaps" in Data Visualization 2000, Proceedings of the Joint - * Eurographics and IEEE TCVG Sumposium on Visualization, 2000, - * pp. 33-42. - * - *

This tree layout is intended to be extended (see {@link pv.Mark#extend}) - * by a {@link pv.Bar}. The data property returns an array of nodes for use by - * other property functions. The following node attributes are supported: - * - *

    - *
  • left - the cell left position. - *
  • top - the cell top position. - *
  • width - the cell width. - *
  • height - the cell height. - *
  • depth - the node depth (tier; the root is 0). - *
  • keys - an array of string keys for the node. - *
  • size - the aggregate node size. - *
  • children - child nodes, if any. - *
  • data - the associated tree element, for leaf nodes. - *
- * - * To produce a default treemap layout, say: - * - *
.add(pv.Bar)
- *   .extend(pv.Layout.treemap(tree))
- * - * To display internal nodes, and color by depth, say: - * - *
.add(pv.Bar)
- *   .extend(pv.Layout.treemap(tree).inset(10))
- *   .fillStyle(pv.Colors.category19().by(function(n) n.depth))
- * - * The format of the tree argument is a hierarchical object whose leaf - * nodes are numbers corresponding to their size. For an example, and - * information on how to convert tabular data into such a tree, see - * {@link pv.Tree}. If the leaf nodes are not numbers, a {@link #size} function - * can be specified to override how the tree is interpreted. This size function - * can also be used to transform the data. - * - *

By default, the treemap fills the full width and height of the parent - * panel, and only leaf nodes are rendered. If an {@link #inset} is specified, - * internal nodes will be rendered, each inset from their parent by the - * specified margins. Rounding can be enabled using {@link #round}. Finally, an - * optional root key can be specified using {@link #root} for convenience. - * - * @param tree a tree (an object) who leaf attributes have sizes. - * @returns {pv.Layout.treemap} a tree layout. - */ -pv.Layout.treemap = function(tree) { - var keys = [], round, inset, sizeof = Number; - - /** @private */ - function rnd(i) { - return round ? Math.round(i) : i; - } - - /** @private */ - function accumulate(map) { - var node = {size: 0, children: [], keys: keys.slice()}; - for (var key in map) { - var child = map[key], size = sizeof(child); - keys.push(key); - if (isNaN(size)) { - child = accumulate(child); - } else { - child = {size: size, data: child, keys: keys.slice()}; - } - node.children.push(child); - node.size += child.size; - keys.pop(); - } - node.children.sort(function(a, b) { return a.size - b.size; }); - return node; - } - - /** @private */ - function scale(node, k) { - node.size *= k; - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - scale(node.children[i], k); - } - } - } - - /** @private */ - function ratio(row, l) { - var rmax = -Infinity, rmin = Infinity, s = 0; - for (var i = 0; i < row.length; i++) { - var r = row[i].size; - if (r < rmin) rmin = r; - if (r > rmax) rmax = r; - s += r; - } - s = s * s; - l = l * l; - return Math.max(l * rmax / s, s / (l * rmin)); - } - - /** @private */ - function squarify(node) { - var row = [], mink = Infinity; - var x = node.left + (inset ? inset.left : 0), - y = node.top + (inset ? inset.top : 0), - w = node.width - (inset ? inset.left + inset.right : 0), - h = node.height - (inset ? inset.top + inset.bottom : 0), - l = Math.min(w, h); - - scale(node, w * h / node.size); - - function position(row) { - var s = pv.sum(row, function(node) { return node.size; }), - hh = (l == 0) ? 0 : rnd(s / l); - - for (var i = 0, d = 0; i < row.length; i++) { - var n = row[i], nw = rnd(n.size / hh); - if (w == l) { - n.left = x + d; - n.top = y; - n.width = nw; - n.height = hh; - } else { - n.left = x; - n.top = y + d; - n.width = hh; - n.height = nw; - } - d += nw; - } - - if (w == l) { - if (n) n.width += w - d; // correct rounding error - y += hh; - h -= hh; - } else { - if (n) n.height += h - d; // correct rounding error - x += hh; - w -= hh; - } - l = Math.min(w, h); - } - - var children = node.children.slice(); // copy - while (children.length > 0) { - var child = children[children.length - 1]; - if (child.size <= 0) { - children.pop(); - continue; - } - row.push(child); - - var k = ratio(row, l); - if (k <= mink) { - children.pop(); - mink = k; - } else { - row.pop(); - position(row); - row.length = 0; - mink = Infinity; - } - } - - if (row.length > 0) { - position(row); - } - - /* correct rounding error */ - if (w == l) { - for (var i = 0; i < row.length; i++) { - row[i].width += w; - } - } else { - for (var i = 0; i < row.length; i++) { - row[i].height += h; - } - } - } - - /** @private */ - function layout(node) { - if (node.children) { - squarify(node); - for (var i = 0; i < node.children.length; i++) { - var child = node.children[i]; - child.depth = node.depth + 1; - layout(child); - } - } - } - - /** @private */ - function flatten(node, array) { - if (node.children) { - for (var i = 0; i < node.children.length; i++) { - flatten(node.children[i], array); - } - } - if (inset || !node.children) { - array.push(node) - } - return array; - } - - /** @private */ - function data() { - var root = accumulate(tree); - root.left = 0; - root.top = 0; - root.width = this.parent.width(); - root.height = this.parent.height(); - root.depth = 0; - layout(root); - return flatten(root, []).reverse(); - } - - /* A dummy mark, like an anchor, which the caller extends. */ - var mark = new pv.Mark() - .data(data) - .left(function(n) { return n.left; }) - .top(function(n) { return n.top; }) - .width(function(n) { return n.width; }) - .height(function(n) { return n.height; }); - - /** - * Enables or disables rounding. When rounding is enabled, the left, top, - * width and height properties will be rounded to integer pixel values. The - * rounding algorithm uses error accumulation to ensure an exact fit. - * - * @param {boolean} v whether rounding should be enabled. - * @function - * @name pv.Layout.treemap.prototype.round - * @returns {pv.Layout.treemap} this. - */ - mark.round = function(v) { - round = v; - return this; - }; - - /** - * Specifies the margins to inset child nodes from their parents; as a side - * effect, this also enables the display of internal nodes, which are hidden - * by default. If only a single argument is specified, this value is used to - * inset all four sides. - * - * @param {number} top the top margin. - * @param {number} [right] the right margin. - * @param {number} [bottom] the bottom margin. - * @param {number} [left] the left margin. - * @function - * @name pv.Layout.treemap.prototype.inset - * @returns {pv.Layout.treemap} this. - */ - mark.inset = function(top, right, bottom, left) { - if (arguments.length == 1) right = bottom = left = top; - inset = {top:top, right:right, bottom:bottom, left:left}; - return this; - }; - - /** - * Specifies the root key; optional. The root key is prepended to the - * keys attribute for all generated nodes. This method is provided - * for convenience and does not affect layout. - * - * @param {string} v the root key. - * @function - * @name pv.Layout.treemap.prototype.root - * @returns {pv.Layout.treemap} this. - */ - mark.root = function(v) { - keys = [v]; - return this; - }; - - /** - * Specifies the sizing function. By default, the sizing function is - * Number. The sizing function is invoked for each node in the tree - * (passed to the constructor): the sizing function must return - * undefined or NaN for internal nodes, and a number for - * leaf nodes. The aggregate sizes of internal nodes will be automatically - * computed by the layout. - * - *

For example, if the tree data structure represents a file system, with - * files as leaf nodes, and each file has a bytes attribute, you can - * specify a size function as: - * - *

.size(function(d) d.bytes)
- * - * This function will return undefined for internal nodes (since - * these do not have a bytes attribute), and a number for leaf nodes. - * - *

Note that the built-in Math.sqrt and Math.log methods - * can be used as sizing functions. These function similarly to - * Number, except perform a root and log scale, respectively. - * - * @param {function} f the new sizing function. - * @function - * @name pv.Layout.treemap.prototype.size - * @returns {pv.Layout.treemap} this. - */ - mark.size = function(f) { - sizeof = f; - return this; - }; - - return mark; -}; - return pv;}();/* - * Parses the Protovis specifications on load, allowing the use of JavaScript - * 1.8 function expressions on browsers that only support JavaScript 1.6. - * - * @see pv.parse - */ -pv.listen(window, "load", function() { - var scripts = document.getElementsByTagName("script"); - for (var i = 0; i < scripts.length; i++) { - var s = scripts[i]; - if (s.type == "text/javascript+protovis") { - try { - pv.Panel.$dom = s; - window.eval(pv.parse(s.textContent || s.innerHTML)); // IE - } catch (e) { - pv.error(e); - } - delete pv.Panel.$dom; - } - } - }); diff --git a/templates/404.html b/templates/404.html deleted file mode 100644 index dbc98cb..0000000 --- a/templates/404.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% block content %} - - -

-
- 404 -

- Oh! Snap -

- This is not the page you were looking for right! - Fix This -
-
- -{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html deleted file mode 100644 index 6c156d7..0000000 --- a/templates/500.html +++ /dev/null @@ -1,37 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% block content %} - - -
-
- 500 -

- Oh! Snap -

- This is not the page you were looking for right! - Fix This -
-
- -{% endblock %} \ No newline at end of file diff --git a/templates/api.html b/templates/api.html deleted file mode 100644 index 85f3cf0..0000000 --- a/templates/api.html +++ /dev/null @@ -1,77 +0,0 @@ -{% extends "base.html" %} - -{% block additional_stylesheets %} - -{% endblock %} - -{% block content %} -

REST API

-

- HyperKitty comes with a small REST API allowing you to programatically retrieve - emails and information. -

- -
-

Formats

-

- This REST API can return the information into several formats. - The default format is html to allow human readibility.
- To change the format, just add - ?format=<FORMAT> to the url -

-

The list of available formats is:

- -
- -
-

Emails /api/email/<list name>/<Message-ID>

-

-Using the address /api/email/<list name>/<Message-ID> you will be able to -retrieve the information known about a specific email on the specified mailing-list. -

-

For example: - /api/email/devel@fp.o/<1312985457.28933.34.camel@ankur.pc>/ - -

-
-
-

Threads /api/thread/<list name>/<ThreadID>

-

-

-

-Using the address /api/thread/<list name>/<Message-ID> you will be able to -retrieve the all the email for a specific thread on the specified mailing-list. -

-

For example: - /api/email/devel@fp.o/1/ - -

-
-
-

Search /api/search/<list name>/<field>/<keyword>

-

-

-

-Using the address /api/search/<list name>/<field>/<keyword> you will be able to -search for all emails of the specified mailing-list containing the provided keyword in the given field. -

-

The list of available field is:

-
    -
  • From
  • -
  • Subject
  • -
  • Content
  • -
  • SubjectContent
  • -
-

For example: - /api/search/devel@fp.o/From/pingoured - -

-
-{% endblock %} diff --git a/templates/base.html b/templates/base.html deleted file mode 100644 index 0ae6d4c..0000000 --- a/templates/base.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - {% block title %}{{ app_name|title }}{% endblock %} - - - - - - {% block additional_stylesheets %} {% endblock %} - - {% load i18n %} - - - - - - - -
- {% block content %} {% endblock %} -
- {% block footer %} {% endblock %} - - - - {% block additionaljs %} {% endblock %} - diff --git a/templates/index.html b/templates/index.html deleted file mode 100644 index 87ba148..0000000 --- a/templates/index.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% block content %} -

{% trans 'Lists' %}

- - - - {% for mlist in lists %} - - - - - - {% endfor %} - -
{{ mlist }} Overview Archives
- -{% endblock %} diff --git a/templates/login.html b/templates/login.html deleted file mode 100644 index a137c97..0000000 --- a/templates/login.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} - -{% block content %} - -

Login with username and password

- - - - -
    -
  • Google
  • - -
  • Yahoo
  • - -
  • -
    - {% csrf_token %} - - Login using BrowserID -
    -
  • - -
- -{% endblock %} - -{% block additionaljs %} - - - - - -{% endblock additionaljs %} diff --git a/templates/message.html b/templates/message.html deleted file mode 100644 index 592e85d..0000000 --- a/templates/message.html +++ /dev/null @@ -1,71 +0,0 @@ -{% extends "base.html" %} -{% load gravatar %} - -{% block additional_stylesheets %} - -{% endblock %} - -{% block content %} - -
- {% include 'messages/first_email.html' with first_mail=message %} -
- -{% endblock %} - -{% block additionaljs %} - - -{% endblock %} diff --git a/templates/messages/first_email.html b/templates/messages/first_email.html deleted file mode 100644 index 36c8468..0000000 --- a/templates/messages/first_email.html +++ /dev/null @@ -1,5 +0,0 @@ -{% load gravatar %} - -
- {% include 'messages/message.html' with email=first_mail unfolded='True' %} -
diff --git a/templates/messages/message.html b/templates/messages/message.html deleted file mode 100644 index feffcb9..0000000 --- a/templates/messages/message.html +++ /dev/null @@ -1,34 +0,0 @@ -{% load gravatar %} - - -{% if unfolded %} -
-{% else %} - - - diff --git a/templates/month_view.html b/templates/month_view.html deleted file mode 100644 index 5ba2445..0000000 --- a/templates/month_view.html +++ /dev/null @@ -1,111 +0,0 @@ -{% extends "base.html" %} -{% load poll_extras %} -{% load gravatar %} - -{% block content %} - -
- {% for email in threads %} - -
-
- {{email.subject}} - {{email.date}} -
-
- {% if email.category_tag %} - - {% else %} - - {% endif %} -
- {% if email.email %} - {% gravatar_img_for_email email.email 40 %} -
- {% endif %} - {{email.sender}} -
-
- {{email.content}} -
-
-
-
    -
  • - Tags: -
  • - {% for tag in email.tags %} -
  • - {{tag}} -
  • - {% endfor %} -
-
    -
  • - {{email.participants|length}} participants -
  • -
  • - {{email.answers}} comments -
  • -
- -
-
- - {% empty %} - Sorry no emails could be found for your search. - {% endfor %} -
-
- {% for key, value in archives_length|sort %} -

{{ key }}

-
- -
- {% endfor %} -
- -{% endblock %} - -{% block additionaljs %} - - -{% endblock %} - diff --git a/templates/recent_activities.html b/templates/recent_activities.html deleted file mode 100644 index d6f4516..0000000 --- a/templates/recent_activities.html +++ /dev/null @@ -1,212 +0,0 @@ -{% extends "base.html" %} -{% load poll_extras %} -{% load gravatar %} - -{% block additional_stylesheets %} - -{% endblock %} - -{% block content %} - -
-
-
- -
-
-
-

Recently active discussions

- {% for email in most_active_threads %} - -
- #{{forloop.counter}} - {{email.subject}} -
- -
-
- - {% endfor %} -
- -
-

Top discussions the last 30 days

- {% for email in top_threads %} - -
- #{{forloop.counter}} - {{email.subject}} -
- -
-
- - {% endfor %} -
- -
-

Prominent discussion maker

- {% for author in top_author %} - -
-
- #{{forloop.counter}} -
-
- {% if author.email %} - {% gravatar_img_for_email author.email 40 %} -
- {% endif %} -
-
- {{author.name}} -
- +{{author.kudos}} kudos -
-
- - {% endfor %} - -

Tag cloud

-
- -
-

Discussion by topic the last 30 days

- {% for category, thread in threads_per_category.items %} -
-

{{category}}

-
    - {% for email in thread %} -
  • - {{email.title}} -
  • - {% endfor %} -
-
- {% endfor %} -
-
-
- {% for key, value in archives_length|sort %} -

{{ key }}

-
- -
- {% endfor %} -
-{% endblock %} - -{% block additionaljs %} - -{% endblock %} - diff --git a/templates/register.html b/templates/register.html deleted file mode 100644 index 1e96907..0000000 --- a/templates/register.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} - -{% block content %} - - - -{% endblock %} diff --git a/templates/search.html b/templates/search.html deleted file mode 100644 index e0e4f8c..0000000 --- a/templates/search.html +++ /dev/null @@ -1,106 +0,0 @@ -{% extends "base.html" %} -{% load poll_extras %} -{% load gravatar %} - -{% block content %} - -{% if threads.object_list %} - -{% endif %} - -{% for email in threads.object_list %} - -
-
- {{email.subject}} - {{email.date}} -
-
- {% if email.category_tag %} - - {% else %} - - {% endif %} -
- {% if email.email %} - {% gravatar_img_for_email email.email 40 %} -
- {% endif %} - {{email.sender}} -
-
- {{email.content}} -
-
-
-
    -
  • - Tags: -
  • - {% for tag in email.tags %} -
  • - {{tag}} -
  • - {% endfor %} -
-
    -
  • - {{email.participants|length}} participants -
  • -
  • - {{email.answers}} comments -
  • -
- -
-
- -{% empty %} -Sorry no emails could be found for your search. -{% endfor %} - -{% if threads.object_list %} - -{% endif %} - -{% endblock %} - -{% block additionaljs %} - - -{% endblock %} - diff --git a/templates/thread.html b/templates/thread.html deleted file mode 100644 index abf6747..0000000 --- a/templates/thread.html +++ /dev/null @@ -1,99 +0,0 @@ -{% extends "base.html" %} - -{% load gravatar %} - -{% block additional_stylesheets %} - -{% endblock %} - -{% block content %} - - {% include 'threads/right_col.html' %} - - -
- - - {% include 'messages/first_email.html' %} - - - {% for email in threads %} -
- - {% include 'messages/message.html' %} - -
- {% endfor %} - -
- - -{% endblock %} - -{% block additionaljs %} - - - - - - -{% endblock %} diff --git a/templates/threads/add_tag_form.html b/templates/threads/add_tag_form.html deleted file mode 100644 index 65f6577..0000000 --- a/templates/threads/add_tag_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block header %} {% endblock %} - -{% block content %} -
- {% csrf_token %} - {{ addtag_form }} - -
-{% endblock %} diff --git a/templates/threads/right_col.html b/templates/threads/right_col.html deleted file mode 100644 index 9a1e448..0000000 --- a/templates/threads/right_col.html +++ /dev/null @@ -1,60 +0,0 @@ -{% load gravatar %} - - -
- -
-
- 21 -
-
- days -
- inactive -
-
- 24 -
-
- days -
- old -
-
-

- Add to favorite discussions -

- -
-
- tags ({{tags|length}}) -
    - {% for tag in tags %} -
  • - {{ tag }} | -
  • - {% endfor %} -
-
-
-
- {% csrf_token %} - {{ addtag_form.as_p }} - -
-
-
- participants ({{participants|length}}) -
    - {% for key,value in participants.items %} -
  • - {% gravatar_img_for_email value.email 20%} - {{key}} -
  • - {% endfor %} -
-
-
- diff --git a/templates/user_profile.html b/templates/user_profile.html deleted file mode 100644 index f0aa27e..0000000 --- a/templates/user_profile.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "base.html" %} -{% load i18n %} -{% load poll_extras %} - -{% block content %} -

User Profile - {{ user }}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{% trans 'User name' %}{{ user.username}}
{% trans 'Firstname' %}{{ user.first_name }}
{% trans 'Lastname' %}{{ user.last_name }}
{% trans 'Email' %}{{ user.email }}
{% trans 'Karma' %}{{ user_profile.karma }}
{% trans 'Date Joined' %}{{ user.date_joined }}
-

Up Votes :

- - - -

Down Votes :

- - - - -{% endblock %} diff --git a/templatetags/__init__.py b/templatetags/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/templatetags/poll_extras.py b/templatetags/poll_extras.py deleted file mode 100644 index a5f1032..0000000 --- a/templatetags/poll_extras.py +++ /dev/null @@ -1,55 +0,0 @@ -from django import template -from django.http import HttpRequest -from django.utils.datastructures import SortedDict -import re - -register = template.Library() - -@register.filter(name="trimString") -def trimString(str): - return re.sub('\s+', ' ', str) - -@register.filter(name='sort') -def listsort(value): - if isinstance(value, dict): - new_dict = SortedDict() - key_list = value.keys() - key_list.sort() - key_list.reverse() - for key in key_list: - values = value[key] - values.sort() - values.reverse() - new_dict[key] = values - return new_dict.items() - elif isinstance(value, list): - new_list = list(value) - new_list.sort() - return new_list - else: - return value - listsort.is_safe = True - -@register.filter(name="tomonth") -def to_month(value): - months = ('January', 'February', 'March', 'April', 'May', 'June', - 'July', 'August', 'September', 'October', 'November', 'December') - return months[value -1] - -@register.filter(name="strip_page") -def strip_page(value): - print repr(value), repr(value)[-2] - if not value: - return value - if value.endswith('/') and value[-3] == '/': - end_with_number = False - try: - if int(value[-2]) in range(0,10): - end_with_number = True - if end_with_number: - output = value.rsplit('/', 2) - except ValueError: - output = value.rsplit('/', 1) - else: - output = value.rsplit('/', 1) - return output[0] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index 4cf4941..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# - -from gsoc.tests.test_views import * -from gsoc.tests.test_models import * -from gsoc.tests.test_forms import * diff --git a/tests/test_forms.py b/tests/test_forms.py deleted file mode 100644 index 9a056cd..0000000 --- a/tests/test_forms.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# diff --git a/tests/test_models.py b/tests/test_models.py deleted file mode 100644 index 9a056cd..0000000 --- a/tests/test_models.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# diff --git a/tests/test_views.py b/tests/test_views.py deleted file mode 100644 index b94ef43..0000000 --- a/tests/test_views.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# - -from django.test import TestCase -from django.test.client import Client -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse - -class AccountViewsTestCase(TestCase): - - def setUp(self): - self.client = Client() - - def test_login(self): - # Try to access user profile (private data) without logging in - response = self.client.get(reverse('user_profile')) - self.assertRedirects(response, "%s?next=%s" % (reverse('user_login'),reverse('user_profile'))) - - def test_profile(self): - User.objects.create_user('testuser', 'syst3m.w0rm+test@gmail.com', 'testPass') - user = self.client.login(username='testuser', password='testPass') - - response = self.client.get(reverse('user_profile')) - self.assertEqual(response.status_code, 200) - - # Verify that user_profile is present in request context - self.assertTrue('user_profile' in response.context) - - # Verify karma for newly created user is 1 - self.assertEqual(response.context['user_profile'].karma, 1) - - - def test_registration(self): - - User.objects.create_user('testuser', 'syst3m.w0rm+test@gmail.com', 'testPass') - user = self.client.login(username='testuser', password='testPass') - - # If the user if already logged in, redirect to index page..don't let him register again - response = self.client.get(reverse('user_registration')) - self.assertRedirects(response, reverse('index')) - self.client.logout() - - # Access the user registration page after logging out and try to register now - response = self.client.get(reverse('user_registration')) - self.assertEqual(response.status_code, 200) - - # @TODO: Try to register a user and verify its working - - - - - - - \ No newline at end of file diff --git a/todo b/todo deleted file mode 100644 index ee1603e..0000000 --- a/todo +++ /dev/null @@ -1,5 +0,0 @@ -1. Better error handling -> log everything and handle exception gracefully. -2. write test cases -3. look at social-authentication application from detailed point of view. -4. make user profile editable. -5. setup.py to install this as a python application - code refactoring diff --git a/urls.py b/urls.py deleted file mode 100644 index 29edb06..0000000 --- a/urls.py +++ /dev/null @@ -1,106 +0,0 @@ -from django.conf.urls.defaults import patterns, include, url -from django.conf import settings -from django.views.generic.simple import direct_to_template -from api import EmailResource, ThreadResource, SearchResource - -from django.contrib.staticfiles.urls import staticfiles_urlpatterns - -# Uncomment the next two lines to enable the admin: -from django.contrib import admin -admin.autodiscover() - -urlpatterns = patterns('', - # Account - url(r'^accounts/login/$', 'views.accounts.user_login', name='user_login'), - url(r'^accounts/logout/$', 'views.accounts.user_logout', name='user_logout'), - url(r'^accounts/profile/$', 'views.accounts.user_profile', name='user_profile'), - url(r'^accounts/register/$', 'views.accounts.user_registration', name='user_registration'), - - - # Index - url(r'^/$', 'views.pages.index', name='index'), - url(r'^$', 'views.pages.index', name='index'), - - # Archives - url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/(?P\d\d?)/$', - 'views.list.archives'), - url(r'^archives/(?P.*@.*)/(?P\d{4})/(?P\d\d?)/$', - 'views.list.archives'), - url(r'^archives/(?P.*@.*)/$', - 'views.list.archives'), - - # Threads - url(r'^thread/(?P.*@.*)/(?P.+)/$', - 'views.thread.thread_index'), - - - # Lists - url(r'^list/$', 'views.pages.index'), # Can I remove this URL? - url(r'^list/(?P.*@.*)/$', - 'views.list.list'), - - # Search Tag - url(r'^tag/(?P.*@.*)\/(?P.*)\/(?P\d+)/$', - 'views.list.search_tag'), - url(r'^tag/(?P.*@.*)\/(?P.*)/$', - 'views.list.search_tag'), - - # Search - # If page number is present in URL - url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)\/(?P\d+)/$', - 'views.list.search_keyword'), - # Show the first page as default when no page number is present in URL - url(r'^search/(?P.*@.*)\/(?P.*)\/(?P.*)/$', - 'views.list.search_keyword'), - url(r'^search/(?P.*@.*)/$', - 'views.list.search'), - - - ### MESSAGE LEVEL VIEWS ### - # Vote a message - url(r'^message/(?P.*@.*)/(?P.+)/$', - 'views.message.index'), - - url(r'^vote/(?P.*@.*)/$', - 'views.message.vote'), - ### MESSAGE LEVEL VIEW ENDS ### - - - - ### THREAD LEVEL VIEWS ### - # Thread view page - url(r'^thread/(?P.*@.*)/(?P.+)/$', - 'views.thread.thread_index'), - - # Add Tag to a thread - url(r'^addtag/(?P.*@.*)\/(?P.*)/$', - 'views.thread.add_tag'), - ### THREAD LEVEL VIEW ENDS ### - - - # REST API - url(r'^api/$', 'views.api.api'), - url(r'^api/email\/(?P.*@.*)\/(?P.*)/', - EmailResource.as_view()), - url(r'^api/thread\/(?P.*@.*)\/(?P.*)/', - ThreadResource.as_view()), - url(r'^api/search\/(?P.*@.*)\/(?P.*)\/(?P.*)/', - SearchResource.as_view()), - - # Uncomment the admin/doc line below to enable admin documentation: - # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), - - # Admin - url(r'^admin/', include(admin.site.urls)), - - # Robots.txt - url(r'^robots\.txt$', direct_to_template, - {'template': 'robots.txt', 'mimetype': 'text/plain'}), - - # Social Auth - url(r'', include('social_auth.urls')), - -) -#) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) -urlpatterns += staticfiles_urlpatterns() - diff --git a/utils.py b/utils.py deleted file mode 100644 index ae55826..0000000 --- a/utils.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# - -import logging -import traceback - -from django.conf import settings -from django.core import mail -from django.views.debug import ExceptionReporter, get_exception_reporter_filter - -LOG_FILE = 'hk.log' - -def log(level, *args, **kwargs): - """Small wrapper around logger functions.""" - { - 'debug': logger.debug, - 'error': logger.error, - 'exception': logger.exception, - 'warn': logger.warn - }[level](*args, **kwargs) - - -class HyperKittyLogHandler(logging.Handler): - """A custom HyperKitty log handler. - - If the request is passed as the first argument to the log record, - request data will be provided in the email report. - """ - - def __init__(self, log_to_file=True, email_admins=True): - logging.Handler.__init__(self) - self.log_to_file = log_to_file - self.email_admins = email_admins - - def emit(self, record): - try: - request = record.request - subject = '%s (%s IP): %s' % ( - record.levelname, - (request.META.get('REMOTE_ADDR') in settings.INTERNAL_IPS - and 'internal' or 'EXTERNAL'), - record.getMessage() - ) - filter = get_exception_reporter_filter(request) - request_repr = filter.get_request_repr(request) - except Exception: - subject = '%s: %s' % ( - record.levelname, - record.getMessage() - ) - request = None - request_repr = "Request repr() unavailable." - subject = self.format_subject(subject) - - if record.exc_info: - exc_info = record.exc_info - stack_trace = '\n'.join(traceback.format_exception(*record.exc_info)) - else: - exc_info = (None, record.getMessage(), None) - stack_trace = 'No stack trace available' - - message = "%s\n\n%s" % (stack_trace, request_repr) - reporter = ExceptionReporter(request, is_email=True, *exc_info) - html_message = reporter.get_traceback_html() - - if self.email_admins: - mail.mail_admins(subject, message, fail_silently=True, html_message=html_message) - - if self.log_to_file: - log_file = open(LOG_FILE, 'a') - log_file.write(message) - log_file.close() - - def format_subject(self, subject): - """ - Escape CR and LF characters, and limit length. - RFC 2822's hard limit is 998 characters per line. So, minus "Subject: " - the actual subject must be no longer than 989 characters. - """ - formatted_subject = subject.replace('\n', '\\n').replace('\r', '\\r') - return formatted_subject[:989] - - -logger = None -if not logger: - logger = logging.getLogger('HyperKitty') - -if not logger.handlers: - logger.addHandler(HyperKittyLogHandler(True, True)) diff --git a/views/__init__.py b/views/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/views/accounts.py b/views/accounts.py deleted file mode 100644 index 3154563..0000000 --- a/views/accounts.py +++ /dev/null @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 1998-2012 by the Free Software Foundation, Inc. -# -# This file is part of HyperKitty. -# -# HyperKitty is free software: you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# HyperKitty 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 General Public License for -# more details. -# -# You should have received a copy of the GNU General Public License along with -# HyperKitty. If not, see . -# -# Author: Aamir Khan -# - -import re -import sys - -from django.conf import settings -from django.contrib import messages -from django.contrib.auth import logout, authenticate, login -from django.contrib.auth.decorators import (login_required, - permission_required, - user_passes_test) -from django.contrib.auth.forms import AuthenticationForm -from gsoc.models import UserProfile -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import render_to_response, redirect -from django.template import Context, loader, RequestContext -from django.utils.translation import gettext as _ -from urllib2 import HTTPError -from urlparse import urlparse - -from forms import RegistrationForm -from gsoc.utils import log - -def user_logout(request): - logout(request) - return redirect('user_login') - -def user_login(request,template = 'login.html'): - - parse_r = urlparse(request.META.get('HTTP_REFERER', 'index')) - previous = '%s%s' % (parse_r.path, parse_r.query) - - next_var = request.POST.get('next', request.GET.get('next', previous)) - - if request.method == 'POST': - form = AuthenticationForm(request.POST) - user = authenticate(username=request.POST.get('username'), - password=request.POST.get('password')) - - if user is not None: - log('debug', user) - if user.is_active: - login(request,user) - return redirect(next_var) - - else: - form = AuthenticationForm() - return render_to_response(template, {'form': form, 'next' : next_var}, - context_instance=RequestContext(request)) - -@login_required -def user_profile(request, user_email = None): - if not request.user.is_authenticated(): - return redirect('user_login') - # try to render the user profile. - try: - user_profile = request.user.get_profile() - # @TODO: Include the error name e.g, ProfileDoesNotExist? - except: - user_profile = UserProfile.objects.create(user=request.user) - - t = loader.get_template('user_profile.html') - - c = RequestContext(request, { - 'user_profile' : user_profile, - }) - - return HttpResponse(t.render(c)) - - -def user_registration(request): - if request.user.is_authenticated(): - # Already registered, redirect back to index page - return redirect('index') - - if request.POST: - form = RegistrationForm(request.POST) - if form.is_valid(): - # Save the user data. - form.save(form.cleaned_data) - user = authenticate(username=form.cleaned_data['username'], - password=form.cleaned_data['password1']) - - if user is not None: - log('debug', user) - if user.is_active: - login(request,user) - return redirect('index') - else: - form = RegistrationForm() - - return render_to_response('register.html', {'form': form}, context_instance=RequestContext(request)) - diff --git a/views/api.py b/views/api.py deleted file mode 100644 index 3a764db..0000000 --- a/views/api.py +++ /dev/null @@ -1,27 +0,0 @@ -import re -import os -import json -import urllib -import django.utils.simplejson as simplejson - -from calendar import timegm -from datetime import datetime, timedelta - -from urlparse import urljoin -from django.http import HttpResponse, HttpResponseRedirect -from django.template import RequestContext, loader -from django.conf import settings -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage -from django.contrib.auth.decorators import (login_required, - permission_required, - user_passes_test) -from gsoc.utils import log - - -def api(request): - t = loader.get_template('api.html') - c = RequestContext(request, { - }) - return HttpResponse(t.render(c)) - - diff --git a/views/forms.py b/views/forms.py deleted file mode 100644 index 97e5550..0000000 --- a/views/forms.py +++ /dev/null @@ -1,59 +0,0 @@ -from django import forms -from django.core import validators -from django.contrib.auth.models import User - -def isValidUsername(username): - try: - User.objects.get(username=username) - except User.DoesNotExist: - return - raise validators.ValidationError('The username "%s" is already taken.' % username) - -class RegistrationForm(forms.Form): - - username = forms.CharField(label='username', help_text=None, - widget=forms.TextInput( - attrs={'placeholder': 'username...'} - ), required = True, validators=[isValidUsername] - ) - - email = forms.EmailField(required=True) - - password1 = forms.CharField(widget=forms.PasswordInput) - - password2 = forms.CharField(widget=forms.PasswordInput) - - def save(self, new_user_data): - u = User.objects.create_user(new_user_data['username'], - new_user_data['email'], - new_user_data['password1']) - u.is_active = True - u.save() - return u - - -class AddTagForm(forms.Form): - tag = forms.CharField(label='', help_text=None, - widget=forms.TextInput( - attrs={'placeholder': 'Add a tag...'} - ) - ) - from_url = forms.CharField(widget=forms.HiddenInput, required=False) - -class SearchForm(forms.Form): - target = forms.CharField(label='', help_text=None, - widget=forms.Select( - choices=(('Subject', 'Subject'), - ('Content', 'Content'), - ('SubjectContent', 'Subject & Content'), - ('From', 'From')) - ) - ) - - keyword = forms.CharField(max_length=100,label='', help_text=None, - widget=forms.TextInput( - attrs={'placeholder': 'Search this list.'} - ) - ) - - diff --git a/views/list.py b/views/list.py deleted file mode 100644 index 17da2c6..0000000 --- a/views/list.py +++ /dev/null @@ -1,291 +0,0 @@ -import re -import os -import json -import urllib -import django.utils.simplejson as simplejson - -from calendar import timegm -from datetime import datetime, timedelta - -from urlparse import urljoin -from django.http import HttpResponse, HttpResponseRedirect -from django.template import RequestContext, loader -from django.conf import settings -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage -from django.contrib.auth.decorators import (login_required, - permission_required, - user_passes_test) -from kittystore.kittysastore import KittySAStore - -from gsoc.models import Rating -from lib.mockup import * -from forms import * -from gsoc.utils import log - - -STORE = KittySAStore(settings.KITTYSTORE_URL) - - -# @TODO : Move this into settings.py -MONTH_PARTICIPANTS = 284 -MONTH_DISCUSSIONS = 82 - - - -def archives(request, mlist_fqdn, year=None, month=None, day=None): - # No year/month: past 32 days - # year and month: find the 32 days for that month - # @TODO : modify url.py to account for page number - - end_date = None - if year or month or day: - try: - start_day = 1 - end_day = 1 - start_month = int(month) - end_month = int(month) + 1 - start_year = int(year) - end_year = int(year) - if day: - start_day = int(day) - end_day = start_day + 1 - end_month = start_month - if start_month == 12: - end_month = 1 - end_year = start_year + 1 - - begin_date = datetime(start_year, start_month, start_day) - end_date = datetime(end_year, end_month, end_day) - month_string = begin_date.strftime('%B %Y') - except ValueError, err: - print err - logger.error('Wrong format given for the date') - - if not end_date: - today = datetime.utcnow() - begin_date = datetime(today.year, today.month, 1) - end_date = datetime(today.year, today.month+1, 1) - month_string = 'Past thirty days' - list_name = mlist_fqdn.split('@')[0] - - search_form = SearchForm(auto_id=False) - t = loader.get_template('month_view.html') - threads = STORE.get_archives(list_name, start=begin_date, - end=end_date) - - participants = set() - cnt = 0 - for msg in threads: - # Statistics on how many participants and threads this month - participants.add(msg.sender) - msg.participants = STORE.get_thread_participants(list_name, - msg.thread_id) - msg.answers = STORE.get_thread_length(list_name, - msg.thread_id) - threads[cnt] = msg - cnt = cnt + 1 - #print msg - - paginator = Paginator(threads, 10) - pageNo = request.GET.get('page') - - try: - threads = paginator.page(pageNo) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - threads = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - threads = paginator.page(paginator.num_pages) - - - archives_length = STORE.get_archives_length(list_name) - - c = RequestContext(request, { - 'list_name' : list_name, - 'list_address': mlist_fqdn, - 'search_form': search_form, - 'month': month_string, - 'month_participants': len(participants), - 'month_discussions': len(threads), - 'threads': threads, - 'archives_length': archives_length, - }) - return HttpResponse(t.render(c)) - -def list(request, mlist_fqdn=None): - if not mlist_fqdn: - return HttpResponseRedirect('/') - t = loader.get_template('recent_activities.html') - search_form = SearchForm(auto_id=False) - list_name = mlist_fqdn.split('@')[0] - - # Get stats for last 30 days - today = datetime.utcnow() - end_date = datetime(today.year, today.month, today.day) - begin_date = end_date - timedelta(days=32) - - threads = STORE.get_archives(list_name=list_name,start=begin_date, - end=end_date) - - participants = set() - dates = {} - cnt = 0 - for msg in threads: - month = msg.date.month - if month < 10: - month = '0%s' % month - day = msg.date.day - if day < 10: - day = '0%s' % day - key = '%s%s%s' % (msg.date.year, month, day) - if key in dates: - dates[key] = dates[key] + 1 - else: - dates[key] = 1 - # Statistics on how many participants and threads this month - participants.add(msg.sender) - msg.participants = STORE.get_thread_participants(list_name, - msg.thread_id) - msg.answers = STORE.get_thread_length(list_name, - msg.thread_id) - threads[cnt] = msg - cnt = cnt + 1 - - # top threads are the one with the most answers - top_threads = sorted(threads, key=lambda entry: entry.answers, reverse=True) - - # active threads are the ones that have the most recent posting - active_threads = sorted(threads, key=lambda entry: entry.date, reverse=True) - - archives_length = STORE.get_archives_length(list_name) - - # top authors are the ones that have the most kudos. How do we determine - # that? Most likes for their post? - authors = generate_top_author() - authors = sorted(authors, key=lambda author: author.kudos) - authors.reverse() - - # Get the list activity per day - days = dates.keys() - days.sort() - dates_string = ["%s/%s/%s" % (key[0:4], key[4:6], key[6:8]) for key in days] - #print days - #print dates_string - evolution = [dates[key] for key in days] - if not evolution: - evolution.append(0) - - # threads per category is the top thread titles in each category - threads_per_category = generate_thread_per_category() - c = RequestContext(request, { - 'list_name' : list_name, - 'list_address': mlist_fqdn, - 'search_form': search_form, - 'month': 'Recent activity', - 'month_participants': len(participants), - 'month_discussions': len(threads), - 'top_threads': top_threads[:5], - 'most_active_threads': active_threads[:5], - 'top_author': authors, - 'threads_per_category': threads_per_category, - 'archives_length': archives_length, - 'evolution': evolution, - 'dates_string': dates_string, - }) - return HttpResponse(t.render(c)) - - -def _search_results_page(request, mlist_fqdn, threads, search_type, - page=1, num_threads=25, limit=None): - search_form = SearchForm(auto_id=False) - t = loader.get_template('search.html') - list_name = mlist_fqdn.split('@')[0] - res_num = len(threads) - - participants = set() - for msg in threads: - participants.add(msg.sender) - - paginator = Paginator(threads, num_threads) - - #If page request is out of range, deliver last page of results. - try: - threads = paginator.page(page) - except (EmptyPage, InvalidPage): - threads = paginator.page(paginator.num_pages) - - cnt = 0 - for msg in threads.object_list: - msg.email = msg.email.strip() - # Statistics on how many participants and threads this month - participants.add(msg.sender) - if msg.thread_id: - msg.participants = STORE.get_thread_participants(list_name, - msg.thread_id) - msg.answers = STORE.get_thread_length(list_name, - msg.thread_id) - else: - msg.participants = 0 - msg.answers = 0 - threads.object_list[cnt] = msg - cnt = cnt + 1 - - c = RequestContext(request, { - 'list_name' : list_name, - 'list_address': mlist_fqdn, - 'search_form': search_form, - 'month': search_type, - 'month_participants': len(participants), - 'month_discussions': res_num, - 'threads': threads, - 'full_path': request.get_full_path(), - }) - return HttpResponse(t.render(c)) - - -def search(request, mlist_fqdn): - keyword = request.GET.get('keyword') - target = request.GET.get('target') - page = request.GET.get('page') - if keyword and target: - url = '/search/%s/%s/%s/' % (mlist_fqdn, target, keyword) - if page: - url += '%s/' % page - else: - url = '/search/%s' % (mlist_fqdn) - return HttpResponseRedirect(url) - - -def search_keyword(request, mlist_fqdn, target, keyword, page=1): - ## Should we remove the code below? - ## If urls.py does it job we should never need it - if not keyword: - keyword = request.GET.get('keyword') - if not target: - target = request.GET.get('target') - if not target: - target = 'Subject' - regex = '%%%s%%' % keyword - list_name = mlist_fqdn.split('@')[0] - if target.lower() == 'subjectcontent': - threads = STORE.search_content_subject(list_name, keyword) - elif target.lower() == 'subject': - threads = STORE.search_subject(list_name, keyword) - elif target.lower() == 'content': - threads = STORE.search_content(list_name, keyword) - elif target.lower() == 'from': - threads = STORE.search_sender(list_name, keyword) - - return _search_results_page(request, mlist_fqdn, threads, 'Search', page) - - -def search_tag(request, mlist_fqdn, tag=None, page=1): - '''Searches both tag and topic''' - if tag: - query_string = {'Category': tag.capitalize()} - else: - query_string = None - return _search_results_page(request, mlist_fqdn, query_string, - 'Tag search', page, limit=50) - diff --git a/views/message.py b/views/message.py deleted file mode 100644 index 0b70969..0000000 --- a/views/message.py +++ /dev/null @@ -1,82 +0,0 @@ -import re -import os -import django.utils.simplejson as simplejson - -from django.http import HttpResponse, HttpResponseRedirect -from django.template import RequestContext, loader -from django.conf import settings -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage -from django.contrib.auth.decorators import (login_required, - permission_required, - user_passes_test) - -from kittystore.kittysastore import KittySAStore - -from gsoc.models import Rating -from lib.mockup import * -from forms import * -from gsoc.utils import log - -STORE = KittySAStore(settings.KITTYSTORE_URL) - - -def index (request, mlist_fqdn, messageid): - ''' Displays a single message identified by its messageid ''' - list_name = mlist_fqdn.split('@')[0] - - search_form = SearchForm(auto_id=False) - t = loader.get_template('message.html') - message = STORE.get_email(list_name, messageid) - message.email = message.email.strip() - # Extract all the votes for this message - try: - votes = Rating.objects.filter(messageid = messageid) - except Rating.DoesNotExist: - votes = {} - - likes = 0 - dislikes = 0 - - for vote in votes: - if vote.vote == 1: - likes = likes + 1 - elif vote.vote == -1: - dislikes = dislikes + 1 - else: - pass - - message.votes = votes - message.likes = likes - message.dislikes = dislikes - - c = RequestContext(request, { - 'list_name' : list_name, - 'list_address': mlist_fqdn, - 'message': message, - 'messageid' : messageid, - }) - return HttpResponse(t.render(c)) - - - -@login_required -def vote (request, mlist_fqdn): - """ Add a rating to a given message identified by messageid. """ - if not request.user.is_authenticated(): - return redirect('user_login') - - value = request.POST['vote'] - messageid = request.POST['messageid'] - - # Checks if the user has already voted for a this message. If yes modify db entry else create a new one. - try: - v = Rating.objects.get(user = request.user, messageid = messageid, list_address = mlist_fqdn) - except Rating.DoesNotExist: - v = Rating(list_address=mlist_fqdn, messageid = messageid, vote = value) - - v.user = request.user - v.vote = value - v.save() - response_dict = { } - - return HttpResponse(simplejson.dumps(response_dict), mimetype='application/javascript') diff --git a/views/pages.py b/views/pages.py deleted file mode 100644 index 189d574..0000000 --- a/views/pages.py +++ /dev/null @@ -1,38 +0,0 @@ -#-*- coding: utf-8 -*- - -import re -import os -import json -import urllib -import django.utils.simplejson as simplejson - -from calendar import timegm -from datetime import datetime, timedelta - -from urlparse import urljoin -from django.http import HttpResponse, HttpResponseRedirect -from django.template import RequestContext, loader -from django.conf import settings -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage -from django.contrib.auth.decorators import (login_required, - permission_required, - user_passes_test) -from gsoc.models import Rating -from lib.mockup import * -from forms import * -from gsoc.utils import log - -def index(request): - t = loader.get_template('index.html') - search_form = SearchForm(auto_id=False) - - base_url = settings.MAILMAN_API_URL % { - 'username': settings.MAILMAN_USER, 'password': settings.MAILMAN_PASS} - - list_data = ['devel@fp.o', 'packaging@fp.o', 'fr-users@fp.o'] - - c = RequestContext(request, { - 'lists': list_data, - 'search_form': search_form, - }) - return HttpResponse(t.render(c)) diff --git a/views/thread.py b/views/thread.py deleted file mode 100644 index 06a1bda..0000000 --- a/views/thread.py +++ /dev/null @@ -1,113 +0,0 @@ -import django.utils.simplejson as simplejson - -from django.http import HttpResponse, HttpResponseRedirect -from django.template import RequestContext, loader -from django.conf import settings -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger, InvalidPage -from django.contrib.auth.decorators import (login_required, - permission_required, - user_passes_test) -from kittystore.kittysastore import KittySAStore - -from gsoc.models import Rating -from lib.mockup import * -from forms import * -from gsoc.utils import log - -STORE = KittySAStore(settings.KITTYSTORE_URL) - - - -def thread_index (request, mlist_fqdn, threadid): - ''' Displays all the email for a given thread identifier ''' - list_name = mlist_fqdn.split('@')[0] - - search_form = SearchForm(auto_id=False) - t = loader.get_template('thread.html') - threads = STORE.get_thread(list_name, threadid) - #prev_thread = mongo.get_thread_name(list_name, int(threadid) - 1) - prev_thread = [] - if len(prev_thread) > 30: - prev_thread = '%s...' % prev_thread[:31] - #next_thread = mongo.get_thread_name(list_name, int(threadid) + 1) - next_thread = [] - if len(next_thread) > 30: - next_thread = '%s...' % next_thread[:31] - - participants = {} - cnt = 0 - - for message in threads: - # @TODO: Move this logic inside KittyStore? - message.email = message.email.strip() - - # Extract all the votes for this message - try: - votes = Rating.objects.filter(messageid = message.message_id) - except Rating.DoesNotExist: - votes = {} - - likes = 0 - dislikes = 0 - - for vote in votes: - if vote.vote == 1: - likes = likes + 1 - elif vote.vote == -1: - dislikes = dislikes + 1 - else: - pass - - message.votes = votes - message.likes = likes - message.dislikes = dislikes - - # Statistics on how many participants and threads this month - participants[message.sender] = {'email': message.email} - cnt = cnt + 1 - - archives_length = STORE.get_archives_length(list_name) - from_url = '/thread/%s/%s/' %(mlist_fqdn, threadid) - tag_form = AddTagForm(initial={'from_url' : from_url}) - - c = RequestContext(request, { - 'list_name' : list_name, - 'list_address': mlist_fqdn, - 'search_form': search_form, - 'addtag_form': tag_form, - 'month': 'Thread', - 'participants': participants, - 'answers': cnt, - 'first_mail': threads[0], - 'threads': threads[1:], - 'next_thread': next_thread, - 'next_thread_id': 0, - 'prev_thread': prev_thread, - 'prev_thread_id': 0, - 'archives_length': archives_length, - }) - return HttpResponse(t.render(c)) - - -@login_required -def add_tag(request, mlist_fqdn, email_id): - """ Add a tag to a given thread. """ - t = loader.get_template('threads/add_tag_form.html') - if request.method == 'POST': - form = AddTagForm(request.POST) - if form.is_valid(): - print "THERE WE ARE" - # TODO: Add the logic to add the tag - if form.data['from_url']: - return HttpResponseRedirect(form.data['from_url']) - else: - return HttpResponseRedirect('/') - else: - form = AddTagForm() - c = RequestContext(request, { - 'list_address': mlist_fqdn, - 'email_id': email_id, - 'addtag_form': form, - }) - return HttpResponse(t.render(c)) - -- cgit