[r-cran-crosstalk] 01/02: New upstream version 1.0.0+dfsg

Andreas Tille tille at debian.org
Wed Dec 20 13:08:45 UTC 2017


This is an automated email from the git hooks/post-receive script.

tille pushed a commit to branch master
in repository r-cran-crosstalk.

commit 48ef241d0ef4f95b5de60c4eb1927083e560e5cc
Author: Andreas Tille <tille at debian.org>
Date:   Wed Dec 20 14:08:13 2017 +0100

    New upstream version 1.0.0+dfsg
---
 DESCRIPTION                      |   58 ++
 LICENSE                          |    3 +
 LICENSE.note                     |  341 +++++++++
 MD5                              |   60 ++
 NAMESPACE                        |   21 +
 R/controls.R                     |  554 ++++++++++++++
 R/crosstalk.R                    |  373 ++++++++++
 R/ggplot2.R                      |   81 +++
 README.md                        |    5 +
 inst/www/css/crosstalk.css       |   27 +
 inst/www/js/crosstalk.js         | 1471 ++++++++++++++++++++++++++++++++++++++
 inst/www/js/crosstalk.js.map     |   37 +
 inst/www/js/crosstalk.min.js     |    2 +
 inst/www/js/crosstalk.min.js.map |    1 +
 man/ClientValue.Rd               |   62 ++
 man/SharedData.Rd                |  106 +++
 man/bscols.Rd                    |   56 ++
 man/crosstalkLibs.Rd             |   13 +
 man/filter_select.Rd             |   49 ++
 man/filter_slider.Rd             |   97 +++
 man/is.SharedData.Rd             |   18 +
 man/maintain_selection.Rd        |   21 +
 man/scale_fill_selection.Rd      |   46 ++
 23 files changed, 3502 insertions(+)

diff --git a/DESCRIPTION b/DESCRIPTION
new file mode 100644
index 0000000..ee14b8e
--- /dev/null
+++ b/DESCRIPTION
@@ -0,0 +1,58 @@
+Package: crosstalk
+Type: Package
+Title: Inter-Widget Interactivity for HTML Widgets
+Version: 1.0.0
+Authors at R: c(
+    person("Joe", "Cheng", role = c("aut", "cre"), email = "joe at rstudio.com"),
+    person(family = "RStudio", role = "cph"),
+    person(family = "jQuery Foundation", role = "cph",
+    comment = "jQuery library and jQuery UI library"),
+    person(family = "jQuery contributors", role = c("ctb", "cph"),
+    comment = "jQuery library; authors listed in inst/www/shared/jquery-AUTHORS.txt"),
+    person("Mark", "Otto", role = "ctb",
+    comment = "Bootstrap library"),
+    person("Jacob", "Thornton", role = "ctb",
+    comment = "Bootstrap library"),
+    person(family = "Bootstrap contributors", role = "ctb",
+    comment = "Bootstrap library"),
+    person(family = "Twitter, Inc", role = "cph",
+    comment = "Bootstrap library"),
+    person("Brian", "Reavis", role = c("ctb", "cph"),
+    comment = "selectize.js library"),
+    person("Kristopher Michael", "Kowal", role = c("ctb", "cph"),
+    comment = "es5-shim library"),
+    person(family = "es5-shim contributors", role = c("ctb", "cph"),
+    comment = "es5-shim library"),
+    person("Denis", "Ineshin", role = c("ctb", "cph"),
+    comment = "ion.rangeSlider library"),
+    person("Sami", "Samhuri", role = c("ctb", "cph"),
+    comment = "Javascript strftime library")
+    )
+Description: Provides building blocks for allowing HTML widgets to communicate
+    with each other, with Shiny or without (i.e. static .html files). Currently
+    supports linked brushing and filtering.
+License: MIT + file LICENSE
+Imports: htmltools (>= 0.3.5), jsonlite, lazyeval, R6, shiny (>= 0.11),
+        ggplot2
+URL: https://rstudio.github.io/crosstalk/
+BugReports: https://github.com/rstudio/crosstalk/issues
+RoxygenNote: 5.0.1
+NeedsCompilation: no
+Packaged: 2016-12-20 20:01:51 UTC; jcheng
+Author: Joe Cheng [aut, cre],
+  RStudio [cph],
+  jQuery Foundation [cph] (jQuery library and jQuery UI library),
+  jQuery contributors [ctb, cph] (jQuery library; authors listed in
+    inst/www/shared/jquery-AUTHORS.txt),
+  Mark Otto [ctb] (Bootstrap library),
+  Jacob Thornton [ctb] (Bootstrap library),
+  Bootstrap contributors [ctb] (Bootstrap library),
+  Twitter, Inc [cph] (Bootstrap library),
+  Brian Reavis [ctb, cph] (selectize.js library),
+  Kristopher Michael Kowal [ctb, cph] (es5-shim library),
+  es5-shim contributors [ctb, cph] (es5-shim library),
+  Denis Ineshin [ctb, cph] (ion.rangeSlider library),
+  Sami Samhuri [ctb, cph] (Javascript strftime library)
+Maintainer: Joe Cheng <joe at rstudio.com>
+Repository: CRAN
+Date/Publication: 2016-12-21 08:30:32
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3c3eda6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,3 @@
+YEAR: 2016
+COPYRIGHT HOLDER: RStudio, Inc.
+
diff --git a/LICENSE.note b/LICENSE.note
new file mode 100644
index 0000000..aa50da8
--- /dev/null
+++ b/LICENSE.note
@@ -0,0 +1,341 @@
+This package includes 3rd party open source software components. The following
+is a list of these components (full copies of the license agreements used by
+these components are included below):
+
+- jQuery, https://github.com/jquery/jquery
+- Bootstrap, https://github.com/twbs/bootstrap
+- selectize.js, https://github.com/brianreavis/selectize.js
+- es5-shim, https://github.com/es-shims/es5-shim
+- ion.rangeSlider, https://github.com/IonDen/ion.rangeSlider
+- strftime for Javascript, https://github.com/samsonjs/strftime
+
+
+jQuery license
+----------------------------------------------------------------------
+
+Copyright jQuery Foundation and other contributors, https://jquery.org/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+Bootstrap License
+----------------------------------------------------------------------
+The MIT License (MIT)
+
+Copyright (c) 2011-2014 Twitter, Inc
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+selectize.js
+----------------------------------------------------------------------
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2013 Brian Reavis
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+
+es5-shim License
+----------------------------------------------------------------------
+
+The MIT License (MIT)
+
+Copyright (C) 2009-2014 Kristopher Michael Kowal and contributors
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+
+ion.rangeSlider License
+----------------------------------------------------------------------
+
+Copyright (C) 2014 by Denis Ineshin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
+strftime for Javascript License
+----------------------------------------------------------------------
+
+The MIT License (MIT)
+Copyright © 2015 Sami Samhuri, http://samhuri.net <sami at samhuri.net>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/MD5 b/MD5
new file mode 100644
index 0000000..c8a054c
--- /dev/null
+++ b/MD5
@@ -0,0 +1,60 @@
+a32f4f5aa5fac0d61ab581af3472681d *DESCRIPTION
+ab30396b0573e2cc7a8f2a3053e2ec18 *LICENSE
+3ed75fea1786a3bc880e021562763259 *LICENSE.note
+855be570e3b4409c3e1b91a322325f58 *NAMESPACE
+9fed83f3f6dd2899723adf61888e2ea8 *R/controls.R
+244f1e8649f0fa630b1303a7a8b2647f *R/crosstalk.R
+9de980b2aaeb35d6016625a91c7e4f29 *R/ggplot2.R
+ca536fd4b1294f5bac4661e7c6685c85 *README.md
+e5f6fb08f469dc836cb3609e23694b3a *inst/lib/bootstrap/css/bootstrap-theme.css
+f3d5c533946452124ee6e24d49e59819 *inst/lib/bootstrap/css/bootstrap-theme.css.map
+f0c8fc013c87173a395444fce28cb123 *inst/lib/bootstrap/css/bootstrap-theme.min.css
+be665bb9f0f7fc89f515adb828fa0a9b *inst/lib/bootstrap/css/bootstrap.css
+73200b7d42667c653330310eba7b2779 *inst/lib/bootstrap/css/bootstrap.css.map
+58a49b3689d699cb72ffda7252d99fcb *inst/lib/bootstrap/css/bootstrap.min.css
+f4769f9bdb7466be65088239c12046d1 *inst/lib/bootstrap/fonts/glyphicons-halflings-regular.eot
+89889688147bd7575d6327160d64e760 *inst/lib/bootstrap/fonts/glyphicons-halflings-regular.svg
+e18bbf611f2a2e43afc071aa2f4e1512 *inst/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf
+fa2772327f55d8198301fdb8bcfc8158 *inst/lib/bootstrap/fonts/glyphicons-halflings-regular.woff
+448c34a56d699c29117adc64c43affeb *inst/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2
+6bfd171748f088ad503cb07c080b1f33 *inst/lib/bootstrap/js/bootstrap.js
+046ba2b5f4cff7d2eaaa1af55caa9fd8 *inst/lib/bootstrap/js/bootstrap.min.js
+ccb7f3909e30b1eb8f65a24393c6e12b *inst/lib/bootstrap/js/npm.js
+ae8fceae0e07d55b5cfaa9af6bc43a6d *inst/lib/bootstrap/shim/html5shiv.min.js
+506fe393e9f296d14b63733c0aff6205 *inst/lib/bootstrap/shim/respond.min.js
+2ae68042ac97e2b2213b713deece387f *inst/lib/ionrangeslider/css/ion.rangeSlider.css
+8d631a7cac12ac3c47d8bfe67f896543 *inst/lib/ionrangeslider/css/ion.rangeSlider.skinFlat.css
+7527c2a31899e27ddbc0fcba8dfc3b8d *inst/lib/ionrangeslider/css/ion.rangeSlider.skinHTML5.css
+06a452d69645df91f1903faf13aa7608 *inst/lib/ionrangeslider/css/ion.rangeSlider.skinModern.css
+796c5169d059bb621ebbb380bf013afd *inst/lib/ionrangeslider/css/ion.rangeSlider.skinNice.css
+76f1f0a32f94da941bab0bce6d67d477 *inst/lib/ionrangeslider/css/ion.rangeSlider.skinShiny.css
+b3989d62c6fe8f0506cce37fed71df96 *inst/lib/ionrangeslider/css/ion.rangeSlider.skinSimple.css
+ae65a946b5385bd48861c6ce3895e3e8 *inst/lib/ionrangeslider/css/normalize.css
+bcdb14f38e27b16edeabb62e6e9a829b *inst/lib/ionrangeslider/img/sprite-skin-flat.png
+6035779c2555ab87be45bbc21f9cae47 *inst/lib/ionrangeslider/img/sprite-skin-modern.png
+41732f58be91fcdc79381f239685c0e1 *inst/lib/ionrangeslider/img/sprite-skin-nice.png
+43c6858e46da90d6e201dfed860e8b86 *inst/lib/ionrangeslider/img/sprite-skin-simple.png
+d4b2be381ee15900642f9ab06c9bfc65 *inst/lib/ionrangeslider/js/ion.rangeSlider.js
+6b41096306931c8df11fddf0513e47b2 *inst/lib/ionrangeslider/js/ion.rangeSlider.min.js
+12b40fddbb08ec43e278e7d8a0ab5543 *inst/lib/jquery/jquery-AUTHORS.txt
+7f38dcbfb11aff050652ff3b754adb63 *inst/lib/jquery/jquery.js
+895323ed2f7258af4fae2c738c8aea49 *inst/lib/jquery/jquery.min.js
+c34c7baffbb76616a99568e01f27930e *inst/lib/jquery/jquery.min.map
+d75b17ebe7200b2ff0b8d20d32853c35 *inst/lib/selectize/css/selectize.bootstrap3.css
+bd681a8efd2f45628dddf749426dc633 *inst/lib/selectize/js/es5-shim.min.js
+146435eeda32f0e12bca8519f0da5ad9 *inst/lib/selectize/js/selectize.min.js
+4feedff422885d51bf57601f8a987d70 *inst/lib/strftime/strftime-min.js
+a03b8a3a33eb8a69d37a03746ba92b4f *inst/www/css/crosstalk.css
+39a2f512456b98c824f12c857a118ea6 *inst/www/js/crosstalk.js
+a1de3ba877618aa3d1e03d8245cef18f *inst/www/js/crosstalk.js.map
+116f1b299d39312303a2be2b5a7ac5d8 *inst/www/js/crosstalk.min.js
+b5d03fc86d1c2dff0f6fca2c6f16c3e9 *inst/www/js/crosstalk.min.js.map
+0f3f4edabc458eec98af2d53df9f7d0d *man/ClientValue.Rd
+5691508c2ec4bf80cf4def417bcfc35b *man/SharedData.Rd
+b03043bd832ee6df8a7c49c80fdd7add *man/bscols.Rd
+279bca8896be3f1e1fcb23f488b6c392 *man/crosstalkLibs.Rd
+942b1bc332e70e64958931aec2d43356 *man/filter_select.Rd
+a955de0dd8e05931c08400d8fb2a5428 *man/filter_slider.Rd
+58d166ec5524213a271db776586103b1 *man/is.SharedData.Rd
+eb1fddd8f36e6fe303de7f2da2643c13 *man/maintain_selection.Rd
+85dd5b5628e7d518f816f2d9d1605f30 *man/scale_fill_selection.Rd
diff --git a/NAMESPACE b/NAMESPACE
new file mode 100644
index 0000000..c72d459
--- /dev/null
+++ b/NAMESPACE
@@ -0,0 +1,21 @@
+# Generated by roxygen2: do not edit by hand
+
+export(ClientValue)
+export(SharedData)
+export(animation_options)
+export(bscols)
+export(crosstalkLibs)
+export(filter_checkbox)
+export(filter_select)
+export(filter_slider)
+export(is.SharedData)
+export(maintain_selection)
+export(scale_color_selection)
+export(scale_fill_selection)
+export(selection_factor)
+import(R6)
+import(htmltools)
+import(shiny)
+importFrom(stats,na.omit)
+importFrom(stats,setNames)
+importFrom(utils,packageVersion)
diff --git a/R/controls.R b/R/controls.R
new file mode 100644
index 0000000..8a9ddc0
--- /dev/null
+++ b/R/controls.R
@@ -0,0 +1,554 @@
+bootstrapLib <- function(theme = NULL) {
+  # Intentionally use an older version of bootstrap. The rendering
+  # environment may use a bootstrap version that has a theme, and
+  # we don't want to trump that just for our little controls.
+  # Ideally we should find a better solution for this.
+  htmlDependency("bootstrap", "3.3.2",
+    system.file("lib/bootstrap", package = "crosstalk"),
+    script = c(
+      "js/bootstrap.min.js"
+    ),
+    stylesheet = if (is.null(theme)) "css/bootstrap.min.css",
+    meta = list(viewport = "width=device-width, initial-scale=1")
+  )
+}
+
+selectizeLib <- function(bootstrap = TRUE) {
+  htmlDependency(
+    "selectize", "0.11.2",
+    system.file("lib/selectize", package = "crosstalk"),
+    stylesheet = if (bootstrap) "css/selectize.bootstrap3.css",
+    script = "js/selectize.min.js"
+  )
+}
+
+jqueryLib <- function() {
+  htmlDependency(
+    "jquery", "1.11.3",
+    system.file("lib/jquery", package = "crosstalk"),
+    script = "jquery.min.js"
+  )
+}
+
+ionrangesliderLibs <- function() {
+  list(
+    jqueryLib(),
+    htmlDependency("ionrangeslider", "2.1.2",
+      system.file("lib/ionrangeslider", package = "crosstalk"),
+      script = "js/ion.rangeSlider.min.js",
+      # ion.rangeSlider also needs normalize.css, which is already included in
+      # Bootstrap.
+      stylesheet = c("css/ion.rangeSlider.css",
+        "css/ion.rangeSlider.skinShiny.css")
+    ),
+    htmlDependency("strftime", "0.9.2",
+      system.file("lib/strftime", package = "crosstalk"),
+      script = "strftime-min.js"
+    )
+  )
+}
+
+makeGroupOptions <- function(sharedData, group, allLevels) {
+  df <- sharedData$data(
+    withSelection = FALSE,
+    withFilter = FALSE,
+    withKey = TRUE
+  )
+
+  if (inherits(group, "formula"))
+    group <- lazyeval::f_eval(group, df)
+
+  if (length(group) < 1) {
+    stop("Can't form options with zero-length group vector")
+  }
+
+  lvls <- if (is.factor(group)) {
+    if (allLevels) {
+      levels(group)
+    } else {
+      levels(droplevels(group))
+    }
+  } else {
+    sort(unique(group))
+  }
+  matches <- match(group, lvls)
+  vals <- lapply(1:length(lvls), function(i) {
+    df$key_[which(matches == i)]
+  })
+
+  lvls_str <- as.character(lvls)
+
+  options <- list(
+    items = data.frame(value = lvls_str, label = lvls_str, stringsAsFactors = FALSE),
+    map = setNames(vals, lvls_str),
+    group = sharedData$groupName()
+  )
+
+  options
+}
+
+#' Categorical filter controls
+#'
+#' Creates a select box or list of checkboxes, for filtering a
+#' \code{\link{SharedData}} object based on categorical data.
+#'
+#' @param id An HTML element ID; must be unique within the web page
+#' @param label A human-readable label
+#' @param sharedData \code{SharedData} object with the data to filter
+#' @param group A one-sided formula whose values will populate this select box.
+#'   Generally this should be a character or factor column; if not, it will be
+#'   coerced to character.
+#' @param allLevels If the vector described by \code{group} is factor-based,
+#'   should all the levels be displayed as options, or only ones that are
+#'   present in the data?
+#' @param multiple Can multiple values be selected?
+#' @param columns Number of columns the options should be arranged into.
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' sd <- SharedData$new(chickwts)
+#' filter_select("feedtype", "Feed type", sd, "feed")
+#'
+#' }
+#'
+#' @export
+filter_select <- function(id, label, sharedData, group, allLevels = FALSE,
+  multiple = TRUE) {
+
+  options <- makeGroupOptions(sharedData, group, allLevels)
+
+  htmltools::browsable(attachDependencies(
+    tags$div(id = id, class = "form-group crosstalk-input-select crosstalk-input",
+      tags$label(class = "control-label", `for` = id, label),
+      tags$div(
+        tags$select(
+          multiple = if (multiple) NA else NULL
+        ),
+        tags$script(type = "application/json",
+          `data-for` = id,
+          jsonlite::toJSON(options, dataframe = "columns", pretty = TRUE)
+        )
+      )
+    ),
+    c(list(jqueryLib(), bootstrapLib(), selectizeLib()), crosstalkLibs())
+  ))
+}
+
+columnize <- function(columnCount, elements) {
+  if (columnCount <= 1 || length(elements) <= 1) {
+    return(elements)
+  }
+
+  columnSize <- ceiling(length(elements) / columnCount)
+  lapply(1:ceiling(length(elements) / columnSize), function(i) {
+    tags$div(class = "crosstalk-options-column",
+      {
+        start <- (i-1) * columnSize + 1
+        end <- i * columnSize
+        elements[start:end]
+      }
+    )
+  })
+}
+
+#' @param inline If \code{TRUE}, render checkbox options horizontally instead of vertically.
+#'
+#' @rdname filter_select
+#' @export
+filter_checkbox <- function(id, label, sharedData, group, allLevels = FALSE, inline = FALSE, columns = 1) {
+  options <- makeGroupOptions(sharedData, group, allLevels)
+
+  labels <- options$items$label
+  values <- options$items$value
+  options$items <- NULL # Doesn't need to be serialized for this type of control
+
+  makeCheckbox <- if (inline) inlineCheckbox else blockCheckbox
+
+  htmltools::browsable(attachDependencies(
+    tags$div(id = id, class = "form-group crosstalk-input-checkboxgroup crosstalk-input",
+      tags$label(class = "control-label", `for` = id, label),
+      tags$div(class = "crosstalk-options-group",
+        columnize(columns,
+          mapply(labels, values, FUN = function(label, value) {
+            makeCheckbox(id, value, label)
+          }, SIMPLIFY = FALSE, USE.NAMES = FALSE)
+        )
+      ),
+      tags$script(type = "application/json",
+        `data-for` = id,
+        jsonlite::toJSON(options, dataframe = "columns", pretty = TRUE)
+      )
+    ),
+    c(list(jqueryLib(), bootstrapLib()), crosstalkLibs())
+  ))
+}
+
+blockCheckbox <- function(id, value, label) {
+  tags$div(class = "checkbox",
+    tags$label(
+      tags$input(type = "checkbox", name = id, value = value),
+      tags$span(label)
+    )
+  )
+}
+
+inlineCheckbox <- function(id, value, label) {
+  tags$label(class = "checkbox-inline",
+    tags$input(type = "checkbox", name = id, value = value),
+    tags$span(label)
+  )
+}
+
+#' Range filter control
+#'
+#' Creates a slider widget that lets users filter observations based on a range
+#' of values.
+#'
+#' @param id An HTML element ID; must be unique within the web page
+#' @param label A human-readable label
+#' @param sharedData \code{SharedData} object with the data to filter
+#' @param column A one-sided formula whose values will be used for this slider.
+#'   The column must be of type \code{\link{Date}}, \code{\link{POSIXt}}, or
+#'   numeric.
+#' @param step Specifies the interval between each selectable value on the
+#'   slider (if \code{NULL}, a heuristic is used to determine the step size). If
+#'   the values are dates, \code{step} is in days; if the values are times
+#'   (POSIXt), \code{step} is in seconds.
+#' @param round \code{TRUE} to round all values to the nearest integer;
+#'   \code{FALSE} if no rounding is desired; or an integer to round to that
+#'   number of digits (for example, 1 will round to the nearest 10, and -2 will
+#'   round to the nearest .01). Any rounding will be applied after snapping to
+#'   the nearest step.
+#' @param ticks \code{FALSE} to hide tick marks, \code{TRUE} to show them
+#'   according to some simple heuristics.
+#' @param animate \code{TRUE} to show simple animation controls with default
+#'   settings; \code{FALSE} not to; or a custom settings list, such as those
+#'   created using \code{\link{animationOptions}}.
+#' @param width The width of the slider control (see
+#'   \code{\link[htmltools]{validateCssUnit}} for valid formats)
+#' @param sep Separator between thousands places in numbers.
+#' @param pre A prefix string to put in front of the value.
+#' @param post A suffix string to put after the value.
+#' @param dragRange This option is used only if it is a range slider (with two
+#'   values). If \code{TRUE} (the default), the range can be dragged. In other
+#'   words, the min and max can be dragged together. If \code{FALSE}, the range
+#'   cannot be dragged.
+#' @param timeFormat Only used if the values are Date or POSIXt objects. A time
+#'   format string, to be passed to the Javascript strftime library. See
+#'   \url{https://github.com/samsonjs/strftime} for more details. The allowed
+#'   format specifications are very similar, but not identical, to those for R's
+#'   \code{\link{strftime}} function. For Dates, the default is \code{"\%F"}
+#'   (like \code{"2015-07-01"}), and for POSIXt, the default is \code{"\%F \%T"}
+#'   (like \code{"2015-07-01 15:32:10"}).
+#' @param timezone Only used if the values are POSIXt objects. A string
+#'   specifying the time zone offset for the displayed times, in the format
+#'   \code{"+HHMM"} or \code{"-HHMM"}. If \code{NULL} (the default), times will
+#'   be displayed in the browser's time zone. The value \code{"+0000"} will
+#'   result in UTC time.
+#'
+#' @examples
+#' ## Only run examples in interactive R sessions
+#' if (interactive()) {
+#'
+#' sd <- SharedData$new(mtcars)
+#' filter_slider("mpg", "Miles per gallon", sd, "mpg")
+#'
+#' }
+#' @export
+filter_slider <- function(id, label, sharedData, column, step = NULL,
+  round = FALSE, ticks = TRUE, animate = FALSE, width = NULL, sep = ",",
+  pre = NULL, post = NULL, timeFormat = NULL,
+  timezone = NULL, dragRange = TRUE)
+{
+  # TODO: Check that this works well with factors
+  # TODO: Handle empty data frame, NA/NaN/Inf/-Inf values
+
+  if (is.character(column)) {
+    column <- lazyeval::f_new(as.symbol(column))
+  }
+
+  df <- sharedData$data(withKey = TRUE)
+  col <- lazyeval::f_eval(column, df)
+  values <- na.omit(col)
+  min <- min(values)
+  max <- max(values)
+  value <- range(values)
+
+  ord <- order(col)
+  options <- list(
+    values = col[ord],
+    keys = df$key_[ord],
+    group = sharedData$groupName()
+  )
+
+  # If step is NULL, use heuristic to set the step size.
+  findStepSize <- function(min, max, step) {
+    if (!is.null(step)) return(step)
+
+    range <- max - min
+    # If short range or decimals, use continuous decimal with ~100 points
+    if (range < 2 || hasDecimals(min) || hasDecimals(max)) {
+      step <- pretty(c(min, max), n = 100)
+      step[2] - step[1]
+    } else {
+      1
+    }
+  }
+
+  if (inherits(min, "Date")) {
+    if (!inherits(max, "Date") || !inherits(value, "Date"))
+      stop("`min`, `max`, and `value must all be Date or non-Date objects")
+    dataType <- "date"
+
+    if (is.null(timeFormat))
+      timeFormat <- "%F"
+
+  } else if (inherits(min, "POSIXt")) {
+    if (!inherits(max, "POSIXt") || !inherits(value, "POSIXt"))
+      stop("`min`, `max`, and `value must all be POSIXt or non-POSIXt objects")
+    dataType <- "datetime"
+
+    if (is.null(timeFormat))
+      timeFormat <- "%F %T"
+
+  } else {
+    dataType <- "number"
+  }
+
+  step <- findStepSize(min, max, step)
+  # Avoid ugliness from floating point errors, e.g.
+  # findStepSize(min(quakes$mag), max(quakes$mag), NULL)
+  # was returning 0.01999999999999957 instead of 0.2
+  step <- signif(step, 14)
+
+  if (dataType %in% c("date", "datetime")) {
+    # For Dates, this conversion uses midnight on that date in UTC
+    to_ms <- function(x) 1000 * as.numeric(as.POSIXct(x))
+
+    # Convert values to milliseconds since epoch (this is the value JS uses)
+    # Find step size in ms
+    step  <- to_ms(max) - to_ms(max - step)
+    min   <- to_ms(min)
+    max   <- to_ms(max)
+    value <- to_ms(value)
+  }
+
+  range <- max - min
+
+  # Try to get a sane number of tick marks
+  if (ticks) {
+    n_steps <- range / step
+
+    # Make sure there are <= 10 steps.
+    # n_ticks can be a noninteger, which is good when the range is not an
+    # integer multiple of the step size, e.g., min=1, max=10, step=4
+    scale_factor <- ceiling(n_steps / 10)
+    n_ticks <- n_steps / scale_factor
+
+  } else {
+    n_ticks <- NULL
+  }
+
+  sliderProps <- dropNulls(list(
+    `data-type` = if (length(value) > 1) "double",
+    `data-min` = formatNoSci(min),
+    `data-max` = formatNoSci(max),
+    `data-from` = formatNoSci(value[1]),
+    `data-to` = if (length(value) > 1) formatNoSci(value[2]),
+    `data-step` = formatNoSci(step),
+    `data-grid` = ticks,
+    `data-grid-num` = n_ticks,
+    `data-grid-snap` = FALSE,
+    `data-prettify-separator` = sep,
+    `data-prefix` = pre,
+    `data-postfix` = post,
+    `data-keyboard` = TRUE,
+    `data-keyboard-step` = step / (max - min) * 100,
+    `data-drag-interval` = dragRange,
+    # The following are ignored by the ion.rangeSlider, but are used by Shiny.
+    `data-data-type` = dataType,
+    `data-time-format` = timeFormat,
+    `data-timezone` = timezone
+  ))
+
+  # Replace any TRUE and FALSE with "true" and "false"
+  sliderProps <- lapply(sliderProps, function(x) {
+    if (identical(x, TRUE)) "true"
+    else if (identical(x, FALSE)) "false"
+    else x
+  })
+
+  sliderTag <- div(
+    class = "form-group crosstalk-input",
+    class = "crosstalk-input-slider js-range-slider",
+    id = id,
+
+    style = if (!is.null(width)) paste0("width: ", validateCssUnit(width), ";"),
+    if (!is.null(label)) controlLabel(id, label),
+    do.call(tags$input, sliderProps),
+    tags$script(type = "application/json",
+      `data-for` = id,
+      jsonlite::toJSON(options, dataframe = "columns", pretty = TRUE)
+    )
+  )
+
+  # Add animation buttons
+  if (identical(animate, TRUE))
+    animate <- animationOptions()
+
+  if (!is.null(animate) && !identical(animate, FALSE)) {
+    if (is.null(animate$playButton))
+      animate$playButton <- shiny::icon('play', lib = 'glyphicon')
+    if (is.null(animate$pauseButton))
+      animate$pauseButton <- shiny::icon('pause', lib = 'glyphicon')
+
+    sliderTag <- tagAppendChild(
+      sliderTag,
+      tags$div(class='slider-animate-container',
+        tags$a(href='#',
+          class='slider-animate-button',
+          'data-target-id'=id,
+          'data-interval'=animate$interval,
+          'data-loop'=animate$loop,
+          span(class = 'play', animate$playButton),
+          span(class = 'pause', animate$pauseButton)
+        )
+      )
+    )
+  }
+
+  htmltools::browsable(attachDependencies(
+    sliderTag,
+    c(ionrangesliderLibs(), crosstalkLibs())
+  ))
+}
+
+hasDecimals <- function(value) {
+  truncatedValue <- round(value)
+  return (!identical(value, truncatedValue))
+}
+
+#' @rdname filter_slider
+#'
+#' @param interval The interval, in milliseconds, between each animation step.
+#' @param loop \code{TRUE} to automatically restart the animation when it
+#'   reaches the end.
+#' @param playButton Specifies the appearance of the play button. Valid values
+#'   are a one-element character vector (for a simple text label), an HTML tag
+#'   or list of tags (using \code{\link{tag}} and friends), or raw HTML (using
+#'   \code{\link{HTML}}).
+#' @param pauseButton Similar to \code{playButton}, but for the pause button.
+#'
+#' @export
+animation_options <- function(interval=1000,
+  loop=FALSE,
+  playButton=NULL,
+  pauseButton=NULL) {
+  list(interval=interval,
+    loop=loop,
+    playButton=playButton,
+    pauseButton=pauseButton)
+}
+
+#' Arrange HTML elements or widgets in Bootstrap columns
+#'
+#' This helper function makes it easy to put HTML elements side by side. It can
+#' be called directly from the console but is especially designed to work in an
+#' R Markdown document. Warning: This will bring in all of Bootstrap!
+#'
+#' @param ... \code{htmltools} tag objects, lists, text, HTML widgets, or
+#'   NULL. These arguments should be unnamed.
+#' @param widths The number of columns that should be assigned to each of the
+#'   \code{...} elements (the total number of columns available is always 12).
+#'   The width vector will be recycled if there are more \code{...} arguments.
+#'   \code{NA} columns will evenly split the remaining columns that are left
+#'   after the widths are recycled and non-\code{NA} values are subtracted.
+#' @param device The class of device which is targeted by these widths; with
+#'   smaller screen sizes the layout will collapse to a one-column,
+#'   top-to-bottom display instead. xs: never collapse, sm: collapse below
+#'   768px, md: 992px, lg: 1200px.
+#'
+#' @return A \code{\link[htmltools]{browsable}} HTML element.
+#'
+#' @examples
+#' library(htmltools)
+#'
+#' # If width is unspecified, equal widths will be used
+#' bscols(
+#'   div(style = css(width="100%", height="400px", background_color="red")),
+#'   div(style = css(width="100%", height="400px", background_color="blue"))
+#' )
+#'
+#' # Use NA to absorb remaining width
+#' bscols(widths = c(2, NA, NA),
+#'   div(style = css(width="100%", height="400px", background_color="red")),
+#'   div(style = css(width="100%", height="400px", background_color="blue")),
+#'   div(style = css(width="100%", height="400px", background_color="green"))
+#' )
+#'
+#' # Recycling widths
+#' bscols(widths = c(2, 4),
+#'   div(style = css(width="100%", height="400px", background_color="red")),
+#'   div(style = css(width="100%", height="400px", background_color="blue")),
+#'   div(style = css(width="100%", height="400px", background_color="red")),
+#'   div(style = css(width="100%", height="400px", background_color="blue"))
+#' )
+#' @export
+bscols <- function(..., widths = NA, device = c("xs", "sm", "md", "lg")) {
+  device <- match.arg(device)
+
+  if (length(list(...)) == 0) {
+    widths = c()
+  } else {
+    if (length(widths) > length(list(...))) {
+      warning("Too many widths provided to bscols; truncating")
+    }
+    widths <- rep_len(widths, length(list(...)))
+
+    if (any(is.na(widths))) {
+      remaining <- 12 - sum(widths, na.rm = TRUE)
+      stretch_cols <- length(which(is.na(widths)))
+      stretch_width <- max(1, floor(remaining / stretch_cols))
+      widths[is.na(widths)] <- stretch_width
+    }
+
+    if (sum(widths) > 12) {
+      warning("Sum of bscol width units is greater than 12")
+    }
+  }
+
+  ui <- tags$div(class = "container-fluid crosstalk-bscols",
+    # Counteract knitr pre/code output blocks
+    tags$div(class = "fluid-row",
+      unname(mapply(list(...), widths, FUN = function(el, width) {
+        div(class = sprintf("col-%s-%s", device, width),
+          el
+        )
+      }, SIMPLIFY = FALSE))
+    )
+  )
+
+  browsable(attachDependencies(ui, list(jqueryLib(), bootstrapLib())))
+}
+
+controlLabel <- function(controlName, label) {
+  if (is.null(label)) {
+    NULL
+  } else {
+    tags$label(class = "control-label", `for` = controlName, label)
+  }
+}
+
+# Given a vector or list, drop all the NULL items in it
+dropNulls <- function(x) {
+  x[!vapply(x, is.null, FUN.VALUE=logical(1))]
+}
+
+# Format a number without sci notation, and keep as many digits as possible (do
+# we really need to go beyond 15 digits?)
+formatNoSci <- function(x) {
+  if (is.null(x)) return(NULL)
+  format(x, scientific = FALSE, digits = 15)
+}
diff --git a/R/crosstalk.R b/R/crosstalk.R
new file mode 100644
index 0000000..3a35081
--- /dev/null
+++ b/R/crosstalk.R
@@ -0,0 +1,373 @@
+#' @import htmltools
+init <- function() {
+  htmltools::attachDependencies(
+    list(),
+    crosstalkLibs()
+  )
+}
+
+#' Crosstalk dependencies
+#'
+#' List of \code{\link[htmltools]{htmlDependency}} objects necessary for
+#' Crosstalk to function. Intended for widget authors.
+#' @importFrom stats na.omit setNames
+#' @importFrom utils packageVersion
+#' @export
+crosstalkLibs <- function() {
+  list(
+    jqueryLib(),
+    htmltools::htmlDependency("crosstalk", packageVersion("crosstalk"),
+      src = system.file("www", package = "crosstalk"),
+      script = "js/crosstalk.min.js",
+      stylesheet = "css/crosstalk.css"
+    )
+  )
+}
+
+#' ClientValue object
+#'
+#' An object that can be used in a \href{http://shiny.rstudio.com}{Shiny} server
+#' function to get or set a crosstalk variable that exists on the client. The
+#' client copy of the variable is the canonical copy, so there is no direct
+#' "set" method that immediately changes the value; instead, there is a
+#' \code{sendUpdate} method that sends a request to the browser to change the
+#' value, which will then cause the new value to be relayed back to the server.
+#'
+#' @section Methods:
+#' \describe{
+#'   \item{\code{initialize(name, group = "default", session = shiny::getDefaultReactiveDomain())}}{
+#'     Create a new ClientValue object to reflect the crosstalk variable
+#'     specified by \code{group} and \code{name}. The \code{session} indicates
+#'     which Shiny session to connect to, and defaults to the current session.
+#'   }
+#'   \item{\code{get()}}{
+#' Read the value. This is a reactive operation akin to reading a reactive
+#' value, and so can only be done in a reactive context (e.g. in a
+#' \code{\link[shiny]{reactive}}, \code{\link[shiny]{observe}}, or
+#' \code{\link[shiny]{isolate}} block).
+#'   }
+#'   \item{\code{sendUpdate(value)}}{
+#'     Send a message to the browser asking it to update the crosstalk var to
+#'     the given value. This update does not happen synchronously, that is, a
+#'     call to \code{get()} immediately following \code{sendUpdate(value)} will
+#'     not reflect the new value. The value must be serializable as JSON using
+#'     jsonlite.
+#'   }
+#' }
+#'
+#' @examples
+#' library(shiny)
+#'
+#' server <- function(input, output, session) {
+#'   cv <- ClientValue$new("var1", "group1")
+#'
+#'   r <- reactive({
+#'     # Don't proceed unless cv$get() is a non-NULL value
+#'     validate(need(cv$get(), message = FALSE))
+#'
+#'     runif(cv$get())
+#'   })
+#'
+#'   observeEvent(input$click, {
+#'     cv$sendUpdate(NULL)
+#'   })
+#' }
+#'
+#' @docType class
+#' @import R6
+#' @format An \code{\link{R6Class}} generator object
+#' @export
+ClientValue <- R6Class(
+  "ClientValue",
+  private = list(
+    .session = "ANY",
+    .name = "ANY",
+    .group = "ANY",
+    .qualifiedName = "ANY",
+    .rv = "ANY"
+  ),
+  public = list(
+    initialize = function(name, group = "default", session = shiny::getDefaultReactiveDomain()) {
+      private$.session <- session
+      private$.name <- name
+      private$.group <- group
+      private$.qualifiedName <- paste0(".clientValue-", group, "-", name)
+    },
+    get = function() {
+      private$.session$input[[private$.qualifiedName]]
+    },
+    sendUpdate = function(value) {
+      private$.session$sendCustomMessage("update-client-value", list(
+        name = private$.name,
+        group = private$.group,
+        value = value
+      ))
+    }
+  )
+)
+
+
+createUniqueId <- function (bytes, prefix = "", suffix = "") {
+  paste(prefix, paste(format(as.hexmode(sample(256, bytes,
+    replace = TRUE) - 1), width = 2), collapse = ""),
+    suffix, sep = "")
+}
+
+
+#' An R6 class that represents a shared data frame
+#'
+#' ...or sufficiently data frame-like object. The primary use for
+#' \code{SharedData} is to be passed to Crosstalk-compatible widgets in place
+#' of a data frame. Each \code{SharedData$new(...)} call makes a new "group"
+#' of widgets that link to each other, but not to widgets in other groups.
+#' You can also use a \code{SharedData} object from Shiny code in order to
+#' react to filtering and brushing from non-widget visualizations (like ggplot2
+#' plots).
+#'
+#' @section Constructor:
+#'
+#' \code{SharedData$new(data, key = NULL, group = createUniqueId(4, prefix = "SharedData"))}
+#'
+#' \describe{
+#'   \item{\code{data}}{
+#'     A data frame-like object, or a Shiny \link[=reactive]{reactive
+#'     expression} that returns a data frame-like object.
+#'   }
+#'   \item{\code{key}}{
+#'     Character vector or one-sided formula that indicates the name of the
+#'     column that represents the key or ID of the data frame. These \emph{must}
+#'     be unique, and ideally will be something intrinsic to the data (a proper
+#'     ID) rather than a transient property like row index.
+#'
+#'     If \code{NULL}, then \code{row.names(data)} will be used.
+#'   }
+#'   \item{\code{group}}{
+#'     The "identity" of the Crosstalk group that widgets will join when you
+#'     pass them this \code{SharedData} object. In some cases, you will want to
+#'     have multiple independent \code{SharedData} objects link up to form a
+#'     single web of widgets that all share selection and filtering state; in
+#'     those cases, you'll give those \code{SharedData} objects the same group
+#'     name. (One example: in Shiny, ui.R and server.R might each need their own
+#'     \code{SharedData} instance, even though they're intended to represent a
+#'     single group.)
+#'   }
+#' }
+#'
+#' @section Methods:
+#'
+#' \describe{
+#'   \item{\code{data(withSelection = FALSE, withFilter = TRUE, withKey = FALSE)}}{
+#'     Return the data (or read and return the data if the data is a Shiny
+#'     reactive expression). If \code{withSelection}, add a \code{selection_}
+#'     column with logical values indicating which rows are in the current
+#'     selection, or \code{NA} if no selection is currently active. If
+#'     \code{withFilter} (the default), only return rows that are part of the
+#'     current filter settings, if any. If \code{withKey}, add a \code{key_}
+#'     column with the key values of each row (normally not needed since the
+#'     key is either one of the other columns or else just the row names).
+#'
+#'     When running in Shiny, calling \code{data()} is a reactive operation
+#'     that will invalidate if the selection or filter change (assuming that
+#'     information was requested), or if the original data is a reactive
+#'     expression that has invalidated.
+#'   }
+#'   \item{\code{origData()}}{
+#'     Return the data frame that was used to create this \code{SharedData}
+#'     instance. If a reactive expression, evaluate the reactive expression.
+#'     Equivalent to \code{data(FALSE, FALSE, FALSE)}.
+#'   }
+#'   \item{\code{groupName()}}{
+#'     Returns the value of \code{group} that was used to create this instance.
+#'   }
+#'   \item{\code{key()}}{
+#'     Returns the vector of key values. Filtering is not applied.
+#'   }
+#'   \item{\code{selection(value, ownerId = "")}}{
+#'     If called without arguments, returns a logical vector of rows that are
+#'     currently selected (brushed), or \code{NULL} if no selection exists.
+#'     Intended to be called from a Shiny reactive context, and invalidates
+#'     whenever the selection changes.
+#'
+#'     If called with one or two arguments, expects \code{value} to be a logical
+#'     vector of \code{nrow(origData())} length, indicating which rows are
+#'     currently selected (brushed). This value is propagated to the web browser
+#'     (assumes an active Shiny app or Shiny R Markdown document).
+#'
+#'     Set the \code{ownerId} argument to the \code{outputId} of a widget if
+#'     conceptually that widget "initiated" the selection (prevents that widget
+#'     from clearing its visual selection box, which is normally cleared when
+#'     the selection changes). For example, if setting the selection based on a
+#'     \code{\link[shiny]{plotOutput}} brush, then \code{ownerId} should be the
+#'     \code{outputId} of the \code{plotOutput}.
+#'   }
+#'   \item{\code{clearSelection(ownerId = "")}}{
+#'     Clears the selection. For the meaning of \code{ownerId}, see the
+#'     \code{selection} method.
+#'   }
+#' }
+#'
+#' @import R6 shiny
+#' @export
+SharedData <- R6Class(
+  "SharedData",
+  private = list(
+    .data = "ANY",
+    .key = "ANY",
+    .filterCV = "ANY",
+    .selectionCV = "ANY",
+    .rv = "ANY",
+    .group = "ANY"
+  ),
+  public = list(
+    initialize = function(data, key = NULL, group = createUniqueId(4, prefix = "SharedData")) {
+      private$.data <- data
+      private$.filterCV <- ClientValue$new("filter", group)
+      private$.selectionCV <- ClientValue$new("selection", group)
+      private$.rv <- shiny::reactiveValues()
+      private$.group <- group
+
+      if (inherits(key, "formula")) {
+        private$.key <- key
+      } else if (is.character(key)) {
+        private$.key <- key
+      } else if (is.function(key)) {
+        private$.key <- key
+      } else if (is.null(key)) {
+        private$.key <- key
+      } else {
+        stop("Unknown key type")
+      }
+
+      if (shiny::is.reactive(private$.data)) {
+        observeEvent(private$.data(), {
+          self$clearSelection()
+        })
+      }
+
+      domain <- shiny::getDefaultReactiveDomain()
+      if (!is.null(domain)) {
+        observe({
+          selection <- private$.selectionCV$get()
+          if (!is.null(selection) && length(selection) > 0) {
+            self$.updateSelection(self$key() %in% selection)
+          } else {
+            self$.updateSelection(NULL)
+          }
+        })
+      }
+    },
+    origData = function() {
+      if (shiny::is.reactive(private$.data)) {
+        private$.data()
+      } else {
+        private$.data
+      }
+    },
+    groupName = function() {
+      private$.group
+    },
+    key = function() {
+      df <- if (shiny::is.reactive(private$.data)) {
+        private$.data()
+      } else {
+        private$.data
+      }
+
+      key <- private$.key
+      if (inherits(key, "formula"))
+        lazyeval::f_eval(key, df)
+      else if (is.character(key))
+        key
+      else if (is.function(key))
+        key(df)
+      else if (!is.null(row.names(df)))
+        row.names(df)
+      else if (nrow(df) > 0)
+        as.character(1:nrow(df))
+      else
+        character()
+    },
+    data = function(withSelection = FALSE, withFilter = TRUE, withKey = FALSE) {
+      df <- if (shiny::is.reactive(private$.data)) {
+        private$.data()
+      } else {
+        private$.data
+      }
+
+      op <- options(shiny.suppressMissingContextError = TRUE)
+      on.exit(options(op), add = TRUE)
+
+      if (withSelection) {
+        if (is.null(private$.rv$selected) || length(private$.rv$selected) == 0) {
+          df$selected_ = NA
+        } else {
+          # TODO: Warn if the length of _selected is different?
+          df$selected_ <- private$.rv$selected
+        }
+      }
+
+      if (withKey) {
+        df$key_ <- self$key()
+      }
+
+      if (withFilter) {
+        if (!is.null(private$.filterCV$get())) {
+          df <- df[self$key() %in% private$.filterCV$get(),]
+        }
+      }
+
+      df
+    },
+    # Public API for selection getting/setting. Setting a selection will
+    # cause an event to be propagated to the client.
+    selection = function(value, ownerId = "") {
+      if (missing(value)) {
+        return(private$.rv$selected)
+      } else {
+        # TODO: Should we even update the server at this time? Or do we
+        # force all such events to originate in the client (much like
+        # updateXXXInput)?
+
+        # .updateSelection needs logical array of length nrow(data)
+        # .selectionCV$sendUpdate needs character array of keys
+        isolate({
+          if (is.null(value)) {
+            self$.updateSelection(NULL)
+            private$.selectionCV$sendUpdate(NULL)
+          } else {
+            key <- self$key()
+            if (is.character(value)) {
+              self$.updateSelection(key %in% value)
+              private$.selectionCV$sendUpdate(value)
+            } else if (is.logical(value)) {
+              self$.updateSelection(value)
+              private$.selectionCV$sendUpdate(key[value])
+            } else if (is.numeric(value)) {
+              self$selection(1:nrow(self$data(FALSE)) %in% value)
+            }
+          }
+        })
+      }
+    },
+    clearSelection = function(ownerId = "") {
+      self$selection(list(), ownerId = "")
+    },
+    # Update selection without sending event
+    .updateSelection = function(value) {
+      force(value)
+      `$<-`(private$.rv, "selected", value)
+    }
+  )
+)
+
+#' Check if an object is \code{SharedData}
+#'
+#' Check if an object is an instance of \code{\link{SharedData}} or not.
+#'
+#' @param x The object that may or may not be an instance of \code{SharedData}
+#' @return logical
+#'
+#' @export
+is.SharedData <- function(x) {
+  inherits(x, "SharedData")
+}
diff --git a/R/ggplot2.R b/R/ggplot2.R
new file mode 100644
index 0000000..39f9025
--- /dev/null
+++ b/R/ggplot2.R
@@ -0,0 +1,81 @@
+#' ggplot2 helpers
+#'
+#' Add \code{scale_fill_selection()} or \code{scale_color_selection} to a ggplot
+#' to customize the scale for fill or color, respectively, for linked brushing.
+#' Use \code{selection_factor} to turn logical vectors representing selection,
+#' to a factor with the levels ordered for use with ggplot2 bar stacking.
+#'
+#' @param color_false The color that should be mapped to unselected rows
+#' @param color_true The color that should be mapped to selected rows
+#'
+#' @examples
+#' \dontrun{
+#' sd <- SharedData$new(iris)
+#' renderPlot({
+#'   df <- sd$data(withSelection = TRUE, withFilter = TRUE)
+#'   ggplot(df, aes(Sepal.Length, Sepal.Width,
+#'     color = selection_factor(df))) +
+#'     geom_point() +
+#'     scale_color_selection("#444444", "skyblue1")
+#' })
+#'
+#' }
+#' @export
+scale_fill_selection <- function(color_false, color_true) {
+  list(
+    ggplot2::scale_fill_manual(values = c("TRUE" = color_true, "FALSE" = color_false)),
+    ggplot2::guides(fill = FALSE)
+  )
+}
+
+#' @rdname scale_fill_selection
+#' @export
+scale_color_selection <- function(color_false, color_true) {
+  list(
+    ggplot2::scale_color_manual(values = c("TRUE" = color_true, "FALSE" = color_false)),
+    ggplot2::guides(colour = FALSE)
+  )
+}
+
+#' @param x Either a data frame with a \code{selected_} column, or, a logical
+#'   vector indicating which rows are selected
+#' @param na.replace The value to use to replace \code{NA} values; choose either
+#'   \code{FALSE}, \code{NA}, or \code{TRUE} based on how you want values to be
+#'   treated when no selection is active
+#' @rdname scale_fill_selection
+#' @export
+selection_factor <- function(x, na.replace = c(FALSE, NA, TRUE)) {
+  if (missing(na.replace))
+    na.replace <- FALSE
+
+  selection <- if (is.logical(x)) {
+    x
+  } else {
+    x$selected_
+  }
+  selection[is.na(selection)] <- na.replace
+  factor(selection, ordered = TRUE, levels = c(TRUE, FALSE))
+}
+
+#' Synchronize Shiny brush selection with shared data
+#'
+#' Waits for a brush to change, and propagates that change to the
+#' \code{sharedData} object.
+#'
+#' @param sharedData The shared data instance
+#' @param brushId Character vector indicating the name of the \code{plotOutput}
+#'   brush
+#' @param ownerId (TBD)
+#'
+#' @export
+maintain_selection <- function(sharedData, brushId, ownerId = "") {
+  force(sharedData)
+  force(brushId)
+  session <- shiny::getDefaultReactiveDomain()
+
+  observeEvent(session$input[[brushId]], {
+    df <- sharedData$data(withKey = TRUE, withFilter = TRUE)
+    df <- shiny::brushedPoints(df, session$input[[brushId]])
+    sharedData$selection(df$key_, ownerId)
+  }, ignoreNULL = FALSE)
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b98d0b9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Crosstalk [![Build Status](https://travis-ci.org/rstudio/crosstalk.svg?branch=master)](https://travis-ci.org/rstudio/crosstalk)
+
+Crosstalk is a package for R that enhances the [htmlwidgets](http://htmlwidgets.org) package. It extends htmlwidgets with a set of classes, functions, and conventions for implementing cross-widget interactions (currently, linked brushing and filtering).
+
+Find out more at the documentation website: http://rstudio.github.io/crosstalk/
diff --git a/inst/www/css/crosstalk.css b/inst/www/css/crosstalk.css
new file mode 100644
index 0000000..46befd2
--- /dev/null
+++ b/inst/www/css/crosstalk.css
@@ -0,0 +1,27 @@
+/* Adjust margins outwards, so column contents line up with the edges of the
+   parent of container-fluid. */
+.container-fluid.crosstalk-bscols {
+  margin-left: -30px;
+  margin-right: -30px;
+  white-space: normal;
+}
+
+/* But don't adjust the margins outwards if we're directly under the body,
+   i.e. we were the top-level of something at the console. */
+body > .container-fluid.crosstalk-bscols {
+  margin-left: auto;
+  margin-right: auto;
+}
+
+.crosstalk-input-checkboxgroup .crosstalk-options-group .crosstalk-options-column {
+  display: inline-block;
+  padding-right: 12px;
+  vertical-align: top;
+}
+
+ at media only screen and (max-width:480px) {
+  .crosstalk-input-checkboxgroup .crosstalk-options-group .crosstalk-options-column {
+    display: block;
+    padding-right: inherit;
+  }
+}
diff --git a/inst/www/js/crosstalk.js b/inst/www/js/crosstalk.js
new file mode 100644
index 0000000..8e6ee30
--- /dev/null
+++ b/inst/www/js/crosstalk.js
@@ -0,0 +1,1471 @@
+(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var Events = function () {
+  function Events() {
+    _classCallCheck(this, Events);
+
+    this._types = {};
+    this._seq = 0;
+  }
+
+  _createClass(Events, [{
+    key: "on",
+    value: function on(eventType, listener) {
+      var subs = this._types[eventType];
+      if (!subs) {
+        subs = this._types[eventType] = {};
+      }
+      var sub = "sub" + this._seq++;
+      subs[sub] = listener;
+      return sub;
+    }
+
+    // Returns false if no match, or string for sub name if matched
+
+  }, {
+    key: "off",
+    value: function off(eventType, listener) {
+      var subs = this._types[eventType];
+      if (typeof listener === "function") {
+        for (var key in subs) {
+          if (subs.hasOwnProperty(key)) {
+            if (subs[key] === listener) {
+              delete subs[key];
+              return key;
+            }
+          }
+        }
+        return false;
+      } else if (typeof listener === "string") {
+        if (subs && subs[listener]) {
+          delete subs[listener];
+          return listener;
+        }
+        return false;
+      } else {
+        throw new Error("Unexpected type for listener");
+      }
+    }
+  }, {
+    key: "trigger",
+    value: function trigger(eventType, arg, thisObj) {
+      var subs = this._types[eventType];
+      for (var key in subs) {
+        if (subs.hasOwnProperty(key)) {
+          subs[key].call(thisObj, arg);
+        }
+      }
+    }
+  }]);
+
+  return Events;
+}();
+
+exports.default = Events;
+
+},{}],2:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.FilterHandle = undefined;
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+var _events = require("./events");
+
+var _events2 = _interopRequireDefault(_events);
+
+var _filterset = require("./filterset");
+
+var _filterset2 = _interopRequireDefault(_filterset);
+
+var _group = require("./group");
+
+var _group2 = _interopRequireDefault(_group);
+
+var _util = require("./util");
+
+var util = _interopRequireWildcard(_util);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function getFilterSet(group) {
+  var fsVar = group.var("filterset");
+  var result = fsVar.get();
+  if (!result) {
+    result = new _filterset2.default();
+    fsVar.set(result);
+  }
+  return result;
+}
+
+var id = 1;
+function nextId() {
+  return id++;
+}
+
+var FilterHandle = exports.FilterHandle = function () {
+  /**
+   * @classdesc
+   * Use this class to contribute to, and listen for changes to, the filter set
+   * for the given group of widgets. Filter input controls should create one
+   * `FilterHandle` and only call {@link FilterHandle#set}. Output widgets that
+   * wish to displayed filtered data should create one `FilterHandle` and use
+   * the {@link FilterHandle#filteredKeys} property and listen for change
+   * events.
+   *
+   * If two (or more) `FilterHandle` instances in the same webpage share the
+   * same group name, they will contribute to a single "filter set". Each
+   * `FilterHandle` starts out with a `null` value, which means they take
+   * nothing away from the set of data that should be shown. To make a
+   * `FilterHandle` actually remove data from the filter set, set its value to
+   * an array of keys which should be displayed. Crosstalk will aggregate the
+   * various key arrays by finding their intersection; only keys that are
+   * present in all non-null filter handles are considered part of the filter
+   * set.
+   *
+   * @param {string} [group] - The name of the Crosstalk group, or if none,
+   *   null or undefined (or any other falsy value). This can be changed later
+   *   via the @{link FilterHandle#setGroup} method.
+   * @param {Object} [extraInfo] - An object whose properties will be copied to
+   *   the event object whenever an event is emitted.
+   */
+  function FilterHandle(group, extraInfo) {
+    _classCallCheck(this, FilterHandle);
+
+    this._eventRelay = new _events2.default();
+    this._emitter = new util.SubscriptionTracker(this._eventRelay);
+
+    // Name of the group we're currently tracking, if any. Can change over time.
+    this._group = null;
+    // The filterSet that we're tracking, if any. Can change over time.
+    this._filterSet = null;
+    // The Var we're currently tracking, if any. Can change over time.
+    this._filterVar = null;
+    // The event handler subscription we currently have on var.on("change").
+    this._varOnChangeSub = null;
+
+    this._extraInfo = util.extend({ sender: this }, extraInfo);
+
+    this._id = "filter" + nextId();
+
+    this.setGroup(group);
+  }
+
+  /**
+   * Changes the Crosstalk group membership of this FilterHandle. If `set()` was
+   * previously called on this handle, switching groups will clear those keys
+   * from the old group's filter set. These keys will not be applied to the new
+   * group's filter set either. In other words, `setGroup()` effectively calls
+   * `clear()` before switching groups.
+   *
+   * @param {string} group - The name of the Crosstalk group, or null (or
+   *   undefined) to clear the group.
+   */
+
+
+  _createClass(FilterHandle, [{
+    key: "setGroup",
+    value: function setGroup(group) {
+      var _this = this;
+
+      // If group is unchanged, do nothing
+      if (this._group === group) return;
+      // Treat null, undefined, and other falsy values the same
+      if (!this._group && !group) return;
+
+      if (this._filterVar) {
+        this._filterVar.off("change", this._varOnChangeSub);
+        this.clear();
+        this._varOnChangeSub = null;
+        this._filterVar = null;
+        this._filterSet = null;
+      }
+
+      this._group = group;
+
+      if (group) {
+        group = (0, _group2.default)(group);
+        this._filterSet = getFilterSet(group);
+        this._filterVar = (0, _group2.default)(group).var("filter");
+        var sub = this._filterVar.on("change", function (e) {
+          _this._eventRelay.trigger("change", e, _this);
+        });
+        this._varOnChangeSub = sub;
+      }
+    }
+
+    /**
+     * Combine the given `extraInfo` (if any) with the handle's default
+     * `_extraInfo` (if any).
+     * @private
+     */
+
+  }, {
+    key: "_mergeExtraInfo",
+    value: function _mergeExtraInfo(extraInfo) {
+      return util.extend({}, this._extraInfo ? this._extraInfo : null, extraInfo ? extraInfo : null);
+    }
+
+    /**
+     * Close the handle. This clears this handle's contribution to the filter set,
+     * and unsubscribes all event listeners.
+     */
+
+  }, {
+    key: "close",
+    value: function close() {
+      this._emitter.removeAllListeners();
+      this.clear();
+      this.setGroup(null);
+    }
+
+    /**
+     * Clear this handle's contribution to the filter set.
+     *
+     * @param {Object} [extraInfo] - Extra properties to be included on the event
+     *   object that's passed to listeners (in addition to any options that were
+     *   passed into the `FilterHandle` constructor).
+     */
+
+  }, {
+    key: "clear",
+    value: function clear(extraInfo) {
+      if (!this._filterSet) return;
+      this._filterSet.clear(this._id);
+      this._onChange(extraInfo);
+    }
+
+    /**
+     * Set this handle's contribution to the filter set. This array should consist
+     * of the keys of the rows that _should_ be displayed; any keys that are not
+     * present in the array will be considered _filtered out_. Note that multiple
+     * `FilterHandle` instances in the group may each contribute an array of keys,
+     * and only those keys that appear in _all_ of the arrays make it through the
+     * filter.
+     *
+     * @param {string[]} keys - Empty array, or array of keys. To clear the
+     *   filter, don't pass an empty array; instead, use the
+     *   {@link FilterHandle#clear} method.
+     * @param {Object} [extraInfo] - Extra properties to be included on the event
+     *   object that's passed to listeners (in addition to any options that were
+     *   passed into the `FilterHandle` constructor).
+     */
+
+  }, {
+    key: "set",
+    value: function set(keys, extraInfo) {
+      if (!this._filterSet) return;
+      this._filterSet.update(this._id, keys);
+      this._onChange(extraInfo);
+    }
+
+    /**
+     * @return {string[]|null} - Either: 1) an array of keys that made it through
+     *   all of the `FilterHandle` instances, or, 2) `null`, which means no filter
+     *   is being applied (all data should be displayed).
+     */
+
+  }, {
+    key: "on",
+
+
+    /**
+     * Subscribe to events on this `FilterHandle`.
+     *
+     * @param {string} eventType - Indicates the type of events to listen to.
+     *   Currently, only `"change"` is supported.
+     * @param {FilterHandle~listener} listener - The callback function that
+     *   will be invoked when the event occurs.
+     * @return {string} - A token to pass to {@link FilterHandle#off} to cancel
+     *   this subscription.
+     */
+    value: function on(eventType, listener) {
+      return this._emitter.on(eventType, listener);
+    }
+
+    /**
+     * Cancel event subscriptions created by {@link FilterHandle#on}.
+     *
+     * @param {string} eventType - The type of event to unsubscribe.
+     * @param {string|FilterHandle~listener} listener - Either the callback
+     *   function previously passed into {@link FilterHandle#on}, or the
+     *   string that was returned from {@link FilterHandle#on}.
+     */
+
+  }, {
+    key: "off",
+    value: function off(eventType, listener) {
+      return this._emitter.off(eventType, listener);
+    }
+  }, {
+    key: "_onChange",
+    value: function _onChange(extraInfo) {
+      if (!this._filterSet) return;
+      this._filterVar.set(this._filterSet.value, this._mergeExtraInfo(extraInfo));
+    }
+
+    /**
+     * @callback FilterHandle~listener
+     * @param {Object} event - An object containing details of the event. For
+     *   `"change"` events, this includes the properties `value` (the new
+     *   value of the filter set, or `null` if no filter set is active),
+     *   `oldValue` (the previous value of the filter set), and `sender` (the
+     *   `FilterHandle` instance that made the change).
+     */
+
+    /**
+     * @event FilterHandle#change
+     * @type {object}
+     * @property {object} value - The new value of the filter set, or `null`
+     *   if no filter set is active.
+     * @property {object} oldValue - The previous value of the filter set.
+     * @property {FilterHandle} sender - The `FilterHandle` instance that
+     *   changed the value.
+     */
+
+  }, {
+    key: "filteredKeys",
+    get: function get() {
+      return this._filterSet ? this._filterSet.value : null;
+    }
+  }]);
+
+  return FilterHandle;
+}();
+
+},{"./events":1,"./filterset":3,"./group":4,"./util":11}],3:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+var _util = require("./util");
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function naturalComparator(a, b) {
+  if (a === b) {
+    return 0;
+  } else if (a < b) {
+    return -1;
+  } else if (a > b) {
+    return 1;
+  }
+}
+
+/**
+ * @private
+ */
+
+var FilterSet = function () {
+  function FilterSet() {
+    _classCallCheck(this, FilterSet);
+
+    this.reset();
+  }
+
+  _createClass(FilterSet, [{
+    key: "reset",
+    value: function reset() {
+      // Key: handle ID, Value: array of selected keys, or null
+      this._handles = {};
+      // Key: key string, Value: count of handles that include it
+      this._keys = {};
+      this._value = null;
+      this._activeHandles = 0;
+    }
+  }, {
+    key: "update",
+    value: function update(handleId, keys) {
+      if (keys !== null) {
+        keys = keys.slice(0); // clone before sorting
+        keys.sort(naturalComparator);
+      }
+
+      var _diffSortedLists = (0, _util.diffSortedLists)(this._handles[handleId], keys),
+          added = _diffSortedLists.added,
+          removed = _diffSortedLists.removed;
+
+      this._handles[handleId] = keys;
+
+      for (var i = 0; i < added.length; i++) {
+        this._keys[added[i]] = (this._keys[added[i]] || 0) + 1;
+      }
+      for (var _i = 0; _i < removed.length; _i++) {
+        this._keys[removed[_i]]--;
+      }
+
+      this._updateValue(keys);
+    }
+
+    /**
+     * @param {string[]} keys Sorted array of strings that indicate
+     * a superset of possible keys.
+     * @private
+     */
+
+  }, {
+    key: "_updateValue",
+    value: function _updateValue() {
+      var keys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this._allKeys;
+
+      var handleCount = Object.keys(this._handles).length;
+      if (handleCount === 0) {
+        this._value = null;
+      } else {
+        this._value = [];
+        for (var i = 0; i < keys.length; i++) {
+          var count = this._keys[keys[i]];
+          if (count === handleCount) {
+            this._value.push(keys[i]);
+          }
+        }
+      }
+    }
+  }, {
+    key: "clear",
+    value: function clear(handleId) {
+      if (typeof this._handles[handleId] === "undefined") {
+        return;
+      }
+
+      var keys = this._handles[handleId];
+      if (!keys) {
+        keys = [];
+      }
+
+      for (var i = 0; i < keys.length; i++) {
+        this._keys[keys[i]]--;
+      }
+      delete this._handles[handleId];
+
+      this._updateValue();
+    }
+  }, {
+    key: "value",
+    get: function get() {
+      return this._value;
+    }
+  }, {
+    key: "_allKeys",
+    get: function get() {
+      var allKeys = Object.keys(this._keys);
+      allKeys.sort(naturalComparator);
+      return allKeys;
+    }
+  }]);
+
+  return FilterSet;
+}();
+
+exports.default = FilterSet;
+
+},{"./util":11}],4:[function(require,module,exports){
+(function (global){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
+exports.default = group;
+
+var _var2 = require("./var");
+
+var _var3 = _interopRequireDefault(_var2);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+// Use a global so that multiple copies of crosstalk.js can be loaded and still
+// have groups behave as singletons across all copies.
+global.__crosstalk_groups = global.__crosstalk_groups || {};
+var groups = global.__crosstalk_groups;
+
+function group(groupName) {
+  if (groupName && typeof groupName === "string") {
+    if (!groups.hasOwnProperty(groupName)) {
+      groups[groupName] = new Group(groupName);
+    }
+    return groups[groupName];
+  } else if ((typeof groupName === "undefined" ? "undefined" : _typeof(groupName)) === "object" && groupName._vars && groupName.var) {
+    // Appears to already be a group object
+    return groupName;
+  } else if (Array.isArray(groupName) && groupName.length == 1 && typeof groupName[0] === "string") {
+    return group(groupName[0]);
+  } else {
+    throw new Error("Invalid groupName argument");
+  }
+}
+
+var Group = function () {
+  function Group(name) {
+    _classCallCheck(this, Group);
+
+    this.name = name;
+    this._vars = {};
+  }
+
+  _createClass(Group, [{
+    key: "var",
+    value: function _var(name) {
+      if (!name || typeof name !== "string") {
+        throw new Error("Invalid var name");
+      }
+
+      if (!this._vars.hasOwnProperty(name)) this._vars[name] = new _var3.default(this, name);
+      return this._vars[name];
+    }
+  }, {
+    key: "has",
+    value: function has(name) {
+      if (!name || typeof name !== "string") {
+        throw new Error("Invalid var name");
+      }
+
+      return this._vars.hasOwnProperty(name);
+    }
+  }]);
+
+  return Group;
+}();
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"./var":12}],5:[function(require,module,exports){
+(function (global){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _group = require("./group");
+
+var _group2 = _interopRequireDefault(_group);
+
+var _selection = require("./selection");
+
+var _filter = require("./filter");
+
+require("./input");
+
+require("./input_selectize");
+
+require("./input_checkboxgroup");
+
+require("./input_slider");
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+var defaultGroup = (0, _group2.default)("default");
+
+function var_(name) {
+  return defaultGroup.var(name);
+}
+
+function has(name) {
+  return defaultGroup.has(name);
+}
+
+if (global.Shiny) {
+  global.Shiny.addCustomMessageHandler("update-client-value", function (message) {
+    if (typeof message.group === "string") {
+      (0, _group2.default)(message.group).var(message.name).set(message.value);
+    } else {
+      var_(message.name).set(message.value);
+    }
+  });
+}
+
+var crosstalk = {
+  group: _group2.default,
+  var: var_,
+  has: has,
+  SelectionHandle: _selection.SelectionHandle,
+  FilterHandle: _filter.FilterHandle
+};
+
+/**
+ * @namespace crosstalk
+ */
+exports.default = crosstalk;
+
+global.crosstalk = crosstalk;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"./filter":2,"./group":4,"./input":6,"./input_checkboxgroup":7,"./input_selectize":8,"./input_slider":9,"./selection":10}],6:[function(require,module,exports){
+(function (global){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.register = register;
+var $ = global.jQuery;
+
+var bindings = {};
+
+function register(reg) {
+  bindings[reg.className] = reg;
+  if (global.document && global.document.readyState !== "complete") {
+    $(function () {
+      bind();
+    });
+  } else if (global.document) {
+    setTimeout(bind, 100);
+  }
+}
+
+function bind() {
+  Object.keys(bindings).forEach(function (className) {
+    var binding = bindings[className];
+    $("." + binding.className).not(".crosstalk-input-bound").each(function (i, el) {
+      bindInstance(binding, el);
+    });
+  });
+}
+
+// Escape jQuery identifier
+function $escape(val) {
+  return val.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g, "\\$1");
+}
+
+function bindEl(el) {
+  var $el = $(el);
+  Object.keys(bindings).forEach(function (className) {
+    if ($el.hasClass(className) && !$el.hasClass("crosstalk-input-bound")) {
+      var binding = bindings[className];
+      bindInstance(binding, el);
+    }
+  });
+}
+
+function bindInstance(binding, el) {
+  var jsonEl = $(el).find("script[type='application/json'][data-for='" + $escape(el.id) + "']");
+  var data = JSON.parse(jsonEl[0].innerText);
+
+  var instance = binding.factory(el, data);
+  $(el).data("crosstalk-instance", instance);
+  $(el).addClass("crosstalk-input-bound");
+}
+
+if (global.Shiny) {
+  (function () {
+    var inputBinding = new global.Shiny.InputBinding();
+    var $ = global.jQuery;
+    $.extend(inputBinding, {
+      find: function find(scope) {
+        return $(scope).find(".crosstalk-input");
+      },
+      initialize: function initialize(el) {
+        if (!$(el).hasClass("crosstalk-input-bound")) {
+          bindEl(el);
+        }
+      },
+      getId: function getId(el) {
+        return el.id;
+      },
+      getValue: function getValue(el) {},
+      setValue: function setValue(el, value) {},
+      receiveMessage: function receiveMessage(el, data) {},
+      subscribe: function subscribe(el, callback) {
+        $(el).data("crosstalk-instance").resume();
+      },
+      unsubscribe: function unsubscribe(el) {
+        $(el).data("crosstalk-instance").suspend();
+      }
+    });
+    global.Shiny.inputBindings.register(inputBinding, "crosstalk.inputBinding");
+  })();
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{}],7:[function(require,module,exports){
+(function (global){
+"use strict";
+
+var _input = require("./input");
+
+var input = _interopRequireWildcard(_input);
+
+var _filter = require("./filter");
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+var $ = global.jQuery;
+
+input.register({
+  className: "crosstalk-input-checkboxgroup",
+
+  factory: function factory(el, data) {
+    /*
+     * map: {"groupA": ["keyA", "keyB", ...], ...}
+     * group: "ct-groupname"
+     */
+    var ctHandle = new _filter.FilterHandle(data.group);
+
+    var lastKnownKeys = void 0;
+    var $el = $(el);
+    $el.on("change", "input[type='checkbox']", function () {
+      var checked = $el.find("input[type='checkbox']:checked");
+      if (checked.length === 0) {
+        lastKnownKeys = null;
+        ctHandle.clear();
+      } else {
+        (function () {
+          var keys = {};
+          checked.each(function () {
+            data.map[this.value].forEach(function (key) {
+              keys[key] = true;
+            });
+          });
+          var keyArray = Object.keys(keys);
+          keyArray.sort();
+          lastKnownKeys = keyArray;
+          ctHandle.set(keyArray);
+        })();
+      }
+    });
+
+    return {
+      suspend: function suspend() {
+        ctHandle.clear();
+      },
+      resume: function resume() {
+        if (lastKnownKeys) ctHandle.set(lastKnownKeys);
+      }
+    };
+  }
+});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"./filter":2,"./input":6}],8:[function(require,module,exports){
+(function (global){
+"use strict";
+
+var _input = require("./input");
+
+var input = _interopRequireWildcard(_input);
+
+var _util = require("./util");
+
+var util = _interopRequireWildcard(_util);
+
+var _filter = require("./filter");
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+var $ = global.jQuery;
+
+input.register({
+  className: "crosstalk-input-select",
+
+  factory: function factory(el, data) {
+    /*
+     * items: {value: [...], label: [...]}
+     * map: {"groupA": ["keyA", "keyB", ...], ...}
+     * group: "ct-groupname"
+     */
+
+    var first = [{ value: "", label: "(All)" }];
+    var items = util.dataframeToD3(data.items);
+    var opts = {
+      options: first.concat(items),
+      valueField: "value",
+      labelField: "label",
+      searchField: "label"
+    };
+
+    var select = $(el).find("select")[0];
+
+    var selectize = $(select).selectize(opts)[0].selectize;
+
+    var ctHandle = new _filter.FilterHandle(data.group);
+
+    var lastKnownKeys = void 0;
+    selectize.on("change", function () {
+      if (selectize.items.length === 0) {
+        lastKnownKeys = null;
+        ctHandle.clear();
+      } else {
+        (function () {
+          var keys = {};
+          selectize.items.forEach(function (group) {
+            data.map[group].forEach(function (key) {
+              keys[key] = true;
+            });
+          });
+          var keyArray = Object.keys(keys);
+          keyArray.sort();
+          lastKnownKeys = keyArray;
+          ctHandle.set(keyArray);
+        })();
+      }
+    });
+
+    return {
+      suspend: function suspend() {
+        ctHandle.clear();
+      },
+      resume: function resume() {
+        if (lastKnownKeys) ctHandle.set(lastKnownKeys);
+      }
+    };
+  }
+});
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"./filter":2,"./input":6,"./util":11}],9:[function(require,module,exports){
+(function (global){
+"use strict";
+
+var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr [...]
+
+var _input = require("./input");
+
+var input = _interopRequireWildcard(_input);
+
+var _filter = require("./filter");
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+var $ = global.jQuery;
+var strftime = global.strftime;
+
+input.register({
+  className: "crosstalk-input-slider",
+
+  factory: function factory(el, data) {
+    /*
+     * map: {"groupA": ["keyA", "keyB", ...], ...}
+     * group: "ct-groupname"
+     */
+    var ctHandle = new _filter.FilterHandle(data.group);
+
+    var opts = {};
+    var $el = $(el).find("input");
+    var dataType = $el.data("data-type");
+    var timeFormat = $el.data("time-format");
+    var timeFormatter = void 0;
+
+    // Set up formatting functions
+    if (dataType === "date") {
+      timeFormatter = strftime.utc();
+      opts.prettify = function (num) {
+        return timeFormatter(timeFormat, new Date(num));
+      };
+    } else if (dataType === "datetime") {
+      var timezone = $el.data("timezone");
+      if (timezone) timeFormatter = strftime.timezone(timezone);else timeFormatter = strftime;
+
+      opts.prettify = function (num) {
+        return timeFormatter(timeFormat, new Date(num));
+      };
+    }
+
+    $el.ionRangeSlider(opts);
+
+    function getValue() {
+      var result = $el.data("ionRangeSlider").result;
+
+      // Function for converting numeric value from slider to appropriate type.
+      var convert = void 0;
+      var dataType = $el.data("data-type");
+      if (dataType === "date") {
+        convert = function convert(val) {
+          return formatDateUTC(new Date(+val));
+        };
+      } else if (dataType === "datetime") {
+        convert = function convert(val) {
+          // Convert ms to s
+          return +val / 1000;
+        };
+      } else {
+        convert = function convert(val) {
+          return +val;
+        };
+      }
+
+      if ($el.data("ionRangeSlider").options.type === "double") {
+        return [convert(result.from), convert(result.to)];
+      } else {
+        return convert(result.from);
+      }
+    }
+
+    var lastKnownKeys = null;
+
+    $el.on("change.crosstalkSliderInput", function (event) {
+      if (!$el.data("updating") && !$el.data("animating")) {
+        var _getValue = getValue(),
+            _getValue2 = _slicedToArray(_getValue, 2),
+            from = _getValue2[0],
+            to = _getValue2[1];
+
+        var keys = [];
+        for (var i = 0; i < data.values.length; i++) {
+          var val = data.values[i];
+          if (val >= from && val <= to) {
+            keys.push(data.keys[i]);
+          }
+        }
+        keys.sort();
+        ctHandle.set(keys);
+        lastKnownKeys = keys;
+      }
+    });
+
+    // let $el = $(el);
+    // $el.on("change", "input[type="checkbox"]", function() {
+    //   let checked = $el.find("input[type="checkbox"]:checked");
+    //   if (checked.length === 0) {
+    //     ctHandle.clear();
+    //   } else {
+    //     let keys = {};
+    //     checked.each(function() {
+    //       data.map[this.value].forEach(function(key) {
+    //         keys[key] = true;
+    //       });
+    //     });
+    //     let keyArray = Object.keys(keys);
+    //     keyArray.sort();
+    //     ctHandle.set(keyArray);
+    //   }
+    // });
+
+    return {
+      suspend: function suspend() {
+        ctHandle.clear();
+      },
+      resume: function resume() {
+        if (lastKnownKeys) ctHandle.set(lastKnownKeys);
+      }
+    };
+  }
+});
+
+// Convert a number to a string with leading zeros
+function padZeros(n, digits) {
+  var str = n.toString();
+  while (str.length < digits) {
+    str = "0" + str;
+  }return str;
+}
+
+// Given a Date object, return a string in yyyy-mm-dd format, using the
+// UTC date. This may be a day off from the date in the local time zone.
+function formatDateUTC(date) {
+  if (date instanceof Date) {
+    return date.getUTCFullYear() + "-" + padZeros(date.getUTCMonth() + 1, 2) + "-" + padZeros(date.getUTCDate(), 2);
+  } else {
+    return null;
+  }
+}
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"./filter":2,"./input":6}],10:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.SelectionHandle = undefined;
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+var _events = require("./events");
+
+var _events2 = _interopRequireDefault(_events);
+
+var _group = require("./group");
+
+var _group2 = _interopRequireDefault(_group);
+
+var _util = require("./util");
+
+var util = _interopRequireWildcard(_util);
+
+function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var SelectionHandle = exports.SelectionHandle = function () {
+
+  /**
+   * @classdesc
+   * Use this class to read and write (and listen for changes to) the selection
+   * for a Crosstalk group. This is intended to be used for linked brushing.
+   *
+   * If two (or more) `SelectionHandle` instances in the same webpage share the
+   * same group name, they will share the same state. Setting the selection using
+   * one `SelectionHandle` instance will result in the `value` property instantly
+   * changing across the others, and `"change"` event listeners on all instances
+   * (including the one that initiated the sending) will fire.
+   *
+   * @param {string} [group] - The name of the Crosstalk group, or if none,
+   *   null or undefined (or any other falsy value). This can be changed later
+   *   via the [SelectionHandle#setGroup](#setGroup) method.
+   * @param {Object} [extraInfo] - An object whose properties will be copied to
+   *   the event object whenever an event is emitted.
+   */
+  function SelectionHandle() {
+    var group = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
+    var extraInfo = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
+
+    _classCallCheck(this, SelectionHandle);
+
+    this._eventRelay = new _events2.default();
+    this._emitter = new util.SubscriptionTracker(this._eventRelay);
+
+    // Name of the group we're currently tracking, if any. Can change over time.
+    this._group = null;
+    // The Var we're currently tracking, if any. Can change over time.
+    this._var = null;
+    // The event handler subscription we currently have on var.on("change").
+    this._varOnChangeSub = null;
+
+    this._extraInfo = util.extend({ sender: this }, extraInfo);
+
+    this.setGroup(group);
+  }
+
+  /**
+   * Changes the Crosstalk group membership of this SelectionHandle. The group
+   * being switched away from (if any) will not have its selection value
+   * modified as a result of calling `setGroup`, even if this handle was the
+   * most recent handle to set the selection of the group.
+   *
+   * The group being switched to (if any) will also not have its selection value
+   * modified as a result of calling `setGroup`. If you want to set the
+   * selection value of the new group, call `set` explicitly.
+   *
+   * @param {string} group - The name of the Crosstalk group, or null (or
+   *   undefined) to clear the group.
+   */
+
+
+  _createClass(SelectionHandle, [{
+    key: "setGroup",
+    value: function setGroup(group) {
+      var _this = this;
+
+      // If group is unchanged, do nothing
+      if (this._group === group) return;
+      // Treat null, undefined, and other falsy values the same
+      if (!this._group && !group) return;
+
+      if (this._var) {
+        this._var.off("change", this._varOnChangeSub);
+        this._var = null;
+        this._varOnChangeSub = null;
+      }
+
+      this._group = group;
+
+      if (group) {
+        this._var = (0, _group2.default)(group).var("selection");
+        var sub = this._var.on("change", function (e) {
+          _this._eventRelay.trigger("change", e, _this);
+        });
+        this._varOnChangeSub = sub;
+      }
+    }
+
+    /**
+     * Retrieves the current selection for the group represented by this
+     * `SelectionHandle`.
+     *
+     * - If no selection is active, then this value will be falsy.
+     * - If a selection is active, but no data points are selected, then this
+     *   value will be an empty array.
+     * - If a selection is active, and data points are selected, then the keys
+     *   of the selected data points will be present in the array.
+     */
+
+  }, {
+    key: "_mergeExtraInfo",
+
+
+    /**
+     * Combines the given `extraInfo` (if any) with the handle's default
+     * `_extraInfo` (if any).
+     * @private
+     */
+    value: function _mergeExtraInfo(extraInfo) {
+      // Important incidental effect: shallow clone is returned
+      return util.extend({}, this._extraInfo ? this._extraInfo : null, extraInfo ? extraInfo : null);
+    }
+
+    /**
+     * Overwrites the current selection for the group, and raises the `"change"`
+     * event among all of the group's '`SelectionHandle` instances (including
+     * this one).
+     *
+     * @fires SelectionHandle#change
+     * @param {string[]} selectedKeys - Falsy, empty array, or array of keys (see
+     *   {@link SelectionHandle#value}).
+     * @param {Object} [extraInfo] - Extra properties to be included on the event
+     *   object that's passed to listeners (in addition to any options that were
+     *   passed into the `SelectionHandle` constructor).
+     */
+
+  }, {
+    key: "set",
+    value: function set(selectedKeys, extraInfo) {
+      if (this._var) this._var.set(selectedKeys, this._mergeExtraInfo(extraInfo));
+    }
+
+    /**
+     * Overwrites the current selection for the group, and raises the `"change"`
+     * event among all of the group's '`SelectionHandle` instances (including
+     * this one).
+     *
+     * @fires SelectionHandle#change
+     * @param {Object} [extraInfo] - Extra properties to be included on the event
+     *   object that's passed to listeners (in addition to any that were passed
+     *   into the `SelectionHandle` constructor).
+     */
+
+  }, {
+    key: "clear",
+    value: function clear(extraInfo) {
+      if (this._var) this.set(void 0, this._mergeExtraInfo(extraInfo));
+    }
+
+    /**
+     * Subscribes to events on this `SelectionHandle`.
+     *
+     * @param {string} eventType - Indicates the type of events to listen to.
+     *   Currently, only `"change"` is supported.
+     * @param {SelectionHandle~listener} listener - The callback function that
+     *   will be invoked when the event occurs.
+     * @return {string} - A token to pass to {@link SelectionHandle#off} to cancel
+     *   this subscription.
+     */
+
+  }, {
+    key: "on",
+    value: function on(eventType, listener) {
+      return this._emitter.on(eventType, listener);
+    }
+
+    /**
+     * Cancels event subscriptions created by {@link SelectionHandle#on}.
+     *
+     * @param {string} eventType - The type of event to unsubscribe.
+     * @param {string|SelectionHandle~listener} listener - Either the callback
+     *   function previously passed into {@link SelectionHandle#on}, or the
+     *   string that was returned from {@link SelectionHandle#on}.
+     */
+
+  }, {
+    key: "off",
+    value: function off(eventType, listener) {
+      return this._emitter.off(eventType, listener);
+    }
+
+    /**
+     * Shuts down the `SelectionHandle` object.
+     *
+     * Removes all event listeners that were added through this handle.
+     */
+
+  }, {
+    key: "close",
+    value: function close() {
+      this._emitter.removeAllListeners();
+      this.setGroup(null);
+    }
+
+    /**
+     * @callback SelectionHandle~listener
+     * @param {Object} event - An object containing details of the event. For
+     *   `"change"` events, this includes the properties `value` (the new
+     *   value of the selection, or `undefined` if no selection is active),
+     *   `oldValue` (the previous value of the selection), and `sender` (the
+     *   `SelectionHandle` instance that made the change).
+     */
+
+    /**
+     * @event SelectionHandle#change
+     * @type {object}
+     * @property {object} value - The new value of the selection, or `undefined`
+     *   if no selection is active.
+     * @property {object} oldValue - The previous value of the selection.
+     * @property {SelectionHandle} sender - The `SelectionHandle` instance that
+     *   changed the value.
+     */
+
+  }, {
+    key: "value",
+    get: function get() {
+      return this._var ? this._var.get() : null;
+    }
+  }]);
+
+  return SelectionHandle;
+}();
+
+},{"./events":1,"./group":4,"./util":11}],11:[function(require,module,exports){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
+exports.extend = extend;
+exports.checkSorted = checkSorted;
+exports.diffSortedLists = diffSortedLists;
+exports.dataframeToD3 = dataframeToD3;
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function extend(target) {
+  for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
+    sources[_key - 1] = arguments[_key];
+  }
+
+  for (var i = 0; i < sources.length; i++) {
+    var src = sources[i];
+    if (typeof src === "undefined" || src === null) continue;
+
+    for (var key in src) {
+      if (src.hasOwnProperty(key)) {
+        target[key] = src[key];
+      }
+    }
+  }
+  return target;
+}
+
+function checkSorted(list) {
+  for (var i = 1; i < list.length; i++) {
+    if (list[i] <= list[i - 1]) {
+      throw new Error("List is not sorted or contains duplicate");
+    }
+  }
+}
+
+function diffSortedLists(a, b) {
+  var i_a = 0;
+  var i_b = 0;
+
+  if (!a) a = [];
+  if (!b) b = [];
+
+  var a_only = [];
+  var b_only = [];
+
+  checkSorted(a);
+  checkSorted(b);
+
+  while (i_a < a.length && i_b < b.length) {
+    if (a[i_a] === b[i_b]) {
+      i_a++;
+      i_b++;
+    } else if (a[i_a] < b[i_b]) {
+      a_only.push(a[i_a++]);
+    } else {
+      b_only.push(b[i_b++]);
+    }
+  }
+
+  if (i_a < a.length) a_only = a_only.concat(a.slice(i_a));
+  if (i_b < b.length) b_only = b_only.concat(b.slice(i_b));
+  return {
+    removed: a_only,
+    added: b_only
+  };
+}
+
+// Convert from wide: { colA: [1,2,3], colB: [4,5,6], ... }
+// to long: [ {colA: 1, colB: 4}, {colA: 2, colB: 5}, ... ]
+function dataframeToD3(df) {
+  var names = [];
+  var length = void 0;
+  for (var name in df) {
+    if (df.hasOwnProperty(name)) names.push(name);
+    if (_typeof(df[name]) !== "object" || typeof df[name].length === "undefined") {
+      throw new Error("All fields must be arrays");
+    } else if (typeof length !== "undefined" && length !== df[name].length) {
+      throw new Error("All fields must be arrays of the same length");
+    }
+    length = df[name].length;
+  }
+  var results = [];
+  var item = void 0;
+  for (var row = 0; row < length; row++) {
+    item = {};
+    for (var col = 0; col < names.length; col++) {
+      item[names[col]] = df[names[col]][row];
+    }
+    results.push(item);
+  }
+  return results;
+}
+
+/**
+ * Keeps track of all event listener additions/removals and lets all active
+ * listeners be removed with a single operation.
+ *
+ * @private
+ */
+
+var SubscriptionTracker = exports.SubscriptionTracker = function () {
+  function SubscriptionTracker(emitter) {
+    _classCallCheck(this, SubscriptionTracker);
+
+    this._emitter = emitter;
+    this._subs = {};
+  }
+
+  _createClass(SubscriptionTracker, [{
+    key: "on",
+    value: function on(eventType, listener) {
+      var sub = this._emitter.on(eventType, listener);
+      this._subs[sub] = eventType;
+      return sub;
+    }
+  }, {
+    key: "off",
+    value: function off(eventType, listener) {
+      var sub = this._emitter.off(eventType, listener);
+      if (sub) {
+        delete this._subs[sub];
+      }
+      return sub;
+    }
+  }, {
+    key: "removeAllListeners",
+    value: function removeAllListeners() {
+      var _this = this;
+
+      var current_subs = this._subs;
+      this._subs = {};
+      Object.keys(current_subs).forEach(function (sub) {
+        _this._emitter.off(current_subs[sub], sub);
+      });
+    }
+  }]);
+
+  return SubscriptionTracker;
+}();
+
+},{}],12:[function(require,module,exports){
+(function (global){
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) [...]
+
+var _events = require("./events");
+
+var _events2 = _interopRequireDefault(_events);
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+var Var = function () {
+  function Var(group, name, /*optional*/value) {
+    _classCallCheck(this, Var);
+
+    this._group = group;
+    this._name = name;
+    this._value = value;
+    this._events = new _events2.default();
+  }
+
+  _createClass(Var, [{
+    key: "get",
+    value: function get() {
+      return this._value;
+    }
+  }, {
+    key: "set",
+    value: function set(value, /*optional*/event) {
+      if (this._value === value) {
+        // Do nothing; the value hasn't changed
+        return;
+      }
+      var oldValue = this._value;
+      this._value = value;
+      // Alert JavaScript listeners that the value has changed
+      var evt = {};
+      if (event && (typeof event === "undefined" ? "undefined" : _typeof(event)) === "object") {
+        for (var k in event) {
+          if (event.hasOwnProperty(k)) evt[k] = event[k];
+        }
+      }
+      evt.oldValue = oldValue;
+      evt.value = value;
+      this._events.trigger("change", evt, this);
+
+      // TODO: Make this extensible, to let arbitrary back-ends know that
+      // something has changed
+      if (global.Shiny && global.Shiny.onInputChange) {
+        global.Shiny.onInputChange(".clientValue-" + (this._group.name !== null ? this._group.name + "-" : "") + this._name, typeof value === "undefined" ? null : value);
+      }
+    }
+  }, {
+    key: "on",
+    value: function on(eventType, listener) {
+      return this._events.on(eventType, listener);
+    }
+  }, {
+    key: "off",
+    value: function off(eventType, listener) {
+      return this._events.off(eventType, listener);
+    }
+  }]);
+
+  return Var;
+}();
+
+exports.default = Var;
+
+}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
+
+},{"./events":1}]},{},[5])
+//# sourceMappingURL=crosstalk.js.map
diff --git a/inst/www/js/crosstalk.js.map b/inst/www/js/crosstalk.js.map
new file mode 100644
index 0000000..508b24f
--- /dev/null
+++ b/inst/www/js/crosstalk.js.map
@@ -0,0 +1,37 @@
+{
+  "version": 3,
+  "sources": [
+    "node_modules/browser-pack/_prelude.js",
+    "javascript/src/events.js",
+    "javascript/src/filter.js",
+    "javascript/src/filterset.js",
+    "javascript/src/group.js",
+    "javascript/src/index.js",
+    "javascript/src/input.js",
+    "javascript/src/input_checkboxgroup.js",
+    "javascript/src/input_selectize.js",
+    "javascript/src/input_slider.js",
+    "javascript/src/selection.js",
+    "javascript/src/util.js",
+    "javascript/src/var.js"
+  ],
+  "names": [],
+  "mappings": "AAAA;;;;;;;;;;;ICAqB,M;AACnB,oBAAc;AAAA;;AACZ,SAAK,MAAL,GAAc,EAAd;AACA,SAAK,IAAL,GAAY,CAAZ;AACD;;;;uBAEE,S,EAAW,Q,EAAU;AACtB,UAAI,OAAO,KAAK,MAAL,CAAY,SAAZ,CAAX;AACA,UAAI,CAAC,IAAL,EAAW;AACT,eAAO,KAAK,MAAL,CAAY,SAAZ,IAAyB,EAAhC;AACD;AACD,UAAI,MAAM,QAAS,KAAK,IAAL,EAAnB;AACA,WAAK,GAAL,IAAY,QAAZ;AACA,aAAO,GAAP;AACD;;AAED;;;;wBACI,S,EAAW,Q,EAAU;AACvB,UAAI,OAAO,KAAK,MAAL,CAAY,SAAZ,CAAX;AACA,UAAI,OAAO,QAAP,KAAqB,UAAzB,EAAqC;AACnC,aAAK,IAAI,GAAT,IAAgB,IAAhB,EAAsB;AACpB,cAAI,KAAK,c [...]
+  "file": "generated.js",
+  "sourceRoot": "",
+  "sourcesContent": [
+    "(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require==\"function\"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error(\"Cannot find module '\"+o+\"'\");throw f.code=\"MODULE_NOT_FOUND\",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require==\"function\"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})",
+    "export default class Events {\n  constructor() {\n    this._types = {};\n    this._seq = 0;\n  }\n\n  on(eventType, listener) {\n    let subs = this._types[eventType];\n    if (!subs) {\n      subs = this._types[eventType] = {};\n    }\n    let sub = \"sub\" + (this._seq++);\n    subs[sub] = listener;\n    return sub;\n  }\n\n  // Returns false if no match, or string for sub name if matched\n  off(eventType, listener) {\n    let subs = this._types[eventType];\n    if (typeof(listene [...]
+    "import Events from \"./events\";\nimport FilterSet from \"./filterset\";\nimport grp from \"./group\";\nimport * as util from \"./util\";\n\nfunction getFilterSet(group) {\n  let fsVar = group.var(\"filterset\");\n  let result = fsVar.get();\n  if (!result) {\n    result = new FilterSet();\n    fsVar.set(result);\n  }\n  return result;\n}\n\nlet id = 1;\nfunction nextId() {\n  return id++;\n}\n\nexport class FilterHandle {\n  /**\n   * @classdesc\n   * Use this class to contribute t [...]
+    "import { diffSortedLists } from \"./util\";\n\nfunction naturalComparator(a, b) {\n  if (a === b) {\n    return 0;\n  } else if (a < b) {\n    return -1;\n  } else if (a > b) {\n    return 1;\n  }\n}\n\n/**\n * @private\n */\nexport default class FilterSet {\n  constructor() {\n    this.reset();\n  }\n\n  reset() {\n    // Key: handle ID, Value: array of selected keys, or null\n    this._handles = {};\n    // Key: key string, Value: count of handles that include it\n    this._keys = [...]
+    "import Var from \"./var\";\n\n// Use a global so that multiple copies of crosstalk.js can be loaded and still\n// have groups behave as singletons across all copies.\nglobal.__crosstalk_groups = global.__crosstalk_groups || {};\nlet groups = global.__crosstalk_groups;\n\nexport default function group(groupName) {\n  if (groupName && typeof(groupName) === \"string\") {\n    if (!groups.hasOwnProperty(groupName)) {\n      groups[groupName] = new Group(groupName);\n    }\n    return gr [...]
+    "import group from \"./group\";\nimport { SelectionHandle } from \"./selection\";\nimport { FilterHandle } from \"./filter\";\nimport \"./input\";\nimport \"./input_selectize\";\nimport \"./input_checkboxgroup\";\nimport \"./input_slider\";\n\nconst defaultGroup = group(\"default\");\n\nfunction var_(name) {\n  return defaultGroup.var(name);\n}\n\nfunction has(name) {\n  return defaultGroup.has(name);\n}\n\nif (global.Shiny) {\n  global.Shiny.addCustomMessageHandler(\"update-client-v [...]
+    "let $ = global.jQuery;\n\nlet bindings = {};\n\nexport function register(reg) {\n  bindings[reg.className] = reg;\n  if (global.document && global.document.readyState !== \"complete\") {\n    $(() => {\n      bind();\n    });\n  } else if (global.document) {\n    setTimeout(bind, 100);\n  }\n}\n\nfunction bind() {\n  Object.keys(bindings).forEach(function(className) {\n    let binding = bindings[className];\n    $(\".\" + binding.className).not(\".crosstalk-input-bound\").each(funct [...]
+    "import * as input from \"./input\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\n\ninput.register({\n  className: \"crosstalk-input-checkboxgroup\",\n\n  factory: function(el, data) {\n    /*\n     * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n     * group: \"ct-groupname\"\n     */\n    let ctHandle = new FilterHandle(data.group);\n\n    let lastKnownKeys;\n    let $el = $(el);\n    $el.on(\"change\", \"input[type='checkbox']\", function() {\n       [...]
+    "import * as input from \"./input\";\nimport * as util from \"./util\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\n\ninput.register({\n  className: \"crosstalk-input-select\",\n\n  factory: function(el, data) {\n    /*\n     * items: {value: [...], label: [...]}\n     * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n     * group: \"ct-groupname\"\n     */\n\n    let first = [{value: \"\", label: \"(All)\"}];\n    let items = util.dataframeToD3(data.ite [...]
+    "import * as input from \"./input\";\nimport { FilterHandle } from \"./filter\";\n\nlet $ = global.jQuery;\nlet strftime = global.strftime;\n\ninput.register({\n  className: \"crosstalk-input-slider\",\n\n  factory: function(el, data) {\n    /*\n     * map: {\"groupA\": [\"keyA\", \"keyB\", ...], ...}\n     * group: \"ct-groupname\"\n     */\n    let ctHandle = new FilterHandle(data.group);\n\n    let opts = {};\n    let $el = $(el).find(\"input\");\n    let dataType = $el.data(\"dat [...]
+    "import Events from \"./events\";\nimport grp from \"./group\";\nimport * as util from \"./util\";\n\nexport class SelectionHandle {\n\n  /**\n   * @classdesc\n   * Use this class to read and write (and listen for changes to) the selection\n   * for a Crosstalk group. This is intended to be used for linked brushing.\n   *\n   * If two (or more) `SelectionHandle` instances in the same webpage share the\n   * same group name, they will share the same state. Setting the selection using\ [...]
+    "export function extend(target, ...sources) {\n  for (let i = 0; i < sources.length; i++) {\n    let src = sources[i];\n    if (typeof(src) === \"undefined\" || src === null)\n      continue;\n\n    for (let key in src) {\n      if (src.hasOwnProperty(key)) {\n        target[key] = src[key];\n      }\n    }\n  }\n  return target;\n}\n\nexport function checkSorted(list) {\n  for (let i = 1; i < list.length; i++) {\n    if (list[i] <= list[i-1]) {\n      throw new Error(\"List is not s [...]
+    "import Events from \"./events\";\n\nexport default class Var {\n  constructor(group, name, /*optional*/ value) {\n    this._group = group;\n    this._name = name;\n    this._value = value;\n    this._events = new Events();\n  }\n\n  get() {\n    return this._value;\n  }\n\n  set(value, /*optional*/ event) {\n    if (this._value === value) {\n      // Do nothing; the value hasn't changed\n      return;\n    }\n    let oldValue = this._value;\n    this._value = value;\n    // Alert Ja [...]
+  ]
+}
\ No newline at end of file
diff --git a/inst/www/js/crosstalk.min.js b/inst/www/js/crosstalk.min.js
new file mode 100644
index 0000000..55262e8
--- /dev/null
+++ b/inst/www/js/crosstalk.min.js
@@ -0,0 +1,2 @@
+!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g<d.length;g++)e(d[g]);return e}({1:[function(a,b,c){"use strict";function d(a,b){if(!( [...]
+//# sourceMappingURL=crosstalk.min.js.map
\ No newline at end of file
diff --git a/inst/www/js/crosstalk.min.js.map b/inst/www/js/crosstalk.min.js.map
new file mode 100644
index 0000000..7b3f2e9
--- /dev/null
+++ b/inst/www/js/crosstalk.min.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["node_modules/browser-pack/_prelude.js","javascript/src/events.js","javascript/src/filter.js","javascript/src/filterset.js","javascript/src/group.js","javascript/src/index.js","javascript/src/input.js","javascript/src/input_checkboxgroup.js","javascript/src/input_selectize.js","javascript/src/input_slider.js","javascript/src/selection.js","javascript/src/util.js","javascript/src/var.js"],"names":["e","t","n","r","s","o","u","a","require","i","f","Error","code","l" [...]
\ No newline at end of file
diff --git a/man/ClientValue.Rd b/man/ClientValue.Rd
new file mode 100644
index 0000000..d780bac
--- /dev/null
+++ b/man/ClientValue.Rd
@@ -0,0 +1,62 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/crosstalk.R
+\docType{class}
+\name{ClientValue}
+\alias{ClientValue}
+\title{ClientValue object}
+\format{An \code{\link{R6Class}} generator object}
+\usage{
+ClientValue
+}
+\description{
+An object that can be used in a \href{http://shiny.rstudio.com}{Shiny} server
+function to get or set a crosstalk variable that exists on the client. The
+client copy of the variable is the canonical copy, so there is no direct
+"set" method that immediately changes the value; instead, there is a
+\code{sendUpdate} method that sends a request to the browser to change the
+value, which will then cause the new value to be relayed back to the server.
+}
+\section{Methods}{
+
+\describe{
+  \item{\code{initialize(name, group = "default", session = shiny::getDefaultReactiveDomain())}}{
+    Create a new ClientValue object to reflect the crosstalk variable
+    specified by \code{group} and \code{name}. The \code{session} indicates
+    which Shiny session to connect to, and defaults to the current session.
+  }
+  \item{\code{get()}}{
+Read the value. This is a reactive operation akin to reading a reactive
+value, and so can only be done in a reactive context (e.g. in a
+\code{\link[shiny]{reactive}}, \code{\link[shiny]{observe}}, or
+\code{\link[shiny]{isolate}} block).
+  }
+  \item{\code{sendUpdate(value)}}{
+    Send a message to the browser asking it to update the crosstalk var to
+    the given value. This update does not happen synchronously, that is, a
+    call to \code{get()} immediately following \code{sendUpdate(value)} will
+    not reflect the new value. The value must be serializable as JSON using
+    jsonlite.
+  }
+}
+}
+\examples{
+library(shiny)
+
+server <- function(input, output, session) {
+  cv <- ClientValue$new("var1", "group1")
+
+  r <- reactive({
+    # Don't proceed unless cv$get() is a non-NULL value
+    validate(need(cv$get(), message = FALSE))
+
+    runif(cv$get())
+  })
+
+  observeEvent(input$click, {
+    cv$sendUpdate(NULL)
+  })
+}
+
+}
+\keyword{datasets}
+
diff --git a/man/SharedData.Rd b/man/SharedData.Rd
new file mode 100644
index 0000000..fc43888
--- /dev/null
+++ b/man/SharedData.Rd
@@ -0,0 +1,106 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/crosstalk.R
+\docType{data}
+\name{SharedData}
+\alias{SharedData}
+\title{An R6 class that represents a shared data frame}
+\format{An object of class \code{R6ClassGenerator} of length 24.}
+\usage{
+SharedData
+}
+\description{
+...or sufficiently data frame-like object. The primary use for
+\code{SharedData} is to be passed to Crosstalk-compatible widgets in place
+of a data frame. Each \code{SharedData$new(...)} call makes a new "group"
+of widgets that link to each other, but not to widgets in other groups.
+You can also use a \code{SharedData} object from Shiny code in order to
+react to filtering and brushing from non-widget visualizations (like ggplot2
+plots).
+}
+\section{Constructor}{
+
+
+\code{SharedData$new(data, key = NULL, group = createUniqueId(4, prefix = "SharedData"))}
+
+\describe{
+  \item{\code{data}}{
+    A data frame-like object, or a Shiny \link[=reactive]{reactive
+    expression} that returns a data frame-like object.
+  }
+  \item{\code{key}}{
+    Character vector or one-sided formula that indicates the name of the
+    column that represents the key or ID of the data frame. These \emph{must}
+    be unique, and ideally will be something intrinsic to the data (a proper
+    ID) rather than a transient property like row index.
+
+    If \code{NULL}, then \code{row.names(data)} will be used.
+  }
+  \item{\code{group}}{
+    The "identity" of the Crosstalk group that widgets will join when you
+    pass them this \code{SharedData} object. In some cases, you will want to
+    have multiple independent \code{SharedData} objects link up to form a
+    single web of widgets that all share selection and filtering state; in
+    those cases, you'll give those \code{SharedData} objects the same group
+    name. (One example: in Shiny, ui.R and server.R might each need their own
+    \code{SharedData} instance, even though they're intended to represent a
+    single group.)
+  }
+}
+}
+
+\section{Methods}{
+
+
+\describe{
+  \item{\code{data(withSelection = FALSE, withFilter = TRUE, withKey = FALSE)}}{
+    Return the data (or read and return the data if the data is a Shiny
+    reactive expression). If \code{withSelection}, add a \code{selection_}
+    column with logical values indicating which rows are in the current
+    selection, or \code{NA} if no selection is currently active. If
+    \code{withFilter} (the default), only return rows that are part of the
+    current filter settings, if any. If \code{withKey}, add a \code{key_}
+    column with the key values of each row (normally not needed since the
+    key is either one of the other columns or else just the row names).
+
+    When running in Shiny, calling \code{data()} is a reactive operation
+    that will invalidate if the selection or filter change (assuming that
+    information was requested), or if the original data is a reactive
+    expression that has invalidated.
+  }
+  \item{\code{origData()}}{
+    Return the data frame that was used to create this \code{SharedData}
+    instance. If a reactive expression, evaluate the reactive expression.
+    Equivalent to \code{data(FALSE, FALSE, FALSE)}.
+  }
+  \item{\code{groupName()}}{
+    Returns the value of \code{group} that was used to create this instance.
+  }
+  \item{\code{key()}}{
+    Returns the vector of key values. Filtering is not applied.
+  }
+  \item{\code{selection(value, ownerId = "")}}{
+    If called without arguments, returns a logical vector of rows that are
+    currently selected (brushed), or \code{NULL} if no selection exists.
+    Intended to be called from a Shiny reactive context, and invalidates
+    whenever the selection changes.
+
+    If called with one or two arguments, expects \code{value} to be a logical
+    vector of \code{nrow(origData())} length, indicating which rows are
+    currently selected (brushed). This value is propagated to the web browser
+    (assumes an active Shiny app or Shiny R Markdown document).
+
+    Set the \code{ownerId} argument to the \code{outputId} of a widget if
+    conceptually that widget "initiated" the selection (prevents that widget
+    from clearing its visual selection box, which is normally cleared when
+    the selection changes). For example, if setting the selection based on a
+    \code{\link[shiny]{plotOutput}} brush, then \code{ownerId} should be the
+    \code{outputId} of the \code{plotOutput}.
+  }
+  \item{\code{clearSelection(ownerId = "")}}{
+    Clears the selection. For the meaning of \code{ownerId}, see the
+    \code{selection} method.
+  }
+}
+}
+\keyword{datasets}
+
diff --git a/man/bscols.Rd b/man/bscols.Rd
new file mode 100644
index 0000000..e4b2801
--- /dev/null
+++ b/man/bscols.Rd
@@ -0,0 +1,56 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/controls.R
+\name{bscols}
+\alias{bscols}
+\title{Arrange HTML elements or widgets in Bootstrap columns}
+\usage{
+bscols(..., widths = NA, device = c("xs", "sm", "md", "lg"))
+}
+\arguments{
+\item{...}{\code{htmltools} tag objects, lists, text, HTML widgets, or
+NULL. These arguments should be unnamed.}
+
+\item{widths}{The number of columns that should be assigned to each of the
+\code{...} elements (the total number of columns available is always 12).
+The width vector will be recycled if there are more \code{...} arguments.
+\code{NA} columns will evenly split the remaining columns that are left
+after the widths are recycled and non-\code{NA} values are subtracted.}
+
+\item{device}{The class of device which is targeted by these widths; with
+smaller screen sizes the layout will collapse to a one-column,
+top-to-bottom display instead. xs: never collapse, sm: collapse below
+768px, md: 992px, lg: 1200px.}
+}
+\value{
+A \code{\link[htmltools]{browsable}} HTML element.
+}
+\description{
+This helper function makes it easy to put HTML elements side by side. It can
+be called directly from the console but is especially designed to work in an
+R Markdown document. Warning: This will bring in all of Bootstrap!
+}
+\examples{
+library(htmltools)
+
+# If width is unspecified, equal widths will be used
+bscols(
+  div(style = css(width="100\%", height="400px", background_color="red")),
+  div(style = css(width="100\%", height="400px", background_color="blue"))
+)
+
+# Use NA to absorb remaining width
+bscols(widths = c(2, NA, NA),
+  div(style = css(width="100\%", height="400px", background_color="red")),
+  div(style = css(width="100\%", height="400px", background_color="blue")),
+  div(style = css(width="100\%", height="400px", background_color="green"))
+)
+
+# Recycling widths
+bscols(widths = c(2, 4),
+  div(style = css(width="100\%", height="400px", background_color="red")),
+  div(style = css(width="100\%", height="400px", background_color="blue")),
+  div(style = css(width="100\%", height="400px", background_color="red")),
+  div(style = css(width="100\%", height="400px", background_color="blue"))
+)
+}
+
diff --git a/man/crosstalkLibs.Rd b/man/crosstalkLibs.Rd
new file mode 100644
index 0000000..1e106a8
--- /dev/null
+++ b/man/crosstalkLibs.Rd
@@ -0,0 +1,13 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/crosstalk.R
+\name{crosstalkLibs}
+\alias{crosstalkLibs}
+\title{Crosstalk dependencies}
+\usage{
+crosstalkLibs()
+}
+\description{
+List of \code{\link[htmltools]{htmlDependency}} objects necessary for
+Crosstalk to function. Intended for widget authors.
+}
+
diff --git a/man/filter_select.Rd b/man/filter_select.Rd
new file mode 100644
index 0000000..215778c
--- /dev/null
+++ b/man/filter_select.Rd
@@ -0,0 +1,49 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/controls.R
+\name{filter_select}
+\alias{filter_checkbox}
+\alias{filter_select}
+\title{Categorical filter controls}
+\usage{
+filter_select(id, label, sharedData, group, allLevels = FALSE,
+  multiple = TRUE)
+
+filter_checkbox(id, label, sharedData, group, allLevels = FALSE,
+  inline = FALSE, columns = 1)
+}
+\arguments{
+\item{id}{An HTML element ID; must be unique within the web page}
+
+\item{label}{A human-readable label}
+
+\item{sharedData}{\code{SharedData} object with the data to filter}
+
+\item{group}{A one-sided formula whose values will populate this select box.
+Generally this should be a character or factor column; if not, it will be
+coerced to character.}
+
+\item{allLevels}{If the vector described by \code{group} is factor-based,
+should all the levels be displayed as options, or only ones that are
+present in the data?}
+
+\item{multiple}{Can multiple values be selected?}
+
+\item{inline}{If \code{TRUE}, render checkbox options horizontally instead of vertically.}
+
+\item{columns}{Number of columns the options should be arranged into.}
+}
+\description{
+Creates a select box or list of checkboxes, for filtering a
+\code{\link{SharedData}} object based on categorical data.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+sd <- SharedData$new(chickwts)
+filter_select("feedtype", "Feed type", sd, "feed")
+
+}
+
+}
+
diff --git a/man/filter_slider.Rd b/man/filter_slider.Rd
new file mode 100644
index 0000000..040ee12
--- /dev/null
+++ b/man/filter_slider.Rd
@@ -0,0 +1,97 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/controls.R
+\name{filter_slider}
+\alias{animation_options}
+\alias{filter_slider}
+\title{Range filter control}
+\usage{
+filter_slider(id, label, sharedData, column, step = NULL, round = FALSE,
+  ticks = TRUE, animate = FALSE, width = NULL, sep = ",", pre = NULL,
+  post = NULL, timeFormat = NULL, timezone = NULL, dragRange = TRUE)
+
+animation_options(interval = 1000, loop = FALSE, playButton = NULL,
+  pauseButton = NULL)
+}
+\arguments{
+\item{id}{An HTML element ID; must be unique within the web page}
+
+\item{label}{A human-readable label}
+
+\item{sharedData}{\code{SharedData} object with the data to filter}
+
+\item{column}{A one-sided formula whose values will be used for this slider.
+The column must be of type \code{\link{Date}}, \code{\link{POSIXt}}, or
+numeric.}
+
+\item{step}{Specifies the interval between each selectable value on the
+slider (if \code{NULL}, a heuristic is used to determine the step size). If
+the values are dates, \code{step} is in days; if the values are times
+(POSIXt), \code{step} is in seconds.}
+
+\item{round}{\code{TRUE} to round all values to the nearest integer;
+\code{FALSE} if no rounding is desired; or an integer to round to that
+number of digits (for example, 1 will round to the nearest 10, and -2 will
+round to the nearest .01). Any rounding will be applied after snapping to
+the nearest step.}
+
+\item{ticks}{\code{FALSE} to hide tick marks, \code{TRUE} to show them
+according to some simple heuristics.}
+
+\item{animate}{\code{TRUE} to show simple animation controls with default
+settings; \code{FALSE} not to; or a custom settings list, such as those
+created using \code{\link{animationOptions}}.}
+
+\item{width}{The width of the slider control (see
+\code{\link[htmltools]{validateCssUnit}} for valid formats)}
+
+\item{sep}{Separator between thousands places in numbers.}
+
+\item{pre}{A prefix string to put in front of the value.}
+
+\item{post}{A suffix string to put after the value.}
+
+\item{timeFormat}{Only used if the values are Date or POSIXt objects. A time
+format string, to be passed to the Javascript strftime library. See
+\url{https://github.com/samsonjs/strftime} for more details. The allowed
+format specifications are very similar, but not identical, to those for R's
+\code{\link{strftime}} function. For Dates, the default is \code{"\%F"}
+(like \code{"2015-07-01"}), and for POSIXt, the default is \code{"\%F \%T"}
+(like \code{"2015-07-01 15:32:10"}).}
+
+\item{timezone}{Only used if the values are POSIXt objects. A string
+specifying the time zone offset for the displayed times, in the format
+\code{"+HHMM"} or \code{"-HHMM"}. If \code{NULL} (the default), times will
+be displayed in the browser's time zone. The value \code{"+0000"} will
+result in UTC time.}
+
+\item{dragRange}{This option is used only if it is a range slider (with two
+values). If \code{TRUE} (the default), the range can be dragged. In other
+words, the min and max can be dragged together. If \code{FALSE}, the range
+cannot be dragged.}
+
+\item{interval}{The interval, in milliseconds, between each animation step.}
+
+\item{loop}{\code{TRUE} to automatically restart the animation when it
+reaches the end.}
+
+\item{playButton}{Specifies the appearance of the play button. Valid values
+are a one-element character vector (for a simple text label), an HTML tag
+or list of tags (using \code{\link{tag}} and friends), or raw HTML (using
+\code{\link{HTML}}).}
+
+\item{pauseButton}{Similar to \code{playButton}, but for the pause button.}
+}
+\description{
+Creates a slider widget that lets users filter observations based on a range
+of values.
+}
+\examples{
+## Only run examples in interactive R sessions
+if (interactive()) {
+
+sd <- SharedData$new(mtcars)
+filter_slider("mpg", "Miles per gallon", sd, "mpg")
+
+}
+}
+
diff --git a/man/is.SharedData.Rd b/man/is.SharedData.Rd
new file mode 100644
index 0000000..a2ebb99
--- /dev/null
+++ b/man/is.SharedData.Rd
@@ -0,0 +1,18 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/crosstalk.R
+\name{is.SharedData}
+\alias{is.SharedData}
+\title{Check if an object is \code{SharedData}}
+\usage{
+is.SharedData(x)
+}
+\arguments{
+\item{x}{The object that may or may not be an instance of \code{SharedData}}
+}
+\value{
+logical
+}
+\description{
+Check if an object is an instance of \code{\link{SharedData}} or not.
+}
+
diff --git a/man/maintain_selection.Rd b/man/maintain_selection.Rd
new file mode 100644
index 0000000..a6dcfef
--- /dev/null
+++ b/man/maintain_selection.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ggplot2.R
+\name{maintain_selection}
+\alias{maintain_selection}
+\title{Synchronize Shiny brush selection with shared data}
+\usage{
+maintain_selection(sharedData, brushId, ownerId = "")
+}
+\arguments{
+\item{sharedData}{The shared data instance}
+
+\item{brushId}{Character vector indicating the name of the \code{plotOutput}
+brush}
+
+\item{ownerId}{(TBD)}
+}
+\description{
+Waits for a brush to change, and propagates that change to the
+\code{sharedData} object.
+}
+
diff --git a/man/scale_fill_selection.Rd b/man/scale_fill_selection.Rd
new file mode 100644
index 0000000..607ce81
--- /dev/null
+++ b/man/scale_fill_selection.Rd
@@ -0,0 +1,46 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/ggplot2.R
+\name{scale_fill_selection}
+\alias{scale_color_selection}
+\alias{scale_fill_selection}
+\alias{selection_factor}
+\title{ggplot2 helpers}
+\usage{
+scale_fill_selection(color_false, color_true)
+
+scale_color_selection(color_false, color_true)
+
+selection_factor(x, na.replace = c(FALSE, NA, TRUE))
+}
+\arguments{
+\item{color_false}{The color that should be mapped to unselected rows}
+
+\item{color_true}{The color that should be mapped to selected rows}
+
+\item{x}{Either a data frame with a \code{selected_} column, or, a logical
+vector indicating which rows are selected}
+
+\item{na.replace}{The value to use to replace \code{NA} values; choose either
+\code{FALSE}, \code{NA}, or \code{TRUE} based on how you want values to be
+treated when no selection is active}
+}
+\description{
+Add \code{scale_fill_selection()} or \code{scale_color_selection} to a ggplot
+to customize the scale for fill or color, respectively, for linked brushing.
+Use \code{selection_factor} to turn logical vectors representing selection,
+to a factor with the levels ordered for use with ggplot2 bar stacking.
+}
+\examples{
+\dontrun{
+sd <- SharedData$new(iris)
+renderPlot({
+  df <- sd$data(withSelection = TRUE, withFilter = TRUE)
+  ggplot(df, aes(Sepal.Length, Sepal.Width,
+    color = selection_factor(df))) +
+    geom_point() +
+    scale_color_selection("#444444", "skyblue1")
+})
+
+}
+}
+

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-science/packages/r-cran-crosstalk.git



More information about the debian-science-commits mailing list